mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-15 15:50:35 -07:00
Jacob314/add radio button keys (#10083)
This commit is contained in:
@@ -25,10 +25,10 @@ describe('useSelectionList', () => {
|
||||
const mockOnHighlight = vi.fn();
|
||||
|
||||
const items: Array<SelectionListItem<string>> = [
|
||||
{ value: 'A' },
|
||||
{ value: 'B', disabled: true },
|
||||
{ value: 'C' },
|
||||
{ value: 'D' },
|
||||
{ value: 'A', key: 'A' },
|
||||
{ value: 'B', disabled: true, key: 'B' },
|
||||
{ value: 'C', key: 'C' },
|
||||
{ value: 'D', key: 'D' },
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -105,9 +105,9 @@ describe('useSelectionList', () => {
|
||||
|
||||
it('should wrap around to find the next enabled item if initialIndex is disabled', () => {
|
||||
const wrappingItems = [
|
||||
{ value: 'A' },
|
||||
{ value: 'B', disabled: true },
|
||||
{ value: 'C', disabled: true },
|
||||
{ value: 'A', key: 'A' },
|
||||
{ value: 'B', disabled: true, key: 'B' },
|
||||
{ value: 'C', disabled: true, key: 'C' },
|
||||
];
|
||||
const { result } = renderHook(() =>
|
||||
useSelectionList({
|
||||
@@ -141,8 +141,8 @@ describe('useSelectionList', () => {
|
||||
|
||||
it('should stick to the initial index if all items are disabled', () => {
|
||||
const allDisabled = [
|
||||
{ value: 'A', disabled: true },
|
||||
{ value: 'B', disabled: true },
|
||||
{ value: 'A', disabled: true, key: 'A' },
|
||||
{ value: 'B', disabled: true, key: 'B' },
|
||||
];
|
||||
const { result } = renderHook(() =>
|
||||
useSelectionList({
|
||||
@@ -208,7 +208,7 @@ describe('useSelectionList', () => {
|
||||
});
|
||||
|
||||
it('should not move or call onHighlight if navigation results in the same index (e.g., single item)', () => {
|
||||
const singleItem = [{ value: 'A' }];
|
||||
const singleItem = [{ value: 'A', key: 'A' }];
|
||||
const { result } = renderHook(() =>
|
||||
useSelectionList({
|
||||
items: singleItem,
|
||||
@@ -223,8 +223,8 @@ describe('useSelectionList', () => {
|
||||
|
||||
it('should not move or call onHighlight if all items are disabled', () => {
|
||||
const allDisabled = [
|
||||
{ value: 'A', disabled: true },
|
||||
{ value: 'B', disabled: true },
|
||||
{ value: 'A', disabled: true, key: 'A' },
|
||||
{ value: 'B', disabled: true, key: 'B' },
|
||||
];
|
||||
const { result } = renderHook(() =>
|
||||
useSelectionList({
|
||||
@@ -423,7 +423,7 @@ describe('useSelectionList', () => {
|
||||
const shortList = items;
|
||||
const longList: Array<SelectionListItem<string>> = Array.from(
|
||||
{ length: 15 },
|
||||
(_, i) => ({ value: `Item ${i + 1}` }),
|
||||
(_, i) => ({ value: `Item ${i + 1}`, key: `Item ${i + 1}` }),
|
||||
);
|
||||
|
||||
const pressNumber = (num: string) => pressKey(num, num);
|
||||
@@ -585,7 +585,7 @@ describe('useSelectionList', () => {
|
||||
it('should highlight but not select a disabled item (timeout case)', () => {
|
||||
// Create a list where the ambiguous prefix points to a disabled item
|
||||
const disabledAmbiguousList = [
|
||||
{ value: 'Item 1 Disabled', disabled: true },
|
||||
{ value: 'Item 1 Disabled', disabled: true, key: 'Item 1 Disabled' },
|
||||
...longList.slice(1),
|
||||
];
|
||||
|
||||
@@ -670,6 +670,28 @@ describe('useSelectionList', () => {
|
||||
expect(result.current.activeIndex).toBe(2);
|
||||
});
|
||||
|
||||
it('should respect a new initialIndex even after user interaction', () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ initialIndex }: { initialIndex: number }) =>
|
||||
useSelectionList({
|
||||
items,
|
||||
onSelect: mockOnSelect,
|
||||
initialIndex,
|
||||
}),
|
||||
{ initialProps: { initialIndex: 0 } },
|
||||
);
|
||||
|
||||
// User navigates, changing the active index
|
||||
pressKey('down');
|
||||
expect(result.current.activeIndex).toBe(2);
|
||||
|
||||
// The component re-renders with a new initial index
|
||||
rerender({ initialIndex: 3 });
|
||||
|
||||
// The hook should now respect the new initial index
|
||||
expect(result.current.activeIndex).toBe(3);
|
||||
});
|
||||
|
||||
it('should validate index when initialIndex prop changes to a disabled item', () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ initialIndex }: { initialIndex: number }) =>
|
||||
@@ -699,7 +721,10 @@ describe('useSelectionList', () => {
|
||||
|
||||
expect(result.current.activeIndex).toBe(3);
|
||||
|
||||
const shorterItems = [{ value: 'X' }, { value: 'Y' }];
|
||||
const shorterItems = [
|
||||
{ value: 'X', key: 'X' },
|
||||
{ value: 'Y', key: 'Y' },
|
||||
];
|
||||
rerender({ items: shorterItems }); // Length 2
|
||||
|
||||
// The useEffect syncs based on the initialIndex (3) which is now out of bounds. It defaults to 0.
|
||||
@@ -707,7 +732,11 @@ describe('useSelectionList', () => {
|
||||
});
|
||||
|
||||
it('should adjust activeIndex if items change and the initialIndex becomes disabled', () => {
|
||||
const initialItems = [{ value: 'A' }, { value: 'B' }, { value: 'C' }];
|
||||
const initialItems = [
|
||||
{ value: 'A', key: 'A' },
|
||||
{ value: 'B', key: 'B' },
|
||||
{ value: 'C', key: 'C' },
|
||||
];
|
||||
const { result, rerender } = renderHook(
|
||||
({ items: testItems }: { items: Array<SelectionListItem<string>> }) =>
|
||||
useSelectionList({
|
||||
@@ -721,9 +750,9 @@ describe('useSelectionList', () => {
|
||||
expect(result.current.activeIndex).toBe(1);
|
||||
|
||||
const newItems = [
|
||||
{ value: 'A' },
|
||||
{ value: 'B', disabled: true },
|
||||
{ value: 'C' },
|
||||
{ value: 'A', key: 'A' },
|
||||
{ value: 'B', disabled: true, key: 'B' },
|
||||
{ value: 'C', key: 'C' },
|
||||
];
|
||||
rerender({ items: newItems });
|
||||
|
||||
@@ -747,10 +776,10 @@ describe('useSelectionList', () => {
|
||||
|
||||
it('should not reset activeIndex when items are deeply equal', () => {
|
||||
const initialItems = [
|
||||
{ value: 'A' },
|
||||
{ value: 'B', disabled: true },
|
||||
{ value: 'C' },
|
||||
{ value: 'D' },
|
||||
{ value: 'A', key: 'A' },
|
||||
{ value: 'B', disabled: true, key: 'B' },
|
||||
{ value: 'C', key: 'C' },
|
||||
{ value: 'D', key: 'D' },
|
||||
];
|
||||
|
||||
const { result, rerender } = renderHook(
|
||||
@@ -775,10 +804,10 @@ describe('useSelectionList', () => {
|
||||
|
||||
// Create new array with same content (deeply equal but not identical)
|
||||
const newItems = [
|
||||
{ value: 'A' },
|
||||
{ value: 'B', disabled: true },
|
||||
{ value: 'C' },
|
||||
{ value: 'D' },
|
||||
{ value: 'A', key: 'A' },
|
||||
{ value: 'B', disabled: true, key: 'B' },
|
||||
{ value: 'C', key: 'C' },
|
||||
{ value: 'D', key: 'D' },
|
||||
];
|
||||
|
||||
rerender({ items: newItems });
|
||||
@@ -791,10 +820,10 @@ describe('useSelectionList', () => {
|
||||
|
||||
it('should update activeIndex when items change structurally', () => {
|
||||
const initialItems = [
|
||||
{ value: 'A' },
|
||||
{ value: 'B', disabled: true },
|
||||
{ value: 'C' },
|
||||
{ value: 'D' },
|
||||
{ value: 'A', key: 'A' },
|
||||
{ value: 'B', disabled: true, key: 'B' },
|
||||
{ value: 'C', key: 'C' },
|
||||
{ value: 'D', key: 'D' },
|
||||
];
|
||||
|
||||
const { result, rerender } = renderHook(
|
||||
@@ -812,7 +841,11 @@ describe('useSelectionList', () => {
|
||||
mockOnHighlight.mockClear();
|
||||
|
||||
// Change item values (not deeply equal)
|
||||
const newItems = [{ value: 'X' }, { value: 'Y' }, { value: 'Z' }];
|
||||
const newItems = [
|
||||
{ value: 'X', key: 'X' },
|
||||
{ value: 'Y', key: 'Y' },
|
||||
{ value: 'Z', key: 'Z' },
|
||||
];
|
||||
|
||||
rerender({ items: newItems });
|
||||
|
||||
@@ -821,7 +854,11 @@ describe('useSelectionList', () => {
|
||||
});
|
||||
|
||||
it('should handle partial changes in items array', () => {
|
||||
const initialItems = [{ value: 'A' }, { value: 'B' }, { value: 'C' }];
|
||||
const initialItems = [
|
||||
{ value: 'A', key: 'A' },
|
||||
{ value: 'B', key: 'B' },
|
||||
{ value: 'C', key: 'C' },
|
||||
];
|
||||
|
||||
const { result, rerender } = renderHook(
|
||||
({ items: testItems }: { items: Array<SelectionListItem<string>> }) =>
|
||||
@@ -837,9 +874,9 @@ describe('useSelectionList', () => {
|
||||
|
||||
// Change only one item's disabled status
|
||||
const newItems = [
|
||||
{ value: 'A' },
|
||||
{ value: 'B', disabled: true },
|
||||
{ value: 'C' },
|
||||
{ value: 'A', key: 'A' },
|
||||
{ value: 'B', disabled: true, key: 'B' },
|
||||
{ value: 'C', key: 'C' },
|
||||
];
|
||||
|
||||
rerender({ items: newItems });
|
||||
@@ -847,6 +884,37 @@ describe('useSelectionList', () => {
|
||||
// Should find next valid index since current became disabled
|
||||
expect(result.current.activeIndex).toBe(2);
|
||||
});
|
||||
|
||||
it('should update selection when a new item is added to the start of the list', () => {
|
||||
const initialItems = [
|
||||
{ value: 'A', key: 'A' },
|
||||
{ value: 'B', key: 'B' },
|
||||
{ value: 'C', key: 'C' },
|
||||
];
|
||||
|
||||
const { result, rerender } = renderHook(
|
||||
({ items: testItems }: { items: Array<SelectionListItem<string>> }) =>
|
||||
useSelectionList({
|
||||
onSelect: mockOnSelect,
|
||||
items: testItems,
|
||||
}),
|
||||
{ initialProps: { items: initialItems } },
|
||||
);
|
||||
|
||||
pressKey('down');
|
||||
expect(result.current.activeIndex).toBe(1);
|
||||
|
||||
const newItems = [
|
||||
{ value: 'D', key: 'D' },
|
||||
{ value: 'A', key: 'A' },
|
||||
{ value: 'B', key: 'B' },
|
||||
{ value: 'C', key: 'C' },
|
||||
];
|
||||
|
||||
rerender({ items: newItems });
|
||||
|
||||
expect(result.current.activeIndex).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Manual Control', () => {
|
||||
@@ -879,7 +947,7 @@ describe('useSelectionList', () => {
|
||||
it('should clear timeout on unmount when timer is active', () => {
|
||||
const longList: Array<SelectionListItem<string>> = Array.from(
|
||||
{ length: 15 },
|
||||
(_, i) => ({ value: `Item ${i + 1}` }),
|
||||
(_, i) => ({ value: `Item ${i + 1}`, key: `Item ${i + 1}` }),
|
||||
);
|
||||
|
||||
const { unmount } = renderHook(() =>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useReducer, useRef, useEffect } from 'react';
|
||||
import { useKeypress } from './useKeypress.js';
|
||||
|
||||
export interface SelectionListItem<T> {
|
||||
key: string;
|
||||
value: T;
|
||||
disabled?: boolean;
|
||||
}
|
||||
@@ -70,27 +71,6 @@ type SelectionListAction<T> =
|
||||
|
||||
const NUMBER_INPUT_TIMEOUT_MS = 1000;
|
||||
|
||||
/**
|
||||
* Performs an equality check on two arrays of SelectionListItem<T>.
|
||||
*
|
||||
* It compares the length of the arrays and then the 'value' and 'disabled'
|
||||
* properties of each item.
|
||||
*/
|
||||
const areItemsEqual = <T>(
|
||||
a: Array<SelectionListItem<T>>,
|
||||
b: Array<SelectionListItem<T>>,
|
||||
): boolean => {
|
||||
if (a.length !== b.length) {
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (a[i]!.value !== b[i]!.value || a[i]!.disabled !== b[i]!.disabled) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function to find the next enabled index in a given direction, supporting wrapping.
|
||||
*/
|
||||
@@ -122,11 +102,20 @@ const findNextValidIndex = <T>(
|
||||
const computeInitialIndex = <T>(
|
||||
initialIndex: number,
|
||||
items: Array<SelectionListItem<T>>,
|
||||
initialKey?: string,
|
||||
): number => {
|
||||
if (items.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (initialKey !== undefined) {
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
if (items[i]!.key === initialKey && !items[i]!.disabled) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let targetIndex = initialIndex;
|
||||
|
||||
if (targetIndex < 0 || targetIndex >= items.length) {
|
||||
@@ -184,13 +173,17 @@ function selectionListReducer<T>(
|
||||
|
||||
case 'INITIALIZE': {
|
||||
const { initialIndex, items } = action.payload;
|
||||
if (
|
||||
state.initialIndex === initialIndex &&
|
||||
areItemsEqual(state.items, items)
|
||||
) {
|
||||
const activeKey =
|
||||
initialIndex === state.initialIndex &&
|
||||
state.activeIndex !== state.initialIndex
|
||||
? state.items[state.activeIndex]?.key
|
||||
: undefined;
|
||||
|
||||
if (items === state.items && initialIndex === state.initialIndex) {
|
||||
return state;
|
||||
}
|
||||
const targetIndex = computeInitialIndex(initialIndex, items);
|
||||
|
||||
const targetIndex = computeInitialIndex(initialIndex, items, activeKey);
|
||||
|
||||
return {
|
||||
...state,
|
||||
|
||||
Reference in New Issue
Block a user