mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-16 16:21:27 -07:00
refactor(cli): better react patterns for BaseSettingsDialog (#21206)
This commit is contained in:
124
packages/cli/src/ui/hooks/useSettingsNavigation.ts
Normal file
124
packages/cli/src/ui/hooks/useSettingsNavigation.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { useMemo, useReducer, useCallback } from 'react';
|
||||
|
||||
export interface UseSettingsNavigationProps {
|
||||
items: Array<{ key: string }>;
|
||||
maxItemsToShow: number;
|
||||
}
|
||||
|
||||
type NavState = {
|
||||
activeItemKey: string | null;
|
||||
windowStart: number;
|
||||
};
|
||||
|
||||
type NavAction = { type: 'MOVE_UP' } | { type: 'MOVE_DOWN' };
|
||||
|
||||
function calculateSlidingWindow(
|
||||
start: number,
|
||||
activeIndex: number,
|
||||
itemCount: number,
|
||||
windowSize: number,
|
||||
): number {
|
||||
// User moves up above the window start
|
||||
if (activeIndex < start) {
|
||||
start = activeIndex;
|
||||
// User moves down below the window end
|
||||
} else if (activeIndex >= start + windowSize) {
|
||||
start = activeIndex - windowSize + 1;
|
||||
}
|
||||
// User is inside the window but performed search or terminal resized
|
||||
const maxScroll = Math.max(0, itemCount - windowSize);
|
||||
const bounded = Math.min(start, maxScroll);
|
||||
return Math.max(0, bounded);
|
||||
}
|
||||
|
||||
function createNavReducer(
|
||||
items: Array<{ key: string }>,
|
||||
maxItemsToShow: number,
|
||||
) {
|
||||
return function navReducer(state: NavState, action: NavAction): NavState {
|
||||
if (items.length === 0) return state;
|
||||
|
||||
const currentIndex = items.findIndex((i) => i.key === state.activeItemKey);
|
||||
const activeIndex = currentIndex !== -1 ? currentIndex : 0;
|
||||
|
||||
switch (action.type) {
|
||||
case 'MOVE_UP': {
|
||||
const newIndex = activeIndex > 0 ? activeIndex - 1 : items.length - 1;
|
||||
return {
|
||||
activeItemKey: items[newIndex].key,
|
||||
windowStart: calculateSlidingWindow(
|
||||
state.windowStart,
|
||||
newIndex,
|
||||
items.length,
|
||||
maxItemsToShow,
|
||||
),
|
||||
};
|
||||
}
|
||||
case 'MOVE_DOWN': {
|
||||
const newIndex = activeIndex < items.length - 1 ? activeIndex + 1 : 0;
|
||||
return {
|
||||
activeItemKey: items[newIndex].key,
|
||||
windowStart: calculateSlidingWindow(
|
||||
state.windowStart,
|
||||
newIndex,
|
||||
items.length,
|
||||
maxItemsToShow,
|
||||
),
|
||||
};
|
||||
}
|
||||
default: {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function useSettingsNavigation({
|
||||
items,
|
||||
maxItemsToShow,
|
||||
}: UseSettingsNavigationProps) {
|
||||
const reducer = useMemo(
|
||||
() => createNavReducer(items, maxItemsToShow),
|
||||
[items, maxItemsToShow],
|
||||
);
|
||||
|
||||
const [state, dispatch] = useReducer(reducer, {
|
||||
activeItemKey: items[0]?.key ?? null,
|
||||
windowStart: 0,
|
||||
});
|
||||
|
||||
// Retain the proper highlighting when items change (e.g. search)
|
||||
const activeIndex = useMemo(() => {
|
||||
if (items.length === 0) return 0;
|
||||
const idx = items.findIndex((i) => i.key === state.activeItemKey);
|
||||
return idx !== -1 ? idx : 0;
|
||||
}, [items, state.activeItemKey]);
|
||||
|
||||
const windowStart = useMemo(
|
||||
() =>
|
||||
calculateSlidingWindow(
|
||||
state.windowStart,
|
||||
activeIndex,
|
||||
items.length,
|
||||
maxItemsToShow,
|
||||
),
|
||||
[state.windowStart, activeIndex, items.length, maxItemsToShow],
|
||||
);
|
||||
|
||||
const moveUp = useCallback(() => dispatch({ type: 'MOVE_UP' }), []);
|
||||
const moveDown = useCallback(() => dispatch({ type: 'MOVE_DOWN' }), []);
|
||||
|
||||
return {
|
||||
activeItemKey: state.activeItemKey,
|
||||
activeIndex,
|
||||
windowStart,
|
||||
moveUp,
|
||||
moveDown,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user