Skip to content

Commit 44dbbd5

Browse files
authored
♿ a11y: Hide Collapsed Thinking Content From Screen Readers (danny-avila#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.
1 parent 8c3c326 commit 44dbbd5

2 files changed

Lines changed: 24 additions & 3 deletions

File tree

client/src/components/Chat/Messages/Content/Parts/Reasoning.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { memo, useMemo, useState, useCallback, useRef } from 'react';
1+
import { memo, useMemo, useState, useCallback, useRef, useId } from 'react';
22
import { useAtom } from 'jotai';
33
import type { MouseEvent, FocusEvent } from 'react';
44
import { ContentTypes } from 'librechat-data-provider';
@@ -36,6 +36,7 @@ type ReasoningProps = {
3636
* For legacy text-based messages, see Thinking.tsx component.
3737
*/
3838
const Reasoning = memo(({ reasoning, isLast }: ReasoningProps) => {
39+
const contentId = useId();
3940
const localize = useLocalize();
4041
const [showThinking] = useAtom(showThinkingAtom);
4142
const [isExpanded, setIsExpanded] = useState(showThinking);
@@ -104,9 +105,14 @@ const Reasoning = memo(({ reasoning, isLast }: ReasoningProps) => {
104105
onClick={handleClick}
105106
label={label}
106107
content={reasoningText}
108+
contentId={contentId}
107109
/>
108110
</div>
109111
<div
112+
id={contentId}
113+
role="group"
114+
aria-label={label}
115+
aria-hidden={!isExpanded || undefined}
110116
className={cn(
111117
'grid transition-all duration-300 ease-out',
112118
nextType !== ContentTypes.THINK && isExpanded && 'mb-4',
@@ -122,6 +128,7 @@ const Reasoning = memo(({ reasoning, isLast }: ReasoningProps) => {
122128
isExpanded={isExpanded}
123129
onClick={handleClick}
124130
content={reasoningText}
131+
contentId={contentId}
125132
/>
126133
</div>
127134
</div>

client/src/components/Chat/Messages/Content/Parts/Thinking.tsx

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState, useMemo, memo, useCallback, useRef, type MouseEvent } from 'react';
1+
import { useState, useMemo, memo, useCallback, useRef, useId, type MouseEvent } from 'react';
22
import { useAtomValue } from 'jotai';
33
import { Clipboard, CheckMark, TooltipAnchor } from '@librechat/client';
44
import { Lightbulb, ChevronDown, ChevronUp } from 'lucide-react';
@@ -35,12 +35,14 @@ export const ThinkingButton = memo(
3535
onClick,
3636
label,
3737
content,
38+
contentId,
3839
showCopyButton = true,
3940
}: {
4041
isExpanded: boolean;
4142
onClick: (e: MouseEvent<HTMLButtonElement>) => void;
4243
label: string;
4344
content?: string;
45+
contentId: string;
4446
showCopyButton?: boolean;
4547
}) => {
4648
const localize = useLocalize();
@@ -66,6 +68,7 @@ export const ThinkingButton = memo(
6668
type="button"
6769
onClick={onClick}
6870
aria-expanded={isExpanded}
71+
aria-controls={contentId}
6972
className={cn(
7073
'group/button flex flex-1 items-center justify-start rounded-lg leading-[18px]',
7174
fontSize,
@@ -132,11 +135,13 @@ export const FloatingThinkingBar = memo(
132135
isExpanded,
133136
onClick,
134137
content,
138+
contentId,
135139
}: {
136140
isVisible: boolean;
137141
isExpanded: boolean;
138142
onClick: (e: MouseEvent<HTMLButtonElement>) => void;
139143
content?: string;
144+
contentId: string;
140145
}) => {
141146
const localize = useLocalize();
142147
const [isCopied, setIsCopied] = useState(false);
@@ -176,6 +181,8 @@ export const FloatingThinkingBar = memo(
176181
tabIndex={isVisible ? 0 : -1}
177182
onClick={onClick}
178183
aria-label={collapseTooltip}
184+
aria-expanded={isExpanded}
185+
aria-controls={contentId}
179186
className={cn(
180187
'flex items-center justify-center rounded-lg bg-surface-secondary p-1.5 text-text-secondary-alt shadow-sm',
181188
'hover:bg-surface-hover hover:text-text-primary',
@@ -240,6 +247,7 @@ const Thinking: React.ElementType = memo(({ children }: { children: React.ReactN
240247
const [isExpanded, setIsExpanded] = useState(showThinking);
241248
const [isBarVisible, setIsBarVisible] = useState(false);
242249
const containerRef = useRef<HTMLDivElement>(null);
250+
const contentId = useId();
243251

244252
const handleClick = useCallback((e: MouseEvent<HTMLButtonElement>) => {
245253
e.preventDefault();
@@ -295,9 +303,14 @@ const Thinking: React.ElementType = memo(({ children }: { children: React.ReactN
295303
onClick={handleClick}
296304
label={label}
297305
content={textContent}
306+
contentId={contentId}
298307
/>
299308
</div>
300309
<div
310+
id={contentId}
311+
role="group"
312+
aria-label={label}
313+
aria-hidden={!isExpanded || undefined}
301314
className={cn('grid transition-all duration-300 ease-out', isExpanded && 'mb-8')}
302315
style={{
303316
gridTemplateRows: isExpanded ? '1fr' : '0fr',
@@ -310,6 +323,7 @@ const Thinking: React.ElementType = memo(({ children }: { children: React.ReactN
310323
isExpanded={isExpanded}
311324
onClick={handleClick}
312325
content={textContent}
326+
contentId={contentId}
313327
/>
314328
</div>
315329
</div>
@@ -322,4 +336,4 @@ ThinkingContent.displayName = 'ThinkingContent';
322336
FloatingThinkingBar.displayName = 'FloatingThinkingBar';
323337
Thinking.displayName = 'Thinking';
324338

325-
export default memo(Thinking);
339+
export default Thinking;

0 commit comments

Comments
 (0)