feat(ui): refine focus highlight for selection lists and settings items (fixes #21493)

This commit is contained in:
Mark McLaughlin
2026-03-06 15:57:27 -08:00
parent 6c3a90645a
commit 213cdccb6c
3 changed files with 102 additions and 7 deletions

View File

@@ -12,10 +12,22 @@ import {
type RenderItemContext,
} from './BaseSelectionList.js';
import { useSelectionList } from '../../hooks/useSelectionList.js';
import { Text } from 'ink';
import { Text, getBoundingBox } from 'ink';
import type { theme } from '../../semantic-colors.js';
import { useTerminalSize } from '../../hooks/useTerminalSize.js';
import { useUIState, type UIState } from '../../contexts/UIStateContext.js';
vi.mock('../../hooks/useSelectionList.js');
vi.mock('../../hooks/useTerminalSize.js');
vi.mock('../../contexts/UIStateContext.js');
vi.mock('ink', async (importOriginal) => {
const actual = await importOriginal<typeof import('ink')>();
return {
...actual,
getBoundingBox: vi.fn(),
};
});
const mockTheme = {
text: { primary: 'COLOR_PRIMARY', secondary: 'COLOR_SECONDARY' },
@@ -57,6 +69,18 @@ describe('BaseSelectionList', () => {
setActiveIndex: vi.fn(),
});
vi.mocked(useTerminalSize).mockReturnValue({
columns: 100,
rows: 40,
});
vi.mocked(getBoundingBox).mockReturnValue({
width: 100,
height: 10,
x: 0,
y: 0,
});
mockRenderItem.mockImplementation(
(
item: { value: string; label: string; disabled?: boolean; key: string },
@@ -82,6 +106,9 @@ describe('BaseSelectionList', () => {
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(useUIState).mockReturnValue({
mainAreaWidth: 100,
} as unknown as UIState);
});
describe('Rendering and Structure', () => {
@@ -92,7 +119,8 @@ describe('BaseSelectionList', () => {
expect(lastFrame()).toContain('Item B');
expect(lastFrame()).toContain('Item C');
expect(mockRenderItem).toHaveBeenCalledTimes(3);
// 3 items. Render count might be higher due to useLayoutEffect for offset.
expect(mockRenderItem).toHaveBeenCalledTimes(items.length);
expect(mockRenderItem).toHaveBeenCalledWith(items[0], expect.any(Object));
unmount();
});
@@ -496,6 +524,37 @@ describe('BaseSelectionList', () => {
unmount();
});
it('should apply full-width highlight and offset when horizontally shifted', async () => {
const horizontalOffset = 10;
const terminalWidth = 100;
vi.mocked(getBoundingBox).mockReturnValue({
width: 80,
height: 10,
x: horizontalOffset,
y: 0,
});
vi.mocked(useTerminalSize).mockReturnValue({
columns: terminalWidth,
rows: 40,
});
const { lastFrame, unmount, waitUntilReady } = await renderComponent(
{},
0,
); // Item A selected
await waitUntilReady();
const output = lastFrame();
expect(output).toContain('Item A');
// Since we can't easily inspect Box props from lastFrame(),
// this test confirms it doesn't crash and renders correctly.
// In a real scenario, we'd use a custom renderer or inspect the tree if possible.
unmount();
});
it('should show arrows and correct items when scrolled to the middle', async () => {
const { lastFrame, unmount } = await renderComponent(
{ items: longList, maxItemsToShow: MAX_ITEMS, showScrollArrows: true },