mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-18 01:51:20 -07:00
Jacob314/add radio button keys (#10083)
This commit is contained in:
@@ -35,16 +35,20 @@ describe('BaseSelectionList', () => {
|
||||
const mockOnHighlight = vi.fn();
|
||||
const mockRenderItem = vi.fn();
|
||||
|
||||
// Define standard test items
|
||||
const items = [
|
||||
{ value: 'A', label: 'Item A' },
|
||||
{ value: 'B', label: 'Item B', disabled: true },
|
||||
{ value: 'C', label: 'Item C' },
|
||||
{ value: 'A', label: 'Item A', key: 'A' },
|
||||
{ value: 'B', label: 'Item B', disabled: true, key: 'B' },
|
||||
{ value: 'C', label: 'Item C', key: 'C' },
|
||||
];
|
||||
|
||||
// Helper to render the component with default props
|
||||
const renderComponent = (
|
||||
props: Partial<BaseSelectionListProps<string, { label: string }>> = {},
|
||||
props: Partial<
|
||||
BaseSelectionListProps<
|
||||
string,
|
||||
{ value: string; label: string; disabled?: boolean; key: string }
|
||||
>
|
||||
> = {},
|
||||
activeIndex: number = 0,
|
||||
) => {
|
||||
vi.mocked(useSelectionList).mockReturnValue({
|
||||
@@ -53,12 +57,16 @@ describe('BaseSelectionList', () => {
|
||||
});
|
||||
|
||||
mockRenderItem.mockImplementation(
|
||||
(item: (typeof items)[0], context: RenderItemContext) => (
|
||||
<Text color={context.titleColor}>{item.label}</Text>
|
||||
),
|
||||
(
|
||||
item: { value: string; label: string; disabled?: boolean; key: string },
|
||||
context: RenderItemContext,
|
||||
) => <Text color={context.titleColor}>{item.label}</Text>,
|
||||
);
|
||||
|
||||
const defaultProps: BaseSelectionListProps<string, { label: string }> = {
|
||||
const defaultProps: BaseSelectionListProps<
|
||||
string,
|
||||
{ value: string; label: string; disabled?: boolean; key: string }
|
||||
> = {
|
||||
items,
|
||||
onSelect: mockOnSelect,
|
||||
onHighlight: mockOnHighlight,
|
||||
@@ -216,6 +224,7 @@ describe('BaseSelectionList', () => {
|
||||
const longList = Array.from({ length: 15 }, (_, i) => ({
|
||||
value: `Item ${i + 1}`,
|
||||
label: `Item ${i + 1}`,
|
||||
key: `Item ${i + 1}`,
|
||||
}));
|
||||
|
||||
// We must increase maxItemsToShow (default 10) to see the 10th item and beyond
|
||||
@@ -249,19 +258,22 @@ describe('BaseSelectionList', () => {
|
||||
const longList = Array.from({ length: 10 }, (_, i) => ({
|
||||
value: `Item ${i + 1}`,
|
||||
label: `Item ${i + 1}`,
|
||||
key: `Item ${i + 1}`,
|
||||
}));
|
||||
const MAX_ITEMS = 3;
|
||||
|
||||
const renderScrollableList = (initialActiveIndex: number = 0) => {
|
||||
// Define the props used for the initial render and subsequent rerenders
|
||||
const componentProps: BaseSelectionListProps<string, { label: string }> =
|
||||
{
|
||||
items: longList,
|
||||
maxItemsToShow: MAX_ITEMS,
|
||||
onSelect: mockOnSelect,
|
||||
onHighlight: mockOnHighlight,
|
||||
renderItem: mockRenderItem,
|
||||
};
|
||||
const componentProps: BaseSelectionListProps<
|
||||
string,
|
||||
{ value: string; label: string; key: string }
|
||||
> = {
|
||||
items: longList,
|
||||
maxItemsToShow: MAX_ITEMS,
|
||||
onSelect: mockOnSelect,
|
||||
onHighlight: mockOnHighlight,
|
||||
renderItem: mockRenderItem,
|
||||
};
|
||||
|
||||
vi.mocked(useSelectionList).mockReturnValue({
|
||||
activeIndex: initialActiveIndex,
|
||||
@@ -428,6 +440,7 @@ describe('BaseSelectionList', () => {
|
||||
const longList = Array.from({ length: 10 }, (_, i) => ({
|
||||
value: `Item ${i + 1}`,
|
||||
label: `Item ${i + 1}`,
|
||||
key: `Item ${i + 1}`,
|
||||
}));
|
||||
const MAX_ITEMS = 3;
|
||||
|
||||
|
||||
@@ -10,14 +10,19 @@ import { Text, Box } from 'ink';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
import { useSelectionList } from '../../hooks/useSelectionList.js';
|
||||
|
||||
import type { SelectionListItem } from '../../hooks/useSelectionList.js';
|
||||
|
||||
export interface RenderItemContext {
|
||||
isSelected: boolean;
|
||||
titleColor: string;
|
||||
numberColor: string;
|
||||
}
|
||||
|
||||
export interface BaseSelectionListProps<T, TItem = Record<string, unknown>> {
|
||||
items: Array<TItem & { value: T; disabled?: boolean }>;
|
||||
export interface BaseSelectionListProps<
|
||||
T,
|
||||
TItem extends SelectionListItem<T> = SelectionListItem<T>,
|
||||
> {
|
||||
items: TItem[];
|
||||
initialIndex?: number;
|
||||
onSelect: (value: T) => void;
|
||||
onHighlight?: (value: T) => void;
|
||||
@@ -25,10 +30,7 @@ export interface BaseSelectionListProps<T, TItem = Record<string, unknown>> {
|
||||
showNumbers?: boolean;
|
||||
showScrollArrows?: boolean;
|
||||
maxItemsToShow?: number;
|
||||
renderItem: (
|
||||
item: TItem & { value: T; disabled?: boolean },
|
||||
context: RenderItemContext,
|
||||
) => React.ReactNode;
|
||||
renderItem: (item: TItem, context: RenderItemContext) => React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -45,7 +47,10 @@ export interface BaseSelectionListProps<T, TItem = Record<string, unknown>> {
|
||||
* Specific components should use this as a base and provide
|
||||
* their own renderItem implementation for custom content.
|
||||
*/
|
||||
export function BaseSelectionList<T, TItem = Record<string, unknown>>({
|
||||
export function BaseSelectionList<
|
||||
T,
|
||||
TItem extends SelectionListItem<T> = SelectionListItem<T>,
|
||||
>({
|
||||
items,
|
||||
initialIndex = 0,
|
||||
onSelect,
|
||||
@@ -123,7 +128,7 @@ export function BaseSelectionList<T, TItem = Record<string, unknown>>({
|
||||
)}.`;
|
||||
|
||||
return (
|
||||
<Box key={itemIndex} alignItems="flex-start">
|
||||
<Box key={item.key} alignItems="flex-start">
|
||||
{/* Radio button indicator */}
|
||||
<Box minWidth={2} flexShrink={0}>
|
||||
<Text
|
||||
|
||||
@@ -40,13 +40,24 @@ describe('DescriptiveRadioButtonSelect', () => {
|
||||
const mockOnHighlight = vi.fn();
|
||||
|
||||
const ITEMS: Array<DescriptiveRadioSelectItem<string>> = [
|
||||
{ title: 'Foo Title', description: 'This is Foo.', value: 'foo' },
|
||||
{ title: 'Bar Title', description: 'This is Bar.', value: 'bar' },
|
||||
{
|
||||
title: 'Foo Title',
|
||||
description: 'This is Foo.',
|
||||
value: 'foo',
|
||||
key: 'foo',
|
||||
},
|
||||
{
|
||||
title: 'Bar Title',
|
||||
description: 'This is Bar.',
|
||||
value: 'bar',
|
||||
key: 'bar',
|
||||
},
|
||||
{
|
||||
title: 'Baz Title',
|
||||
description: 'This is Baz.',
|
||||
value: 'baz',
|
||||
disabled: true,
|
||||
key: 'baz',
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -8,12 +8,11 @@ import type React from 'react';
|
||||
import { Text, Box } from 'ink';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
import { BaseSelectionList } from './BaseSelectionList.js';
|
||||
import type { SelectionListItem } from '../../hooks/useSelectionList.js';
|
||||
|
||||
export interface DescriptiveRadioSelectItem<T> {
|
||||
value: T;
|
||||
export interface DescriptiveRadioSelectItem<T> extends SelectionListItem<T> {
|
||||
title: string;
|
||||
description: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export interface DescriptiveRadioButtonSelectProps<T> {
|
||||
@@ -61,7 +60,7 @@ export function DescriptiveRadioButtonSelect<T>({
|
||||
showScrollArrows={showScrollArrows}
|
||||
maxItemsToShow={maxItemsToShow}
|
||||
renderItem={(item, { titleColor }) => (
|
||||
<Box flexDirection="column">
|
||||
<Box flexDirection="column" key={item.key}>
|
||||
<Text color={titleColor}>{item.title}</Text>
|
||||
<Text color={theme.text.secondary}>{item.description}</Text>
|
||||
</Box>
|
||||
|
||||
@@ -62,9 +62,9 @@ describe('RadioButtonSelect', () => {
|
||||
const mockOnHighlight = vi.fn();
|
||||
|
||||
const ITEMS: Array<RadioSelectItem<string>> = [
|
||||
{ label: 'Option 1', value: 'one' },
|
||||
{ label: 'Option 2', value: 'two' },
|
||||
{ label: 'Option 3', value: 'three', disabled: true },
|
||||
{ label: 'Option 1', value: 'one', key: 'one' },
|
||||
{ label: 'Option 2', value: 'two', key: 'two' },
|
||||
{ label: 'Option 3', value: 'three', disabled: true, key: 'three' },
|
||||
];
|
||||
|
||||
const renderComponent = (
|
||||
@@ -155,6 +155,7 @@ describe('RadioButtonSelect', () => {
|
||||
value: 'a-light',
|
||||
themeNameDisplay: 'Theme A',
|
||||
themeTypeDisplay: '(Light)',
|
||||
key: 'a-light',
|
||||
};
|
||||
|
||||
const result = renderItem(themeItem, mockContext);
|
||||
@@ -186,6 +187,7 @@ describe('RadioButtonSelect', () => {
|
||||
label: 'Incomplete Theme',
|
||||
value: 'incomplete',
|
||||
themeNameDisplay: 'Only Name',
|
||||
key: 'incomplete',
|
||||
};
|
||||
|
||||
const result = renderItem(partialThemeItem, mockContext);
|
||||
|
||||
@@ -8,15 +8,14 @@ import type React from 'react';
|
||||
import { Text } from 'ink';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
import { BaseSelectionList } from './BaseSelectionList.js';
|
||||
import type { SelectionListItem } from '../../hooks/useSelectionList.js';
|
||||
|
||||
/**
|
||||
* Represents a single option for the RadioButtonSelect.
|
||||
* Requires a label for display and a value to be returned on selection.
|
||||
*/
|
||||
export interface RadioSelectItem<T> {
|
||||
export interface RadioSelectItem<T> extends SelectionListItem<T> {
|
||||
label: string;
|
||||
value: T;
|
||||
disabled?: boolean;
|
||||
themeNameDisplay?: string;
|
||||
themeTypeDisplay?: string;
|
||||
}
|
||||
@@ -74,7 +73,7 @@ export function RadioButtonSelect<T>({
|
||||
// Handle special theme display case for ThemeDialog compatibility
|
||||
if (item.themeNameDisplay && item.themeTypeDisplay) {
|
||||
return (
|
||||
<Text color={titleColor} wrap="truncate">
|
||||
<Text color={titleColor} wrap="truncate" key={item.key}>
|
||||
{item.themeNameDisplay}{' '}
|
||||
<Text color={theme.text.secondary}>{item.themeTypeDisplay}</Text>
|
||||
</Text>
|
||||
|
||||
@@ -27,7 +27,10 @@ export function ScopeSelector({
|
||||
isFocused,
|
||||
initialScope,
|
||||
}: ScopeSelectorProps): React.JSX.Element {
|
||||
const scopeItems = getScopeItems();
|
||||
const scopeItems = getScopeItems().map((item) => ({
|
||||
...item,
|
||||
key: item.value,
|
||||
}));
|
||||
|
||||
const initialIndex = scopeItems.findIndex(
|
||||
(item) => item.value === initialScope,
|
||||
|
||||
Reference in New Issue
Block a user