Fix(accessibility): add screen reader support to RewindViewer (#20750)

This commit is contained in:
Horizon_Architect_07
2026-03-06 21:18:36 +05:30
committed by GitHub
parent 0452f787b2
commit d97eaf3420
3 changed files with 149 additions and 4 deletions

View File

@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { Box, Text } from 'ink';
import { Box, Text, useIsScreenReaderEnabled } from 'ink';
import type React from 'react';
import { useMemo } from 'react';
import { theme } from '../semantic-colors.js';
@@ -58,6 +58,7 @@ export const RewindConfirmation: React.FC<RewindConfirmationProps> = ({
terminalWidth,
timestamp,
}) => {
const isScreenReaderEnabled = useIsScreenReaderEnabled();
useKeypress(
(key) => {
if (keyMatchers[Command.ESCAPE](key)) {
@@ -83,6 +84,53 @@ export const RewindConfirmation: React.FC<RewindConfirmationProps> = ({
option.value !== RewindOutcome.RevertOnly,
);
}, [stats]);
if (isScreenReaderEnabled) {
return (
<Box flexDirection="column" width={terminalWidth}>
<Text bold>Confirm Rewind</Text>
{stats && (
<Box flexDirection="column">
<Text>
{stats.fileCount === 1
? `File: ${stats.details?.at(0)?.fileName}`
: `${stats.fileCount} files affected`}
</Text>
<Text>Lines added: {stats.addedLines}</Text>
<Text>Lines removed: {stats.removedLines}</Text>
{timestamp && <Text>({formatTimeAgo(timestamp)})</Text>}
<Text>
Note: Rewinding does not affect files edited manually or by the
shell tool.
</Text>
</Box>
)}
{!stats && (
<Box>
<Text color={theme.text.secondary}>No code changes to revert.</Text>
{timestamp && (
<Text color={theme.text.secondary}>
{' '}
({formatTimeAgo(timestamp)})
</Text>
)}
</Box>
)}
<Text>Select an action:</Text>
<Text color={theme.text.secondary}>
Use arrow keys to navigate, Enter to confirm, Esc to cancel.
</Text>
<RadioButtonSelect
items={options}
onSelect={handleSelect}
isFocused={true}
/>
</Box>
);
}
return (
<Box

View File

@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, afterEach } from 'vitest';
import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest';
import { act } from 'react';
import { renderWithProviders } from '../../test-utils/render.js';
import { RewindViewer } from './RewindViewer.js';
@@ -14,6 +14,11 @@ import type {
MessageRecord,
} from '@google/gemini-cli-core';
vi.mock('ink', async () => {
const actual = await vi.importActual<typeof import('ink')>('ink');
return { ...actual, useIsScreenReaderEnabled: vi.fn(() => false) };
});
vi.mock('./CliSpinner.js', () => ({
CliSpinner: () => 'MockSpinner',
}));
@@ -71,6 +76,35 @@ describe('RewindViewer', () => {
vi.restoreAllMocks();
});
describe('Screen Reader Accessibility', () => {
beforeEach(async () => {
const { useIsScreenReaderEnabled } = await import('ink');
vi.mocked(useIsScreenReaderEnabled).mockReturnValue(true);
});
afterEach(async () => {
const { useIsScreenReaderEnabled } = await import('ink');
vi.mocked(useIsScreenReaderEnabled).mockReturnValue(false);
});
it('renders the rewind viewer with conversation items', async () => {
const conversation = createConversation([
{ type: 'user', content: 'Hello', id: '1', timestamp: '1' },
]);
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<RewindViewer
conversation={conversation}
onExit={vi.fn()}
onRewind={vi.fn()}
/>,
);
await waitUntilReady();
expect(lastFrame()).toContain('Rewind');
expect(lastFrame()).toContain('Hello');
unmount();
});
});
describe('Rendering', () => {
it.each([
{ name: 'nothing interesting for empty conversation', messages: [] },
@@ -400,3 +434,31 @@ describe('RewindViewer', () => {
unmount2();
});
});
it('renders accessible screen reader view when screen reader is enabled', async () => {
const { useIsScreenReaderEnabled } = await import('ink');
vi.mocked(useIsScreenReaderEnabled).mockReturnValue(true);
const messages: MessageRecord[] = [
{ type: 'user', content: 'Hello world', id: '1', timestamp: '1' },
{ type: 'user', content: 'Second message', id: '2', timestamp: '2' },
];
const conversation = createConversation(messages);
const onExit = vi.fn();
const onRewind = vi.fn();
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<RewindViewer
conversation={conversation}
onExit={onExit}
onRewind={onRewind}
/>,
);
await waitUntilReady();
const frame = lastFrame();
expect(frame).toContain('Rewind - Select a conversation point:');
expect(frame).toContain('Stay at current position');
vi.mocked(useIsScreenReaderEnabled).mockReturnValue(false);
unmount();
});

View File

@@ -6,7 +6,7 @@
import type React from 'react';
import { useMemo, useState } from 'react';
import { Box, Text } from 'ink';
import { Box, Text, useIsScreenReaderEnabled } from 'ink';
import { useUIState } from '../contexts/UIStateContext.js';
import {
type ConversationRecord,
@@ -50,6 +50,7 @@ export const RewindViewer: React.FC<RewindViewerProps> = ({
}) => {
const [isRewinding, setIsRewinding] = useState(false);
const { terminalWidth, terminalHeight } = useUIState();
const isScreenReaderEnabled = useIsScreenReaderEnabled();
const {
selectedMessageId,
getStats,
@@ -128,7 +129,6 @@ export const RewindViewer: React.FC<RewindViewerProps> = ({
5,
terminalHeight - DIALOG_PADDING - HEADER_HEIGHT - CONTROLS_HEIGHT - 2,
);
const maxItemsToShow = Math.max(1, Math.floor(listHeight / 4));
if (selectedMessageId) {
@@ -182,6 +182,41 @@ export const RewindViewer: React.FC<RewindViewerProps> = ({
);
}
if (isScreenReaderEnabled) {
return (
<Box flexDirection="column" width={terminalWidth}>
<Text bold>Rewind - Select a conversation point:</Text>
<BaseSelectionList
items={items}
initialIndex={items.length - 1}
isFocused={true}
showNumbers={true}
wrapAround={false}
onSelect={(item: MessageRecord) => {
if (item?.id) {
if (item.id === 'current-position') {
onExit();
} else {
selectMessage(item.id);
}
}
}}
renderItem={(itemWrapper) => {
const item = itemWrapper.value;
const text =
item.id === 'current-position'
? 'Stay at current position'
: getCleanedRewindText(item);
return <Text>{text}</Text>;
}}
/>
<Text color={theme.text.secondary}>
Press Esc to exit, Enter to select, arrow keys to navigate.
</Text>
</Box>
);
}
return (
<Box
borderStyle="round"