mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-21 02:40:14 +01:00
🛂 fix: Address Accessibility Issues - Axe Rating: Serious (#10607)
* feat: wrap main content of page in <main> tag for screen reader landmarks (439) * feat: add italic on active convo when selected so that selection state does not rely on bg contrast ratio (562) * feat: add border ring around SearchBar so that it passes focus contrast minimums (577) * fix: hide decorative SVGs from screen readers (578) * fix: stop clipping of focus outlines in My Files modal (593) * feat: programmatically declare state of Temporary Chat toggle for screen readers (606) * feat: add sr-only components to warn screen readers that footer links open in new tab (611) * feat: add aria-labels to archived chat table buttons * feat: add screen reader heading for prompt edit page (776) * feat: increase contrast to threshold minimum for production tag in prompts advanced view (773) * feat: increase contrast to thehold minimums for production tag and version card border highlights (770) * fix: h2 now reads as 'control bar' to screen readers in edit prompt page (768) * feat: add selected state tracking for simple / advanced toggle for screen readers (765) * feat: add left padding to theme selector in prompts side nav panel so that focus outline doesnt clip * feat: darken orange bg for warning toasts to hit 3:1 contrast minimum with white text (725) * fix: return focus to triggering element on modal close for image preview in attach files panel (717) * fix: hide SVG for AddMultiConvo button from screen readers (708) * feat: add persistent label to Filter Memories... input in memory side panel
This commit is contained in:
parent
1143f73f59
commit
39cecc97bd
22 changed files with 81 additions and 67 deletions
|
|
@ -20,6 +20,9 @@ export default function Footer({ className }: { className?: string }) {
|
|||
rel="noreferrer"
|
||||
>
|
||||
{localize('com_ui_privacy_policy')}
|
||||
{privacyPolicy.openNewTab === true && (
|
||||
<span className="sr-only">{' ' + localize('com_ui_opens_new_tab')}</span>
|
||||
)}
|
||||
</a>
|
||||
);
|
||||
|
||||
|
|
@ -31,6 +34,9 @@ export default function Footer({ className }: { className?: string }) {
|
|||
rel="noreferrer"
|
||||
>
|
||||
{localize('com_ui_terms_of_service')}
|
||||
{termsOfService.openNewTab === true && (
|
||||
<span className="sr-only">{' ' + localize('com_ui_opens_new_tab')}</span>
|
||||
)}
|
||||
</a>
|
||||
);
|
||||
|
||||
|
|
@ -66,6 +72,7 @@ export default function Footer({ className }: { className?: string }) {
|
|||
{...otherProps}
|
||||
>
|
||||
{children}
|
||||
<span className="sr-only">{' ' + localize('com_ui_opens_new_tab')}</span>
|
||||
</a>
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { Maximize2 } from 'lucide-react';
|
||||
import { FileSources } from 'librechat-data-provider';
|
||||
import { OGDialog, OGDialogContent } from '@librechat/client';
|
||||
|
|
@ -13,11 +13,6 @@ type styleProps = {
|
|||
backgroundRepeat?: string;
|
||||
};
|
||||
|
||||
interface CloseModalEvent {
|
||||
stopPropagation: () => void;
|
||||
preventDefault: () => void;
|
||||
}
|
||||
|
||||
const ImagePreview = ({
|
||||
imageBase64,
|
||||
url,
|
||||
|
|
@ -35,53 +30,32 @@ const ImagePreview = ({
|
|||
}) => {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const [previousActiveElement, setPreviousActiveElement] = useState<Element | null>(null);
|
||||
const triggerRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const openModal = useCallback(() => {
|
||||
setPreviousActiveElement(document.activeElement);
|
||||
setIsModalOpen(true);
|
||||
}, []);
|
||||
|
||||
const closeModal = useCallback(
|
||||
(e: CloseModalEvent): void => {
|
||||
setIsModalOpen(false);
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
if (
|
||||
previousActiveElement instanceof HTMLElement &&
|
||||
!previousActiveElement.closest('[data-skip-refocus="true"]')
|
||||
) {
|
||||
previousActiveElement.focus();
|
||||
}
|
||||
},
|
||||
[previousActiveElement],
|
||||
);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
closeModal(e);
|
||||
}
|
||||
},
|
||||
[closeModal],
|
||||
);
|
||||
const handleOpenChange = useCallback((open: boolean) => {
|
||||
setIsModalOpen(open);
|
||||
if (!open && triggerRef.current) {
|
||||
requestAnimationFrame(() => {
|
||||
triggerRef.current?.focus({ preventScroll: true });
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isModalOpen) {
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
document.body.style.overflow = 'hidden';
|
||||
const closeButton = document.querySelector('[aria-label="Close full view"]') as HTMLElement;
|
||||
if (closeButton) {
|
||||
setTimeout(() => closeButton.focus(), 0);
|
||||
}
|
||||
} else {
|
||||
document.body.style.overflow = 'unset';
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
document.body.style.overflow = 'unset';
|
||||
};
|
||||
}, [isModalOpen, handleKeyDown]);
|
||||
}, [isModalOpen]);
|
||||
|
||||
const baseStyle: styleProps = {
|
||||
backgroundSize: 'cover',
|
||||
|
|
@ -117,6 +91,7 @@ const ImagePreview = ({
|
|||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
<button
|
||||
ref={triggerRef}
|
||||
type="button"
|
||||
className="size-full overflow-hidden rounded-xl"
|
||||
style={style}
|
||||
|
|
@ -158,7 +133,7 @@ const ImagePreview = ({
|
|||
<SourceIcon source={source} aria-label={source ? `Source: ${source}` : undefined} />
|
||||
</div>
|
||||
|
||||
<OGDialog open={isModalOpen} onOpenChange={setIsModalOpen}>
|
||||
<OGDialog open={isModalOpen} onOpenChange={handleOpenChange}>
|
||||
<OGDialogContent
|
||||
showCloseButton={false}
|
||||
className="w-11/12 overflow-x-auto bg-transparent p-0 sm:w-auto"
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ export const columns: ColumnDef<TFile>[] = [
|
|||
<div className="flex gap-2">
|
||||
<ImagePreview
|
||||
url={file.filepath}
|
||||
className="relative h-10 w-10 shrink-0 overflow-hidden rounded-md"
|
||||
className="relative h-10 w-10 shrink-0 overflow-visible rounded-md"
|
||||
source={file.source}
|
||||
/>
|
||||
<span className="self-center truncate">{file.filename}</span>
|
||||
|
|
|
|||
|
|
@ -158,8 +158,8 @@ export default function DataTable<TData, TValue>({ columns, data }: DataTablePro
|
|||
{headerGroup.headers.map((header, index) => {
|
||||
const style: Style = {};
|
||||
if (index === 0 && header.id === 'select') {
|
||||
style.width = '35px';
|
||||
style.minWidth = '35px';
|
||||
style.width = '36px';
|
||||
style.minWidth = '36px';
|
||||
} else if (header.id === 'filename') {
|
||||
style.width = isSmallScreen ? '60%' : '40%';
|
||||
} else {
|
||||
|
|
@ -204,7 +204,10 @@ export default function DataTable<TData, TValue>({ columns, data }: DataTablePro
|
|||
return (
|
||||
<TableCell
|
||||
key={cell.id}
|
||||
className="align-start overflow-x-auto px-2 py-1 text-xs sm:px-4 sm:py-2 sm:text-sm [tr[data-disabled=true]_&]:opacity-50"
|
||||
className={cn(
|
||||
'align-start px-2 py-1 text-xs sm:px-4 sm:py-2 sm:text-sm [tr[data-disabled=true]_&]:opacity-50',
|
||||
cell.column.id === 'select' ? 'overflow-visible' : 'overflow-x-auto',
|
||||
)}
|
||||
style={style}
|
||||
>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export default function OpenSidebar({
|
|||
})
|
||||
}
|
||||
>
|
||||
<Sidebar />
|
||||
<Sidebar aria-hidden="true" />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ export function TemporaryChat() {
|
|||
<button
|
||||
onClick={handleBadgeToggle}
|
||||
aria-label={localize(temporaryBadge.label)}
|
||||
aria-pressed={isTemporary}
|
||||
className={cn(
|
||||
'inline-flex size-10 flex-shrink-0 items-center justify-center rounded-xl border border-border-light text-text-primary transition-all ease-in-out hover:bg-surface-tertiary',
|
||||
isTemporary
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue