Skip to content

Commit 8e567c9

Browse files
committed
Merge branch 'main' into placeholder-fixeroos
2 parents 5eaaf1e + 4f6b3c2 commit 8e567c9

File tree

7 files changed

+231
-42
lines changed

7 files changed

+231
-42
lines changed

.changeset/moody-baboons-greet.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@clack/core": patch
3+
---
4+
5+
Set initial values of auto complete prompt to first option when multiple is false.

.changeset/strong-ravens-greet.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@clack/prompts": minor
3+
---
4+
5+
Add a `required` option to autocomplete multiselect.

packages/core/src/prompts/autocomplete.ts

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,10 @@ export default class AutocompletePrompt<T extends OptionLike> extends Prompt {
9595
} else {
9696
initialValues = opts.initialValue.slice(0, 1);
9797
}
98-
} else if (!this.multiple && this.options.length > 0) {
99-
initialValues = [this.options[0].value];
98+
} else {
99+
if (!this.multiple && this.options.length > 0) {
100+
initialValues = [this.options[0].value];
101+
}
100102
}
101103

102104
if (initialValues) {
@@ -152,14 +154,19 @@ export default class AutocompletePrompt<T extends OptionLike> extends Prompt {
152154
}
153155
this.isNavigating = true;
154156
} else {
155-
if (
156-
this.multiple &&
157-
this.focusedValue !== undefined &&
158-
(key.name === 'tab' || (this.isNavigating && key.name === 'space'))
159-
) {
160-
this.toggleSelected(this.focusedValue);
157+
if (this.multiple) {
158+
if (
159+
this.focusedValue !== undefined &&
160+
(key.name === 'tab' || (this.isNavigating && key.name === 'space'))
161+
) {
162+
this.toggleSelected(this.focusedValue);
163+
} else {
164+
this.isNavigating = false;
165+
}
161166
} else {
162-
this.isNavigating = false;
167+
if (this.focusedValue) {
168+
this.selectedValues = [this.focusedValue];
169+
}
163170
}
164171
}
165172
}

packages/core/test/prompts/autocomplete.test.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,31 @@ describe('AutocompletePrompt', () => {
9393
expect(instance.selectedValues).to.deep.equal(['cherry']);
9494
});
9595

96+
test('initialValue defaults to first option when non-multiple', () => {
97+
const instance = new AutocompletePrompt({
98+
input,
99+
output,
100+
render: () => 'foo',
101+
options: testOptions,
102+
});
103+
104+
expect(instance.cursor).to.equal(0);
105+
expect(instance.selectedValues).to.deep.equal(['apple']);
106+
});
107+
108+
test('initialValue is empty when multiple', () => {
109+
const instance = new AutocompletePrompt({
110+
input,
111+
output,
112+
render: () => 'foo',
113+
options: testOptions,
114+
multiple: true,
115+
});
116+
117+
expect(instance.cursor).to.equal(0);
118+
expect(instance.selectedValues).to.deep.equal([]);
119+
});
120+
96121
test('filtering through value event', () => {
97122
const instance = new AutocompletePrompt({
98123
input,
@@ -136,4 +161,37 @@ describe('AutocompletePrompt', () => {
136161

137162
expect(instance.filteredOptions).toEqual([]);
138163
});
164+
165+
test('submit without nav resolves to first option in non-multiple', async () => {
166+
const instance = new AutocompletePrompt({
167+
input,
168+
output,
169+
render: () => 'foo',
170+
options: testOptions,
171+
});
172+
173+
const promise = instance.prompt();
174+
input.emit('keypress', '', { name: 'return' });
175+
const result = await promise;
176+
177+
expect(instance.selectedValues).to.deep.equal(['apple']);
178+
expect(result).to.equal('apple');
179+
});
180+
181+
test('submit without nav resolves to [] in multiple', async () => {
182+
const instance = new AutocompletePrompt({
183+
input,
184+
output,
185+
render: () => 'foo',
186+
options: testOptions,
187+
multiple: true,
188+
});
189+
190+
const promise = instance.prompt();
191+
input.emit('keypress', '', { name: 'return' });
192+
const result = await promise;
193+
194+
expect(instance.selectedValues).to.deep.equal([]);
195+
expect(result).to.deep.equal([]);
196+
});
139197
});

packages/prompts/src/autocomplete.ts

Lines changed: 21 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ function getSelectedOptions<T>(values: T[], options: Option<T>[]): Option<T>[] {
4141
return results;
4242
}
4343

44-
export interface AutocompleteOptions<Value> extends CommonOptions {
44+
interface AutocompleteSharedOptions<Value> extends CommonOptions {
4545
/**
4646
* The message to display to the user.
4747
*/
@@ -50,10 +50,6 @@ export interface AutocompleteOptions<Value> extends CommonOptions {
5050
* Available options for the autocomplete prompt.
5151
*/
5252
options: Option<Value>[];
53-
/**
54-
* The initial selected value.
55-
*/
56-
initialValue?: Value;
5753
/**
5854
* Maximum number of items to display at once.
5955
*/
@@ -64,6 +60,13 @@ export interface AutocompleteOptions<Value> extends CommonOptions {
6460
placeholder?: string;
6561
}
6662

63+
export interface AutocompleteOptions<Value> extends AutocompleteSharedOptions<Value> {
64+
/**
65+
* The initial selected value.
66+
*/
67+
initialValue?: Value;
68+
}
69+
6770
export const autocomplete = <Value>(opts: AutocompleteOptions<Value>) => {
6871
const prompt = new AutocompletePrompt({
6972
options: opts.options,
@@ -162,35 +165,15 @@ export const autocomplete = <Value>(opts: AutocompleteOptions<Value>) => {
162165
};
163166

164167
// Type definition for the autocompleteMultiselect component
165-
export interface AutocompleteMultiSelectOptions<Value> {
166-
/**
167-
* The message to display to the user
168-
*/
169-
message: string;
170-
/**
171-
* The options for the user to choose from
172-
*/
173-
options: Option<Value>[];
168+
export interface AutocompleteMultiSelectOptions<Value> extends AutocompleteSharedOptions<Value> {
174169
/**
175170
* The initial selected values
176171
*/
177172
initialValues?: Value[];
178173
/**
179-
* The maximum number of items that can be selected
174+
* If true, at least one option must be selected
180175
*/
181-
maxItems?: number;
182-
/**
183-
* The placeholder to display in the input
184-
*/
185-
placeholder?: string;
186-
/**
187-
* The stream to read from
188-
*/
189-
input?: NodeJS.ReadStream;
190-
/**
191-
* The stream to write to
192-
*/
193-
output?: NodeJS.WriteStream;
176+
required?: boolean;
194177
}
195178

196179
/**
@@ -224,6 +207,12 @@ export const autocompleteMultiselect = <Value>(opts: AutocompleteMultiSelectOpti
224207
filter: (search, opt) => {
225208
return getFilteredOption(search, opt);
226209
},
210+
validate: () => {
211+
if (opts.required && prompt.selectedValues.length === 0) {
212+
return 'Please select at least one item';
213+
}
214+
return undefined;
215+
},
227216
initialValue: opts.initialValues,
228217
input: opts.input,
229218
output: opts.output,
@@ -272,6 +261,9 @@ export const autocompleteMultiselect = <Value>(opts: AutocompleteMultiSelectOpti
272261
? [`${color.cyan(S_BAR)} ${color.yellow('No matches found')}`]
273262
: [];
274263

264+
const errorMessage =
265+
this.state === 'error' ? [`${color.cyan(S_BAR)} ${color.yellow(this.error)}`] : [];
266+
275267
// Get limited options for display
276268
const displayOptions = limitOptions({
277269
cursor: this.cursor,
@@ -287,6 +279,7 @@ export const autocompleteMultiselect = <Value>(opts: AutocompleteMultiSelectOpti
287279
title,
288280
`${color.cyan(S_BAR)} ${color.dim('Search:')} ${searchText}${matches}`,
289281
...noResults,
282+
...errorMessage,
290283
...displayOptions.map((option) => `${color.cyan(S_BAR)} ${option}`),
291284
`${color.cyan(S_BAR)} ${color.dim(instructions.join(' • '))}`,
292285
`${color.cyan(S_BAR_END)}`,

packages/prompts/test/__snapshots__/autocomplete.test.ts.snap

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@ exports[`autocomplete > limits displayed options when maxItems is set 1`] = `
1515
│ ...
1616
│ ↑/↓ to select • Enter: confirm • Type: to search
1717
└",
18+
"<cursor.backward count=999><cursor.up count=11>",
19+
"<cursor.down count=1>",
20+
"<erase.down>",
21+
"◇ Select an option
22+
│ Option 0",
23+
"
24+
",
25+
"<cursor.show>",
1826
]
1927
`;
2028
@@ -32,6 +40,14 @@ exports[`autocomplete > renders initial UI with message and instructions 1`] = `
3240
│ ○ Orange
3341
│ ↑/↓ to select • Enter: confirm • Type: to search
3442
└",
43+
"<cursor.backward count=999><cursor.up count=10>",
44+
"<cursor.down count=1>",
45+
"<erase.down>",
46+
"◇ Select a fruit
47+
│ Apple",
48+
"
49+
",
50+
"<cursor.show>",
3551
]
3652
`;
3753
@@ -121,6 +137,14 @@ exports[`autocomplete > shows hint when option has hint and is focused 1`] = `
121137
│ ● Kiwi (New Zealand)
122138
│ ↑/↓ to select • Enter: confirm • Type: to search
123139
└",
140+
"<cursor.backward count=999><cursor.up count=11>",
141+
"<cursor.down count=1>",
142+
"<erase.down>",
143+
"◇ Select a fruit
144+
│ Kiwi",
145+
"
146+
",
147+
"<cursor.show>",
124148
]
125149
`;
126150
@@ -145,6 +169,14 @@ exports[`autocomplete > shows no matches message when search has no results 1`]
145169
│ No matches found
146170
│ ↑/↓ to select • Enter: confirm • Type: to search
147171
└",
172+
"<cursor.backward count=999><cursor.up count=6>",
173+
"<cursor.down count=1>",
174+
"<erase.down>",
175+
"◇ Select a fruit
176+
│ Apple",
177+
"
178+
",
179+
"<cursor.show>",
148180
]
149181
`;
150182
@@ -233,3 +265,55 @@ exports[`autocomplete > supports initialValue 1`] = `
233265
"<cursor.show>",
234266
]
235267
`;
268+
269+
exports[`autocompleteMultiselect > renders error when empty selection & required is true 1`] = `
270+
[
271+
"<cursor.hide>",
272+
"│
273+
◆ Select a fruit
274+
275+
│ Search: _
276+
│ ◻ Apple
277+
│ ◻ Banana
278+
│ ◻ Cherry
279+
│ ◻ Grape
280+
│ ◻ Orange
281+
│ ↑/↓ to navigate • Space: select • Enter: confirm • Type: to search
282+
└",
283+
"<cursor.backward count=999><cursor.up count=10>",
284+
"<cursor.down count=1>",
285+
"<erase.down>",
286+
"▲ Select a fruit
287+
288+
│ Search: _
289+
│ Please select at least one item
290+
│ ◻ Apple
291+
│ ◻ Banana
292+
│ ◻ Cherry
293+
│ ◻ Grape
294+
│ ◻ Orange
295+
│ ↑/↓ to navigate • Space: select • Enter: confirm • Type: to search
296+
└",
297+
"<cursor.backward count=999><cursor.up count=11>",
298+
"<cursor.down count=1>",
299+
"<erase.down>",
300+
"◆ Select a fruit
301+
302+
│ Search: _
303+
│ ◼ Apple
304+
│ ◻ Banana
305+
│ ◻ Cherry
306+
│ ◻ Grape
307+
│ ◻ Orange
308+
│ ↑/↓ to navigate • Space: select • Enter: confirm • Type: to search
309+
└",
310+
"<cursor.backward count=999><cursor.up count=10>",
311+
"<cursor.down count=1>",
312+
"<erase.down>",
313+
"◇ Select a fruit
314+
│ 1 items selected",
315+
"
316+
",
317+
"<cursor.show>",
318+
]
319+
`;

0 commit comments

Comments
 (0)