From f2bc46b5a3a83139d75099caacdb07c3d8b77b97 Mon Sep 17 00:00:00 2001 From: kai-natori <129699611+kai-natori@users.noreply.github.com> Date: Tue, 22 Sep 2026 23:55:17 +0800 Subject: [PATCH] fix: treat IME-committed ideographic space as the space key (#608) Co-authored-by: James Garbutt <43081j@users.noreply.github.com> --- .changeset/olive-donuts-switch.md | 5 +++++ packages/core/src/prompts/group-multiselect.ts | 7 +++++++ packages/core/src/prompts/multi-select.ts | 8 +++++++- .../test/prompts/group-multiselect.test.ts | 18 ++++++++++++++++++ .../core/test/prompts/multi-select.test.ts | 15 +++++++++++++++ 5 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 .changeset/olive-donuts-switch.md diff --git a/.changeset/olive-donuts-switch.md b/.changeset/olive-donuts-switch.md new file mode 100644 index 0000000..b11b292 --- /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 e549672..f68e0fe 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 88d76a8..dd59051 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 fa370f7..755bbf0 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 e8af630..edef73e 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, -- knotmirror 0.0.1