Skip to content

Commit 23201ba

Browse files
MazenSamehRSBoudrias
authored andcommitted
feat(@inquirer/rawlist) Add support for default
1 parent 8713b89 commit 23201ba

File tree

3 files changed

+68
-16
lines changed

3 files changed

+68
-16
lines changed

packages/rawlist/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ const answer = await rawlist({
7272
| message | `string` | yes | The question to ask |
7373
| choices | `Choice[]` | yes | List of the available choices. |
7474
| 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. |
75+
| default | `Value` | no | The value of the choice to preselect. If the value is not found, no choice is preselected. |
7576
| theme | [See Theming](#Theming) | no | Customize look of the prompt. |
7677

7778
`Separator` objects can be used in the `choices` array 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.

packages/rawlist/rawlist.test.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { describe, it, expect } from 'vitest';
1+
import { describe, expectTypeOf, it, expect } from 'vitest';
22
import { render } from '@inquirer/testing';
33
import rawlist, { Separator } from './src/index.ts';
44

@@ -389,4 +389,51 @@ describe('rawlist prompt', () => {
389389

390390
await expect(answer).resolves.toEqual('second');
391391
});
392+
393+
describe('default', () => {
394+
it('preselects the default value', async () => {
395+
const { answer, events, getScreen } = await render(rawlist, {
396+
message: 'Select a number',
397+
choices: numberedChoices,
398+
default: 2,
399+
});
400+
401+
expect(getScreen()).toMatchInlineSnapshot(`
402+
"? Select a number 2
403+
1) 1
404+
2) 2
405+
3) 3
406+
4) 4
407+
5) 5"
408+
`);
409+
410+
events.keypress('enter');
411+
expectTypeOf(answer).resolves.toEqualTypeOf<number>();
412+
expect(getScreen()).toMatchInlineSnapshot('"✔ Select a number 2"');
413+
await expect(answer).resolves.toEqual(2);
414+
});
415+
416+
it('ignores default value if not found', async () => {
417+
const { answer, events, getScreen } = await render(rawlist, {
418+
message: 'Select a fruit',
419+
choices: [
420+
{ name: 'Apple', value: 'apple' },
421+
{ name: 'Banana', value: 'banana' },
422+
],
423+
// Forcing an invalid default value
424+
default: 'Oops! not in the list' as 'banana',
425+
});
426+
427+
expect(getScreen()).toMatchInlineSnapshot(`
428+
"? Select a fruit
429+
1) Apple
430+
2) Banana"
431+
`);
432+
433+
events.type('1');
434+
events.keypress('enter');
435+
expectTypeOf(answer).resolves.toEqualTypeOf<'apple' | 'banana'>();
436+
await expect(answer).resolves.toEqual('apple');
437+
});
438+
});
392439
});

packages/rawlist/src/index.ts

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -32,18 +32,12 @@ type NormalizedChoice<Value> = {
3232
key: string;
3333
};
3434

35-
type RawlistConfig<
36-
Value,
37-
ChoicesObject =
38-
| ReadonlyArray<string | Separator>
39-
| ReadonlyArray<Choice<Value> | Separator>,
40-
> = {
35+
type RawlistConfig<Value> = {
4136
message: string;
42-
choices: ChoicesObject extends ReadonlyArray<string | Separator>
43-
? ChoicesObject
44-
: ReadonlyArray<Choice<Value> | Separator>;
37+
choices: ReadonlyArray<Value | Choice<Value> | Separator>;
4538
loop?: boolean;
4639
theme?: PartialDeep<Theme>;
40+
default?: NoInfer<Value>;
4741
};
4842

4943
function isSelectableChoice<T>(
@@ -53,18 +47,19 @@ function isSelectableChoice<T>(
5347
}
5448

5549
function normalizeChoices<Value>(
56-
choices: ReadonlyArray<string | Separator> | ReadonlyArray<Choice<Value> | Separator>,
50+
choices: ReadonlyArray<Value | Choice<Value> | Separator>,
5751
): Array<NormalizedChoice<Value> | Separator> {
5852
let index = 0;
5953
return choices.map((choice) => {
6054
if (Separator.isSeparator(choice)) return choice;
6155

6256
index += 1;
63-
if (typeof choice === 'string') {
57+
if (typeof choice !== 'object' || choice === null || !('value' in choice)) {
58+
const name = String(choice);
6459
return {
65-
value: choice as Value,
66-
name: choice,
67-
short: choice,
60+
value: choice,
61+
name,
62+
short: name,
6863
key: String(index),
6964
};
7065
}
@@ -105,7 +100,16 @@ export default createPrompt(
105100
const { loop = true } = config;
106101
const choices = useMemo(() => normalizeChoices(config.choices), [config.choices]);
107102
const [status, setStatus] = useState<Status>('idle');
108-
const [value, setValue] = useState<string>('');
103+
const [value, setValue] = useState<string>(() => {
104+
const defaultChoice =
105+
config.default == null
106+
? undefined
107+
: choices.find(
108+
(choice): choice is NormalizedChoice<Value> =>
109+
isSelectableChoice(choice) && choice.value === config.default,
110+
);
111+
return defaultChoice?.key ?? '';
112+
});
109113
const [errorMsg, setError] = useState<string>();
110114
const theme = makeTheme(config.theme);
111115
const prefix = usePrefix({ status, theme });

0 commit comments

Comments
 (0)