Add keyboard shortcut to support quick selecting recent searches
Summary: Currently there's no way of quickly selecting recent searches other than manually opening the recent search history and then clicking one of the search terms. This diff seeks to add a new feature that would allow not only a keyboard short cut to open the recent search history drop down but also number the most recent 5 search terms so that the user could quickly select them with a number on their keyboard Additionally, fixed bug found in terms of the search bar not showing the current search value correctly `Changelog`: Introduced keyboard shortcut(ctrl + f) option to toggle the search history dropdown along with numbers attached to the options in order to quickly navigate to recent search terms. Have to first enable the option(search shortcut) in menu in order to use the feature. Also added a new button in the options menu that would trigger the search result toggle as triggered by the keyboard shortcut `ctrl` before(`ctrl` + `t` now) WARNING: The current behavior of "ctrl" toggling back and forth to focus the selected item has been migrated to "ctrl + t" key combo Reviewed By: mweststrate Differential Revision: D37685738 fbshipit-source-id: a7ac4dd3dceb846a98258de2d884ebc279ee5995
This commit is contained in:
committed by
Facebook GitHub Bot
parent
07e1a856bb
commit
499275af8a
@@ -46,7 +46,7 @@ import {TableSearch} from './TableSearch';
|
||||
import styled from '@emotion/styled';
|
||||
import {theme} from '../theme';
|
||||
import {tableContextMenuFactory} from './TableContextMenu';
|
||||
import {Menu, Switch, Typography} from 'antd';
|
||||
import {Menu, Switch, InputRef, Typography} from 'antd';
|
||||
import {CoffeeOutlined, SearchOutlined, PushpinFilled} from '@ant-design/icons';
|
||||
import {useAssertStableRef} from '../../utils/useAssertStableRef';
|
||||
import {Formatter} from '../DataFormatter';
|
||||
@@ -176,6 +176,7 @@ export function DataTable<T extends object>(
|
||||
|
||||
const stateRef = useRef(tableState);
|
||||
stateRef.current = tableState;
|
||||
const searchInputRef = useRef<InputRef>(null) as MutableRefObject<InputRef>;
|
||||
const lastOffset = useRef(0);
|
||||
const dragging = useRef(false);
|
||||
|
||||
@@ -300,6 +301,7 @@ export function DataTable<T extends object>(
|
||||
let handled = true;
|
||||
const shiftPressed = e.shiftKey;
|
||||
const outputSize = dataView.size;
|
||||
const controlPressed = e.ctrlKey;
|
||||
const windowSize = props.scrollable
|
||||
? virtualizerRef.current?.virtualItems.length ?? 0
|
||||
: dataView.size;
|
||||
@@ -341,12 +343,21 @@ export function DataTable<T extends object>(
|
||||
case 'Escape':
|
||||
tableManager.clearSelection();
|
||||
break;
|
||||
case 'Control':
|
||||
tableManager.toggleSearchValue();
|
||||
case 't':
|
||||
if (controlPressed) {
|
||||
tableManager.toggleSearchValue();
|
||||
}
|
||||
break;
|
||||
case 'H':
|
||||
tableManager.toggleHighlightSearch();
|
||||
break;
|
||||
case 'f':
|
||||
if (controlPressed && searchInputRef?.current) {
|
||||
searchInputRef?.current.focus();
|
||||
tableManager.showSearchDropdown(true);
|
||||
tableManager.setShowNumberedHistory(true);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
handled = false;
|
||||
}
|
||||
@@ -580,10 +591,13 @@ export function DataTable<T extends object>(
|
||||
searchValue={searchValue}
|
||||
useRegex={tableState.useRegex}
|
||||
filterSearchHistory={tableState.filterSearchHistory}
|
||||
showHistory={tableState.showSearchHistory}
|
||||
showNumbered={tableState.showNumberedHistory}
|
||||
dispatch={dispatch as any}
|
||||
searchHistory={tableState.searchHistory}
|
||||
contextMenu={props.enableContextMenu ? contexMenu : undefined}
|
||||
extraActions={!props.viewId ? props.extraActions : undefined}
|
||||
searchInputRef={searchInputRef}
|
||||
/>
|
||||
)}
|
||||
</Layout.Container>
|
||||
|
||||
@@ -115,7 +115,9 @@ type DataManagerActions<T> =
|
||||
| Action<'toggleHighlightSearch'>
|
||||
| Action<'setSearchHighlightColor', {color: string}>
|
||||
| Action<'toggleFilterSearchHistory'>
|
||||
| Action<'toggleSideBySide'>;
|
||||
| Action<'toggleSideBySide'>
|
||||
| Action<'showSearchDropdown', {show: boolean}>
|
||||
| Action<'setShowNumberedHistory', {showNumberedHistory: boolean}>;
|
||||
|
||||
type DataManagerConfig<T> = {
|
||||
dataSource: DataSource<T, T[keyof T]>;
|
||||
@@ -138,6 +140,8 @@ export type DataManagerState<T> = {
|
||||
selection: Selection;
|
||||
useRegex: boolean;
|
||||
filterSearchHistory: boolean;
|
||||
showSearchHistory: boolean;
|
||||
showNumberedHistory: boolean;
|
||||
autoScroll: boolean;
|
||||
searchValue: string;
|
||||
/** Used to remember the record entry to lookup when user presses ctrl */
|
||||
@@ -335,6 +339,14 @@ export const dataTableManagerReducer = produce<
|
||||
draft.sideBySide = !draft.sideBySide;
|
||||
break;
|
||||
}
|
||||
case 'showSearchDropdown': {
|
||||
draft.showSearchHistory = action.show;
|
||||
break;
|
||||
}
|
||||
case 'setShowNumberedHistory': {
|
||||
draft.showNumberedHistory = action.showNumberedHistory;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new Error('Unknown action ' + (action as any).type);
|
||||
}
|
||||
@@ -369,6 +381,8 @@ export type DataTableManager<T> = {
|
||||
toggleHighlightSearch(): void;
|
||||
setSearchHighlightColor(color: string): void;
|
||||
toggleSideBySide(): void;
|
||||
showSearchDropdown(show: boolean): void;
|
||||
setShowNumberedHistory(showNumberedHistory: boolean): void;
|
||||
};
|
||||
|
||||
export function createDataTableManager<T>(
|
||||
@@ -427,6 +441,12 @@ export function createDataTableManager<T>(
|
||||
toggleSideBySide() {
|
||||
dispatch({type: 'toggleSideBySide'});
|
||||
},
|
||||
showSearchDropdown(show) {
|
||||
dispatch({type: 'showSearchDropdown', show});
|
||||
},
|
||||
setShowNumberedHistory(showNumberedHistory) {
|
||||
dispatch({type: 'setShowNumberedHistory', showNumberedHistory});
|
||||
},
|
||||
dataView,
|
||||
};
|
||||
}
|
||||
@@ -478,6 +498,8 @@ export function createInitialState<T>(
|
||||
color: theme.searchHighlightBackground.yellow,
|
||||
},
|
||||
sideBySide: false,
|
||||
showSearchHistory: false,
|
||||
showNumberedHistory: false,
|
||||
};
|
||||
// @ts-ignore
|
||||
res.config[immerable] = false; // optimization: never proxy anything in config
|
||||
|
||||
@@ -8,32 +8,58 @@
|
||||
*/
|
||||
|
||||
import {HistoryOutlined, MenuOutlined} from '@ant-design/icons';
|
||||
import {Button, Dropdown, Input, AutoComplete} from 'antd';
|
||||
import React, {memo, useCallback, useMemo, useState} from 'react';
|
||||
import {Button, Dropdown, Input, AutoComplete, InputRef} from 'antd';
|
||||
import React, {memo, useCallback, useMemo} from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import {Layout} from '../Layout';
|
||||
import {theme} from '../theme';
|
||||
import type {DataTableDispatch} from './DataTableManager';
|
||||
|
||||
const MAX_RECENT = 5;
|
||||
|
||||
export const TableSearch = memo(function TableSearch({
|
||||
searchValue,
|
||||
useRegex,
|
||||
filterSearchHistory,
|
||||
showHistory,
|
||||
showNumbered,
|
||||
dispatch,
|
||||
searchHistory,
|
||||
extraActions,
|
||||
contextMenu,
|
||||
searchInputRef,
|
||||
}: {
|
||||
searchValue: string;
|
||||
useRegex: boolean;
|
||||
filterSearchHistory: boolean;
|
||||
showHistory: boolean;
|
||||
showNumbered: boolean;
|
||||
dispatch: DataTableDispatch<any>;
|
||||
searchHistory: string[];
|
||||
extraActions?: React.ReactElement;
|
||||
contextMenu: undefined | (() => JSX.Element);
|
||||
searchInputRef?: React.MutableRefObject<InputRef>;
|
||||
}) {
|
||||
const [showHistory, setShowHistory] = useState(false);
|
||||
const filteredSearchHistory = useMemo(
|
||||
() =>
|
||||
filterSearchHistory
|
||||
? searchHistory.filter(
|
||||
(value) =>
|
||||
value.toUpperCase().indexOf(searchValue.toUpperCase()) !== -1,
|
||||
)
|
||||
: searchHistory,
|
||||
[filterSearchHistory, searchHistory, searchValue],
|
||||
);
|
||||
|
||||
const options = useMemo(() => {
|
||||
return filteredSearchHistory.map((value, index) => ({
|
||||
label:
|
||||
showNumbered && index < MAX_RECENT ? `${index + 1}: ${value}` : value,
|
||||
value,
|
||||
}));
|
||||
}, [filteredSearchHistory, showNumbered]);
|
||||
|
||||
const onSearch = useCallback(
|
||||
(value: string, addToHistory: boolean) => {
|
||||
dispatch({type: 'setSearchValue', value, addToHistory});
|
||||
@@ -48,6 +74,71 @@ export const TableSearch = memo(function TableSearch({
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
const toggleSearchDropdown = useCallback(
|
||||
(show: boolean) => {
|
||||
dispatch({type: 'showSearchDropdown', show: show});
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
const toggleShowNumberedHistory = useCallback(
|
||||
(showNumberedHistory: boolean) => {
|
||||
dispatch({type: 'setShowNumberedHistory', showNumberedHistory});
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
const onKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent<any>) => {
|
||||
switch (e.key) {
|
||||
case 'f':
|
||||
if (e.ctrlKey && searchHistory.length > 0) {
|
||||
if (!showHistory) {
|
||||
toggleShowNumberedHistory(true);
|
||||
}
|
||||
toggleSearchDropdown(!showHistory);
|
||||
}
|
||||
break;
|
||||
case 'Control':
|
||||
if (showHistory) {
|
||||
toggleShowNumberedHistory(true);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
const possNumber = Number(e.key);
|
||||
if (
|
||||
e.ctrlKey &&
|
||||
possNumber &&
|
||||
showNumbered &&
|
||||
possNumber <= Math.min(MAX_RECENT, filteredSearchHistory.length)
|
||||
) {
|
||||
toggleSearchDropdown(false);
|
||||
onSearch(filteredSearchHistory[possNumber - 1], false);
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
e.stopPropagation();
|
||||
},
|
||||
[
|
||||
searchHistory.length,
|
||||
showHistory,
|
||||
showNumbered,
|
||||
filteredSearchHistory,
|
||||
toggleSearchDropdown,
|
||||
toggleShowNumberedHistory,
|
||||
onSearch,
|
||||
],
|
||||
);
|
||||
const onKeyUp = useCallback(
|
||||
(e: React.KeyboardEvent<any>) => {
|
||||
switch (e.key) {
|
||||
case 'Control':
|
||||
toggleShowNumberedHistory(false);
|
||||
break;
|
||||
}
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
},
|
||||
[toggleShowNumberedHistory],
|
||||
);
|
||||
const regexError = useMemo(() => {
|
||||
if (!useRegex || !searchValue) {
|
||||
return;
|
||||
@@ -59,40 +150,33 @@ export const TableSearch = memo(function TableSearch({
|
||||
}
|
||||
}, [useRegex, searchValue]);
|
||||
|
||||
const options = useMemo(
|
||||
() => searchHistory.map((value) => ({label: value, value})),
|
||||
[searchHistory],
|
||||
);
|
||||
|
||||
return (
|
||||
<Searchbar gap>
|
||||
<Searchbar gap onKeyDown={onKeyDown} onKeyUp={onKeyUp}>
|
||||
<AutoComplete
|
||||
defaultOpen={false}
|
||||
open={showHistory}
|
||||
options={options}
|
||||
filterOption={(inputValue, option) =>
|
||||
!filterSearchHistory ||
|
||||
option!.value.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1
|
||||
}
|
||||
onSelect={(value: string) => {
|
||||
setShowHistory(false);
|
||||
toggleSearchDropdown(false);
|
||||
onSearch(value, false);
|
||||
}}
|
||||
onDropdownVisibleChange={(open) => {
|
||||
if (!open) {
|
||||
setShowHistory(false);
|
||||
toggleSearchDropdown(false);
|
||||
}
|
||||
}}
|
||||
value={searchValue}>
|
||||
<Input.Search
|
||||
allowClear
|
||||
ref={searchInputRef}
|
||||
value={searchValue}
|
||||
placeholder="Search..."
|
||||
suffix={
|
||||
<>
|
||||
{options.length ? (
|
||||
<RegexButton
|
||||
onClick={() => {
|
||||
setShowHistory((v) => !v);
|
||||
toggleSearchDropdown(!showHistory);
|
||||
}}>
|
||||
<HistoryOutlined />
|
||||
</RegexButton>
|
||||
|
||||
Reference in New Issue
Block a user