diff --git a/.changeset/olive-donuts-switch.md b/.changeset/olive-donuts-switch.md new file mode 100644 index 00000000..b11b2923 --- /dev/null +++ b/.changeset/olive-donuts-switch.md @@ -0,0 +1,5 @@ +--- +"@clack/core": patch +--- + +Treat an ideographic space (U+3000) committed by IMEs (e.g. Japanese or Chinese input) as space when toggling options in multi-select prompts. diff --git a/packages/core/src/prompts/group-multiselect.ts b/packages/core/src/prompts/group-multiselect.ts index e5496728..f68e0fe5 100644 --- a/packages/core/src/prompts/group-multiselect.ts +++ b/packages/core/src/prompts/group-multiselect.ts @@ -67,6 +67,13 @@ export default class GroupMultiSelectPrompt extends Pr this.#selectableGroups ? 0 : 1 ); + this.on('key', (char, key) => { + // Treat ideographic spaces as if they were normal spaces. + if (key.name === undefined && char === ' ') { + this.toggleValue(); + } + }); + this.on('cursor', (key) => { switch (key) { case 'left': diff --git a/packages/core/src/prompts/multi-select.ts b/packages/core/src/prompts/multi-select.ts index 88d76a8e..dd590518 100644 --- a/packages/core/src/prompts/multi-select.ts +++ b/packages/core/src/prompts/multi-select.ts @@ -60,13 +60,19 @@ export default class MultiSelectPrompt extends Prompt(cursor, 1, this.options) : cursor; - this.on('key', (_char, key) => { + this.on('key', (char, key) => { if (key.name === 'a') { this.toggleAll(); } if (key.name === 'i') { this.toggleInvert(); } + // Some IMEs (e.g. Japanese or Chinese input) commit an ideographic space (U+3000) + // with no key name when the space key is pressed. Treat it as space so + // toggling options still works. + if (key.name === undefined && char === ' ') { + this.toggleValue(); + } }); this.on('cursor', (key) => { diff --git a/packages/core/test/prompts/group-multiselect.test.ts b/packages/core/test/prompts/group-multiselect.test.ts index fa370f7f..755bbf0c 100644 --- a/packages/core/test/prompts/group-multiselect.test.ts +++ b/packages/core/test/prompts/group-multiselect.test.ts @@ -30,6 +30,24 @@ describe('GroupMultiSelectPrompt', () => { expect(output.buffer).to.deep.equal([cursor.hide, 'foo']); }); + test('toggles option with ideographic space (U+3000) committed by IME', () => { + const instance = new GroupMultiSelectPrompt({ + input, + output, + render: () => 'foo', + options: { + group: [{ value: 'foo' }, { value: 'bar' }], + }, + selectableGroups: false, + }); + instance.prompt(); + + input.emit('keypress', ' ', { sequence: '\u3000' }); + expect(instance.value).toEqual(['foo']); + input.emit('keypress', ' ', { sequence: '\u3000' }); + expect(instance.value).toEqual([]); + }); + test('does not throw if empty options are provided', () => { const instance = new GroupMultiSelectPrompt({ input, diff --git a/packages/core/test/prompts/multi-select.test.ts b/packages/core/test/prompts/multi-select.test.ts index e8af630d..edef73e4 100644 --- a/packages/core/test/prompts/multi-select.test.ts +++ b/packages/core/test/prompts/multi-select.test.ts @@ -143,6 +143,21 @@ describe('MultiSelectPrompt', () => { expect(instance.value).toEqual(['foo']); }); + test('toggles option with ideographic space (U+3000) committed by IME', () => { + const instance = new MultiSelectPrompt({ + input, + output, + render: () => 'foo', + options: [{ value: 'foo' }, { value: 'bar' }], + }); + instance.prompt(); + + input.emit('keypress', ' ', { sequence: '\u3000' }); + expect(instance.value).toEqual(['foo']); + input.emit('keypress', ' ', { sequence: '\u3000' }); + expect(instance.value).toEqual([]); + }); + test('disabled options are skipped', () => { const instance = new MultiSelectPrompt({ input,