mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-02-26 20:34:10 +01:00
♿ a11y: Hide Collapsed Thinking Content From Screen Readers (#11927)
* fix(a11y): hide collapsed thinking content from screen readers and link toggle to controlled region
The thinking/reasoning toggle button visually collapsed content using a CSS
grid animation (gridTemplateRows: 0fr + overflow-hidden), but the content
remained in the DOM and fully accessible to screen readers, cluttering the
reading flow for assistive technology users.
- Add aria-hidden={!isExpanded} to the collapsible content region in both
the legacy Thinking component and the modern Reasoning component, so
screen readers skip collapsed thoughts entirely
- Add role="region" and a unique id (via useId) to each collapsible content
div, giving it a semantic landmark for assistive technology
- Add contentId prop to the shared ThinkingButton and wire it to
aria-controls on the toggle button, establishing an explicit relationship
between the button and the region it expands/collapses
- aria-expanded was already present on the button; combined with
aria-controls, screen readers can now fully convey the toggle state and
its target
* fix(a11y): add aria-label to collapsible content regions in Thinking and Reasoning components
Enhanced accessibility by adding aria-label attributes to the collapsible content regions in both the Thinking and Reasoning components. This change ensures that screen readers can provide better context for users navigating through the content.
* fix(a11y): update roles and aria attributes in Thinking and Reasoning components
Changed role from "region" to "group" for collapsible content areas in both Thinking and Reasoning components to better align with ARIA practices. Updated aria-hidden to handle undefined values correctly and ensured contentId is passed to relevant components for improved accessibility and screen reader support.
This commit is contained in:
parent
8c3c326440
commit
44dbbd5328
2 changed files with 24 additions and 3 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
import { memo, useMemo, useState, useCallback, useRef } from 'react';
|
import { memo, useMemo, useState, useCallback, useRef, useId } from 'react';
|
||||||
import { useAtom } from 'jotai';
|
import { useAtom } from 'jotai';
|
||||||
import type { MouseEvent, FocusEvent } from 'react';
|
import type { MouseEvent, FocusEvent } from 'react';
|
||||||
import { ContentTypes } from 'librechat-data-provider';
|
import { ContentTypes } from 'librechat-data-provider';
|
||||||
|
|
@ -36,6 +36,7 @@ type ReasoningProps = {
|
||||||
* For legacy text-based messages, see Thinking.tsx component.
|
* For legacy text-based messages, see Thinking.tsx component.
|
||||||
*/
|
*/
|
||||||
const Reasoning = memo(({ reasoning, isLast }: ReasoningProps) => {
|
const Reasoning = memo(({ reasoning, isLast }: ReasoningProps) => {
|
||||||
|
const contentId = useId();
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const [showThinking] = useAtom(showThinkingAtom);
|
const [showThinking] = useAtom(showThinkingAtom);
|
||||||
const [isExpanded, setIsExpanded] = useState(showThinking);
|
const [isExpanded, setIsExpanded] = useState(showThinking);
|
||||||
|
|
@ -104,9 +105,14 @@ const Reasoning = memo(({ reasoning, isLast }: ReasoningProps) => {
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
label={label}
|
label={label}
|
||||||
content={reasoningText}
|
content={reasoningText}
|
||||||
|
contentId={contentId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
id={contentId}
|
||||||
|
role="group"
|
||||||
|
aria-label={label}
|
||||||
|
aria-hidden={!isExpanded || undefined}
|
||||||
className={cn(
|
className={cn(
|
||||||
'grid transition-all duration-300 ease-out',
|
'grid transition-all duration-300 ease-out',
|
||||||
nextType !== ContentTypes.THINK && isExpanded && 'mb-4',
|
nextType !== ContentTypes.THINK && isExpanded && 'mb-4',
|
||||||
|
|
@ -122,6 +128,7 @@ const Reasoning = memo(({ reasoning, isLast }: ReasoningProps) => {
|
||||||
isExpanded={isExpanded}
|
isExpanded={isExpanded}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
content={reasoningText}
|
content={reasoningText}
|
||||||
|
contentId={contentId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useState, useMemo, memo, useCallback, useRef, type MouseEvent } from 'react';
|
import { useState, useMemo, memo, useCallback, useRef, useId, type MouseEvent } from 'react';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
import { Clipboard, CheckMark, TooltipAnchor } from '@librechat/client';
|
import { Clipboard, CheckMark, TooltipAnchor } from '@librechat/client';
|
||||||
import { Lightbulb, ChevronDown, ChevronUp } from 'lucide-react';
|
import { Lightbulb, ChevronDown, ChevronUp } from 'lucide-react';
|
||||||
|
|
@ -35,12 +35,14 @@ export const ThinkingButton = memo(
|
||||||
onClick,
|
onClick,
|
||||||
label,
|
label,
|
||||||
content,
|
content,
|
||||||
|
contentId,
|
||||||
showCopyButton = true,
|
showCopyButton = true,
|
||||||
}: {
|
}: {
|
||||||
isExpanded: boolean;
|
isExpanded: boolean;
|
||||||
onClick: (e: MouseEvent<HTMLButtonElement>) => void;
|
onClick: (e: MouseEvent<HTMLButtonElement>) => void;
|
||||||
label: string;
|
label: string;
|
||||||
content?: string;
|
content?: string;
|
||||||
|
contentId: string;
|
||||||
showCopyButton?: boolean;
|
showCopyButton?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
|
|
@ -66,6 +68,7 @@ export const ThinkingButton = memo(
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
aria-expanded={isExpanded}
|
aria-expanded={isExpanded}
|
||||||
|
aria-controls={contentId}
|
||||||
className={cn(
|
className={cn(
|
||||||
'group/button flex flex-1 items-center justify-start rounded-lg leading-[18px]',
|
'group/button flex flex-1 items-center justify-start rounded-lg leading-[18px]',
|
||||||
fontSize,
|
fontSize,
|
||||||
|
|
@ -132,11 +135,13 @@ export const FloatingThinkingBar = memo(
|
||||||
isExpanded,
|
isExpanded,
|
||||||
onClick,
|
onClick,
|
||||||
content,
|
content,
|
||||||
|
contentId,
|
||||||
}: {
|
}: {
|
||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
isExpanded: boolean;
|
isExpanded: boolean;
|
||||||
onClick: (e: MouseEvent<HTMLButtonElement>) => void;
|
onClick: (e: MouseEvent<HTMLButtonElement>) => void;
|
||||||
content?: string;
|
content?: string;
|
||||||
|
contentId: string;
|
||||||
}) => {
|
}) => {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const [isCopied, setIsCopied] = useState(false);
|
const [isCopied, setIsCopied] = useState(false);
|
||||||
|
|
@ -176,6 +181,8 @@ export const FloatingThinkingBar = memo(
|
||||||
tabIndex={isVisible ? 0 : -1}
|
tabIndex={isVisible ? 0 : -1}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
aria-label={collapseTooltip}
|
aria-label={collapseTooltip}
|
||||||
|
aria-expanded={isExpanded}
|
||||||
|
aria-controls={contentId}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center justify-center rounded-lg bg-surface-secondary p-1.5 text-text-secondary-alt shadow-sm',
|
'flex items-center justify-center rounded-lg bg-surface-secondary p-1.5 text-text-secondary-alt shadow-sm',
|
||||||
'hover:bg-surface-hover hover:text-text-primary',
|
'hover:bg-surface-hover hover:text-text-primary',
|
||||||
|
|
@ -240,6 +247,7 @@ const Thinking: React.ElementType = memo(({ children }: { children: React.ReactN
|
||||||
const [isExpanded, setIsExpanded] = useState(showThinking);
|
const [isExpanded, setIsExpanded] = useState(showThinking);
|
||||||
const [isBarVisible, setIsBarVisible] = useState(false);
|
const [isBarVisible, setIsBarVisible] = useState(false);
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const contentId = useId();
|
||||||
|
|
||||||
const handleClick = useCallback((e: MouseEvent<HTMLButtonElement>) => {
|
const handleClick = useCallback((e: MouseEvent<HTMLButtonElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
@ -295,9 +303,14 @@ const Thinking: React.ElementType = memo(({ children }: { children: React.ReactN
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
label={label}
|
label={label}
|
||||||
content={textContent}
|
content={textContent}
|
||||||
|
contentId={contentId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
id={contentId}
|
||||||
|
role="group"
|
||||||
|
aria-label={label}
|
||||||
|
aria-hidden={!isExpanded || undefined}
|
||||||
className={cn('grid transition-all duration-300 ease-out', isExpanded && 'mb-8')}
|
className={cn('grid transition-all duration-300 ease-out', isExpanded && 'mb-8')}
|
||||||
style={{
|
style={{
|
||||||
gridTemplateRows: isExpanded ? '1fr' : '0fr',
|
gridTemplateRows: isExpanded ? '1fr' : '0fr',
|
||||||
|
|
@ -310,6 +323,7 @@ const Thinking: React.ElementType = memo(({ children }: { children: React.ReactN
|
||||||
isExpanded={isExpanded}
|
isExpanded={isExpanded}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
content={textContent}
|
content={textContent}
|
||||||
|
contentId={contentId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -322,4 +336,4 @@ ThinkingContent.displayName = 'ThinkingContent';
|
||||||
FloatingThinkingBar.displayName = 'FloatingThinkingBar';
|
FloatingThinkingBar.displayName = 'FloatingThinkingBar';
|
||||||
Thinking.displayName = 'Thinking';
|
Thinking.displayName = 'Thinking';
|
||||||
|
|
||||||
export default memo(Thinking);
|
export default Thinking;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue