Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions packages/checkbox/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,15 @@ const answer = await checkbox({

## Options

| Property | Type | Required | Description |
| -------- | ------------------------------------------------------------------------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| message | `string` | yes | The question to ask |
| choices | `Array<{ value: string, name?: string, disabled?: boolean \| string, checked?: boolean } \| Separator>` | yes | List of the available choices. The `value` will be returned as the answer, and used as display if no `name` is defined. Choices who're `disabled` will be displayed, but not selectable. |
| pageSize | `number` | no | By default, lists of choice longer than 7 will be paginated. Use this option to control how many choices will appear on the screen at once. |
| loop | `boolean` | no | Defaults to `true`. When set to `false`, the cursor will be constrained to the top and bottom of the choice list without looping. |
| required | `boolean` | no | When set to `true`, ensures at least one choice must be selected. |
| validate | `string\[\] => boolean \| string \| Promise<string \| boolean>` | no | On submit, validate the choices. When returning a string, it'll be used as the error message displayed to the user. Note: returning a rejected promise, we'll assume a code error happened and crash. |
| Property | Type | Required | Description |
| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| message | `string` | yes | The question to ask |
| choices | `Array<{ value: string, name?: string, disabled?: boolean \| string, checked?: boolean } \| Separator>` | yes | List of the available choices. The `value` will be returned as the answer, and used as display if no `name` is defined. Choices who're `disabled` will be displayed, but not selectable. |
| pageSize | `number` | no | By default, lists of choice longer than 7 will be paginated. Use this option to control how many choices will appear on the screen at once. |
| loop | `boolean` | no | Defaults to `true`. When set to `false`, the cursor will be constrained to the top and bottom of the choice list without looping. |
| required | `boolean` | no | When set to `true`, ensures at least one choice must be selected. |
| validate | `string[] => boolean \| string \| Promise<string \| boolean>` | no | On submit, validate the choices. When returning a string, it'll be used as the error message displayed to the user. Note: returning a rejected promise, we'll assume a code error happened and crash. |
| renderChoices | `(Array<{ value: string, name?: string, disabled?: boolean \| string, checked?: boolean }>?, Array<{ value: string, name?: string, disabled?: boolean \| string, checked?: boolean } \| Separator>?) => string` | no | After the prompt has been confirmed with Enter, this option can be used to specify how the selected choices are displayed on the command line. |

The `Separator` object can be used to render non-selectable lines in the choice list. By default it'll render a line, but you can provide the text as argument (`new Separator('-- Dependencies --')`). This option is often used to add labels to groups within long list of options.

Expand Down
90 changes: 80 additions & 10 deletions packages/checkbox/checkbox.test.mts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { describe, it, expect } from 'vitest';
import { render } from '@inquirer/testing';
import checkbox, { Separator } from './src/index.mjs';
import { Prompt } from '@inquirer/type';
import checkbox, { Separator, Config } from './src/index.mjs';

const numberedChoices = [
{ value: 1 },
Expand Down Expand Up @@ -654,16 +655,19 @@ describe('checkbox prompt', () => {
});

it('uses custom validation', async () => {
const { answer, events, getScreen } = await render(checkbox, {
message: 'Select a number',
choices: numberedChoices,
validate(items: any) {
if (items.length !== 1) {
return 'Please select only one choice';
}
return true;
const { answer, events, getScreen } = await render<Prompt<any, Config<number>>>(
checkbox,
{
message: 'Select a number',
choices: numberedChoices,
validate(items) {
if (items.length !== 1) {
return 'Please select only one choice';
}
return true;
},
},
});
);

events.keypress('enter');
await Promise.resolve();
Expand All @@ -685,4 +689,70 @@ describe('checkbox prompt', () => {
events.keypress('enter');
await expect(answer).resolves.toEqual([1]);
});

it('uses the default renderChoices function', async () => {
const { answer, events, getScreen } = await render(checkbox, {
message: 'Select a number',
choices: numberedChoices,
});

events.keypress('space');
events.keypress('down');
events.keypress('down');
events.keypress('space');
events.keypress('down');
events.keypress('space');
events.keypress('enter');

await expect(answer).resolves.toEqual([1, 3, 4]);
expect(getScreen()).toMatchInlineSnapshot('"? Select a number 1, 3, 4"');
});

it('uses the given renderChoices function - using only the selected choices', async () => {
const { answer, events, getScreen } = await render<Prompt<any, Config<number>>>(
checkbox,
{
message: 'Select a number',
choices: numberedChoices,
renderChoices: (selected) =>
selected.map((choice) => choice.value * 2).join(', '),
},
);

events.keypress('space');
events.keypress('down');
events.keypress('down');
events.keypress('space');
events.keypress('down');
events.keypress('space');
events.keypress('enter');

await expect(answer).resolves.toEqual([1, 3, 4]);
expect(getScreen()).toMatchInlineSnapshot('"? Select a number 2, 6, 8"');
});

it('uses the given renderChoices function - using selected and all choices', async () => {
const { answer, events, getScreen } = await render<Prompt<any, Config<number>>>(
checkbox,
{
message: 'Select a number',
choices: numberedChoices,
renderChoices: (selected, choices) =>
` ---> Selected ${selected.length} out of ${
choices.filter((choice) => choice.type !== 'separator').length
} options.`,
},
);

events.keypress('space');
events.keypress('down');
events.keypress('down');
events.keypress('space');
events.keypress('enter');

await expect(answer).resolves.toEqual([1, 3]);
expect(getScreen()).toMatchInlineSnapshot(
'"? Select a number ---> Selected 2 out of 12 options."',
);
});
});
16 changes: 11 additions & 5 deletions packages/checkbox/src/index.mts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ type Choice<Value> = {
type?: never;
};

type Config<Value> = PromptConfig<{
export type Config<Value> = PromptConfig<{
prefix?: string;
pageSize?: number;
instructions?: string | boolean;
Expand All @@ -36,6 +36,10 @@ type Config<Value> = PromptConfig<{
validate?: (
items: ReadonlyArray<Item<Value>>,
) => boolean | string | Promise<string | boolean>;
renderChoices?: (
selected: ReadonlyArray<Choice<Value>>,
choices: ReadonlyArray<Choice<Value> | Separator>,
) => string;
}>;

type Item<Value> = Separator | Choice<Value>;
Expand Down Expand Up @@ -86,6 +90,9 @@ export default createPrompt(
choices,
required,
validate = () => true,
// We always provide a value for choices. It is only typed as optional to give the user as much flexibility as possible
renderChoices = (selectedChoices) =>
selectedChoices!.map((choice) => choice.name || choice.value).join(', '),
} = config;
const [status, setStatus] = useState('pending');
const [items, setItems] = useState<ReadonlyArray<Item<Value>>>(
Expand Down Expand Up @@ -168,10 +175,9 @@ export default createPrompt(
});

if (status === 'done') {
const selection = items
.filter(isChecked)
.map((choice) => choice.name || choice.value);
return `${prefix} ${message} ${chalk.cyan(selection.join(', '))}`;
const selectedChoices = items.filter(isChecked);

return `${prefix} ${message} ${chalk.cyan(renderChoices(selectedChoices, items))}`;
}

let helpTip = '';
Expand Down