Skip to content

Commit

Permalink
Add hightlighter to the tree control (#36480)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
mdperez86 committed Mar 1, 2023
1 parent 25497c4 commit e612114
Show file tree
Hide file tree
Showing 7 changed files with 137 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: dev

Add highlighter to the tree control
Original file line number Diff line number Diff line change
@@ -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 };
}
Original file line number Diff line number Diff line change
Expand Up @@ -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( {
Expand All @@ -18,6 +19,7 @@ export function useTreeItem( {
index,
getLabel,
shouldItemBeExpanded,
shouldItemBeHighlighted,
onSelect,
onRemove,
...props
Expand All @@ -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,
Expand All @@ -60,6 +70,7 @@ export function useTreeItem( {
selected: selection.selected,
getItemLabel: getLabel,
shouldItemBeExpanded,
shouldItemBeHighlighted,
onSelect: selection.onSelectChildren,
onRemove: selection.onRemoveChildren,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export function useTree( {
selected,
getItemLabel,
shouldItemBeExpanded,
shouldItemBeHighlighted,
onSelect,
onRemove,
...props
Expand All @@ -31,6 +32,7 @@ export function useTree( {
selected,
getLabel: getItemLabel,
shouldItemBeExpanded,
shouldItemBeHighlighted,
onSelect,
onRemove,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 ] );

Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export const TreeItem = forwardRef( function ForwardedTreeItem(
treeProps,
expander: { isExpanded, onToggleExpand },
selection,
highlighter: { isHighlighted },
getLabel,
} = useTreeItem( {
...props,
Expand All @@ -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,
}
) }
>
Expand Down
15 changes: 15 additions & 0 deletions packages/js/components/src/experimental-tree-control/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 &
Expand Down

0 comments on commit e612114

Please sign in to comment.