Skip to content

Commit 2e904d1

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

File tree

4 files changed

+127
-89
lines changed

4 files changed

+127
-89
lines changed

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
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<T['value'][]> {
1111
options: T[];
1212
cursor = 0;
1313

@@ -57,11 +57,15 @@ export default class MultiSelectPrompt<T extends { value: any }> extends Prompt<
5757
switch (key) {
5858
case 'left':
5959
case 'up':
60-
this.cursor = this.cursor === 0 ? this.options.length - 1 : this.cursor - 1;
60+
const lastCursor = this.cursor === 0 ? this.options.length - 1 : this.cursor - 1;
61+
const lastOption = this.options[lastCursor];
62+
this.cursor = !lastOption.disabled ? lastCursor : lastCursor === 0 ? this.options.length - 1 : lastCursor - 1;
6163
break;
6264
case 'down':
6365
case 'right':
64-
this.cursor = this.cursor === this.options.length - 1 ? 0 : this.cursor + 1;
66+
const nextCursor = this.cursor === this.options.length - 1 ? 0 : this.cursor + 1;
67+
const nextOption = this.options[nextCursor];
68+
this.cursor = !nextOption.disabled ? nextCursor : nextCursor === this.options.length - 1 ? 0 : nextCursor + 1;
6569
break;
6670
case 'space':
6771
this.toggleValue();

packages/core/src/prompts/select.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
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<T['value']> {
99
options: T[];
1010
cursor = 0;
1111

@@ -29,11 +29,15 @@ export default class SelectPrompt<T extends { value: any }> extends Prompt<T['va
2929
switch (key) {
3030
case 'left':
3131
case 'up':
32-
this.cursor = this.cursor === 0 ? this.options.length - 1 : this.cursor - 1;
32+
const lastCursor = this.cursor === 0 ? this.options.length - 1 : this.cursor - 1;
33+
const lastOption = this.options[lastCursor];
34+
this.cursor = !lastOption.disabled ? lastCursor : lastCursor === 0 ? this.options.length - 1 : lastCursor - 1;
3335
break;
3436
case 'down':
3537
case 'right':
36-
this.cursor = this.cursor === this.options.length - 1 ? 0 : this.cursor + 1;
38+
const nextCursor = this.cursor === this.options.length - 1 ? 0 : this.cursor + 1;
39+
const nextOption = this.options[nextCursor];
40+
this.cursor = !nextOption.disabled ? nextCursor : nextCursor === this.options.length - 1 ? 0 : nextCursor + 1;
3741
break;
3842
}
3943
this.changeValue();

packages/prompts/src/multi-select.ts

Lines changed: 56 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { MultiSelectPrompt } from '@clack/core';
2-
import color from 'picocolors';
1+
import { MultiSelectPrompt } from "@clack/core";
2+
import color from "picocolors";
33
import {
44
type CommonOptions,
55
S_BAR,
@@ -8,9 +8,9 @@ import {
88
S_CHECKBOX_INACTIVE,
99
S_CHECKBOX_SELECTED,
1010
symbol,
11-
} from './common.js';
12-
import { limitOptions } from './limit-options.js';
13-
import type { Option } from './select.js';
11+
} from "./common.js";
12+
import { limitOptions } from "./limit-options.js";
13+
import type { Option } from "./select.js";
1414

1515
export interface MultiSelectOptions<Value> extends CommonOptions {
1616
message: string;
@@ -23,28 +23,36 @@ 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);
29-
if (state === 'active') {
30-
return `${color.cyan(S_CHECKBOX_ACTIVE)} ${label}${
31-
option.hint ? ` ${color.dim(`(${option.hint})`)}` : ''
32-
}`;
36+
if (state === "disabled") {
37+
return `${color.black(S_CHECKBOX_SELECTED)} ${color.dim(label)}${option.hint ? ` ${color.dim(`(${option.hint})`)}` : ""
38+
}`;
3339
}
34-
if (state === 'selected') {
35-
return `${color.green(S_CHECKBOX_SELECTED)} ${color.dim(label)}${
36-
option.hint ? ` ${color.dim(`(${option.hint})`)}` : ''
37-
}`;
40+
if (state === "active") {
41+
return `${color.cyan(S_CHECKBOX_ACTIVE)} ${label}${option.hint ? ` ${color.dim(`(${option.hint})`)}` : ""
42+
}`;
3843
}
39-
if (state === 'cancelled') {
44+
if (state === "selected") {
45+
return `${color.green(S_CHECKBOX_SELECTED)} ${color.dim(label)}${option.hint ? ` ${color.dim(`(${option.hint})`)}` : ""
46+
}`;
47+
}
48+
if (state === "cancelled") {
4049
return `${color.strikethrough(color.dim(label))}`;
4150
}
42-
if (state === 'active-selected') {
43-
return `${color.green(S_CHECKBOX_SELECTED)} ${label}${
44-
option.hint ? ` ${color.dim(`(${option.hint})`)}` : ''
45-
}`;
51+
if (state === "active-selected") {
52+
return `${color.green(S_CHECKBOX_SELECTED)} ${label}${option.hint ? ` ${color.dim(`(${option.hint})`)}` : ""
53+
}`;
4654
}
47-
if (state === 'submitted') {
55+
if (state === "submitted") {
4856
return `${color.dim(label)}`;
4957
}
5058
return `${color.dim(S_CHECKBOX_INACTIVE)} ${color.dim(label)}`;
@@ -63,52 +71,58 @@ export const multiselect = <Value>(opts: MultiSelectOptions<Value>) => {
6371
if (required && (selected === undefined || selected.length === 0))
6472
return `Please select at least one option.\n${color.reset(
6573
color.dim(
66-
`Press ${color.gray(color.bgWhite(color.inverse(' space ')))} to select, ${color.gray(
67-
color.bgWhite(color.inverse(' enter '))
74+
`Press ${color.gray(
75+
color.bgWhite(color.inverse(" space "))
76+
)} to select, ${color.gray(
77+
color.bgWhite(color.inverse(" enter "))
6878
)} to submit`
6979
)
7080
)}`;
7181
},
7282
render() {
73-
const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`;
83+
const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message
84+
}\n`;
7485
const value = this.value ?? [];
7586

7687
const styleOption = (option: Option<Value>, active: boolean) => {
88+
if(option.disabled) {
89+
return opt(option, "disabled");
90+
}
7791
const selected = value.includes(option.value);
7892
if (active && selected) {
79-
return opt(option, 'active-selected');
93+
return opt(option, "active-selected");
8094
}
8195
if (selected) {
82-
return opt(option, 'selected');
96+
return opt(option, "selected");
8397
}
84-
return opt(option, active ? 'active' : 'inactive');
98+
return opt(option, active ? "active" : "inactive");
8599
};
86100

87101
switch (this.state) {
88-
case 'submit': {
89-
return `${title}${color.gray(S_BAR)} ${
90-
this.options
102+
case "submit": {
103+
return `${title}${color.gray(S_BAR)} ${this.options
91104
.filter(({ value: optionValue }) => value.includes(optionValue))
92-
.map((option) => opt(option, 'submitted'))
93-
.join(color.dim(', ')) || color.dim('none')
94-
}`;
105+
.map((option) => opt(option, "submitted"))
106+
.join(color.dim(", ")) || color.dim("none")
107+
}`;
95108
}
96-
case 'cancel': {
109+
case "cancel": {
97110
const label = this.options
98111
.filter(({ value: optionValue }) => value.includes(optionValue))
99-
.map((option) => opt(option, 'cancelled'))
100-
.join(color.dim(', '));
101-
return `${title}${color.gray(S_BAR)}${
102-
label.trim() ? ` ${label}\n${color.gray(S_BAR)}` : ''
103-
}`;
112+
.map((option) => opt(option, "cancelled"))
113+
.join(color.dim(", "));
114+
return `${title}${color.gray(S_BAR)}${label.trim() ? ` ${label}\n${color.gray(S_BAR)}` : ""
115+
}`;
104116
}
105-
case 'error': {
117+
case "error": {
106118
const footer = this.error
107-
.split('\n')
119+
.split("\n")
108120
.map((ln, i) =>
109-
i === 0 ? `${color.yellow(S_BAR_END)} ${color.yellow(ln)}` : ` ${ln}`
121+
i === 0
122+
? `${color.yellow(S_BAR_END)} ${color.yellow(ln)}`
123+
: ` ${ln}`
110124
)
111-
.join('\n');
125+
.join("\n");
112126
return `${title + color.yellow(S_BAR)} ${limitOptions({
113127
output: opts.output,
114128
options: this.options,

packages/prompts/src/select.ts

Lines changed: 55 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -14,41 +14,55 @@ type Primitive = Readonly<string | boolean | number>;
1414

1515
export type Option<Value> = Value extends Primitive
1616
? {
17-
/**
18-
* Internal data for this option.
19-
*/
20-
value: Value;
21-
/**
22-
* The optional, user-facing text for this option.
23-
*
24-
* By default, the `value` is converted to a string.
25-
*/
26-
label?: string;
27-
/**
28-
* An optional hint to display to the user when
29-
* this option might be selected.
30-
*
31-
* By default, no `hint` is displayed.
32-
*/
33-
hint?: string;
34-
}
17+
/**
18+
* Internal data for this option.
19+
*/
20+
value: Value;
21+
/**
22+
* The optional, user-facing text for this option.
23+
*
24+
* By default, the `value` is converted to a string.
25+
*/
26+
label?: string;
27+
/**
28+
* An optional hint to display to the user when
29+
* this option might be selected.
30+
*
31+
* By default, no `hint` is displayed.
32+
*/
33+
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;
41+
}
3542
: {
36-
/**
37-
* Internal data for this option.
38-
*/
39-
value: Value;
40-
/**
41-
* Required. The user-facing text for this option.
42-
*/
43-
label: string;
44-
/**
45-
* An optional hint to display to the user when
46-
* this option might be selected.
47-
*
48-
* By default, no `hint` is displayed.
49-
*/
50-
hint?: string;
51-
};
43+
/**
44+
* Internal data for this option.
45+
*/
46+
value: Value;
47+
/**
48+
* Required. The user-facing text for this option.
49+
*/
50+
label: string;
51+
/**
52+
* An optional hint to display to the user when
53+
* this option might be selected.
54+
*
55+
* By default, no `hint` is displayed.
56+
*/
57+
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;
65+
};
5266

5367
export interface SelectOptions<Value> extends CommonOptions {
5468
message: string;
@@ -58,15 +72,17 @@ 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 = (option: Option<Value>, state: 'inactive' | 'active' | 'selected' | 'cancelled' | 'disabled') => {
6276
const label = option.label ?? String(option.value);
6377
switch (state) {
78+
case 'disabled':
79+
return `${color.black(S_RADIO_ACTIVE)} ${color.dim(label)}${option.hint ? ` ${color.dim(`(${option.hint})`)}` : ''
80+
}`;
6481
case 'selected':
6582
return `${color.dim(label)}`;
6683
case 'active':
67-
return `${color.green(S_RADIO_ACTIVE)} ${label}${
68-
option.hint ? ` ${color.dim(`(${option.hint})`)}` : ''
69-
}`;
84+
return `${color.green(S_RADIO_ACTIVE)} ${label}${option.hint ? ` ${color.dim(`(${option.hint})`)}` : ''
85+
}`;
7086
case 'cancelled':
7187
return `${color.strikethrough(color.dim(label))}`;
7288
default:
@@ -97,7 +113,7 @@ export const select = <Value>(opts: SelectOptions<Value>) => {
97113
cursor: this.cursor,
98114
options: this.options,
99115
maxItems: opts.maxItems,
100-
style: (item, active) => opt(item, active ? 'active' : 'inactive'),
116+
style: (item, active) => opt(item, item.disabled ? 'disabled' : active ? 'active' : 'inactive'),
101117
}).join(`\n${color.cyan(S_BAR)} `)}\n${color.cyan(S_BAR_END)}\n`;
102118
}
103119
}

0 commit comments

Comments
 (0)