mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-01-24 03:06:12 +01:00
🙌 a11y: Accessibility Improvements (#4978)
* 🔃 fix: Safeguard against null token in SSE refresh token handling * 🔃 fix: Update import path for AnnounceOptions in LiveAnnouncer component * 🔃 a11y: Add aria-live attribute for accessibility in error messages * fix: prevent double screen reader notification for toast * 🔃 a11y: Enhance accessibility for main menus and buttons with ARIA roles and labels * refactor: better alt text for logo on login page #4095 * refactor: remove unused import for DropdownNoState in Voices component * fix: Focus management issue in the Export Options Modal #4100
This commit is contained in:
parent
763693cc1b
commit
0a5bc503b0
18 changed files with 102 additions and 26 deletions
|
|
@ -1,11 +1,11 @@
|
|||
import { useState, useId } from 'react';
|
||||
import { useState, useId, useRef } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import * as Ariakit from '@ariakit/react';
|
||||
import { Upload, Share2 } from 'lucide-react';
|
||||
import { ShareButton } from '~/components/Conversations/ConvoOptions';
|
||||
import { useMediaQuery, useLocalize } from '~/hooks';
|
||||
import ExportModal from '~/components/Nav/ExportConversation/ExportModal';
|
||||
import { DropdownPopup } from '~/components/ui';
|
||||
import { ExportModal } from '../Nav';
|
||||
import store from '~/store';
|
||||
|
||||
export default function ExportAndShareMenu({
|
||||
|
|
@ -19,6 +19,7 @@ export default function ExportAndShareMenu({
|
|||
const [showShareDialog, setShowShareDialog] = useState(false);
|
||||
|
||||
const menuId = useId();
|
||||
const exportButtonRef = useRef<HTMLButtonElement>(null);
|
||||
const isSmallScreen = useMediaQuery('(max-width: 768px)');
|
||||
const conversation = useRecoilValue(store.conversationByIndex(0));
|
||||
|
||||
|
|
@ -68,6 +69,7 @@ export default function ExportAndShareMenu({
|
|||
setIsOpen={setIsPopoverActive}
|
||||
trigger={
|
||||
<Ariakit.MenuButton
|
||||
ref={exportButtonRef}
|
||||
id="export-menu-button"
|
||||
aria-label="Export options"
|
||||
className="inline-flex size-10 items-center justify-center rounded-lg border border-border-light bg-transparent text-text-primary transition-all ease-in-out hover:bg-surface-tertiary disabled:pointer-events-none disabled:opacity-50 radix-state-open:bg-surface-tertiary"
|
||||
|
|
@ -91,7 +93,8 @@ export default function ExportAndShareMenu({
|
|||
open={showExports}
|
||||
onOpenChange={onOpenChange}
|
||||
conversation={conversation}
|
||||
aria-label="Export conversation modal"
|
||||
triggerRef={exportButtonRef}
|
||||
aria-label={localize('com_ui_export_convo_modal')}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import type { FC } from 'react';
|
|||
import { useChatContext, useAgentsMapContext, useAssistantsMapContext } from '~/Providers';
|
||||
import { mapEndpoints, getEntity } from '~/utils';
|
||||
import EndpointItems from './Endpoints/MenuItems';
|
||||
import useLocalize from '~/hooks/useLocalize';
|
||||
import TitleButton from './UI/TitleButton';
|
||||
|
||||
const EndpointsMenu: FC = () => {
|
||||
|
|
@ -12,6 +13,7 @@ const EndpointsMenu: FC = () => {
|
|||
select: mapEndpoints,
|
||||
});
|
||||
|
||||
const localize = useLocalize();
|
||||
const agentsMap = useAgentsMapContext();
|
||||
const assistantMap = useAssistantsMapContext();
|
||||
const { conversation } = useChatContext();
|
||||
|
|
@ -51,6 +53,9 @@ const EndpointsMenu: FC = () => {
|
|||
<Content
|
||||
side="bottom"
|
||||
align="start"
|
||||
role="listbox"
|
||||
id="llm-endpoint-menu"
|
||||
aria-label={localize('com_ui_endpoints_available')}
|
||||
className="mt-2 max-h-[65vh] min-w-[340px] overflow-y-auto rounded-lg border border-border-light bg-header-primary text-text-primary shadow-lg lg:max-h-[75vh]"
|
||||
>
|
||||
<EndpointItems endpoints={endpoints} selected={endpoint} />
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { useState } from 'react';
|
||||
import { Trigger } from '@radix-ui/react-popover';
|
||||
import type { TModelSpec, TEndpointsConfig } from 'librechat-data-provider';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
|
@ -20,6 +21,8 @@ export default function MenuButton({
|
|||
endpointsConfig: TEndpointsConfig;
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
return (
|
||||
<Trigger asChild>
|
||||
<button
|
||||
|
|
@ -28,7 +31,13 @@ export default function MenuButton({
|
|||
className,
|
||||
)}
|
||||
type="button"
|
||||
aria-label={`Select ${primaryText}`}
|
||||
aria-label={localize('com_ui_llm_menu')}
|
||||
role="combobox"
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded={isExpanded}
|
||||
aria-controls="llm-menu"
|
||||
aria-activedescendant={isExpanded ? 'selected-llm' : undefined}
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
>
|
||||
{selected && selected.showIconInHeader === true && (
|
||||
<SpecIcon currentSpec={selected} endpointsConfig={endpointsConfig} />
|
||||
|
|
|
|||
|
|
@ -54,7 +54,9 @@ const MenuItem: FC<MenuItemProps> = ({
|
|||
return (
|
||||
<>
|
||||
<div
|
||||
role="menuitem"
|
||||
id={selected ? 'selected-llm' : undefined}
|
||||
role="option"
|
||||
aria-selected={selected}
|
||||
className="group m-1.5 flex cursor-pointer gap-2 rounded px-1 py-2.5 !pr-3 text-sm !opacity-100 hover:bg-black/5 focus:ring-0 radix-disabled:pointer-events-none radix-disabled:opacity-50 dark:hover:bg-white/5"
|
||||
tabIndex={0}
|
||||
{...rest}
|
||||
|
|
|
|||
|
|
@ -6,14 +6,14 @@ import MenuSeparator from '~/components/Chat/Menus/UI/MenuSeparator';
|
|||
import ModelSpec from './ModelSpec';
|
||||
|
||||
const ModelSpecs: FC<{
|
||||
specs?: TModelSpec[];
|
||||
specs?: Array<TModelSpec | undefined>;
|
||||
selected?: TModelSpec;
|
||||
setSelected?: (spec: TModelSpec) => void;
|
||||
endpointsConfig: TEndpointsConfig;
|
||||
}> = ({ specs = [], selected, setSelected = () => ({}), endpointsConfig }) => {
|
||||
return (
|
||||
<>
|
||||
{specs &&
|
||||
{specs.length &&
|
||||
specs.map((spec, i) => {
|
||||
if (!spec) {
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
|
|||
import { EModelEndpoint, isAssistantsEndpoint } from 'librechat-data-provider';
|
||||
import type { TModelSpec, TConversation, TEndpointsConfig } from 'librechat-data-provider';
|
||||
import { useChatContext, useAssistantsMapContext } from '~/Providers';
|
||||
import { useDefaultConvo, useNewConvo, useLocalize } from '~/hooks';
|
||||
import { getConvoSwitchLogic, getModelSpecIconURL } from '~/utils';
|
||||
import { useDefaultConvo, useNewConvo } from '~/hooks';
|
||||
import MenuButton from './MenuButton';
|
||||
import ModelSpecs from './ModelSpecs';
|
||||
import store from '~/store';
|
||||
|
|
@ -15,6 +15,7 @@ export default function ModelSpecsMenu({ modelSpecs }: { modelSpecs?: TModelSpec
|
|||
const { conversation } = useChatContext();
|
||||
const { newConversation } = useNewConvo();
|
||||
|
||||
const localize = useLocalize();
|
||||
const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery();
|
||||
const modularChat = useRecoilValue(store.modularChat);
|
||||
const getDefaultConversation = useDefaultConvo();
|
||||
|
|
@ -111,6 +112,9 @@ export default function ModelSpecsMenu({ modelSpecs }: { modelSpecs?: TModelSpec
|
|||
<Content
|
||||
side="bottom"
|
||||
align="start"
|
||||
id="llm-menu"
|
||||
role="listbox"
|
||||
aria-label={localize('com_ui_llms_available')}
|
||||
className="models-scrollbar mt-2 max-h-[65vh] min-w-[340px] max-w-xs overflow-y-auto rounded-lg border border-gray-100 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-700 dark:text-white lg:max-h-[75vh]"
|
||||
>
|
||||
<ModelSpecs
|
||||
|
|
|
|||
|
|
@ -32,7 +32,9 @@ const MenuItem: FC<MenuItemProps> = ({
|
|||
}) => {
|
||||
return (
|
||||
<div
|
||||
role="menuitem"
|
||||
id={selected ? 'selected-endpoint' : undefined}
|
||||
role="option"
|
||||
aria-selected={selected}
|
||||
aria-label={title}
|
||||
data-testid="chat-menu-item"
|
||||
className={cn(
|
||||
|
|
|
|||
|
|
@ -1,18 +1,22 @@
|
|||
import { useState } from 'react';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
import { Trigger } from '@radix-ui/react-popover';
|
||||
import useLocalize from '~/hooks/useLocalize';
|
||||
|
||||
export default function TitleButton({ primaryText = '', secondaryText = '' }) {
|
||||
const localize = useLocalize();
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
return (
|
||||
<Trigger asChild>
|
||||
<button
|
||||
className="group flex cursor-pointer items-center gap-2 rounded-lg px-3 py-1.5 text-lg font-medium transition-colors duration-200 hover:bg-surface-hover radix-state-open:bg-surface-hover"
|
||||
aria-label={`Select ${primaryText}`}
|
||||
aria-haspopup="dialog"
|
||||
aria-label={localize('com_ui_endpoint_menu')}
|
||||
aria-expanded={isExpanded}
|
||||
aria-controls="radix-:r6:"
|
||||
role="combobox"
|
||||
aria-haspopup="listbox"
|
||||
aria-controls="llm-endpoint-menu"
|
||||
aria-activedescendant={isExpanded ? 'selected-endpoint' : undefined}
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
>
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -56,6 +56,8 @@ export const ErrorMessage = ({
|
|||
return (
|
||||
<Container message={message}>
|
||||
<div
|
||||
role="alert"
|
||||
aria-live="assertive"
|
||||
className={cn(
|
||||
'rounded-xl border border-red-500/20 bg-red-500/5 px-3 py-2 text-sm text-gray-600 dark:text-gray-200',
|
||||
className,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue