diff --git a/frontend/package.json b/frontend/package.json index c0983d65a5..522e135697 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -78,7 +78,7 @@ "lodash.clonedeep": "4.5.0", "msw": "0.42.0", "pkginfo": "^0.4.1", - "plausible-tracker": "^0.3.5", + "plausible-tracker": "0.3.7", "prettier": "2.6.2", "prop-types": "15.8.1", "react": "17.0.2", @@ -94,11 +94,11 @@ "swr": "1.3.0", "tss-react": "3.7.0", "typescript": "4.7.3", - "vite": "2.9.9", + "vite": "2.9.10", "vite-plugin-env-compatible": "^1.1.1", "vite-plugin-svgr": "2.1.0", "vite-tsconfig-paths": "3.5.0", - "vitest": "0.14.0", + "vitest": "0.14.1", "whatwg-fetch": "^3.6.2" }, "jest": { 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; +}; diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 8c95bc34fd..4cab69194f 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -5041,10 +5041,10 @@ pkginfo@^0.4.1: resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.4.1.tgz#b5418ef0439de5425fc4995042dced14fb2a84ff" integrity sha1-tUGO8EOd5UJfxJlQQtztFPsqhP8= -plausible-tracker@^0.3.5: - version "0.3.5" - resolved "https://registry.yarnpkg.com/plausible-tracker/-/plausible-tracker-0.3.5.tgz#49c09a7eb727f1d5c859c3fc8072837b13ee9b85" - integrity sha512-6c6VPdPtI9KmIsfr8zLBViIDMt369eeaNA1J8JrAmAtrpSkeJWvjwcJ+cLn7gVJn5AtQWC8NgSEee2d/5RNytA== +plausible-tracker@0.3.7: + version "0.3.7" + resolved "https://registry.yarnpkg.com/plausible-tracker/-/plausible-tracker-0.3.7.tgz#e93d241a1c46bf70f05317a6f177feff6c4865d5" + integrity sha512-yhM3VJekqMIEDbvx/PqratMyHpF4T/skTO4owzxSo3YMB/ZVmAYwh9c2iKRuJnkE3b4NwsMMW9b0Vw5VD5Gpyw== postcss@^8.4.13: version "8.4.13" @@ -6028,7 +6028,19 @@ vite-tsconfig-paths@3.5.0: recrawl-sync "^2.0.3" tsconfig-paths "^4.0.0" -vite@2.9.9, vite@^2.9.9: +vite@2.9.10: + version "2.9.10" + resolved "https://registry.yarnpkg.com/vite/-/vite-2.9.10.tgz#f574d96655622c2e0fbc662edd0ed199c60fe91a" + integrity sha512-TwZRuSMYjpTurLqXspct+HZE7ONiW9d+wSWgvADGxhDPPyoIcNywY+RX4ng+QpK30DCa1l/oZgi2PLZDibhzbQ== + dependencies: + esbuild "^0.14.27" + postcss "^8.4.13" + resolve "^1.22.0" + rollup "^2.59.0" + optionalDependencies: + fsevents "~2.3.2" + +vite@^2.9.9: version "2.9.9" resolved "https://registry.yarnpkg.com/vite/-/vite-2.9.9.tgz#8b558987db5e60fedec2f4b003b73164cb081c5e" integrity sha512-ffaam+NgHfbEmfw/Vuh6BHKKlI/XIAhxE5QSS7gFLIngxg171mg1P3a4LSRME0z2ZU1ScxoKzphkipcYwSD5Ew== @@ -6040,10 +6052,10 @@ vite@2.9.9, vite@^2.9.9: optionalDependencies: fsevents "~2.3.2" -vitest@0.14.0: - version "0.14.0" - resolved "https://registry.yarnpkg.com/vitest/-/vitest-0.14.0.tgz#8fe92608d5379972b8f5a2424187fc4a3e453cd5" - integrity sha512-3Ns7c6TindS6OmweY4dU7sWT7g/YJBenwsMdUMEO/oS81bzoCEaA58vlg/9U1WRjrNFwQYsuyISehAvCKwoMZQ== +vitest@0.14.1: + version "0.14.1" + resolved "https://registry.yarnpkg.com/vitest/-/vitest-0.14.1.tgz#f2fd8b31abdbbadb9ee895f8fde35a068ea2a5f5" + integrity sha512-2UUm6jYgkwh7Y3VKSRR8OuaNCm+iA5LPDnal7jyITN39maZK9L+JVxqjtQ39PSFo5Fl3/BgaJvER6GGHX9JLxg== dependencies: "@types/chai" "^4.3.1" "@types/chai-subset" "^1.3.3"