Add hightlighter to the tree control (#36480)
* Add tree-control expand/collapse on click the expander button or by a custom logic * Add stories * Upgrade WP components dependency to v19.8.5 to support indeterminate checkbox control * Add styles to fit the disign * Add type definitions * Add custom hook to manage highlight * Add hightlighter to the tree control * Add stories * Add changelog file * Fix rebase conflicts * Add comment suggestions
This commit is contained in:
parent
25497c4faa
commit
e61211406f
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: dev
|
||||||
|
|
||||||
|
Add highlighter to the tree control
|
|
@ -0,0 +1,31 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import { CheckedStatus, TreeItemProps } from '../types';
|
||||||
|
|
||||||
|
export function useHighlighter( {
|
||||||
|
item,
|
||||||
|
multiple,
|
||||||
|
checkedStatus,
|
||||||
|
shouldItemBeHighlighted,
|
||||||
|
}: Pick< TreeItemProps, 'item' | 'multiple' | 'shouldItemBeHighlighted' > & {
|
||||||
|
checkedStatus: CheckedStatus;
|
||||||
|
} ) {
|
||||||
|
const isHighlighted = useMemo( () => {
|
||||||
|
if ( typeof shouldItemBeHighlighted === 'function' ) {
|
||||||
|
if ( multiple || item.children.length === 0 ) {
|
||||||
|
return shouldItemBeHighlighted( item );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ( ! multiple ) {
|
||||||
|
return checkedStatus === 'checked';
|
||||||
|
}
|
||||||
|
}, [ item, multiple, checkedStatus, shouldItemBeHighlighted ] );
|
||||||
|
|
||||||
|
return { isHighlighted };
|
||||||
|
}
|
|
@ -8,6 +8,7 @@ import React from 'react';
|
||||||
*/
|
*/
|
||||||
import { TreeItemProps } from '../types';
|
import { TreeItemProps } from '../types';
|
||||||
import { useExpander } from './use-expander';
|
import { useExpander } from './use-expander';
|
||||||
|
import { useHighlighter } from './use-highlighter';
|
||||||
import { useSelection } from './use-selection';
|
import { useSelection } from './use-selection';
|
||||||
|
|
||||||
export function useTreeItem( {
|
export function useTreeItem( {
|
||||||
|
@ -18,6 +19,7 @@ export function useTreeItem( {
|
||||||
index,
|
index,
|
||||||
getLabel,
|
getLabel,
|
||||||
shouldItemBeExpanded,
|
shouldItemBeExpanded,
|
||||||
|
shouldItemBeHighlighted,
|
||||||
onSelect,
|
onSelect,
|
||||||
onRemove,
|
onRemove,
|
||||||
...props
|
...props
|
||||||
|
@ -39,11 +41,19 @@ export function useTreeItem( {
|
||||||
onRemove,
|
onRemove,
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
const highlighter = useHighlighter( {
|
||||||
|
item,
|
||||||
|
checkedStatus: selection.checkedStatus,
|
||||||
|
multiple,
|
||||||
|
shouldItemBeHighlighted,
|
||||||
|
} );
|
||||||
|
|
||||||
return {
|
return {
|
||||||
item,
|
item,
|
||||||
level: nextLevel,
|
level: nextLevel,
|
||||||
expander,
|
expander,
|
||||||
selection,
|
selection,
|
||||||
|
highlighter,
|
||||||
getLabel,
|
getLabel,
|
||||||
treeItemProps: {
|
treeItemProps: {
|
||||||
...props,
|
...props,
|
||||||
|
@ -60,6 +70,7 @@ export function useTreeItem( {
|
||||||
selected: selection.selected,
|
selected: selection.selected,
|
||||||
getItemLabel: getLabel,
|
getItemLabel: getLabel,
|
||||||
shouldItemBeExpanded,
|
shouldItemBeExpanded,
|
||||||
|
shouldItemBeHighlighted,
|
||||||
onSelect: selection.onSelectChildren,
|
onSelect: selection.onSelectChildren,
|
||||||
onRemove: selection.onRemoveChildren,
|
onRemove: selection.onRemoveChildren,
|
||||||
},
|
},
|
||||||
|
|
|
@ -15,6 +15,7 @@ export function useTree( {
|
||||||
selected,
|
selected,
|
||||||
getItemLabel,
|
getItemLabel,
|
||||||
shouldItemBeExpanded,
|
shouldItemBeExpanded,
|
||||||
|
shouldItemBeHighlighted,
|
||||||
onSelect,
|
onSelect,
|
||||||
onRemove,
|
onRemove,
|
||||||
...props
|
...props
|
||||||
|
@ -31,6 +32,7 @@ export function useTree( {
|
||||||
selected,
|
selected,
|
||||||
getLabel: getItemLabel,
|
getLabel: getItemLabel,
|
||||||
shouldItemBeExpanded,
|
shouldItemBeExpanded,
|
||||||
|
shouldItemBeHighlighted,
|
||||||
onSelect,
|
onSelect,
|
||||||
onRemove,
|
onRemove,
|
||||||
},
|
},
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
*/
|
*/
|
||||||
import interpolate from '@automattic/interpolate-components';
|
import interpolate from '@automattic/interpolate-components';
|
||||||
import { BaseControl, TextControl } from '@wordpress/components';
|
import { BaseControl, TextControl } from '@wordpress/components';
|
||||||
import React, { createElement, useCallback, useState } from 'react';
|
import React, { createElement, useCallback, useRef, useState } from 'react';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
|
@ -121,6 +121,30 @@ function getItemLabel( item: LinkedTree, text: string ) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const CustomItemLabelOnSearch: React.FC = () => {
|
||||||
|
const [ text, setText ] = useState( '' );
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TextControl value={ text } onChange={ setText } />
|
||||||
|
<BaseControl
|
||||||
|
label="Custom item label on search"
|
||||||
|
id="custom-item-label-on-search"
|
||||||
|
>
|
||||||
|
<TreeControl
|
||||||
|
id="custom-item-label-on-search"
|
||||||
|
items={ listItems }
|
||||||
|
getItemLabel={ ( item ) => getItemLabel( item, text ) }
|
||||||
|
shouldItemBeExpanded={ useCallback(
|
||||||
|
( item ) => shouldItemBeExpanded( item, text ),
|
||||||
|
[ text ]
|
||||||
|
) }
|
||||||
|
/>
|
||||||
|
</BaseControl>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const SelectionSingle: React.FC = () => {
|
export const SelectionSingle: React.FC = () => {
|
||||||
const [ selected, setSelected ] = useState( listItems[ 1 ] );
|
const [ selected, setSelected ] = useState( listItems[ 1 ] );
|
||||||
|
|
||||||
|
@ -183,6 +207,53 @@ export const SelectionMultiple: React.FC = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function getFirstMatchingItem(
|
||||||
|
item: LinkedTree,
|
||||||
|
text: string,
|
||||||
|
memo: Record< string, string >
|
||||||
|
) {
|
||||||
|
if ( ! text ) return false;
|
||||||
|
if ( memo[ text ] === item.data.value ) return true;
|
||||||
|
|
||||||
|
const matcher = new RegExp( text, 'ig' );
|
||||||
|
if ( matcher.test( item.data.label ) ) {
|
||||||
|
if ( ! memo[ text ] ) {
|
||||||
|
memo[ text ] = item.data.value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HighlightFirstMatchingItem: React.FC = () => {
|
||||||
|
const [ text, setText ] = useState( '' );
|
||||||
|
const memo = useRef< Record< string, string > >( {} );
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TextControl value={ text } onChange={ setText } />
|
||||||
|
<BaseControl
|
||||||
|
label="Highlight first matching item"
|
||||||
|
id="highlight-first-matching-item"
|
||||||
|
>
|
||||||
|
<TreeControl
|
||||||
|
id="highlight-first-matching-item"
|
||||||
|
items={ listItems }
|
||||||
|
getItemLabel={ ( item ) => getItemLabel( item, text ) }
|
||||||
|
shouldItemBeExpanded={ useCallback(
|
||||||
|
( item ) => shouldItemBeExpanded( item, text ),
|
||||||
|
[ text ]
|
||||||
|
) }
|
||||||
|
shouldItemBeHighlighted={ ( item ) =>
|
||||||
|
getFirstMatchingItem( item, text, memo.current )
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</BaseControl>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'WooCommerce Admin/experimental/TreeControl',
|
title: 'WooCommerce Admin/experimental/TreeControl',
|
||||||
component: TreeControl,
|
component: TreeControl,
|
||||||
|
|
|
@ -25,6 +25,7 @@ export const TreeItem = forwardRef( function ForwardedTreeItem(
|
||||||
treeProps,
|
treeProps,
|
||||||
expander: { isExpanded, onToggleExpand },
|
expander: { isExpanded, onToggleExpand },
|
||||||
selection,
|
selection,
|
||||||
|
highlighter: { isHighlighted },
|
||||||
getLabel,
|
getLabel,
|
||||||
} = useTreeItem( {
|
} = useTreeItem( {
|
||||||
...props,
|
...props,
|
||||||
|
@ -39,8 +40,7 @@ export const TreeItem = forwardRef( function ForwardedTreeItem(
|
||||||
'experimental-woocommerce-tree-item',
|
'experimental-woocommerce-tree-item',
|
||||||
{
|
{
|
||||||
'experimental-woocommerce-tree-item--highlighted':
|
'experimental-woocommerce-tree-item--highlighted':
|
||||||
! selection.multiple &&
|
isHighlighted,
|
||||||
selection.checkedStatus === 'checked',
|
|
||||||
}
|
}
|
||||||
) }
|
) }
|
||||||
>
|
>
|
||||||
|
|
|
@ -39,6 +39,21 @@ type BaseTreeProps = {
|
||||||
* @param value The unselection
|
* @param value The unselection
|
||||||
*/
|
*/
|
||||||
onRemove?( value: Item | Item[] ): void;
|
onRemove?( value: Item | Item[] ): void;
|
||||||
|
/**
|
||||||
|
* It provides a way to determine whether the current rendering
|
||||||
|
* item is highlighted or not from outside the tree.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* <Tree
|
||||||
|
* shouldItemBeHighlighted={ isFirstChild }
|
||||||
|
* />
|
||||||
|
*
|
||||||
|
* @param item The current linked tree item, useful to
|
||||||
|
* traverse the entire linked tree from this item.
|
||||||
|
*
|
||||||
|
* @see {@link LinkedTree}
|
||||||
|
*/
|
||||||
|
shouldItemBeHighlighted?( item: LinkedTree ): boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TreeProps = BaseTreeProps &
|
export type TreeProps = BaseTreeProps &
|
||||||
|
|
Loading…
Reference in New Issue