/ src / components / CustomSelect / select.tsx
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  }