1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +01:00

Search keyboard shortcut (#1048)

* feat: search keyboard shortcut

* fix: search input placeholder snapshot update

* fix: update apple device recognition

Co-authored-by: Nuno Góis <github@nunogois.com>

* 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 <github@nunogois.com>
Co-authored-by: andreas-unleash <andreas@getunleash.ai>
Co-authored-by: olav <mail@olav.io>
This commit is contained in:
Tymoteusz Czech 2022-06-06 14:23:48 +02:00 committed by GitHub
parent 6dceedbd35
commit 53b12604b8
5 changed files with 124 additions and 7 deletions

View File

@ -14,7 +14,7 @@ interface ITableSearchProps {
export const TableSearch: FC<ITableSearchProps> = ({
initialValue,
onChange = () => {},
placeholder = 'Search',
placeholder,
hasFilters,
getSearchContext,
}) => {
@ -30,7 +30,7 @@ export const TableSearch: FC<ITableSearchProps> = ({
<TableSearchField
value={searchInputState!}
onChange={onSearchChange}
placeholder={`${placeholder}`}
placeholder={placeholder}
hasFilters={hasFilters}
getSearchContext={getSearchContext}
/>

View File

@ -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<HTMLInputElement>();
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 (
<div className={styles.container}>
@ -40,6 +55,7 @@ export const TableSearchField = ({
className={classnames(styles.searchIcon, 'search-icon')}
/>
<InputBase
inputRef={ref}
placeholder={placeholder}
classes={{
root: classnames(styles.inputRoot, 'input-container'),
@ -64,6 +80,7 @@ export const TableSearchField = ({
size="small"
onClick={() => {
onChange('');
ref.current?.focus();
}}
>
<Close className={styles.clearIcon} />

View File

@ -53,13 +53,13 @@ exports[`renders an empty list correctly 1`] = `
onClick={[Function]}
>
<input
aria-label="Search"
aria-label="Search (Ctrl+K)"
className="MuiInputBase-input mui-j79lc6-MuiInputBase-input"
onAnimationStart={[Function]}
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
placeholder="Search"
placeholder="Search (Ctrl+K)"
type="text"
value=""
/>

View File

@ -0,0 +1,24 @@
import { useEffect, useState } from 'react';
export const useIsAppleDevice = () => {
const [isAppleDevice, setIsAppleDevice] = useState<boolean>();
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;
};

View File

@ -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;
};