From e17b0c79b7e4d70ed1471eeed1433fb0837c1e94 Mon Sep 17 00:00:00 2001 From: Simon Boudrias Date: Sun, 1 Sep 2024 16:28:51 -0400 Subject: [PATCH] Feat(@inquirer/checkbox): Add support for choice description (like select and search prompts) Fix #1512 --- packages/checkbox/README.md | 3 ++ packages/checkbox/checkbox.test.mts | 44 +++++++++++++++++++++++++++++ packages/checkbox/src/index.mts | 16 ++++++++++- 3 files changed, 62 insertions(+), 1 deletion(-) diff --git a/packages/checkbox/README.md b/packages/checkbox/README.md index a349f1b6a..5bef6ba48 100644 --- a/packages/checkbox/README.md +++ b/packages/checkbox/README.md @@ -93,6 +93,7 @@ The `Choice` object is typed as type Choice = { value: Value; name?: string; + description?: string; short?: string; checked?: boolean; disabled?: boolean | string; @@ -103,6 +104,7 @@ Here's each property: - `value`: The value is what will be returned by `await checkbox()`. - `name`: This is the string displayed in the choice list. +- `description`: Option for a longer description string that'll appear under the list when the cursor highlight a given choice. - `short`: Once the prompt is done (press enter), we'll use `short` if defined to render next to the question. By default we'll use `name`. - `checked`: If `true`, the option will be checked by default. - `disabled`: Disallow the option from being selected. If `disabled` is a string, it'll be used as a help tip explaining why the choice isn't available. @@ -131,6 +133,7 @@ type Theme = { highlight: (text: string) => string; key: (text: string) => string; disabledChoice: (text: string) => string; + description: (text: string) => string; renderSelectedChoices: ( selectedChoices: ReadonlyArray>, allChoices: ReadonlyArray | Separator>, diff --git a/packages/checkbox/checkbox.test.mts b/packages/checkbox/checkbox.test.mts index 0ca1da1f2..b0a3719db 100644 --- a/packages/checkbox/checkbox.test.mts +++ b/packages/checkbox/checkbox.test.mts @@ -668,6 +668,50 @@ describe('checkbox prompt', () => { await expect(answer).resolves.toEqual([1]); }); + it('shows description of the highlighted choice', async () => { + const choices = [ + { value: 'Stark', description: 'Winter is coming' }, + { value: 'Lannister', description: 'Hear me roar' }, + { value: 'Targaryen', description: 'Fire and blood' }, + ]; + + const { answer, events, getScreen } = await render(checkbox, { + message: 'Select a family', + choices: choices, + }); + + expect(getScreen()).toMatchInlineSnapshot(` + "? Select a family (Press to select, to toggle all, to invert + selection, and to proceed) + ❯◯ Stark + ◯ Lannister + ◯ Targaryen + Winter is coming" + `); + + events.keypress('down'); + expect(getScreen()).toMatchInlineSnapshot(` + "? Select a family (Press to select, to toggle all, to invert + selection, and to proceed) + ◯ Stark + ❯◯ Lannister + ◯ Targaryen + Hear me roar" + `); + + events.keypress('space'); + expect(getScreen()).toMatchInlineSnapshot(` + "? Select a family + ◯ Stark + ❯◉ Lannister + ◯ Targaryen + Hear me roar" + `); + + events.keypress('enter'); + await expect(answer).resolves.toEqual(['Lannister']); + }); + it('uses custom validation', async () => { const { answer, events, getScreen } = await render(checkbox, { message: 'Select a number', diff --git a/packages/checkbox/src/index.mts b/packages/checkbox/src/index.mts index adc6f92f1..8ac07a5c3 100644 --- a/packages/checkbox/src/index.mts +++ b/packages/checkbox/src/index.mts @@ -33,6 +33,7 @@ type CheckboxTheme = { selectedChoices: ReadonlyArray>, allChoices: ReadonlyArray | Separator>, ) => string; + description: (text: string) => string; }; helpMode: 'always' | 'never' | 'auto'; }; @@ -47,6 +48,7 @@ const checkboxTheme: CheckboxTheme = { disabledChoice: (text: string) => colors.dim(`- ${text}`), renderSelectedChoices: (selectedChoices) => selectedChoices.map((choice) => choice.short).join(', '), + description: (text: string) => colors.cyan(text), }, helpMode: 'auto', }; @@ -54,6 +56,7 @@ const checkboxTheme: CheckboxTheme = { type Choice = { value: Value; name?: string; + description?: string; short?: string; disabled?: boolean | string; checked?: boolean; @@ -63,6 +66,7 @@ type Choice = { type NormalizedChoice = { value: Value; name: string; + description?: string; short: string; disabled: boolean | string; checked: boolean; @@ -130,6 +134,7 @@ function normalizeChoices( value: choice.value, name, short: choice.short ?? name, + description: choice.description, disabled: choice.disabled ?? false, checked: choice.checked ?? false, }; @@ -217,6 +222,7 @@ export default createPrompt( const message = theme.style.message(config.message); + let description; const page = usePagination({ items, active, @@ -231,6 +237,10 @@ export default createPrompt( return theme.style.disabledChoice(`${item.name} ${disabledLabel}`); } + if (isActive) { + description = item.description; + } + const checkbox = item.checked ? theme.icon.checked : theme.icon.unchecked; const color = isActive ? theme.style.highlight : (x: string) => x; const cursor = isActive ? theme.icon.cursor : ' '; @@ -279,12 +289,16 @@ export default createPrompt( } } + const choiceDescription = description + ? `\n${theme.style.description(description)}` + : ``; + let error = ''; if (errorMsg) { error = `\n${theme.style.error(errorMsg)}`; } - return `${prefix} ${message}${helpTipTop}\n${page}${helpTipBottom}${error}${ansiEscapes.cursorHide}`; + return `${prefix} ${message}${helpTipTop}\n${page}${helpTipBottom}${choiceDescription}${error}${ansiEscapes.cursorHide}`; }, );