Keyboard shortcuts and accessibility
This commit is contained in:
parent
8590a047c7
commit
20a892247f
87 changed files with 3652 additions and 711 deletions
|
@ -19,15 +19,11 @@ export type OwnProps = {
|
|||
export type Props = OwnProps &
|
||||
Pick<
|
||||
EmojiPickerProps,
|
||||
| 'onClose'
|
||||
| 'doSend'
|
||||
| 'onPickEmoji'
|
||||
| 'onSetSkinTone'
|
||||
| 'recentEmojis'
|
||||
| 'skinTone'
|
||||
'doSend' | 'onPickEmoji' | 'onSetSkinTone' | 'recentEmojis' | 'skinTone'
|
||||
>;
|
||||
|
||||
export const EmojiButton = React.memo(
|
||||
// tslint:disable-next-line:max-func-body-length
|
||||
({
|
||||
i18n,
|
||||
doSend,
|
||||
|
@ -35,7 +31,6 @@ export const EmojiButton = React.memo(
|
|||
skinTone,
|
||||
onSetSkinTone,
|
||||
recentEmojis,
|
||||
onClose,
|
||||
}: Props) => {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [popperRoot, setPopperRoot] = React.useState<HTMLElement | null>(
|
||||
|
@ -56,9 +51,8 @@ export const EmojiButton = React.memo(
|
|||
const handleClose = React.useCallback(
|
||||
() => {
|
||||
setOpen(false);
|
||||
onClose();
|
||||
},
|
||||
[setOpen, onClose]
|
||||
[setOpen]
|
||||
);
|
||||
|
||||
// Create popper root and handle outside clicks
|
||||
|
@ -71,7 +65,6 @@ export const EmojiButton = React.memo(
|
|||
const handleOutsideClick = ({ target }: MouseEvent) => {
|
||||
if (!root.contains(target as Node)) {
|
||||
setOpen(false);
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
document.addEventListener('click', handleOutsideClick);
|
||||
|
@ -88,6 +81,29 @@ export const EmojiButton = React.memo(
|
|||
[open, setOpen, setPopperRoot]
|
||||
);
|
||||
|
||||
// Install keyboard shortcut to open emoji picker
|
||||
React.useEffect(
|
||||
() => {
|
||||
const handleKeydown = (event: KeyboardEvent) => {
|
||||
const { ctrlKey, key, metaKey, shiftKey } = event;
|
||||
const ctrlOrCommand = metaKey || ctrlKey;
|
||||
|
||||
if (ctrlOrCommand && shiftKey && key === 'e') {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
setOpen(!open);
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', handleKeydown);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeydown);
|
||||
};
|
||||
},
|
||||
[open, setOpen]
|
||||
);
|
||||
|
||||
return (
|
||||
<Manager>
|
||||
<Reference>
|
||||
|
|
|
@ -33,7 +33,7 @@ export type OwnProps = {
|
|||
|
||||
export type Props = OwnProps & Pick<React.HTMLProps<HTMLDivElement>, 'style'>;
|
||||
|
||||
function focusRef(el: HTMLElement | null) {
|
||||
function focusOnRender(el: HTMLElement | null) {
|
||||
if (el) {
|
||||
el.focus();
|
||||
}
|
||||
|
@ -69,6 +69,7 @@ export const EmojiPicker = React.memo(
|
|||
}: Props,
|
||||
ref
|
||||
) => {
|
||||
const focusRef = React.useRef<HTMLButtonElement>(null);
|
||||
// Per design: memoize the initial recent emojis so the grid only updates after re-opening the picker.
|
||||
const firstRecent = React.useMemo(() => {
|
||||
return recentEmojis;
|
||||
|
@ -140,11 +141,14 @@ export const EmojiPicker = React.memo(
|
|||
// Handle escape key
|
||||
React.useEffect(
|
||||
() => {
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
if (searchMode && e.key === 'Escape') {
|
||||
const handler = (event: KeyboardEvent) => {
|
||||
if (searchMode && event.key === 'Escape') {
|
||||
setSearchText('');
|
||||
setSearchMode(false);
|
||||
setScrollToRow(0);
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
} else if (
|
||||
!searchMode &&
|
||||
![
|
||||
|
@ -155,21 +159,38 @@ export const EmojiPicker = React.memo(
|
|||
'Shift',
|
||||
'Tab',
|
||||
' ', // Space
|
||||
].includes(e.key)
|
||||
].includes(event.key)
|
||||
) {
|
||||
onClose();
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keyup', handler);
|
||||
document.addEventListener('keydown', handler);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keyup', handler);
|
||||
document.removeEventListener('keydown', handler);
|
||||
};
|
||||
},
|
||||
[onClose, searchMode]
|
||||
);
|
||||
|
||||
// Restore focus on teardown
|
||||
React.useEffect(() => {
|
||||
const lastFocused = document.activeElement as any;
|
||||
if (focusRef.current) {
|
||||
focusRef.current.focus();
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (lastFocused && lastFocused.focus) {
|
||||
lastFocused.focus();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const emojiGrid = React.useMemo(
|
||||
() => {
|
||||
if (searchText) {
|
||||
|
@ -287,6 +308,7 @@ export const EmojiPicker = React.memo(
|
|||
<div className="module-emoji-picker" ref={ref} style={style}>
|
||||
<header className="module-emoji-picker__header">
|
||||
<button
|
||||
ref={focusRef}
|
||||
onClick={handleToggleSearch}
|
||||
title={i18n('EmojiPicker--search-placeholder')}
|
||||
className={classNames(
|
||||
|
@ -300,7 +322,7 @@ export const EmojiPicker = React.memo(
|
|||
{searchMode ? (
|
||||
<div className="module-emoji-picker__header__search-field">
|
||||
<input
|
||||
ref={focusRef}
|
||||
ref={focusOnRender}
|
||||
className="module-emoji-picker__header__search-field__input"
|
||||
placeholder={i18n('EmojiPicker--search-placeholder')}
|
||||
onChange={handleSearchChange}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue