Introduce highlight manager, add scroll on highlight
Summary: Introduced a highlight manager, which prevents drilling the highlight through the entire component tree and causing too many re-renders. Also smartly optimizes that non-matched highlighted text doesn't render unnecessarily, and debounces the updates. Finally, automatically scroll to the first highlight. Reviewed By: jknoxville Differential Revision: D21348575 fbshipit-source-id: 71f7ba2e981ad3fc1ea7f5e7043645e6b6811fb7
This commit is contained in:
committed by
Facebook GitHub Bot
parent
fd84820ee5
commit
fdff6aeae0
121
desktop/app/src/ui/components/Highlight.tsx
Normal file
121
desktop/app/src/ui/components/Highlight.tsx
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
/**
|
||||||
|
* 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 {colors} from './colors';
|
||||||
|
import React, {
|
||||||
|
useEffect,
|
||||||
|
memo,
|
||||||
|
useState,
|
||||||
|
useRef,
|
||||||
|
useMemo,
|
||||||
|
createContext,
|
||||||
|
useContext,
|
||||||
|
} from 'react';
|
||||||
|
import {debounce} from 'lodash';
|
||||||
|
|
||||||
|
const Highlighted = styled.span({
|
||||||
|
backgroundColor: colors.lemon,
|
||||||
|
});
|
||||||
|
|
||||||
|
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 [_update, 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);
|
||||||
|
if (index === -1) {
|
||||||
|
return <span>{text}</span>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<span ref={elem}>
|
||||||
|
{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 = useMemo(() => createHighlightManager(text), []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
highlightManager.setFilter(text);
|
||||||
|
}, [text]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HighlightContext.Provider value={highlightManager}>
|
||||||
|
{children}
|
||||||
|
</HighlightContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useHighlighter(): HighlightManager {
|
||||||
|
return useContext(HighlightContext);
|
||||||
|
}
|
||||||
@@ -18,7 +18,7 @@ import {colors} from '../colors';
|
|||||||
import Input from '../Input';
|
import Input from '../Input';
|
||||||
import React, {KeyboardEvent} from 'react';
|
import React, {KeyboardEvent} from 'react';
|
||||||
import Glyph from '../Glyph';
|
import Glyph from '../Glyph';
|
||||||
import {Highlight} from './Highlight';
|
import {HighlightContext} from '../Highlight';
|
||||||
|
|
||||||
const NullValue = styled.span({
|
const NullValue = styled.span({
|
||||||
color: 'rgb(128, 128, 128)',
|
color: 'rgb(128, 128, 128)',
|
||||||
@@ -85,7 +85,6 @@ type DataDescriptionProps = {
|
|||||||
value: any;
|
value: any;
|
||||||
extra?: any;
|
extra?: any;
|
||||||
setValue: DataInspectorSetValue | null | undefined;
|
setValue: DataInspectorSetValue | null | undefined;
|
||||||
highlight?: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type DescriptionCommitOptions = {
|
type DescriptionCommitOptions = {
|
||||||
@@ -280,14 +279,13 @@ export default class DataDescription extends PureComponent<
|
|||||||
editable={Boolean(this.props.setValue)}
|
editable={Boolean(this.props.setValue)}
|
||||||
commit={this.commit}
|
commit={this.commit}
|
||||||
onEdit={this.onEditStart}
|
onEdit={this.onEditStart}
|
||||||
highlight={this.props.highlight}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ColorEditor extends Component<{
|
class ColorEditor extends PureComponent<{
|
||||||
value: any;
|
value: any;
|
||||||
colorSet?: Array<string | number>;
|
colorSet?: Array<string | number>;
|
||||||
commit: (opts: DescriptionCommitOptions) => void;
|
commit: (opts: DescriptionCommitOptions) => void;
|
||||||
@@ -449,7 +447,6 @@ class DataDescriptionPreview extends Component<{
|
|||||||
editable: boolean;
|
editable: boolean;
|
||||||
commit: (opts: DescriptionCommitOptions) => void;
|
commit: (opts: DescriptionCommitOptions) => void;
|
||||||
onEdit?: () => void;
|
onEdit?: () => void;
|
||||||
highlight?: string;
|
|
||||||
}> {
|
}> {
|
||||||
onClick = () => {
|
onClick = () => {
|
||||||
const {onEdit} = this.props;
|
const {onEdit} = this.props;
|
||||||
@@ -467,7 +464,6 @@ class DataDescriptionPreview extends Component<{
|
|||||||
value={value}
|
value={value}
|
||||||
editable={this.props.editable}
|
editable={this.props.editable}
|
||||||
commit={this.props.commit}
|
commit={this.props.commit}
|
||||||
highlight={this.props.highlight}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -548,8 +544,10 @@ class DataDescriptionContainer extends Component<{
|
|||||||
value: any;
|
value: any;
|
||||||
editable: boolean;
|
editable: boolean;
|
||||||
commit: (opts: DescriptionCommitOptions) => void;
|
commit: (opts: DescriptionCommitOptions) => void;
|
||||||
highlight?: string;
|
|
||||||
}> {
|
}> {
|
||||||
|
static contextType = HighlightContext; // Replace with useHighlighter
|
||||||
|
context!: React.ContextType<typeof HighlightContext>;
|
||||||
|
|
||||||
onChangeCheckbox = (e: React.ChangeEvent<HTMLInputElement>) => {
|
onChangeCheckbox = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
this.props.commit({
|
this.props.commit({
|
||||||
clear: true,
|
clear: true,
|
||||||
@@ -561,6 +559,7 @@ class DataDescriptionContainer extends Component<{
|
|||||||
|
|
||||||
render(): any {
|
render(): any {
|
||||||
const {type, editable, value: val} = this.props;
|
const {type, editable, value: val} = this.props;
|
||||||
|
const highlighter = this.context;
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'number':
|
case 'number':
|
||||||
@@ -615,9 +614,7 @@ class DataDescriptionContainer extends Component<{
|
|||||||
if (val.startsWith('http://') || val.startsWith('https://')) {
|
if (val.startsWith('http://') || val.startsWith('https://')) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Link href={val}>
|
<Link href={val}>{highlighter.render(val)}</Link>
|
||||||
<Highlight text={val} highlight={this.props.highlight} />
|
|
||||||
</Link>
|
|
||||||
<Glyph
|
<Glyph
|
||||||
name="pencil"
|
name="pencil"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -629,21 +626,12 @@ class DataDescriptionContainer extends Component<{
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<StringValue>
|
<StringValue>{highlighter.render(`"${val || ''}"`)}</StringValue>
|
||||||
<Highlight
|
|
||||||
text={`"${val || ''}"`}
|
|
||||||
highlight={this.props.highlight}
|
|
||||||
/>
|
|
||||||
</StringValue>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'enum':
|
case 'enum':
|
||||||
return (
|
return <StringValue>{highlighter.render(val)}</StringValue>;
|
||||||
<StringValue>
|
|
||||||
<Highlight text={val} highlight={this.props.highlight} />
|
|
||||||
</StringValue>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 'boolean':
|
case 'boolean':
|
||||||
return editable ? (
|
return editable ? (
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import deepEqual from 'deep-equal';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {TooltipOptions} from '../TooltipProvider';
|
import {TooltipOptions} from '../TooltipProvider';
|
||||||
import {shallowEqual} from 'react-redux';
|
import {shallowEqual} from 'react-redux';
|
||||||
import {Highlight} from './Highlight';
|
import {HighlightContext} from '../Highlight';
|
||||||
|
|
||||||
export {DataValueExtractor} from './DataPreview';
|
export {DataValueExtractor} from './DataPreview';
|
||||||
|
|
||||||
@@ -154,10 +154,6 @@ type DataInspectorProps = {
|
|||||||
* Object of properties that will have tooltips
|
* Object of properties that will have tooltips
|
||||||
*/
|
*/
|
||||||
tooltips?: any;
|
tooltips?: any;
|
||||||
/**
|
|
||||||
* Text to highlight, in case searching is used
|
|
||||||
*/
|
|
||||||
highlight?: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultValueExtractor: DataValueExtractor = (value: any) => {
|
const defaultValueExtractor: DataValueExtractor = (value: any) => {
|
||||||
@@ -321,6 +317,9 @@ export default class DataInspector extends Component<
|
|||||||
DataInspectorProps,
|
DataInspectorProps,
|
||||||
DataInspectorState
|
DataInspectorState
|
||||||
> {
|
> {
|
||||||
|
static contextType = HighlightContext; // Replace with useHighlighter
|
||||||
|
context!: React.ContextType<typeof HighlightContext>;
|
||||||
|
|
||||||
static defaultProps: {
|
static defaultProps: {
|
||||||
expanded: DataInspectorExpanded;
|
expanded: DataInspectorExpanded;
|
||||||
depth: number;
|
depth: number;
|
||||||
@@ -390,8 +389,7 @@ export default class DataInspector extends Component<
|
|||||||
nextProps.onDelete !== props.onDelete ||
|
nextProps.onDelete !== props.onDelete ||
|
||||||
nextProps.setValue !== props.setValue ||
|
nextProps.setValue !== props.setValue ||
|
||||||
nextProps.collapsed !== props.collapsed ||
|
nextProps.collapsed !== props.collapsed ||
|
||||||
nextProps.expandRoot !== props.expandRoot ||
|
nextProps.expandRoot !== props.expandRoot
|
||||||
nextProps.highlight !== props.highlight
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -558,10 +556,10 @@ export default class DataInspector extends Component<
|
|||||||
ancestry,
|
ancestry,
|
||||||
collapsed,
|
collapsed,
|
||||||
tooltips,
|
tooltips,
|
||||||
highlight,
|
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const {resDiff, isExpandable, isExpanded, res} = this.state;
|
const {resDiff, isExpandable, isExpanded, res} = this.state;
|
||||||
|
const highlighter = this.context; // useHighlighter();
|
||||||
|
|
||||||
if (!res) {
|
if (!res) {
|
||||||
return null;
|
return null;
|
||||||
@@ -619,7 +617,6 @@ export default class DataInspector extends Component<
|
|||||||
data={metadata.data}
|
data={metadata.data}
|
||||||
diff={metadata.diff}
|
diff={metadata.diff}
|
||||||
tooltips={tooltips}
|
tooltips={tooltips}
|
||||||
highlight={highlight}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -657,9 +654,7 @@ export default class DataInspector extends Component<
|
|||||||
title={tooltips != null && tooltips[name]}
|
title={tooltips != null && tooltips[name]}
|
||||||
key="name"
|
key="name"
|
||||||
options={nameTooltipOptions}>
|
options={nameTooltipOptions}>
|
||||||
<InspectorName>
|
<InspectorName>{highlighter.render(name)}</InspectorName>
|
||||||
<Highlight text={name} highlight={this.props.highlight} />
|
|
||||||
</InspectorName>
|
|
||||||
</Tooltip>,
|
</Tooltip>,
|
||||||
);
|
);
|
||||||
nameElems.push(<span key="sep">: </span>);
|
nameElems.push(<span key="sep">: </span>);
|
||||||
@@ -675,7 +670,6 @@ export default class DataInspector extends Component<
|
|||||||
type={type}
|
type={type}
|
||||||
value={value}
|
value={value}
|
||||||
extra={extra}
|
extra={extra}
|
||||||
highlight={highlight}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ export default class DataPreview extends PureComponent<{
|
|||||||
|
|
||||||
propertyNodes.push(
|
propertyNodes.push(
|
||||||
<span key={key}>
|
<span key={key}>
|
||||||
<InspectorName>Highlight{key}</InspectorName>
|
<InspectorName>{key}</InspectorName>
|
||||||
{ellipsis}
|
{ellipsis}
|
||||||
</span>,
|
</span>,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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 {colors} from '../colors';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
const Highlighted = styled.span({
|
|
||||||
backgroundColor: colors.lemon,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const Highlight: React.FC<{text: string; highlight?: string}> = ({
|
|
||||||
text,
|
|
||||||
highlight,
|
|
||||||
}) => {
|
|
||||||
if (!highlight) {
|
|
||||||
return <span>{text}</span>;
|
|
||||||
}
|
|
||||||
const index = text.toLowerCase().indexOf(highlight.toLowerCase());
|
|
||||||
if (index === -1) {
|
|
||||||
return <span>{text}</span>;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<span>
|
|
||||||
{text.substr(0, index)}
|
|
||||||
<Highlighted>{text.substr(index, highlight.length)}</Highlighted>
|
|
||||||
{text.substr(index + highlight.length)}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -12,6 +12,7 @@ import {PureComponent} from 'react';
|
|||||||
import DataInspector from './DataInspector';
|
import DataInspector from './DataInspector';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {DataValueExtractor} from './DataPreview';
|
import {DataValueExtractor} from './DataPreview';
|
||||||
|
import {HighlightProvider} from '../Highlight';
|
||||||
|
|
||||||
type ManagedDataInspectorProps = {
|
type ManagedDataInspectorProps = {
|
||||||
/**
|
/**
|
||||||
@@ -147,6 +148,7 @@ export default class ManagedDataInspector extends PureComponent<
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
|
<HighlightProvider text={this.props.filter}>
|
||||||
<DataInspector
|
<DataInspector
|
||||||
data={this.props.data}
|
data={this.props.data}
|
||||||
diff={this.props.diff}
|
diff={this.props.diff}
|
||||||
@@ -158,8 +160,8 @@ export default class ManagedDataInspector extends PureComponent<
|
|||||||
expandRoot={this.props.expandRoot}
|
expandRoot={this.props.expandRoot}
|
||||||
collapsed={this.props.filter ? true : this.props.collapsed}
|
collapsed={this.props.filter ? true : this.props.collapsed}
|
||||||
tooltips={this.props.tooltips}
|
tooltips={this.props.tooltips}
|
||||||
highlight={this.props.filter}
|
|
||||||
/>
|
/>
|
||||||
|
</HighlightProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {render, fireEvent, waitFor} from '@testing-library/react';
|
import {render, fireEvent, waitFor, act} from '@testing-library/react';
|
||||||
|
|
||||||
jest.mock('../../../../fb/Logger');
|
jest.mock('../../../../fb/Logger');
|
||||||
import ManagedDataInspector from '../ManagedDataInspector';
|
import ManagedDataInspector from '../ManagedDataInspector';
|
||||||
|
import {sleep} from '../../../../utils';
|
||||||
|
|
||||||
const mocks = {
|
const mocks = {
|
||||||
requestIdleCallback(fn: Function) {
|
requestIdleCallback(fn: Function) {
|
||||||
@@ -105,6 +106,8 @@ test('can filter for data', async () => {
|
|||||||
);
|
);
|
||||||
await res.findByText(/awesomely/); // everything is shown
|
await res.findByText(/awesomely/); // everything is shown
|
||||||
|
|
||||||
|
// act here is used to make sure the highlight changes have propagated
|
||||||
|
await act(async () => {
|
||||||
res.rerender(
|
res.rerender(
|
||||||
<ManagedDataInspector
|
<ManagedDataInspector
|
||||||
data={json}
|
data={json}
|
||||||
@@ -113,6 +116,9 @@ test('can filter for data', async () => {
|
|||||||
filter="sOn"
|
filter="sOn"
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
await sleep(200);
|
||||||
|
});
|
||||||
|
|
||||||
const element = await res.findByText(/son/); // N.B. search for 'son', as the text was split up
|
const element = await res.findByText(/son/); // N.B. search for 'son', as the text was split up
|
||||||
// snapshot to make sure the hilighiting did it's job
|
// snapshot to make sure the hilighiting did it's job
|
||||||
expect(element.parentElement).toMatchInlineSnapshot(`
|
expect(element.parentElement).toMatchInlineSnapshot(`
|
||||||
@@ -132,6 +138,7 @@ test('can filter for data', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// find by key
|
// find by key
|
||||||
|
await act(async () => {
|
||||||
res.rerender(
|
res.rerender(
|
||||||
<ManagedDataInspector
|
<ManagedDataInspector
|
||||||
data={json}
|
data={json}
|
||||||
@@ -140,15 +147,27 @@ test('can filter for data', async () => {
|
|||||||
filter="somel"
|
filter="somel"
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
await sleep(200);
|
||||||
|
});
|
||||||
|
|
||||||
await res.findByText(/cool/);
|
await res.findByText(/cool/);
|
||||||
// hides the other part of the tree
|
// hides the other part of the tree
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(res.queryByText(/json/)).toBeNull();
|
expect(res.queryByText(/json/)).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
res.rerender(
|
res.rerender(
|
||||||
<ManagedDataInspector data={json} collapsed={false} expandRoot filter="" />,
|
<ManagedDataInspector
|
||||||
|
data={json}
|
||||||
|
collapsed={false}
|
||||||
|
expandRoot
|
||||||
|
filter=""
|
||||||
|
/>,
|
||||||
);
|
);
|
||||||
|
await sleep(200);
|
||||||
|
});
|
||||||
|
|
||||||
// everything visible again
|
// everything visible again
|
||||||
await res.findByText(/awesomely/);
|
await res.findByText(/awesomely/);
|
||||||
await res.findByText(/json/);
|
await res.findByText(/json/);
|
||||||
|
|||||||
@@ -131,6 +131,7 @@ const ElementsRowAttributeValue = styled.span({
|
|||||||
});
|
});
|
||||||
ElementsRowAttributeValue.displayName = 'Elements:ElementsRowAttributeValue';
|
ElementsRowAttributeValue.displayName = 'Elements:ElementsRowAttributeValue';
|
||||||
|
|
||||||
|
// Merge this functionality with components/Highlight
|
||||||
class PartialHighlight extends PureComponent<{
|
class PartialHighlight extends PureComponent<{
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
highlighted: string | undefined | null;
|
highlighted: string | undefined | null;
|
||||||
|
|||||||
@@ -175,3 +175,4 @@ export {default as Info} from './components/Info';
|
|||||||
export {default as Bordered} from './components/Bordered';
|
export {default as Bordered} from './components/Bordered';
|
||||||
export {default as AlternatingRows} from './components/AlternatingRows';
|
export {default as AlternatingRows} from './components/AlternatingRows';
|
||||||
export {default as Layout} from './components/Layout';
|
export {default as Layout} from './components/Layout';
|
||||||
|
export * from './components/Highlight';
|
||||||
|
|||||||
Reference in New Issue
Block a user