Skip to content

Commit

Permalink
Checkbox - add validation if no choice is selected (#1316)
Browse files Browse the repository at this point in the history
* added validation in case no choice is selected

* added unit test for validation logic, edited existing unit tests since they were failing because of new validation logic

* Revert "added unit test for validation logic, edited existing unit tests since they were failing because of new validation logic"

This reverts commit 3b4e0e7.

* added 'required' prop, changed validation logic so that its more flexible, fixed unit tests

* updated readme, fixed type of required

* Update packages/checkbox/src/index.mts

---------

Co-authored-by: Adam Šrenkel <adam.srenkel@student.tuke.sk>
Co-authored-by: Simon Boudrias <admin@simonboudrias.com>
  • Loading branch information
3 people committed Oct 15, 2023
1 parent 3fff3fa commit 6fb4d9c
Show file tree
Hide file tree
Showing 3 changed files with 63 additions and 4 deletions.
1 change: 1 addition & 0 deletions packages/checkbox/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const answer = await checkbox({
| 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. |

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
39 changes: 39 additions & 0 deletions packages/checkbox/checkbox.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -624,4 +624,43 @@ describe('checkbox prompt', () => {
'"[checkbox prompt] No selectable choices. All choices are disabled."',
);
});

it('shows validation message if user did not select any choice', async () => {
const { answer, events, getScreen } = await render(checkbox, {
message: 'Select a number',
choices: numberedChoices,
required: true,
});

events.keypress('enter');
expect(getScreen()).toMatchInlineSnapshot(`
"? Select a number (Press <space> to select, <a> to toggle all, <i> to invert
selection, and <enter> to proceed)
❯◯ 1
◯ 2
◯ 3
◯ 4
◯ 5
◯ 6
◯ 7
(Use arrow keys to reveal more choices)
> At least one choice must be selected"
`);

events.keypress('space');
expect(getScreen()).toMatchInlineSnapshot(`
"? Select a number
❯◉ 1
◯ 2
◯ 3
◯ 4
◯ 5
◯ 6
◯ 7
(Use arrow keys to reveal more choices)"
`);

events.keypress('enter');
await expect(answer).resolves.toEqual([1]);
});
});
27 changes: 23 additions & 4 deletions packages/checkbox/src/index.mts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ type Config<Value> = PromptConfig<{
instructions?: string | boolean;
choices: ReadonlyArray<Choice<Value> | Separator>;
loop?: boolean;
required?: boolean;
}>;

type Item<Value> = Separator | Choice<Value>;
Expand Down Expand Up @@ -74,7 +75,14 @@ function renderItem<Value>({ item, isActive }: { item: Item<Value>; isActive: bo

export default createPrompt(
<Value extends unknown>(config: Config<Value>, done: (value: Array<Value>) => void) => {
const { prefix = usePrefix(), instructions, pageSize, loop = true, choices } = config;
const {
prefix = usePrefix(),
instructions,
pageSize,
loop = true,
choices,
required,
} = config;
const [status, setStatus] = useState('pending');
const [items, setItems] = useState<ReadonlyArray<Item<Value>>>(
choices.map((choice) => ({ ...choice })),
Expand All @@ -96,11 +104,16 @@ export default createPrompt(

const [active, setActive] = useState(bounds.first);
const [showHelpTip, setShowHelpTip] = useState(true);
const [errorMsg, setError] = useState<string | undefined>(undefined);

useKeypress((key) => {
if (isEnterKey(key)) {
setStatus('done');
done(items.filter(isChecked).map((choice) => choice.value));
if (required && !items.some(isChecked)) {
setError('At least one choice must be selected');
} else {
setStatus('done');
done(items.filter(isChecked).map((choice) => choice.value));
}
} else if (isUpKey(key) || isDownKey(key)) {
if (
loop ||
Expand All @@ -115,6 +128,7 @@ export default createPrompt(
setActive(next);
}
} else if (isSpaceKey(key)) {
setError(undefined);
setShowHelpTip(false);
setItems(items.map((choice, i) => (i === active ? toggle(choice) : choice)));
} else if (key.name === 'a') {
Expand Down Expand Up @@ -167,7 +181,12 @@ export default createPrompt(
}
}

return `${prefix} ${message}${helpTip}\n${page}${ansiEscapes.cursorHide}`;
let error = '';
if (errorMsg) {
error = chalk.red(`> ${errorMsg}`);
}

return `${prefix} ${message}${helpTip}\n${page}\n${error}${ansiEscapes.cursorHide}`;
},
);

Expand Down

0 comments on commit 6fb4d9c

Please sign in to comment.