mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-16 08:20:14 +01:00
📢 fix: Remove Side Panel Elements from Screen Reader when Hidden (#10648)
* fix: remove side panel elements from screen reader when hidden There's both left & right side panels; elements of both of them are hidden when dismissed. However, currently they are being hidden by using classes to hide their UI (such as making the sidebar zero width). That works for visually dismissing these elements, but they can still be viewed by a screen reader (using the tab key to jump between interactable elements). That can be a rather confusing experience for anyone visually impaired (such as duplicate buttons, or buttons that do nothing). -------- I've changed it so hidden elements are fully removed from the render. This prevents them from being interactable via keyboard. I leveraged Motion to duplicate the animations as they happened before. I subtly cleaned up the animations while I was at it. * Implemented reasonable suggestions from Copilot review
This commit is contained in:
parent
9211d59388
commit
ffcca3254e
5 changed files with 49 additions and 44 deletions
|
|
@ -11,6 +11,7 @@ import BookmarkMenu from './Menus/BookmarkMenu';
|
|||
import { TemporaryChat } from './TemporaryChat';
|
||||
import AddMultiConvo from './AddMultiConvo';
|
||||
import { useHasAccess } from '~/hooks';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
|
||||
const defaultInterface = getConfigDefaults().interface;
|
||||
|
||||
|
|
@ -38,24 +39,24 @@ export default function Header() {
|
|||
return (
|
||||
<div className="sticky top-0 z-10 flex h-14 w-full items-center justify-between bg-white p-2 font-semibold text-text-primary dark:bg-gray-800">
|
||||
<div className="hide-scrollbar flex w-full items-center justify-between gap-2 overflow-x-auto">
|
||||
<div className="mx-1 flex items-center gap-2">
|
||||
<div
|
||||
className={`flex items-center gap-2 ${
|
||||
!isSmallScreen ? 'transition-all duration-200 ease-in-out' : ''
|
||||
} ${
|
||||
!navVisible
|
||||
? 'translate-x-0 opacity-100'
|
||||
: 'pointer-events-none translate-x-[-100px] opacity-0'
|
||||
}`}
|
||||
>
|
||||
<OpenSidebar setNavVisible={setNavVisible} className="max-md:hidden" />
|
||||
<HeaderNewChat />
|
||||
</div>
|
||||
<div
|
||||
className={`flex items-center gap-2 ${
|
||||
!isSmallScreen ? 'transition-all duration-200 ease-in-out' : ''
|
||||
} ${!navVisible ? 'translate-x-0' : 'translate-x-[-100px]'}`}
|
||||
>
|
||||
<div className="mx-1 flex items-center">
|
||||
<AnimatePresence initial={false}>
|
||||
{!navVisible && (
|
||||
<motion.div
|
||||
className={`flex items-center gap-2`}
|
||||
initial={{ width: 0, opacity: 0 }}
|
||||
animate={{ width: 'auto', opacity: 1 }}
|
||||
exit={{ width: 0, opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
key="header-buttons"
|
||||
>
|
||||
<OpenSidebar setNavVisible={setNavVisible} className="max-md:hidden" />
|
||||
<HeaderNewChat />
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<div className={navVisible ? 'flex items-center gap-2' : 'ml-2 flex items-center gap-2'}>
|
||||
<ModelSelector startupConfig={startupConfig} />
|
||||
{interfaceConfig.presets === true && interfaceConfig.modelSelect && <PresetsMenu />}
|
||||
{hasAccessToBookmarks === true && <BookmarkMenu />}
|
||||
|
|
|
|||
|
|
@ -9,8 +9,10 @@ import { clearMessagesCache } from '~/utils';
|
|||
import store from '~/store';
|
||||
|
||||
export default function MobileNav({
|
||||
navVisible,
|
||||
setNavVisible,
|
||||
}: {
|
||||
navVisible: boolean;
|
||||
setNavVisible: Dispatch<SetStateAction<boolean>>;
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
|
|
@ -25,7 +27,7 @@ export default function MobileNav({
|
|||
type="button"
|
||||
data-testid="mobile-header-new-chat-button"
|
||||
aria-label={localize('com_nav_open_sidebar')}
|
||||
className="m-1 inline-flex size-10 items-center justify-center rounded-full hover:bg-surface-hover"
|
||||
className={`m-1 inline-flex size-10 items-center justify-center rounded-full hover:bg-surface-hover ${navVisible ? 'invisible' : ''}`}
|
||||
onClick={() =>
|
||||
setNavVisible((prev) => {
|
||||
localStorage.setItem('navVisible', JSON.stringify(!prev));
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { useCallback, useEffect, useState, useMemo, memo, lazy, Suspense, useRef } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { useMediaQuery } from '@librechat/client';
|
||||
import { PermissionTypes, Permissions } from 'librechat-data-provider';
|
||||
import type { ConversationListResponse } from 'librechat-data-provider';
|
||||
|
|
@ -190,22 +191,21 @@ const Nav = memo(
|
|||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
data-testid="nav"
|
||||
className={cn(
|
||||
'nav active max-w-[320px] flex-shrink-0 transform overflow-x-hidden bg-surface-primary-alt transition-all duration-200 ease-in-out',
|
||||
'md:max-w-[260px]',
|
||||
)}
|
||||
style={{
|
||||
width: navVisible ? navWidth : '0px',
|
||||
transform: navVisible ? 'translateX(0)' : 'translateX(-100%)',
|
||||
}}
|
||||
>
|
||||
<div className="h-full w-[320px] md:w-[260px]">
|
||||
<div className="flex h-full flex-col">
|
||||
<div
|
||||
className={`flex h-full flex-col transition-opacity duration-200 ease-in-out ${navVisible ? 'opacity-100' : 'opacity-0'}`}
|
||||
>
|
||||
<AnimatePresence initial={false}>
|
||||
{navVisible && (
|
||||
<motion.div
|
||||
data-testid="nav"
|
||||
className={cn(
|
||||
'nav active max-w-[320px] flex-shrink-0 overflow-x-hidden bg-surface-primary-alt',
|
||||
'md:max-w-[260px]',
|
||||
)}
|
||||
initial={{ width: 0 }}
|
||||
animate={{ width: navWidth }}
|
||||
exit={{ width: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
key="nav"
|
||||
>
|
||||
<div className="h-full w-[320px] md:w-[260px]">
|
||||
<div className="flex h-full flex-col">
|
||||
<nav
|
||||
id="chat-history-nav"
|
||||
|
|
@ -235,9 +235,9 @@ const Nav = memo(
|
|||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
{isSmallScreen && <NavMask navVisible={navVisible} toggleNavVisible={toggleNavVisible} />}
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -147,11 +147,13 @@ const SidePanelGroup = memo(
|
|||
{artifacts != null && isSmallScreen && (
|
||||
<div className="fixed inset-0 z-[100]">{artifacts}</div>
|
||||
)}
|
||||
<button
|
||||
aria-label="Close right side panel"
|
||||
className={`nav-mask ${!isCollapsed ? 'active' : ''}`}
|
||||
onClick={handleClosePanel}
|
||||
/>
|
||||
{!hideSidePanel && interfaceConfig.sidePanel === true && (
|
||||
<button
|
||||
aria-label="Close right side panel"
|
||||
className={`nav-mask ${!isCollapsed ? 'active' : ''}`}
|
||||
onClick={handleClosePanel}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ export default function Root() {
|
|||
<div className="relative z-0 flex h-full w-full overflow-hidden">
|
||||
<Nav navVisible={navVisible} setNavVisible={setNavVisible} />
|
||||
<div className="relative flex h-full max-w-full flex-1 flex-col overflow-hidden">
|
||||
<MobileNav setNavVisible={setNavVisible} />
|
||||
<MobileNav navVisible={navVisible} setNavVisible={setNavVisible} />
|
||||
<Outlet context={{ navVisible, setNavVisible } satisfies ContextType} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue