Jacob314/add radio button keys (#10083)

This commit is contained in:
Jacob Richman
2025-09-28 14:50:47 -07:00
committed by GitHub
parent 1bd75f060d
commit 62ba330612
26 changed files with 263 additions and 112 deletions

View File

@@ -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(() =>