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:
Maikel David Pérez Gómez 2023-03-01 13:52:23 -03:00 committed by GitHub
parent 25497c4faa
commit e61211406f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 137 additions and 3 deletions

View File

@ -0,0 +1,4 @@
Significance: minor
Type: dev
Add highlighter to the tree control

View File

@ -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 };
}

View File

@ -8,6 +8,7 @@ import React from 'react';
*/
import { TreeItemProps } from '../types';
import { useExpander } from './use-expander';
import { useHighlighter } from './use-highlighter';
import { useSelection } from './use-selection';
export function useTreeItem( {
@ -18,6 +19,7 @@ export function useTreeItem( {
index,
getLabel,
shouldItemBeExpanded,
shouldItemBeHighlighted,
onSelect,
onRemove,
...props
@ -39,11 +41,19 @@ export function useTreeItem( {
onRemove,
} );
const highlighter = useHighlighter( {
item,
checkedStatus: selection.checkedStatus,
multiple,
shouldItemBeHighlighted,
} );
return {
item,
level: nextLevel,
expander,
selection,
highlighter,
getLabel,
treeItemProps: {
...props,
@ -60,6 +70,7 @@ export function useTreeItem( {
selected: selection.selected,
getItemLabel: getLabel,
shouldItemBeExpanded,
shouldItemBeHighlighted,
onSelect: selection.onSelectChildren,
onRemove: selection.onRemoveChildren,
},

View File

@ -15,6 +15,7 @@ export function useTree( {
selected,
getItemLabel,
shouldItemBeExpanded,
shouldItemBeHighlighted,
onSelect,
onRemove,
...props
@ -31,6 +32,7 @@ export function useTree( {
selected,
getLabel: getItemLabel,
shouldItemBeExpanded,
shouldItemBeHighlighted,
onSelect,
onRemove,
},

View File

@ -3,7 +3,7 @@
*/
import interpolate from '@automattic/interpolate-components';
import { BaseControl, TextControl } from '@wordpress/components';
import React, { createElement, useCallback, useState } from 'react';
import React, { createElement, useCallback, useRef, useState } from 'react';
/**
* 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 = () => {
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 {
title: 'WooCommerce Admin/experimental/TreeControl',
component: TreeControl,

View File

@ -25,6 +25,7 @@ export const TreeItem = forwardRef( function ForwardedTreeItem(
treeProps,
expander: { isExpanded, onToggleExpand },
selection,
highlighter: { isHighlighted },
getLabel,
} = useTreeItem( {
...props,
@ -39,8 +40,7 @@ export const TreeItem = forwardRef( function ForwardedTreeItem(
'experimental-woocommerce-tree-item',
{
'experimental-woocommerce-tree-item--highlighted':
! selection.multiple &&
selection.checkedStatus === 'checked',
isHighlighted,
}
) }
>

View File

@ -39,6 +39,21 @@ type BaseTreeProps = {
* @param value The unselection
*/
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 &