Move highlighter to flipper-plugin

Summary: This stack moves the DataInspector component to flipper-plugin. The first diffs will move some utility abstractions to turn this into smaller steps

Reviewed By: passy

Differential Revision: D27591890

fbshipit-source-id: d0285ca31b6c9b334000dd15c722b9bda7638d73
This commit is contained in:
Michel Weststrate
2021-04-07 07:52:47 -07:00
committed by Facebook GitHub Bot
parent cd27e4c010
commit b6674bf96b
8 changed files with 25 additions and 17 deletions

View File

@@ -85,6 +85,13 @@ export {createDataSource, DataSource} from './state/DataSource';
export {DataTable, DataTableColumn} from './ui/data-table/DataTable';
export {DataTableManager} from './ui/data-table/DataTableManager';
export {
HighlightContext as _HighlightContext,
HighlightProvider as _HighlightProvider,
HighlightManager as _HighlightManager,
useHighlighter as _useHighlighter,
} from './ui/Highlight';
export {
Interactive as _Interactive,
InteractiveProps as _InteractiveProps,

View File

@@ -0,0 +1,125 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import styled from '@emotion/styled';
import React, {
useEffect,
memo,
useState,
useRef,
createContext,
useContext,
} from 'react';
import {debounce} from 'lodash';
import {theme} from './theme';
const Highlighted = styled.span({
backgroundColor: theme.textColorActive,
});
export interface HighlightManager {
setFilter(text: string | undefined): void;
render(text: string): React.ReactNode;
}
function createHighlightManager(initialText: string = ''): HighlightManager {
const callbacks = new Set<(prev: string, next: string) => void>();
let matches = 0;
let currentFilter = initialText;
const Highlight: React.FC<{text: string}> = memo(({text}) => {
const [, setUpdate] = useState(0);
const elem = useRef<HTMLSpanElement | null>(null);
useEffect(() => {
function onChange(prevHighlight: string, newHighlight: string) {
const prevIndex = text.toLowerCase().indexOf(prevHighlight);
const newIndex = text.toLowerCase().indexOf(newHighlight);
if (prevIndex !== newIndex || newIndex !== -1) {
// either we had a result, and we have no longer,
// or we still have a result, but the highlightable text changed
if (newIndex !== -1) {
if (++matches === 1) {
elem.current?.parentElement?.parentElement?.scrollIntoView?.();
}
}
setUpdate((s) => s + 1);
}
}
callbacks.add(onChange);
return () => {
callbacks.delete(onChange);
};
}, [text]);
const index = text.toLowerCase().indexOf(currentFilter);
return (
<span ref={elem}>
{index === -1 ? (
text
) : (
<>
{text.substr(0, index)}
<Highlighted>
{text.substr(index, currentFilter.length)}
</Highlighted>
{text.substr(index + currentFilter.length)}
</>
)}
</span>
);
});
return {
setFilter: debounce((text: string = '') => {
if (currentFilter !== text) {
matches = 0;
const base = currentFilter;
currentFilter = text.toLowerCase();
callbacks.forEach((cb) => cb(base, currentFilter));
}
}, 100),
render(text: string) {
return <Highlight text={text} />;
},
};
}
export const HighlightContext = createContext<HighlightManager>({
setFilter(_text: string) {
throw new Error('Cannot set the filter of a stub highlight manager');
},
render(text: string) {
// stub implementation in case we render a component without a Highlight context
return text;
},
});
export function HighlightProvider({
text,
children,
}: {
text: string | undefined;
children: React.ReactElement;
}) {
const [highlightManager] = useState(() => createHighlightManager(text));
useEffect(() => {
highlightManager.setFilter(text);
}, [text, highlightManager]);
return (
<HighlightContext.Provider value={highlightManager}>
{children}
</HighlightContext.Provider>
);
}
export function useHighlighter(): HighlightManager {
return useContext(HighlightContext);
}