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:
parent
6dceedbd35
commit
53b12604b8
@ -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}
|
||||
/>
|
||||
|
@ -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} />
|
||||
|
@ -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=""
|
||||
/>
|
||||
|
24
frontend/src/hooks/useIsAppleDevice.ts
Normal file
24
frontend/src/hooks/useIsAppleDevice.ts
Normal 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;
|
||||
};
|
76
frontend/src/hooks/useKeyboardShortcut.ts
Normal file
76
frontend/src/hooks/useKeyboardShortcut.ts
Normal 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;
|
||||
};
|
Loading…
Reference in New Issue
Block a user