From 44dbbd5328ea15b523f670d785eff284c8d2cb51 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Tue, 24 Feb 2026 20:59:56 -0500 Subject: [PATCH] =?UTF-8?q?=E2=99=BF=20a11y:=20Hide=20Collapsed=20Thinking?= =?UTF-8?q?=20Content=20From=20Screen=20Readers=20(#11927)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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. --- .../Chat/Messages/Content/Parts/Reasoning.tsx | 9 ++++++++- .../Chat/Messages/Content/Parts/Thinking.tsx | 18 ++++++++++++++++-- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/client/src/components/Chat/Messages/Content/Parts/Reasoning.tsx b/client/src/components/Chat/Messages/Content/Parts/Reasoning.tsx index 85d1b00b4b..44d646fcd8 100644 --- a/client/src/components/Chat/Messages/Content/Parts/Reasoning.tsx +++ b/client/src/components/Chat/Messages/Content/Parts/Reasoning.tsx @@ -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 type { MouseEvent, FocusEvent } from 'react'; import { ContentTypes } from 'librechat-data-provider'; @@ -36,6 +36,7 @@ type ReasoningProps = { * For legacy text-based messages, see Thinking.tsx component. */ const Reasoning = memo(({ reasoning, isLast }: ReasoningProps) => { + const contentId = useId(); const localize = useLocalize(); const [showThinking] = useAtom(showThinkingAtom); const [isExpanded, setIsExpanded] = useState(showThinking); @@ -104,9 +105,14 @@ const Reasoning = memo(({ reasoning, isLast }: ReasoningProps) => { onClick={handleClick} label={label} content={reasoningText} + contentId={contentId} />
{ isExpanded={isExpanded} onClick={handleClick} content={reasoningText} + contentId={contentId} />
diff --git a/client/src/components/Chat/Messages/Content/Parts/Thinking.tsx b/client/src/components/Chat/Messages/Content/Parts/Thinking.tsx index 0c5992f4ab..7641738c15 100644 --- a/client/src/components/Chat/Messages/Content/Parts/Thinking.tsx +++ b/client/src/components/Chat/Messages/Content/Parts/Thinking.tsx @@ -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 { Clipboard, CheckMark, TooltipAnchor } from '@librechat/client'; import { Lightbulb, ChevronDown, ChevronUp } from 'lucide-react'; @@ -35,12 +35,14 @@ export const ThinkingButton = memo( onClick, label, content, + contentId, showCopyButton = true, }: { isExpanded: boolean; onClick: (e: MouseEvent) => void; label: string; content?: string; + contentId: string; showCopyButton?: boolean; }) => { const localize = useLocalize(); @@ -66,6 +68,7 @@ export const ThinkingButton = memo( type="button" onClick={onClick} aria-expanded={isExpanded} + aria-controls={contentId} className={cn( 'group/button flex flex-1 items-center justify-start rounded-lg leading-[18px]', fontSize, @@ -132,11 +135,13 @@ export const FloatingThinkingBar = memo( isExpanded, onClick, content, + contentId, }: { isVisible: boolean; isExpanded: boolean; onClick: (e: MouseEvent) => void; content?: string; + contentId: string; }) => { const localize = useLocalize(); const [isCopied, setIsCopied] = useState(false); @@ -176,6 +181,8 @@ export const FloatingThinkingBar = memo( tabIndex={isVisible ? 0 : -1} onClick={onClick} aria-label={collapseTooltip} + aria-expanded={isExpanded} + aria-controls={contentId} className={cn( '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', @@ -240,6 +247,7 @@ const Thinking: React.ElementType = memo(({ children }: { children: React.ReactN const [isExpanded, setIsExpanded] = useState(showThinking); const [isBarVisible, setIsBarVisible] = useState(false); const containerRef = useRef(null); + const contentId = useId(); const handleClick = useCallback((e: MouseEvent) => { e.preventDefault(); @@ -295,9 +303,14 @@ const Thinking: React.ElementType = memo(({ children }: { children: React.ReactN onClick={handleClick} label={label} content={textContent} + contentId={contentId} />
@@ -322,4 +336,4 @@ ThinkingContent.displayName = 'ThinkingContent'; FloatingThinkingBar.displayName = 'FloatingThinkingBar'; Thinking.displayName = 'Thinking'; -export default memo(Thinking); +export default Thinking;