Skip to content

Commit d98e033

Browse files
authored
feat: add invert selection for multiselect prompt (#358)
1 parent 7b009df commit d98e033

File tree

3 files changed

+147
-0
lines changed

3 files changed

+147
-0
lines changed

.changeset/upset-showers-grow.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+
add invert selection for multiselect prompt

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ export default class MultiSelectPrompt<T extends { value: any }> extends Prompt<
2020
this.value = allSelected ? [] : this.options.map((v) => v.value);
2121
}
2222

23+
private toggleInvert() {
24+
const notSelected = this.options.filter((v) => !this.value.includes(v.value));
25+
this.value = notSelected.map((v) => v.value);
26+
}
27+
2328
private toggleValue() {
2429
if (this.value === undefined) {
2530
this.value = [];
@@ -43,6 +48,9 @@ export default class MultiSelectPrompt<T extends { value: any }> extends Prompt<
4348
if (char === 'a') {
4449
this.toggleAll();
4550
}
51+
if (char === 'i') {
52+
this.toggleInvert();
53+
}
4654
});
4755

4856
this.on('cursor', (key) => {
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import { cursor } from 'sisteransi';
2+
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
3+
import { default as MultiSelectPrompt } from '../../src/prompts/multi-select.js';
4+
import { MockReadable } from '../mock-readable.js';
5+
import { MockWritable } from '../mock-writable.js';
6+
7+
describe('MultiSelectPrompt', () => {
8+
let input: MockReadable;
9+
let output: MockWritable;
10+
11+
beforeEach(() => {
12+
input = new MockReadable();
13+
output = new MockWritable();
14+
});
15+
16+
afterEach(() => {
17+
vi.restoreAllMocks();
18+
});
19+
20+
test('renders render() result', () => {
21+
const instance = new MultiSelectPrompt({
22+
input,
23+
output,
24+
render: () => 'foo',
25+
options: [{ value: 'foo' }, { value: 'bar' }],
26+
});
27+
instance.prompt();
28+
expect(output.buffer).to.deep.equal([cursor.hide, 'foo']);
29+
});
30+
31+
describe('cursor', () => {
32+
test('cursor is index of selected item', () => {
33+
const instance = new MultiSelectPrompt({
34+
input,
35+
output,
36+
render: () => 'foo',
37+
options: [{ value: 'foo' }, { value: 'bar' }],
38+
});
39+
40+
instance.prompt();
41+
42+
expect(instance.cursor).to.equal(0);
43+
input.emit('keypress', 'down', { name: 'down' });
44+
expect(instance.cursor).to.equal(1);
45+
});
46+
47+
test('cursor loops around', () => {
48+
const instance = new MultiSelectPrompt({
49+
input,
50+
output,
51+
render: () => 'foo',
52+
options: [{ value: 'foo' }, { value: 'bar' }, { value: 'baz' }],
53+
});
54+
55+
instance.prompt();
56+
57+
expect(instance.cursor).to.equal(0);
58+
input.emit('keypress', 'up', { name: 'up' });
59+
expect(instance.cursor).to.equal(2);
60+
input.emit('keypress', 'down', { name: 'down' });
61+
expect(instance.cursor).to.equal(0);
62+
});
63+
64+
test('left behaves as up', () => {
65+
const instance = new MultiSelectPrompt({
66+
input,
67+
output,
68+
render: () => 'foo',
69+
options: [{ value: 'foo' }, { value: 'bar' }, { value: 'baz' }],
70+
});
71+
72+
instance.prompt();
73+
74+
input.emit('keypress', 'left', { name: 'left' });
75+
expect(instance.cursor).to.equal(2);
76+
});
77+
78+
test('right behaves as down', () => {
79+
const instance = new MultiSelectPrompt({
80+
input,
81+
output,
82+
render: () => 'foo',
83+
options: [{ value: 'foo' }, { value: 'bar' }],
84+
});
85+
86+
instance.prompt();
87+
88+
input.emit('keypress', 'left', { name: 'left' });
89+
expect(instance.cursor).to.equal(1);
90+
});
91+
92+
test('initial values is selected', () => {
93+
const instance = new MultiSelectPrompt({
94+
input,
95+
output,
96+
render: () => 'foo',
97+
options: [{ value: 'foo' }, { value: 'bar' }],
98+
initialValues: ['bar'],
99+
});
100+
instance.prompt();
101+
expect(instance.value).toEqual(['bar']);
102+
});
103+
104+
test('select all when press "a" key', () => {
105+
const instance = new MultiSelectPrompt({
106+
input,
107+
output,
108+
render: () => 'foo',
109+
options: [{ value: 'foo' }, { value: 'bar' }],
110+
});
111+
instance.prompt();
112+
113+
input.emit('keypress', 'down', { name: 'down' });
114+
input.emit('keypress', 'space', { name: 'space' });
115+
input.emit('keypress', 'a', { name: 'a' });
116+
expect(instance.value).toEqual(['foo', 'bar']);
117+
});
118+
119+
test('select invert when press "i" key', () => {
120+
const instance = new MultiSelectPrompt({
121+
input,
122+
output,
123+
render: () => 'foo',
124+
options: [{ value: 'foo' }, { value: 'bar' }],
125+
});
126+
instance.prompt();
127+
128+
input.emit('keypress', 'down', { name: 'down' });
129+
input.emit('keypress', 'space', { name: 'space' });
130+
input.emit('keypress', 'i', { name: 'i' });
131+
expect(instance.value).toEqual(['foo']);
132+
});
133+
});
134+
});

0 commit comments

Comments
 (0)