select.tsx
1 import { Box, Text } from 'ink' 2 import React, { type ReactNode } from 'react' 3 import { SelectOption } from './select-option.js' 4 import { type Theme } from './theme.js' 5 import { useSelectState } from './use-select-state.js' 6 import { useSelect } from './use-select.js' 7 import { Option, useComponentTheme } from '@inkjs/ui' 8 9 export type OptionSubtree = { 10 /** 11 * Header to show above sub-options. 12 */ 13 readonly header?: string 14 15 /** 16 * Options. 17 */ 18 readonly options: (Option | OptionSubtree)[] 19 } 20 21 export type OptionHeader = { 22 readonly header: string 23 24 readonly optionValues: string[] 25 } 26 27 export const optionHeaderKey = (optionHeader: OptionHeader): string => 28 `HEADER-${optionHeader.optionValues.join(',')}` 29 30 export type SelectProps = { 31 /** 32 * When disabled, user input is ignored. 33 * 34 * @default false 35 */ 36 readonly isDisabled?: boolean 37 38 /** 39 * Number of visible options. 40 * 41 * @default 5 42 */ 43 readonly visibleOptionCount?: number 44 45 /** 46 * Highlight text in option labels. 47 */ 48 readonly highlightText?: string 49 50 /** 51 * Options. 52 */ 53 readonly options: (Option | OptionSubtree)[] 54 55 /** 56 * Default value. 57 */ 58 readonly defaultValue?: string 59 60 /** 61 * Callback when selected option changes. 62 */ 63 readonly onChange?: (value: string) => void 64 65 /** 66 * Callback when focused option changes. 67 */ 68 readonly onFocus?: (value: string) => void 69 70 /** 71 * Value to focus 72 */ 73 readonly focusValue?: string 74 } 75 76 export function Select({ 77 isDisabled = false, 78 visibleOptionCount = 5, 79 highlightText, 80 options, 81 defaultValue, 82 onChange, 83 onFocus, 84 focusValue, 85 }: SelectProps) { 86 const state = useSelectState({ 87 visibleOptionCount, 88 options, 89 defaultValue, 90 onChange, 91 onFocus, 92 focusValue, 93 }) 94 95 useSelect({ isDisabled, state }) 96 97 const { styles } = useComponentTheme<Theme>('Select') 98 99 return ( 100 <Box {...styles.container()}> 101 {state.visibleOptions.map(option => { 102 const key = 'value' in option ? option.value : optionHeaderKey(option) 103 const isFocused = 104 !isDisabled && 105 state.focusedValue !== undefined && 106 ('value' in option 107 ? state.focusedValue === option.value 108 : option.optionValues.includes(state.focusedValue)) 109 const isSelected = 110 !!state.value && 111 ('value' in option 112 ? state.value === option.value 113 : option.optionValues.includes(state.value)) 114 const smallPointer = 'header' in option 115 const labelText = 'label' in option ? option.label : option.header 116 let label: ReactNode = labelText 117 118 if (highlightText && labelText.includes(highlightText)) { 119 const index = labelText.indexOf(highlightText) 120 121 label = ( 122 <> 123 {labelText.slice(0, index)} 124 <Text {...styles.highlightedText()}>{highlightText}</Text> 125 {labelText.slice(index + highlightText.length)} 126 </> 127 ) 128 } 129 130 return ( 131 <SelectOption 132 key={key} 133 isFocused={isFocused} 134 isSelected={isSelected} 135 smallPointer={smallPointer} 136 > 137 {label} 138 </SelectOption> 139 ) 140 })} 141 </Box> 142 ) 143 }