mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-10-21 09:32:24 +00:00
♿ fix: Honor reduced motion on manual phase disclosure
The entrance already respected the preference, but manually opening or closing a phase did not: `useExpandCollapse` writes its transition as an inline style, which cannot carry a `prefers-reduced-motion` media query, and there is no global reduced-motion reset in the stylesheet. Before this PR the phase used `<details>`, which had no animation at all — so the swap to an animated disclosure handed reduced-motion readers a 300ms fold they did not have. Resolve the preference in the hook and drop the transition outright. Every expanding panel in the message content shares it, so tool calls, thinking blocks, attachments and web-search sources are covered by the same change. The chevron and the fold's own utility classes get `motion-reduce` overrides, which the inline styles cannot express.
This commit is contained in:
parent
7dc8c06934
commit
3b9bd2181d
3 changed files with 66 additions and 4 deletions
|
|
@ -18,7 +18,7 @@ import { cn } from '~/utils';
|
|||
/** Matches `EXPAND_TRANSITION` so the header, the panel, and the card chrome
|
||||
* all resolve on the same curve — three properties animating on two different
|
||||
* easings is what makes a fold read as two separate movements. */
|
||||
const FOLD_EASING = 'duration-300 ease-[cubic-bezier(0.16,1,0.3,1)]';
|
||||
const FOLD_EASING = 'duration-300 ease-[cubic-bezier(0.16,1,0.3,1)] motion-reduce:transition-none';
|
||||
|
||||
type ActivityPhasePart = Extract<TMessageContentParts, { type: ContentTypes.ACTIVITY_LABEL }> & {
|
||||
activity_label_type?: 'phase';
|
||||
|
|
@ -200,7 +200,7 @@ export default function ActivityPhaseGroup({
|
|||
</span>
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
'size-4 shrink-0 transition-transform duration-200 ease-out',
|
||||
'size-4 shrink-0 transition-transform duration-200 ease-out motion-reduce:transition-none',
|
||||
isExpanded && 'rotate-180',
|
||||
)}
|
||||
aria-hidden="true"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,54 @@
|
|||
import { renderHook } from '@testing-library/react';
|
||||
import useExpandCollapse, { EXPAND_TRANSITION } from '../useExpandCollapse';
|
||||
|
||||
function stubReducedMotion(reduce: boolean) {
|
||||
window.matchMedia = jest.fn().mockImplementation((query: string) => ({
|
||||
matches: query.includes('prefers-reduced-motion') ? reduce : false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: jest.fn(),
|
||||
removeListener: jest.fn(),
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
dispatchEvent: jest.fn(),
|
||||
})) as unknown as typeof window.matchMedia;
|
||||
}
|
||||
|
||||
describe('useExpandCollapse', () => {
|
||||
const original = window.matchMedia;
|
||||
|
||||
afterEach(() => {
|
||||
window.matchMedia = original;
|
||||
});
|
||||
|
||||
test('animates the disclosure by default', () => {
|
||||
stubReducedMotion(false);
|
||||
|
||||
const { result } = renderHook(() => useExpandCollapse(true));
|
||||
|
||||
expect(result.current.style.transition).toBe(EXPAND_TRANSITION);
|
||||
expect(result.current.style.gridTemplateRows).toBe('1fr');
|
||||
expect(result.current.style.opacity).toBe(1);
|
||||
});
|
||||
|
||||
/** The transition is an inline style, so it cannot carry a media query.
|
||||
* Without this the panel still folds over 300ms for a reader who asked
|
||||
* the platform for no motion. */
|
||||
test('drops the transition under prefers-reduced-motion', () => {
|
||||
stubReducedMotion(true);
|
||||
|
||||
const { result } = renderHook(() => useExpandCollapse(true));
|
||||
|
||||
expect(result.current.style.transition).toBe('none');
|
||||
});
|
||||
|
||||
test('still collapses to a zero row under reduced motion', () => {
|
||||
stubReducedMotion(true);
|
||||
|
||||
const { result } = renderHook(() => useExpandCollapse(false));
|
||||
|
||||
expect(result.current.style.transition).toBe('none');
|
||||
expect(result.current.style.gridTemplateRows).toBe('0fr');
|
||||
expect(result.current.style.opacity).toBe(0);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,14 +1,22 @@
|
|||
import { useRef, useLayoutEffect, useMemo } from 'react';
|
||||
import { useMediaQuery } from '@librechat/client';
|
||||
import type { CSSProperties } from 'react';
|
||||
|
||||
export const EXPAND_TRANSITION =
|
||||
'grid-template-rows 0.3s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.3s cubic-bezier(0.16, 1, 0.3, 1)';
|
||||
|
||||
/**
|
||||
* The disclosure motion is an inline style, so it cannot carry a
|
||||
* `prefers-reduced-motion` media query the way a utility class can. Resolve
|
||||
* the preference here instead and drop the transition outright — every
|
||||
* expanding panel in the message content shares this hook.
|
||||
*/
|
||||
export default function useExpandCollapse(isExpanded: boolean): {
|
||||
style: CSSProperties;
|
||||
ref: React.RefObject<HTMLDivElement>;
|
||||
} {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const reducedMotion = useMediaQuery('(prefers-reduced-motion: reduce)');
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const el = ref.current;
|
||||
|
|
@ -27,10 +35,10 @@ export default function useExpandCollapse(isExpanded: boolean): {
|
|||
() => ({
|
||||
display: 'grid',
|
||||
gridTemplateRows: isExpanded ? '1fr' : '0fr',
|
||||
transition: EXPAND_TRANSITION,
|
||||
transition: reducedMotion ? 'none' : EXPAND_TRANSITION,
|
||||
opacity: isExpanded ? 1 : 0,
|
||||
}),
|
||||
[isExpanded],
|
||||
[isExpanded, reducedMotion],
|
||||
);
|
||||
|
||||
return { style, ref };
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue