Typing while the emoji picker is up should enter search mode
This commit is contained in:
parent
e9f4e28b3d
commit
a52bb25731
4 changed files with 65 additions and 30 deletions
|
@ -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(
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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'));
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue