Typing while the emoji picker is up should enter search mode

This commit is contained in:
Jordan Rose 2022-08-22 16:31:35 -07:00 committed by GitHub
parent e9f4e28b3d
commit a52bb25731
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 65 additions and 30 deletions

View file

@ -54,7 +54,7 @@ export const ReactionPicker = React.forwardRef<HTMLDivElement, Props>(
// Handle escape key // Handle escape key
React.useEffect(() => { React.useEffect(() => {
const handler = (e: KeyboardEvent) => { const handler = (e: KeyboardEvent) => {
if (onClose && e.key === 'Escape') { if (onClose && e.key === 'Escape' && !pickingOther) {
onClose(); onClose();
} }
}; };
@ -64,7 +64,7 @@ export const ReactionPicker = React.forwardRef<HTMLDivElement, Props>(
return () => { return () => {
document.removeEventListener('keydown', handler); document.removeEventListener('keydown', handler);
}; };
}, [onClose]); }, [onClose, pickingOther]);
// Handle EmojiPicker::onPickEmoji // Handle EmojiPicker::onPickEmoji
const onPickEmoji: EmojiPickerProps['onPickEmoji'] = React.useCallback( const onPickEmoji: EmojiPickerProps['onPickEmoji'] = React.useCallback(

View file

@ -23,6 +23,7 @@ import FocusTrap from 'focus-trap-react';
import { Emoji } from './Emoji'; import { Emoji } from './Emoji';
import { dataByCategory, search } from './lib'; import { dataByCategory, search } from './lib';
import type { LocalizerType } from '../../types/Util'; import type { LocalizerType } from '../../types/Util';
import { isSingleGrapheme } from '../../util/grapheme';
export type EmojiPickDataType = { export type EmojiPickDataType = {
skinTone?: number; skinTone?: number;
@ -152,36 +153,46 @@ export const EmojiPicker = React.memo(
[doSend, onPickEmoji, selectedTone] [doSend, onPickEmoji, selectedTone]
); );
// Handle escape key // Handle key presses, particularly Escape
React.useEffect(() => { React.useEffect(() => {
const handler = (event: KeyboardEvent) => { const handler = (event: KeyboardEvent) => {
if (searchMode && event.key === 'Escape') { if (event.key === 'Escape') {
if (searchMode) {
setScrollToRow(0); setScrollToRow(0);
setSearchText(''); setSearchText('');
setSearchMode(false); setSearchMode(false);
} else {
onClose?.();
}
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
} else if ( } else if (!searchMode && !event.ctrlKey && !event.metaKey) {
!searchMode && if (
!event.ctrlKey && [
![
'ArrowUp', 'ArrowUp',
'ArrowDown', 'ArrowDown',
'ArrowLeft', 'ArrowLeft',
'ArrowRight', 'ArrowRight',
'Enter',
'Shift', 'Shift',
'Tab', 'Tab',
' ', // Space ' ', // Space
].includes(event.key) ].includes(event.key)
) { ) {
if (onClose) { // Do nothing, these can be used to navigate around the picker.
onClose(); } else if (isSingleGrapheme(event.key)) {
} // A single grapheme means the user is typing text. Switch to search mode.
setSelectedCategory(categories[0]);
setSearchMode(true);
// Continue propagation, typing the first letter for search.
} else {
// For anything else, assume it's a special key that isn't one of the ones
// above (such as Delete or ContextMenu).
onClose?.();
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
} }
}
}; };
document.addEventListener('keydown', handler); document.addEventListener('keydown', handler);
@ -189,7 +200,7 @@ export const EmojiPicker = React.memo(
return () => { return () => {
document.removeEventListener('keydown', handler); document.removeEventListener('keydown', handler);
}; };
}, [onClose, searchMode]); }, [onClose, searchMode, setSearchMode]);
const [, ...renderableCategories] = categories; const [, ...renderableCategories] = categories;

View file

@ -1,9 +1,9 @@
// Copyright 2021 Signal Messenger, LLC // Copyright 2021-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai'; import { assert } from 'chai';
import { getGraphemes, count } from '../../util/grapheme'; import { getGraphemes, count, isSingleGrapheme } from '../../util/grapheme';
describe('grapheme utilities', () => { describe('grapheme utilities', () => {
describe('getGraphemes', () => { describe('getGraphemes', () => {
@ -63,4 +63,20 @@ describe('grapheme utilities', () => {
assert.strictEqual(count('L̷̳͔̲͝Ģ̵̮̯̤̩̙͍̬̟͉̹̘̹͍͈̮̦̰̣͟͝O̶̴̮̻̮̗͘͡!̴̷̟͓͓'), 4); assert.strictEqual(count('L̷̳͔̲͝Ģ̵̮̯̤̩̙͍̬̟͉̹̘̹͍͈̮̦̰̣͟͝O̶̴̮̻̮̗͘͡!̴̷̟͓͓'), 4);
}); });
}); });
describe('isSingleGrapheme', () => {
it('returns false for the empty string', () => {
assert.isFalse(isSingleGrapheme(''));
});
it('returns true for single graphemes', () => {
assert.isTrue(isSingleGrapheme('a'));
assert.isTrue(isSingleGrapheme('å'));
assert.isTrue(isSingleGrapheme('😍'));
});
it('returns false for multiple graphemes', () => {
assert.isFalse(isSingleGrapheme('ab'));
assert.isFalse(isSingleGrapheme('a😍'));
assert.isFalse(isSingleGrapheme('😍a'));
});
});
}); });

View file

@ -1,4 +1,4 @@
// Copyright 2021 Signal Messenger, LLC // Copyright 2021-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { map, size } from './iterables'; import { map, size } from './iterables';
@ -12,3 +12,11 @@ export function count(str: string): number {
const segments = new Intl.Segmenter().segment(str); const segments = new Intl.Segmenter().segment(str);
return size(segments); return size(segments);
} }
export function isSingleGrapheme(str: string): boolean {
if (str === '') {
return false;
}
const segments = new Intl.Segmenter().segment(str);
return segments.containing(0).segment === str;
}