Skip to content

Commit 6404dc1

Browse files
authored
fix: support disabled options in autocomplete (#466)
1 parent 14ac87d commit 6404dc1

File tree

6 files changed

+335
-11
lines changed

6 files changed

+335
-11
lines changed

‎.changeset/bitter-sides-accept.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@clack/prompts": patch
3+
"@clack/core": patch
4+
---
5+
6+
Disallow selection of disabled options in autocomplete.

‎packages/core/src/prompts/autocomplete.ts‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -203,8 +203,14 @@ export default class AutocompletePrompt<T extends OptionLike> extends Prompt<
203203
} else {
204204
this.filteredOptions = [...options];
205205
}
206-
this.#cursor = getCursorForValue(this.focusedValue, this.filteredOptions);
207-
this.focusedValue = this.filteredOptions[this.#cursor]?.value;
206+
const valueCursor = getCursorForValue(this.focusedValue, this.filteredOptions);
207+
this.#cursor = findCursor(valueCursor, 0, this.filteredOptions);
208+
const focusedOption = this.filteredOptions[this.#cursor];
209+
if (focusedOption && !focusedOption.disabled) {
210+
this.focusedValue = focusedOption.value;
211+
} else {
212+
this.focusedValue = undefined;
213+
}
208214
if (!this.multiple) {
209215
if (this.focusedValue !== undefined) {
210216
this.toggleSelected(this.focusedValue);

‎packages/core/src/utils/cursor.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ export function findCursor<T extends { disabled?: boolean }>(
33
delta: number,
44
options: T[]
55
) {
6+
const hasEnabledOptions = options.some((opt) => !opt.disabled);
7+
if (!hasEnabledOptions) {
8+
return cursor;
9+
}
610
const newCursor = cursor + delta;
711
const maxCursor = Math.max(options.length - 1, 0);
812
const clampedCursor = newCursor < 0 ? maxCursor : newCursor > maxCursor ? 0 : newCursor;

‎packages/prompts/src/autocomplete.ts‎

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,19 @@ export const autocomplete = <Value>(opts: AutocompleteOptions<Value>) => {
104104
const options = this.options;
105105
const placeholder = opts.placeholder;
106106
const showPlaceholder = userInput === '' && placeholder !== undefined;
107+
const opt = (option: Option<Value>, state: 'inactive' | 'active' | 'disabled') => {
108+
const label = getLabel(option);
109+
const hint =
110+
option.hint && option.value === this.focusedValue ? color.dim(` (${option.hint})`) : '';
111+
switch (state) {
112+
case 'active':
113+
return `${color.green(S_RADIO_ACTIVE)} ${label}${hint}`;
114+
case 'inactive':
115+
return `${color.dim(S_RADIO_INACTIVE)} ${color.dim(label)}`;
116+
case 'disabled':
117+
return `${color.gray(S_RADIO_INACTIVE)} ${color.strikethrough(color.gray(label))}`;
118+
}
119+
};
107120

108121
// Handle different states
109122
switch (this.state) {
@@ -180,15 +193,10 @@ export const autocomplete = <Value>(opts: AutocompleteOptions<Value>) => {
180193
columnPadding: hasGuide ? 3 : 0, // for `| ` when guide is shown
181194
rowPadding: headings.length + footers.length,
182195
style: (option, active) => {
183-
const label = getLabel(option);
184-
const hint =
185-
option.hint && option.value === this.focusedValue
186-
? color.dim(` (${option.hint})`)
187-
: '';
188-
189-
return active
190-
? `${color.green(S_RADIO_ACTIVE)} ${label}${hint}`
191-
: `${color.dim(S_RADIO_INACTIVE)} ${color.dim(label)}${hint}`;
196+
return opt(
197+
option,
198+
option.disabled ? 'disabled' : active ? 'active' : 'inactive'
199+
);
192200
},
193201
maxItems: opts.maxItems,
194202
output: opts.output,
@@ -239,6 +247,9 @@ export const autocompleteMultiselect = <Value>(opts: AutocompleteMultiSelectOpti
239247
: '';
240248
const checkbox = isSelected ? color.green(S_CHECKBOX_SELECTED) : color.dim(S_CHECKBOX_INACTIVE);
241249

250+
if (option.disabled) {
251+
return `${color.gray(S_CHECKBOX_INACTIVE)} ${color.strikethrough(color.gray(label))}`;
252+
}
242253
if (active) {
243254
return `${checkbox} ${label}${hint}`;
244255
}

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

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,115 @@ exports[`autocomplete > can be aborted by a signal 1`] = `
2020
]
2121
`;
2222
23+
exports[`autocomplete > cannot select disabled options when only one left 1`] = `
24+
[
25+
"<cursor.hide>",
26+
"│
27+
◆ Select a fruit
28+
│
29+
│ Search: _
30+
│ ● Apple
31+
│ ○ Banana
32+
│ ○ Cherry
33+
│ ○ Grape
34+
│ ○ Orange
35+
│ ○ Kiwi
36+
│ ↑/↓ to select • Enter: confirm • Type: to search
37+
└",
38+
"<cursor.backward count=999><cursor.up count=11>",
39+
"<cursor.down count=3>",
40+
"<erase.down>",
41+
"│ Search: k█ (1 match)
42+
│ ○ Kiwi
43+
│ ↑/↓ to select • Enter: confirm • Type: to search
44+
└",
45+
"<cursor.backward count=999><cursor.up count=6>",
46+
"<cursor.down count=1>",
47+
"<erase.down>",
48+
"◇ Select a fruit
49+
│",
50+
"
51+
",
52+
"<cursor.show>",
53+
]
54+
`;
55+
56+
exports[`autocomplete > displays disabled options correctly 1`] = `
57+
[
58+
"<cursor.hide>",
59+
"│
60+
◆ Select a fruit
61+
│
62+
│ Search: _
63+
│ ● Apple
64+
│ ○ Banana
65+
│ ○ Cherry
66+
│ ○ Grape
67+
│ ○ Orange
68+
│ ○ Kiwi
69+
│ ↑/↓ to select • Enter: confirm • Type: to search
70+
└",
71+
"<cursor.backward count=999><cursor.up count=11>",
72+
"<cursor.down count=3>",
73+
"<erase.down>",
74+
"│ Search:
75+
│ ○ Apple
76+
│ ● Banana
77+
│ ○ Cherry
78+
│ ○ Grape
79+
│ ○ Orange
80+
│ ○ Kiwi
81+
│ ↑/↓ to select • Enter: confirm • Type: to search
82+
└",
83+
"<cursor.backward count=999><cursor.up count=11>",
84+
"<cursor.down count=5>",
85+
"<erase.down>",
86+
"│ ○ Banana
87+
│ ● Cherry
88+
│ ○ Grape
89+
│ ○ Orange
90+
│ ○ Kiwi
91+
│ ↑/↓ to select • Enter: confirm • Type: to search
92+
└",
93+
"<cursor.backward count=999><cursor.up count=11>",
94+
"<cursor.down count=6>",
95+
"<erase.down>",
96+
"│ ○ Cherry
97+
│ ● Grape
98+
│ ○ Orange
99+
│ ○ Kiwi
100+
│ ↑/↓ to select • Enter: confirm • Type: to search
101+
└",
102+
"<cursor.backward count=999><cursor.up count=11>",
103+
"<cursor.down count=7>",
104+
"<erase.down>",
105+
"│ ○ Grape
106+
│ ● Orange
107+
│ ○ Kiwi
108+
│ ↑/↓ to select • Enter: confirm • Type: to search
109+
└",
110+
"<cursor.backward count=999><cursor.up count=11>",
111+
"<cursor.down count=4>",
112+
"<erase.down>",
113+
"│ ● Apple
114+
│ ○ Banana
115+
│ ○ Cherry
116+
│ ○ Grape
117+
│ ○ Orange
118+
│ ○ Kiwi
119+
│ ↑/↓ to select • Enter: confirm • Type: to search
120+
└",
121+
"<cursor.backward count=999><cursor.up count=11>",
122+
"<cursor.down count=1>",
123+
"<erase.down>",
124+
"◇ Select a fruit
125+
│ Apple",
126+
"
127+
",
128+
"<cursor.show>",
129+
]
130+
`;
131+
23132
exports[`autocomplete > limits displayed options when maxItems is set 1`] = `
24133
[
25134
"<cursor.hide>",
@@ -504,6 +613,120 @@ exports[`autocompleteMultiselect > can use navigation keys to select options 1`]
504613
]
505614
`;
506615
616+
exports[`autocompleteMultiselect > cannot select disabled options when only one left 1`] = `
617+
[
618+
"<cursor.hide>",
619+
"│
620+
◆ Select a fruit
621+
│
622+
│ Search: _
623+
│ ◻ Apple
624+
│ ◻ Banana
625+
│ ◻ Cherry
626+
│ ◻ Grape
627+
│ ◻ Orange
628+
│ ◻ Kiwi
629+
│ ↑/↓ to navigate • Tab: select • Enter: confirm • Type: to search
630+
└",
631+
"<cursor.backward count=999><cursor.up count=11>",
632+
"<cursor.down count=3>",
633+
"<erase.down>",
634+
"│ Search: k█ (1 match)
635+
│ ◻ Kiwi
636+
│ ↑/↓ to navigate • Tab: select • Enter: confirm • Type: to search
637+
└",
638+
"<cursor.backward count=999><cursor.up count=6>",
639+
"<cursor.down count=1>",
640+
"<erase.down>",
641+
"◇ Select a fruit
642+
│ 0 items selected",
643+
"
644+
",
645+
"<cursor.show>",
646+
]
647+
`;
648+
649+
exports[`autocompleteMultiselect > displays disabled options correctly 1`] = `
650+
[
651+
"<cursor.hide>",
652+
"│
653+
◆ Select a fruit
654+
│
655+
│ Search: _
656+
│ ◻ Apple
657+
│ ◻ Banana
658+
│ ◻ Cherry
659+
│ ◻ Grape
660+
│ ◻ Orange
661+
│ ◻ Kiwi
662+
│ ↑/↓ to navigate • Tab: select • Enter: confirm • Type: to search
663+
└",
664+
"<cursor.backward count=999><cursor.up count=11>",
665+
"<cursor.down count=3>",
666+
"<erase.down>",
667+
"│ Search: 
668+
│ ◻ Apple
669+
│ ◻ Banana
670+
│ ◻ Cherry
671+
│ ◻ Grape
672+
│ ◻ Orange
673+
│ ◻ Kiwi
674+
│ ↑/↓ to navigate • Space/Tab: select • Enter: confirm • Type: to search
675+
└",
676+
"<cursor.backward count=999><cursor.up count=11>",
677+
"<cursor.down count=5>",
678+
"<erase.down>",
679+
"│ ◻ Banana
680+
│ ◻ Cherry
681+
│ ◻ Grape
682+
│ ◻ Orange
683+
│ ◻ Kiwi
684+
│ ↑/↓ to navigate • Space/Tab: select • Enter: confirm • Type: to search
685+
└",
686+
"<cursor.backward count=999><cursor.up count=11>",
687+
"<cursor.down count=6>",
688+
"<erase.down>",
689+
"│ ◻ Cherry
690+
│ ◻ Grape
691+
│ ◻ Orange
692+
│ ◻ Kiwi
693+
│ ↑/↓ to navigate • Space/Tab: select • Enter: confirm • Type: to search
694+
└",
695+
"<cursor.backward count=999><cursor.up count=11>",
696+
"<cursor.down count=7>",
697+
"<erase.down>",
698+
"│ ◻ Grape
699+
│ ◻ Orange
700+
│ ◻ Kiwi
701+
│ ↑/↓ to navigate • Space/Tab: select • Enter: confirm • Type: to search
702+
└",
703+
"<cursor.backward count=999><cursor.up count=11>",
704+
"<cursor.down count=4>",
705+
"<erase.down>",
706+
"│ ◻ Apple
707+
│ ◻ Banana
708+
│ ◻ Cherry
709+
│ ◻ Grape
710+
│ ◻ Orange
711+
│ ◻ Kiwi
712+
│ ↑/↓ to navigate • Space/Tab: select • Enter: confirm • Type: to search
713+
└",
714+
"<cursor.backward count=999><cursor.up count=11>",
715+
"<cursor.down count=4>",
716+
"<erase.line><cursor.left count=1>",
717+
"│ ◼ Apple",
718+
"<cursor.down count=7>",
719+
"<cursor.backward count=999><cursor.up count=11>",
720+
"<cursor.down count=1>",
721+
"<erase.down>",
722+
"◇ Select a fruit
723+
│ 1 items selected",
724+
"
725+
",
726+
"<cursor.show>",
727+
]
728+
`;
729+
507730
exports[`autocompleteMultiselect > renders error when empty selection & required is true 1`] = `
508731
[
509732
"<cursor.hide>",

0 commit comments

Comments
 (0)