Added color options for highlighting search terms

Summary:
This diff builds on the previous ones by enabling other colors to be used as highlights for the search terms. Current color options are: yellow(default), red, blue, green. Possible extensions to this feature could include allow the user to enter a custom hex-color string and use that as the highlight color.

Changelog: DataTable will now have option to have its search terms highlighted in the search results by toggling and customizing the highlight colors in the menu bar

Reviewed By: mweststrate

Differential Revision: D37383163

fbshipit-source-id: c81e383c0570ef5efbf3171b92b81a8fb2e55ea7
This commit is contained in:
Feiyu Wong
2022-06-29 10:36:52 -07:00
committed by Facebook GitHub Bot
parent 2f39ede6f7
commit f46cf2b0ce
10 changed files with 106 additions and 32 deletions

View File

@@ -7,7 +7,6 @@
* @format * @format
*/ */
import styled from '@emotion/styled';
import React, { import React, {
useEffect, useEffect,
memo, memo,
@@ -19,19 +18,20 @@ import React, {
import {debounce} from 'lodash'; import {debounce} from 'lodash';
import {theme} from './theme'; import {theme} from './theme';
const Highlighted = styled.span({
backgroundColor: theme.searchHighlightBackground,
});
export interface HighlightManager { export interface HighlightManager {
setFilter(text: string | undefined): void; setFilter(text: string | undefined): void;
render(text: string): React.ReactNode; render(text: string): React.ReactNode;
setHighlightColor(color: string | undefined): void;
} }
function createHighlightManager(initialText: string = ''): HighlightManager { function createHighlightManager(
initialText: string = '',
initialHighlightColor: string = theme.searchHighlightBackground.yellow,
): HighlightManager {
const callbacks = new Set<(prev: string, next: string) => void>(); const callbacks = new Set<(prev: string, next: string) => void>();
let matches = 0; let matches = 0;
let currentFilter = initialText; let currentFilter = initialText;
let currHighlightColor = initialHighlightColor;
const Highlight: React.FC<{text: string}> = memo(({text}) => { const Highlight: React.FC<{text: string}> = memo(({text}) => {
const [, setUpdate] = useState(0); const [, setUpdate] = useState(0);
@@ -65,9 +65,9 @@ function createHighlightManager(initialText: string = ''): HighlightManager {
) : ( ) : (
<> <>
{text.substr(0, index)} {text.substr(0, index)}
<Highlighted> <span style={{backgroundColor: currHighlightColor}}>
{text.substr(index, currentFilter.length)} {text.substr(index, currentFilter.length)}
</Highlighted> </span>
{text.substr(index + currentFilter.length)} {text.substr(index + currentFilter.length)}
</> </>
)} )}
@@ -87,6 +87,12 @@ function createHighlightManager(initialText: string = ''): HighlightManager {
render(text: string) { render(text: string) {
return <Highlight text={text} />; return <Highlight text={text} />;
}, },
setHighlightColor(color: string) {
if (color !== currHighlightColor) {
currHighlightColor = color;
callbacks.forEach((cb) => cb(currentFilter, currentFilter));
}
},
}; };
} }
@@ -98,21 +104,32 @@ export const HighlightContext = createContext<HighlightManager>({
// stub implementation in case we render a component without a Highlight context // stub implementation in case we render a component without a Highlight context
return text; return text;
}, },
setHighlightColor(_color: string) {
throw new Error('Cannot set the color of a stub highlight manager');
},
}); });
export function HighlightProvider({ export function HighlightProvider({
text, text,
highlightColor,
children, children,
}: { }: {
text: string | undefined; text: string | undefined;
highlightColor?: string | undefined;
children: React.ReactElement; children: React.ReactElement;
}) { }) {
const [highlightManager] = useState(() => createHighlightManager(text)); const [highlightManager] = useState(() =>
createHighlightManager(text, highlightColor),
);
useEffect(() => { useEffect(() => {
highlightManager.setFilter(text); highlightManager.setFilter(text);
}, [text, highlightManager]); }, [text, highlightManager]);
useEffect(() => {
highlightManager.setHighlightColor(highlightColor);
}, [highlightColor, highlightManager]);
return ( return (
<HighlightContext.Provider value={highlightManager}> <HighlightContext.Provider value={highlightManager}>
{children} {children}

View File

@@ -115,9 +115,7 @@ test.local('can filter for data', async () => {
expect(element.parentElement).toMatchInlineSnapshot(` expect(element.parentElement).toMatchInlineSnapshot(`
<span> <span>
"j "j
<span <span>
class="css-1cfwmd7-Highlighted eiud9hg0"
>
son son
</span> </span>
" "

View File

@@ -609,6 +609,10 @@ export function DataTable<T extends object>(
tableState.highlightSearchSetting.highlightEnabled tableState.highlightSearchSetting.highlightEnabled
? tableState.searchValue ? tableState.searchValue
: '' : ''
}
highlightColor={
tableState.highlightSearchSetting.color ||
theme.searchHighlightBackground.yellow
}> }>
{mainSection} {mainSection}
</HighlightProvider> </HighlightProvider>

View File

@@ -12,6 +12,7 @@ import {Percentage} from '../../utils/widthUtils';
import {MutableRefObject, Reducer} from 'react'; import {MutableRefObject, Reducer} from 'react';
import {DataSource, DataSourceVirtualizer} from '../../data-source/index'; import {DataSource, DataSourceVirtualizer} from '../../data-source/index';
import produce, {castDraft, immerable, original} from 'immer'; import produce, {castDraft, immerable, original} from 'immer';
import {theme} from '../theme';
export type OnColumnResize = (id: string, size: number | Percentage) => void; export type OnColumnResize = (id: string, size: number | Percentage) => void;
export type Sorting<T = any> = { export type Sorting<T = any> = {
@@ -106,7 +107,8 @@ type DataManagerActions<T> =
| Action<'setAutoScroll', {autoScroll: boolean}> | Action<'setAutoScroll', {autoScroll: boolean}>
| Action<'toggleSearchValue'> | Action<'toggleSearchValue'>
| Action<'clearSearchHistory'> | Action<'clearSearchHistory'>
| Action<'toggleHighlightSearch'>; | Action<'toggleHighlightSearch'>
| Action<'setSearchHighlightColor', {color: string}>;
type DataManagerConfig<T> = { type DataManagerConfig<T> = {
dataSource: DataSource<T, T[keyof T]>; dataSource: DataSource<T, T[keyof T]>;
@@ -309,6 +311,12 @@ export const dataTableManagerReducer = produce<
!draft.highlightSearchSetting.highlightEnabled; !draft.highlightSearchSetting.highlightEnabled;
break; break;
} }
case 'setSearchHighlightColor': {
if (draft.highlightSearchSetting.color !== action.color) {
draft.highlightSearchSetting.color = action.color;
}
break;
}
default: { default: {
throw new Error('Unknown action ' + (action as any).type); throw new Error('Unknown action ' + (action as any).type);
} }
@@ -341,6 +349,7 @@ export type DataTableManager<T> = {
dataSource: DataSource<T, T[keyof T]>; dataSource: DataSource<T, T[keyof T]>;
toggleSearchValue(): void; toggleSearchValue(): void;
toggleHighlightSearch(): void; toggleHighlightSearch(): void;
setSearchHighlightColor(color: string): void;
}; };
export function createDataTableManager<T>( export function createDataTableManager<T>(
@@ -393,6 +402,9 @@ export function createDataTableManager<T>(
toggleHighlightSearch() { toggleHighlightSearch() {
dispatch({type: 'toggleHighlightSearch'}); dispatch({type: 'toggleHighlightSearch'});
}, },
setSearchHighlightColor(color) {
dispatch({type: 'setSearchHighlightColor', color});
},
dataSource, dataSource,
}; };
} }
@@ -440,7 +452,7 @@ export function createInitialState<T>(
autoScroll: prefs?.autoScroll ?? config.autoScroll ?? false, autoScroll: prefs?.autoScroll ?? config.autoScroll ?? false,
highlightSearchSetting: prefs?.highlightSearchSetting ?? { highlightSearchSetting: prefs?.highlightSearchSetting ?? {
highlightEnabled: false, highlightEnabled: false,
color: '', color: theme.searchHighlightBackground.yellow,
}, },
}; };
// @ts-ignore // @ts-ignore

View File

@@ -8,7 +8,7 @@
*/ */
import {CopyOutlined, FilterOutlined, TableOutlined} from '@ant-design/icons'; import {CopyOutlined, FilterOutlined, TableOutlined} from '@ant-design/icons';
import {Checkbox, Menu, Switch} from 'antd'; import {Badge, Checkbox, Menu, Select, Switch} from 'antd';
import {Layout} from 'flipper-plugin'; import {Layout} from 'flipper-plugin';
import { import {
DataTableDispatch, DataTableDispatch,
@@ -25,8 +25,10 @@ import {toFirstUpper} from '../../utils/toFirstUpper';
import {DataSource} from '../../data-source/index'; import {DataSource} from '../../data-source/index';
import {renderColumnValue} from './TableRow'; import {renderColumnValue} from './TableRow';
import {textContent} from '../../utils/textContent'; import {textContent} from '../../utils/textContent';
import {theme} from '../theme';
const {Item, SubMenu} = Menu; const {Item, SubMenu} = Menu;
const {Option} = Select;
export function tableContextMenuFactory<T>( export function tableContextMenuFactory<T>(
datasource: DataSource<T, T[keyof T]>, datasource: DataSource<T, T[keyof T]>,
@@ -197,6 +199,7 @@ export function tableContextMenuFactory<T>(
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
}}> }}>
Highlight search terms
<Switch <Switch
checked={highlightSearchSetting.highlightEnabled} checked={highlightSearchSetting.highlightEnabled}
size="small" size="small"
@@ -206,7 +209,42 @@ export function tableContextMenuFactory<T>(
}); });
}} }}
/> />
Highlight search terms </Layout.Horizontal>
</Menu.Item>
<Menu.Item key="highlight search color">
<Layout.Horizontal
gap
center
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
}}>
Highlight search color
<Select
style={{width: '7em'}}
defaultValue={highlightSearchSetting.color}
onChange={(color: string) => {
dispatch({
type: 'setSearchHighlightColor',
color: color,
});
}}>
{Object.entries(theme.searchHighlightBackground).map(
([colorName, color]) => (
<Option key={colorName} value={color}>
<Badge
text={
<span style={{backgroundColor: color}}>
{colorName.charAt(0).toUpperCase() +
colorName.slice(1)}
</span>
}
color={color}
/>
</Option>
),
)}
</Select>
</Layout.Horizontal> </Layout.Horizontal>
</Menu.Item> </Menu.Item>
</SubMenu> </SubMenu>

View File

@@ -59,7 +59,7 @@ test('update and append', async () => {
> >
<span> <span>
<span <span
class="css-1cfwmd7-Highlighted eiud9hg0" style="background-color: rgb(255, 245, 102);"
/> />
test DataTable test DataTable
</span> </span>
@@ -70,7 +70,7 @@ test('update and append', async () => {
> >
<span> <span>
<span <span
class="css-1cfwmd7-Highlighted eiud9hg0" style="background-color: rgb(255, 245, 102);"
/> />
true true
</span> </span>
@@ -123,7 +123,7 @@ test('column visibility', async () => {
> >
<span> <span>
<span <span
class="css-1cfwmd7-Highlighted eiud9hg0" style="background-color: rgb(255, 245, 102);"
/> />
test DataTable test DataTable
</span> </span>
@@ -134,7 +134,7 @@ test('column visibility', async () => {
> >
<span> <span>
<span <span
class="css-1cfwmd7-Highlighted eiud9hg0" style="background-color: rgb(255, 245, 102);"
/> />
true true
</span> </span>
@@ -160,7 +160,7 @@ test('column visibility', async () => {
> >
<span> <span>
<span <span
class="css-1cfwmd7-Highlighted eiud9hg0" style="background-color: rgb(255, 245, 102);"
/> />
test DataTable test DataTable
</span> </span>

View File

@@ -52,7 +52,7 @@ test('update and append', async () => {
> >
<span> <span>
<span <span
class="css-1cfwmd7-Highlighted eiud9hg0" style="background-color: rgb(255, 245, 102);"
/> />
test DataTable test DataTable
</span> </span>
@@ -63,7 +63,7 @@ test('update and append', async () => {
> >
<span> <span>
<span <span
class="css-1cfwmd7-Highlighted eiud9hg0" style="background-color: rgb(255, 245, 102);"
/> />
true true
</span> </span>

View File

@@ -118,7 +118,7 @@ class PartialHighlight extends PureComponent<{
content: string; content: string;
}> { }> {
static HighlightedText = styled.span<{selected: boolean}>((props) => ({ static HighlightedText = styled.span<{selected: boolean}>((props) => ({
backgroundColor: theme.searchHighlightBackground, backgroundColor: theme.searchHighlightBackground.yellow,
color: props.selected ? `${theme.textColorPrimary} !important` : 'auto', color: props.selected ? `${theme.textColorPrimary} !important` : 'auto',
})); }));

View File

@@ -21,7 +21,12 @@ export const theme = {
textColorSecondary: 'var(--flipper-text-color-secondary)', textColorSecondary: 'var(--flipper-text-color-secondary)',
textColorPlaceholder: 'var(--flipper-text-color-placeholder)', textColorPlaceholder: 'var(--flipper-text-color-placeholder)',
textColorActive: 'var(--light-color-button-active)', textColorActive: 'var(--light-color-button-active)',
searchHighlightBackground: antColors.yellow[3], searchHighlightBackground: {
yellow: antColors.yellow[3],
red: antColors.red[3],
green: antColors.green[3],
blue: antColors.blue[3],
} as const,
selectionBackgroundColor: 'var(--flipper-primary-background-wash)', selectionBackgroundColor: 'var(--flipper-primary-background-wash)',
disabledColor: 'var(--flipper-disabled-color)', disabledColor: 'var(--flipper-disabled-color)',
backgroundDefault: 'var(--flipper-background-default)', backgroundDefault: 'var(--flipper-background-default)',

View File

@@ -129,7 +129,7 @@ test('It can render rows', async () => {
> >
<span> <span>
<span <span
class="css-1cfwmd7-Highlighted eiud9hg0" style="background-color: rgb(255, 245, 102);"
/> />
00:00:00.000 00:00:00.000
</span> </span>
@@ -140,7 +140,7 @@ test('It can render rows', async () => {
> >
<span> <span>
<span <span
class="css-1cfwmd7-Highlighted eiud9hg0" style="background-color: rgb(255, 245, 102);"
/> />
Android Phone Android Phone
</span> </span>
@@ -151,7 +151,7 @@ test('It can render rows', async () => {
> >
<span> <span>
<span <span
class="css-1cfwmd7-Highlighted eiud9hg0" style="background-color: rgb(255, 245, 102);"
/> />
FB4A FB4A
</span> </span>
@@ -162,7 +162,7 @@ test('It can render rows', async () => {
> >
<span> <span>
<span <span
class="css-1cfwmd7-Highlighted eiud9hg0" style="background-color: rgb(255, 245, 102);"
/> />
unique-string unique-string
</span> </span>
@@ -173,7 +173,7 @@ test('It can render rows', async () => {
> >
<span> <span>
<span <span
class="css-1cfwmd7-Highlighted eiud9hg0" style="background-color: rgb(255, 245, 102);"
/> />
</span> </span>
</div> </div>
@@ -183,7 +183,7 @@ test('It can render rows', async () => {
> >
<span> <span>
<span <span
class="css-1cfwmd7-Highlighted eiud9hg0" style="background-color: rgb(255, 245, 102);"
/> />
</span> </span>
</div> </div>
@@ -193,7 +193,7 @@ test('It can render rows', async () => {
> >
<span> <span>
<span <span
class="css-1cfwmd7-Highlighted eiud9hg0" style="background-color: rgb(255, 245, 102);"
/> />
toClient:send toClient:send
</span> </span>