From 53b12604b80c54790fd4ec6ede90760c6f16ed0c Mon Sep 17 00:00:00 2001 From: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com> Date: Mon, 6 Jun 2022 14:23:48 +0200 Subject: [PATCH] Search keyboard shortcut (#1048) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: search keyboard shortcut * fix: search input placeholder snapshot update * fix: update apple device recognition Co-authored-by: Nuno Góis * refactor: return hotkey from useKeyboardShortcut * fix: don't close non-empty search field * Archive table new sort parameter * Revert "Archive table" This reverts commit 171806352c2a01ce439ce7bd77476797d544275c. * update search field focus * refactor: clarify hotkey description * fix: make variant payload text box multiline (#1060) * fix: make variant payload text box multiline * refactor: adjust min/max rows * refactor: use fixed number of rows to avoid MUI render loop bug * fix: toggle search on escape only in focused * fix: add hotkey to custom placeholders Co-authored-by: Nuno Góis Co-authored-by: andreas-unleash Co-authored-by: olav --- .../common/Table/TableSearch/TableSearch.tsx | 4 +- .../TableSearchField/TableSearchField.tsx | 23 +++++- .../__snapshots__/TagTypeList.test.tsx.snap | 4 +- frontend/src/hooks/useIsAppleDevice.ts | 24 ++++++ frontend/src/hooks/useKeyboardShortcut.ts | 76 +++++++++++++++++++ 5 files changed, 124 insertions(+), 7 deletions(-) create mode 100644 frontend/src/hooks/useIsAppleDevice.ts create mode 100644 frontend/src/hooks/useKeyboardShortcut.ts diff --git a/frontend/src/component/common/Table/TableSearch/TableSearch.tsx b/frontend/src/component/common/Table/TableSearch/TableSearch.tsx index e7d9a041ce..d47faef67b 100644 --- a/frontend/src/component/common/Table/TableSearch/TableSearch.tsx +++ b/frontend/src/component/common/Table/TableSearch/TableSearch.tsx @@ -14,7 +14,7 @@ interface ITableSearchProps { export const TableSearch: FC = ({ initialValue, onChange = () => {}, - placeholder = 'Search', + placeholder, hasFilters, getSearchContext, }) => { @@ -30,7 +30,7 @@ export const TableSearch: FC = ({ diff --git a/frontend/src/component/common/Table/TableSearch/TableSearchField/TableSearchField.tsx b/frontend/src/component/common/Table/TableSearch/TableSearchField/TableSearchField.tsx index 9581b25ec1..bfe6658e3d 100644 --- a/frontend/src/component/common/Table/TableSearch/TableSearchField/TableSearchField.tsx +++ b/frontend/src/component/common/Table/TableSearch/TableSearchField/TableSearchField.tsx @@ -1,17 +1,18 @@ +import { useRef, useState } from 'react'; import { IconButton, InputBase, Tooltip } from '@mui/material'; import { Search, Close } from '@mui/icons-material'; import classnames from 'classnames'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { useStyles } from './TableSearchField.styles'; import { TableSearchFieldSuggestions } from './TableSearchFieldSuggestions/TableSearchFieldSuggestions'; -import { useState } from 'react'; import { IGetSearchContextOutput } from 'hooks/useSearch'; +import { useKeyboardShortcut } from 'hooks/useKeyboardShortcut'; interface ITableSearchFieldProps { value: string; onChange: (value: string) => void; className?: string; - placeholder: string; + placeholder?: string; hasFilters?: boolean; getSearchContext?: () => IGetSearchContextOutput; } @@ -20,12 +21,26 @@ export const TableSearchField = ({ value = '', onChange, className, - placeholder, + placeholder: customPlaceholder, hasFilters, getSearchContext, }: ITableSearchFieldProps) => { + const ref = useRef(); const { classes: styles } = useStyles(); const [showSuggestions, setShowSuggestions] = useState(false); + const hotkey = useKeyboardShortcut( + { modifiers: ['ctrl'], key: 'k', preventDefault: true }, + () => { + ref.current?.focus(); + setShowSuggestions(true); + } + ); + useKeyboardShortcut({ key: 'Escape' }, () => { + if (document.activeElement === ref.current) { + setShowSuggestions(suggestions => !suggestions); + } + }); + const placeholder = `${customPlaceholder ?? 'Search'} (${hotkey})`; return (
@@ -40,6 +55,7 @@ export const TableSearchField = ({ className={classnames(styles.searchIcon, 'search-icon')} /> { onChange(''); + ref.current?.focus(); }} > diff --git a/frontend/src/component/tags/TagTypeList/__tests__/__snapshots__/TagTypeList.test.tsx.snap b/frontend/src/component/tags/TagTypeList/__tests__/__snapshots__/TagTypeList.test.tsx.snap index 1a967eb47b..c078905686 100644 --- a/frontend/src/component/tags/TagTypeList/__tests__/__snapshots__/TagTypeList.test.tsx.snap +++ b/frontend/src/component/tags/TagTypeList/__tests__/__snapshots__/TagTypeList.test.tsx.snap @@ -53,13 +53,13 @@ exports[`renders an empty list correctly 1`] = ` onClick={[Function]} > diff --git a/frontend/src/hooks/useIsAppleDevice.ts b/frontend/src/hooks/useIsAppleDevice.ts new file mode 100644 index 0000000000..cd968c9f87 --- /dev/null +++ b/frontend/src/hooks/useIsAppleDevice.ts @@ -0,0 +1,24 @@ +import { useEffect, useState } from 'react'; + +export const useIsAppleDevice = () => { + const [isAppleDevice, setIsAppleDevice] = useState(); + + useEffect(() => { + const platform = + ( + navigator as unknown as { + userAgentData: { platform: string }; + } + )?.userAgentData?.platform || + navigator?.platform || + 'unknown'; + + setIsAppleDevice( + platform.toLowerCase().includes('mac') || + platform === 'iPhone' || + platform === 'iPad' + ); + }, []); + + return isAppleDevice; +}; diff --git a/frontend/src/hooks/useKeyboardShortcut.ts b/frontend/src/hooks/useKeyboardShortcut.ts new file mode 100644 index 0000000000..421845624b --- /dev/null +++ b/frontend/src/hooks/useKeyboardShortcut.ts @@ -0,0 +1,76 @@ +import { useEffect, useMemo } from 'react'; +import { useIsAppleDevice } from './useIsAppleDevice'; + +export const useKeyboardShortcut = ( + { + key, + modifiers = [], + preventDefault = false, + }: { + key: string; + modifiers?: Array<'ctrl' | 'alt' | 'shift'>; + preventDefault?: boolean; + }, + callback: () => void +) => { + const isAppleDevice = useIsAppleDevice(); + useEffect(() => { + const onKeyDown = (event: KeyboardEvent) => { + if (key !== event.key) { + return; + } + if (modifiers.includes('ctrl')) { + if (isAppleDevice) { + if (!event.metaKey) { + return; + } + } else { + if (!event.ctrlKey) { + return; + } + } + } + if (modifiers.includes('alt') && !event.altKey) { + return; + } + if (modifiers.includes('shift') && !event.shiftKey) { + return; + } + if (preventDefault) { + event.preventDefault(); + } + + callback(); + }; + + window.addEventListener('keydown', onKeyDown); + + return () => { + window.removeEventListener('keydown', onKeyDown); + }; + }, [isAppleDevice, key, modifiers, preventDefault, callback]); + + const formattedModifiers = useMemo( + () => + modifiers.map( + modifier => + ({ + ctrl: isAppleDevice ? '⌘' : 'Ctrl', + alt: 'Alt', + shift: 'Shift', + }[modifier]) + ), + [isAppleDevice, modifiers] + ); + + const hotkeyDescription = useMemo( + () => + [ + ...formattedModifiers, + `${key[0].toUpperCase()}${key.slice(1)}`, + ].join('+'), + [formattedModifiers, key] + ); + + return hotkeyDescription; +};