Skip to content

Commit

Permalink
fix: stop processing input when composing with an IME (#1226)
Browse files Browse the repository at this point in the history
* fix: stop processing input when composing with an IME

* add test

* prevent processing keydown if composition is in progress

* fix: make it work for both React and default renderer

---------

Co-authored-by: Aymeric Giraudet <aymeric.giraudet@algolia.com>
  • Loading branch information
dhayab and aymeric-giraudet committed Jan 3, 2024
1 parent bb80dbb commit 7f5ba08
Show file tree
Hide file tree
Showing 7 changed files with 164 additions and 1 deletion.
132 changes: 131 additions & 1 deletion packages/autocomplete-core/src/__tests__/getInputProps.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { waitFor } from '@testing-library/dom';
import { fireEvent, waitFor } from '@testing-library/dom';
import userEvent from '@testing-library/user-event';

import {
Expand Down Expand Up @@ -645,6 +645,66 @@ describe('getInputProps', () => {

expect(environment.clearTimeout).toHaveBeenLastCalledWith(999);
});

test('stops process if IME composition is in progress', () => {
const getSources = jest.fn((..._args: any[]) => {
return [
createSource({
getItems() {
return [{ label: '1' }, { label: '2' }];
},
}),
];
});
const { inputElement } = createPlayground(createAutocomplete, {
getSources,
});

// Typing 木 using the Wubihua input method
// see:
// - https://en.wikipedia.org/wiki/Stroke_count_method
// - https://developer.mozilla.org/en-US/docs/Web/API/Element/compositionend_event
const character = '木';
const strokes = ['一', '丨', '丿', '丶', character];

strokes.forEach((stroke, index) => {
const isFirst = index === 0;
const isLast = index === strokes.length - 1;
const query = isLast ? stroke : strokes.slice(0, index + 1).join('');

if (isFirst) {
fireEvent.compositionStart(inputElement);
}

fireEvent.compositionUpdate(inputElement, {
data: query,
});

fireEvent.input(inputElement, {
isComposing: true,
target: {
value: query,
},
});

if (isLast) {
fireEvent.compositionEnd(inputElement, {
data: query,
target: {
value: query,
},
});
}
});

expect(inputElement).toHaveValue(character);
expect(getSources).toHaveBeenCalledTimes(1);
expect(getSources).toHaveBeenLastCalledWith(
expect.objectContaining({
query: character,
})
);
});
});

describe('onKeyDown', () => {
Expand Down Expand Up @@ -1913,6 +1973,76 @@ describe('getInputProps', () => {
);
});
});

test('stops process if IME is in progress', () => {
const onStateChange = jest.fn();
const { inputElement } = createPlayground(createAutocomplete, {
openOnFocus: true,
onStateChange,
initialState: {
collections: [
createCollection({
source: { sourceId: 'testSource' },
items: [
{ label: '1' },
{ label: '2' },
{ label: '3' },
{ label: '4' },
],
}),
],
},
});

inputElement.focus();

// 1. Pressing Arrow Down to select the first item
fireEvent.keyDown(inputElement, { key: 'ArrowDown' });
expect(onStateChange).toHaveBeenLastCalledWith(
expect.objectContaining({
state: expect.objectContaining({
activeItemId: 0,
}),
})
);

// 2. Typing かくてい with a Japanese IME
const strokes = ['か', 'く', 'て', 'い'];
strokes.forEach((_stroke, index) => {
const isFirst = index === 0;
const query = strokes.slice(0, index + 1).join('');

if (isFirst) {
fireEvent.compositionStart(inputElement);
}

fireEvent.compositionUpdate(inputElement, {
data: query,
});

fireEvent.input(inputElement, {
isComposing: true,
data: query,
target: {
value: query,
},
});
});

// 3. Selecting the 3rd suggestion on the IME window
fireEvent.keyDown(inputElement, { key: 'ArrowDown', isComposing: true });
fireEvent.keyDown(inputElement, { key: 'ArrowDown', isComposing: true });
fireEvent.keyDown(inputElement, { key: 'ArrowDown', isComposing: true });

// 4. Checking that activeItemId has not changed
expect(onStateChange).toHaveBeenLastCalledWith(
expect.objectContaining({
state: expect.objectContaining({
activeItemId: 0,
}),
})
);
});
});

describe('onFocus', () => {
Expand Down
24 changes: 24 additions & 0 deletions packages/autocomplete-core/src/getPropGetters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
getAutocompleteElementId,
isOrContainsNode,
isSamsung,
getNativeEvent,
} from './utils';

interface GetPropGettersOptions<TItem extends BaseItem>
Expand Down Expand Up @@ -219,6 +220,25 @@ export function getPropGetters<
maxLength,
type: 'search',
onChange: (event) => {
const value = (
(event as unknown as Event).currentTarget as HTMLInputElement
).value;

if (getNativeEvent(event as unknown as InputEvent).isComposing) {
setters.setQuery(value);
return;
}

onInput({
event,
props,
query: value.slice(0, maxLength),
refresh,
store,
...setters,
});
},
onCompositionEnd: (event) => {
onInput({
event,
props,
Expand All @@ -231,6 +251,10 @@ export function getPropGetters<
});
},
onKeyDown: (event) => {
if (getNativeEvent(event as unknown as InputEvent).isComposing) {
return;
}

onKeyDown({
event: event as unknown as KeyboardEvent,
props,
Expand Down
3 changes: 3 additions & 0 deletions packages/autocomplete-core/src/utils/getNativeEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function getNativeEvent<TEvent>(event: TEvent) {
return (event as unknown as { nativeEvent: TEvent }).nativeEvent || event;
}
1 change: 1 addition & 0 deletions packages/autocomplete-core/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export * from './getAutocompleteElementId';
export * from './isOrContainsNode';
export * from './isSamsung';
export * from './mapToAlgoliaResponse';
export * from './getNativeEvent';
3 changes: 3 additions & 0 deletions packages/autocomplete-js/src/utils/setProperties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@ function getNormalizedName(name: string): string {
switch (name) {
case 'onChange':
return 'onInput';
// see: https://github.com/preactjs/preact/issues/1978
case 'onCompositionEnd':
return 'oncompositionend';
default:
return name;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export type GetInputProps<TEvent, TMouseEvent, TKeyboardEvent> = (props: {
'aria-controls': string | undefined;
'aria-labelledby': string;
onChange(event: TEvent): void;
onCompositionEnd(event: TEvent): void;
onKeyDown(event: TKeyboardEvent): void;
onFocus(event: TEvent): void;
onBlur(): void;
Expand Down
1 change: 1 addition & 0 deletions test/utils/createPlayground.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export function createPlayground<TItem extends Record<string, unknown>>(
const formProps = autocomplete.getFormProps({ inputElement });
inputElement.addEventListener('blur', inputProps.onBlur);
inputElement.addEventListener('input', inputProps.onChange);
inputElement.addEventListener('compositionend', inputProps.onCompositionEnd);
inputElement.addEventListener('click', inputProps.onClick);
inputElement.addEventListener('focus', inputProps.onFocus);
inputElement.addEventListener('keydown', inputProps.onKeyDown);
Expand Down

0 comments on commit 7f5ba08

Please sign in to comment.