diff --git a/packages/expand/src/index.mts b/packages/expand/src/index.mts index cd07aff18..8a2633f6d 100644 --- a/packages/expand/src/index.mts +++ b/packages/expand/src/index.mts @@ -11,31 +11,37 @@ import { import type { PartialDeep } from '@inquirer/type'; import colors from 'yoctocolors-cjs'; -type Choice = - | { key: string; name: string } - | { key: string; value: string } - | { key: string; name: string; value: string }; +type Choice = + | { key: string; value: Value } + | { key: string; name: string; value: Value }; -type NormalizedChoice = { - value: string; +type NormalizedChoice = { + value: Value; name: string; key: string; }; -type ExpandConfig = { +type ExpandConfig< + Value, + ChoicesObject = readonly { key: string; name: string }[] | readonly Choice[], +> = { message: string; - choices: ReadonlyArray; + choices: ChoicesObject extends readonly { key: string; name: string }[] + ? ChoicesObject + : readonly Choice[]; default?: string; expanded?: boolean; theme?: PartialDeep; }; -function normalizeChoices(choices: readonly Choice[]): NormalizedChoice[] { +function normalizeChoices( + choices: readonly { key: string; name: string }[] | readonly Choice[], +): NormalizedChoice[] { return choices.map((choice) => { const name: string = 'name' in choice ? choice.name : String(choice.value); const value = 'value' in choice ? choice.value : name; return { - value, + value: value as Value, name, key: choice.key.toLowerCase(), }; @@ -48,91 +54,95 @@ const helpChoice = { value: undefined, }; -export default createPrompt((config, done) => { - const { default: defaultKey = 'h' } = config; - const choices = useMemo(() => normalizeChoices(config.choices), [config.choices]); - const [status, setStatus] = useState('pending'); - const [value, setValue] = useState(''); - const [expanded, setExpanded] = useState(config.expanded ?? false); - const [errorMsg, setError] = useState(); - const theme = makeTheme(config.theme); - const prefix = usePrefix({ theme }); - - useKeypress((event, rl) => { - if (isEnterKey(event)) { - const answer = (value || defaultKey).toLowerCase(); - if (answer === 'h' && !expanded) { - setExpanded(true); - } else { - const selectedChoice = choices.find(({ key }) => key === answer); - if (selectedChoice) { - setStatus('done'); - // Set the value as we might've selected the default one. - setValue(answer); - done(selectedChoice.value); - } else if (value === '') { - setError('Please input a value'); +export default createPrompt( + (config: ExpandConfig, done: (value: Value) => void) => { + const { default: defaultKey = 'h' } = config; + const choices = useMemo(() => normalizeChoices(config.choices), [config.choices]); + const [status, setStatus] = useState('pending'); + const [value, setValue] = useState(''); + const [expanded, setExpanded] = useState(config.expanded ?? false); + const [errorMsg, setError] = useState(); + const theme = makeTheme(config.theme); + const prefix = usePrefix({ theme }); + + useKeypress((event, rl) => { + if (isEnterKey(event)) { + const answer = (value || defaultKey).toLowerCase(); + if (answer === 'h' && !expanded) { + setExpanded(true); } else { - setError(`"${colors.red(value)}" isn't an available option`); + const selectedChoice = choices.find(({ key }) => key === answer); + if (selectedChoice) { + setStatus('done'); + // Set the value as we might've selected the default one. + setValue(answer); + done(selectedChoice.value); + } else if (value === '') { + setError('Please input a value'); + } else { + setError(`"${colors.red(value)}" isn't an available option`); + } } + } else { + setValue(rl.line); + setError(undefined); } - } else { - setValue(rl.line); - setError(undefined); - } - }); + }); - const message = theme.style.message(config.message); + const message = theme.style.message(config.message); - if (status === 'done') { - // If the prompt is done, it's safe to assume there is a selected value. - const selectedChoice = choices.find(({ key }) => key === value) as NormalizedChoice; - return `${prefix} ${message} ${theme.style.answer(selectedChoice.name)}`; - } - - const allChoices = expanded ? choices : [...choices, helpChoice]; - - // Collapsed display style - let longChoices = ''; - let shortChoices = allChoices - .map((choice) => { - if (choice.key === defaultKey) { - return choice.key.toUpperCase(); - } + if (status === 'done') { + // If the prompt is done, it's safe to assume there is a selected value. + const selectedChoice = choices.find( + ({ key }) => key === value, + ) as NormalizedChoice; + return `${prefix} ${message} ${theme.style.answer(selectedChoice.name)}`; + } - return choice.key; - }) - .join(''); - shortChoices = ` ${theme.style.defaultAnswer(shortChoices)}`; + const allChoices = expanded ? choices : [...choices, helpChoice]; - // Expanded display style - if (expanded) { - shortChoices = ''; - longChoices = allChoices + // Collapsed display style + let longChoices = ''; + let shortChoices = allChoices .map((choice) => { - const line = ` ${choice.key}) ${choice.name}`; - if (choice.key === value.toLowerCase()) { - return theme.style.highlight(line); + if (choice.key === defaultKey) { + return choice.key.toUpperCase(); } - return line; + return choice.key; }) - .join('\n'); - } - - let helpTip = ''; - const currentOption = allChoices.find(({ key }) => key === value.toLowerCase()); - if (currentOption) { - helpTip = `${colors.cyan('>>')} ${currentOption.name}`; - } - - let error = ''; - if (errorMsg) { - error = theme.style.error(errorMsg); - } - - return [ - `${prefix} ${message}${shortChoices} ${value}`, - [longChoices, helpTip, error].filter(Boolean).join('\n'), - ]; -}); + .join(''); + shortChoices = ` ${theme.style.defaultAnswer(shortChoices)}`; + + // Expanded display style + if (expanded) { + shortChoices = ''; + longChoices = allChoices + .map((choice) => { + const line = ` ${choice.key}) ${choice.name}`; + if (choice.key === value.toLowerCase()) { + return theme.style.highlight(line); + } + + return line; + }) + .join('\n'); + } + + let helpTip = ''; + const currentOption = allChoices.find(({ key }) => key === value.toLowerCase()); + if (currentOption) { + helpTip = `${colors.cyan('>>')} ${currentOption.name}`; + } + + let error = ''; + if (errorMsg) { + error = theme.style.error(errorMsg); + } + + return [ + `${prefix} ${message}${shortChoices} ${value}`, + [longChoices, helpTip, error].filter(Boolean).join('\n'), + ]; + }, +);