Skip to content

Commit 55fb42b

Browse files
committed
feat: support disabled for select and multiselect prompt
1 parent 3280bc0 commit 55fb42b

File tree

4 files changed

+86
-17
lines changed

4 files changed

+86
-17
lines changed

packages/core/src/prompts/multi-select.ts

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import Prompt, { type PromptOptions } from './prompt.js';
22

3-
interface MultiSelectOptions<T extends { value: any }>
3+
interface MultiSelectOptions<T extends { value: any; disabled?: boolean }>
44
extends PromptOptions<T['value'][], MultiSelectPrompt<T>> {
55
options: T[];
66
initialValues?: T['value'][];
77
required?: boolean;
88
cursorAt?: T['value'];
99
}
10-
export default class MultiSelectPrompt<T extends { value: any }> extends Prompt<T['value'][]> {
10+
export default class MultiSelectPrompt<T extends { value: any; disabled?: boolean }> extends Prompt<
11+
T['value'][]
12+
> {
1113
options: T[];
1214
cursor = 0;
1315

@@ -17,11 +19,11 @@ export default class MultiSelectPrompt<T extends { value: any }> extends Prompt<
1719

1820
private toggleAll() {
1921
const allSelected = this.value !== undefined && this.value.length === this.options.length;
20-
this.value = allSelected ? [] : this.options.map((v) => v.value);
22+
this.value = allSelected ? [] : this.options.filter((v) => !v.disabled).map((v) => v.value);
2123
}
2224

2325
private toggleInvert() {
24-
const notSelected = this.options.filter((v) => !this.value.includes(v.value));
26+
const notSelected = this.options.filter((v) => !v.disabled && !this.value.includes(v.value));
2527
this.value = notSelected.map((v) => v.value);
2628
}
2729

@@ -56,13 +58,27 @@ export default class MultiSelectPrompt<T extends { value: any }> extends Prompt<
5658
this.on('cursor', (key) => {
5759
switch (key) {
5860
case 'left':
59-
case 'up':
60-
this.cursor = this.cursor === 0 ? this.options.length - 1 : this.cursor - 1;
61+
case 'up': {
62+
const lastCursor = this.cursor === 0 ? this.options.length - 1 : this.cursor - 1;
63+
const lastOption = this.options[lastCursor];
64+
this.cursor = !lastOption.disabled
65+
? lastCursor
66+
: lastCursor === 0
67+
? this.options.length - 1
68+
: lastCursor - 1;
6169
break;
70+
}
6271
case 'down':
63-
case 'right':
64-
this.cursor = this.cursor === this.options.length - 1 ? 0 : this.cursor + 1;
72+
case 'right': {
73+
const nextCursor = this.cursor === this.options.length - 1 ? 0 : this.cursor + 1;
74+
const nextOption = this.options[nextCursor];
75+
this.cursor = !nextOption.disabled
76+
? nextCursor
77+
: nextCursor === this.options.length - 1
78+
? 0
79+
: nextCursor + 1;
6580
break;
81+
}
6682
case 'space':
6783
this.toggleValue();
6884
break;

packages/core/src/prompts/select.ts

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import Prompt, { type PromptOptions } from './prompt.js';
22

3-
interface SelectOptions<T extends { value: any }>
3+
interface SelectOptions<T extends { value: any; disabled?: boolean }>
44
extends PromptOptions<T['value'], SelectPrompt<T>> {
55
options: T[];
66
initialValue?: T['value'];
77
}
8-
export default class SelectPrompt<T extends { value: any }> extends Prompt<T['value']> {
8+
export default class SelectPrompt<T extends { value: any; disabled?: boolean }> extends Prompt<
9+
T['value']
10+
> {
911
options: T[];
1012
cursor = 0;
1113

@@ -28,13 +30,27 @@ export default class SelectPrompt<T extends { value: any }> extends Prompt<T['va
2830
this.on('cursor', (key) => {
2931
switch (key) {
3032
case 'left':
31-
case 'up':
32-
this.cursor = this.cursor === 0 ? this.options.length - 1 : this.cursor - 1;
33+
case 'up': {
34+
const lastCursor = this.cursor === 0 ? this.options.length - 1 : this.cursor - 1;
35+
const lastOption = this.options[lastCursor];
36+
this.cursor = !lastOption.disabled
37+
? lastCursor
38+
: lastCursor === 0
39+
? this.options.length - 1
40+
: lastCursor - 1;
3341
break;
42+
}
3443
case 'down':
35-
case 'right':
36-
this.cursor = this.cursor === this.options.length - 1 ? 0 : this.cursor + 1;
44+
case 'right': {
45+
const nextCursor = this.cursor === this.options.length - 1 ? 0 : this.cursor + 1;
46+
const nextOption = this.options[nextCursor];
47+
this.cursor = !nextOption.disabled
48+
? nextCursor
49+
: nextCursor === this.options.length - 1
50+
? 0
51+
: nextCursor + 1;
3752
break;
53+
}
3854
}
3955
this.changeValue();
4056
});

packages/prompts/src/multi-select.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,21 @@ export interface MultiSelectOptions<Value> extends CommonOptions {
2323
export const multiselect = <Value>(opts: MultiSelectOptions<Value>) => {
2424
const opt = (
2525
option: Option<Value>,
26-
state: 'inactive' | 'active' | 'selected' | 'active-selected' | 'submitted' | 'cancelled'
26+
state:
27+
| 'inactive'
28+
| 'active'
29+
| 'selected'
30+
| 'active-selected'
31+
| 'submitted'
32+
| 'cancelled'
33+
| 'disabled'
2734
) => {
2835
const label = option.label ?? String(option.value);
36+
if (state === 'disabled') {
37+
return `${color.black(S_CHECKBOX_SELECTED)} ${color.dim(label)}${
38+
option.hint ? ` ${color.dim(`(${option.hint})`)}` : ''
39+
}`;
40+
}
2941
if (state === 'active') {
3042
return `${color.cyan(S_CHECKBOX_ACTIVE)} ${label}${
3143
option.hint ? ` ${color.dim(`(${option.hint})`)}` : ''
@@ -74,6 +86,9 @@ export const multiselect = <Value>(opts: MultiSelectOptions<Value>) => {
7486
const value = this.value ?? [];
7587

7688
const styleOption = (option: Option<Value>, active: boolean) => {
89+
if (option.disabled) {
90+
return opt(option, 'disabled');
91+
}
7792
const selected = value.includes(option.value);
7893
if (active && selected) {
7994
return opt(option, 'active-selected');

packages/prompts/src/select.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@ export type Option<Value> = Value extends Primitive
3131
* By default, no `hint` is displayed.
3232
*/
3333
hint?: string;
34+
/**
35+
* Whether this option is disabled.
36+
* Disabled options are visible but cannot be selected.
37+
*
38+
* By default, options are not disabled.
39+
*/
40+
disabled?: boolean;
3441
}
3542
: {
3643
/**
@@ -48,6 +55,13 @@ export type Option<Value> = Value extends Primitive
4855
* By default, no `hint` is displayed.
4956
*/
5057
hint?: string;
58+
/**
59+
* Whether this option is disabled.
60+
* Disabled options are visible but cannot be selected.
61+
*
62+
* By default, options are not disabled.
63+
*/
64+
disabled?: boolean;
5165
};
5266

5367
export interface SelectOptions<Value> extends CommonOptions {
@@ -58,9 +72,16 @@ export interface SelectOptions<Value> extends CommonOptions {
5872
}
5973

6074
export const select = <Value>(opts: SelectOptions<Value>) => {
61-
const opt = (option: Option<Value>, state: 'inactive' | 'active' | 'selected' | 'cancelled') => {
75+
const opt = (
76+
option: Option<Value>,
77+
state: 'inactive' | 'active' | 'selected' | 'cancelled' | 'disabled'
78+
) => {
6279
const label = option.label ?? String(option.value);
6380
switch (state) {
81+
case 'disabled':
82+
return `${color.black(S_RADIO_ACTIVE)} ${color.dim(label)}${
83+
option.hint ? ` ${color.dim(`(${option.hint})`)}` : ''
84+
}`;
6485
case 'selected':
6586
return `${color.dim(label)}`;
6687
case 'active':
@@ -97,7 +118,8 @@ export const select = <Value>(opts: SelectOptions<Value>) => {
97118
cursor: this.cursor,
98119
options: this.options,
99120
maxItems: opts.maxItems,
100-
style: (item, active) => opt(item, active ? 'active' : 'inactive'),
121+
style: (item, active) =>
122+
opt(item, item.disabled ? 'disabled' : active ? 'active' : 'inactive'),
101123
}).join(`\n${color.cyan(S_BAR)} `)}\n${color.cyan(S_BAR_END)}\n`;
102124
}
103125
}

0 commit comments

Comments
 (0)