Show comma separated list in ready only mode of select tree control (#38052)

* Add read-only state to selected items in select control

* Only close menu on focus outside of select control

* Add changelog entry

* Remove errant comment out of onBlur line

* Fix regression of input focus behavior

* Hide input when in read only mode

* Turn off read only mode when focused

* Show select control input on single item dropdowns and when no selections have been made in multiple dropdowns

* Prevent focus loss when removing an item

* Prevent loss of field focus when clicking on selected item tags

* Fix broken assertion with comma separated list

* Add product editor changelog entry
This commit is contained in:
Joshua T Flowers 2023-05-08 00:17:52 -07:00 committed by GitHub
parent 3f0219b1bc
commit c4806c3ac8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 110 additions and 23 deletions

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
Show comma separated list in ready only mode of select tree control

View File

@ -75,8 +75,8 @@ export const ComboBox = ( {
<input
{ ...inputProps }
ref={ ( node ) => {
inputRef.current = node;
if ( typeof inputProps.ref === 'function' ) {
inputRef.current = node;
(
inputProps.ref as unknown as (
node: HTMLInputElement | null

View File

@ -12,6 +12,18 @@
border-color: var( --wp-admin-theme-color );
}
&.is-read-only.is-multiple.has-selected-items {
.woocommerce-experimental-select-control__combo-box-wrapper {
cursor: default;
}
.woocommerce-experimental-select-control__input {
opacity: 0;
width: 0;
height: 0;
}
}
&__label {
display: inline-block;
margin-bottom: $gap-smaller;

View File

@ -9,13 +9,14 @@ import {
useMultipleSelection,
GetInputPropsOptions,
} from 'downshift';
import { useInstanceId } from '@wordpress/compose';
import {
useState,
useEffect,
createElement,
Fragment,
} from '@wordpress/element';
import { search } from '@wordpress/icons';
import { chevronDown } from '@wordpress/icons';
/**
* Internal dependencies
@ -123,12 +124,16 @@ function SelectControl< ItemType = DefaultItemType >( {
className,
disabled,
inputProps = {},
suffix = <SuffixIcon icon={ search } />,
suffix = <SuffixIcon icon={ chevronDown } />,
showToggleButton = false,
__experimentalOpenMenuOnFocus = false,
}: SelectControlProps< ItemType > ) {
const [ isFocused, setIsFocused ] = useState( false );
const [ inputValue, setInputValue ] = useState( '' );
const instanceId = useInstanceId(
SelectControl,
'woocommerce-experimental-select-control'
);
let selectedItems = selected === null ? [] : selected;
selectedItems = Array.isArray( selectedItems )
@ -230,15 +235,24 @@ function SelectControl< ItemType = DefaultItemType >( {
},
} );
const isEventOutside = ( event: React.FocusEvent ) => {
return ! document
.querySelector( '.' + instanceId )
?.contains( event.relatedTarget );
};
const onRemoveItem = ( item: ItemType ) => {
selectItem( null );
removeSelectedItem( item );
onRemove( item );
};
const isReadOnly = ! isOpen && ! isFocused;
const selectedItemTags = multiple ? (
<SelectedItems
items={ selectedItems }
isReadOnly={ isReadOnly }
getItemLabel={ getItemLabel }
getItemValue={ getItemValue }
getSelectedItemProps={ getSelectedItemProps }
@ -251,8 +265,12 @@ function SelectControl< ItemType = DefaultItemType >( {
className={ classnames(
'woocommerce-experimental-select-control',
className,
instanceId,
{
'is-read-only': isReadOnly,
'is-focused': isFocused,
'is-multiple': multiple,
'has-selected-items': selectedItems.length,
}
) }
>
@ -282,7 +300,11 @@ function SelectControl< ItemType = DefaultItemType >( {
openMenu();
}
},
onBlur: () => setIsFocused( false ),
onBlur: ( event: React.FocusEvent ) => {
if ( isEventOutside( event ) ) {
setIsFocused( false );
}
},
placeholder,
disabled,
...inputProps,

View File

@ -1,3 +1,9 @@
.woocommerce-experimental-select-control__selected-items.is-read-only {
font-size: 13px;
color: $gray-900;
font-family: var(--wp--preset--font-family--system-font);
}
.woocommerce-experimental-select-control__selected-item {
margin-right: $gap-smallest;
margin-top: 2px;

View File

@ -1,7 +1,8 @@
/**
* External dependencies
*/
import { createElement, Fragment } from '@wordpress/element';
import classnames from 'classnames';
import { createElement } from '@wordpress/element';
/**
* Internal dependencies
@ -10,6 +11,7 @@ import Tag from '../tag';
import { getItemLabelType, getItemValueType } from './types';
type SelectedItemsProps< ItemType > = {
isReadOnly: boolean;
items: ItemType[];
getItemLabel: getItemLabelType< ItemType >;
getItemValue: getItemValueType< ItemType >;
@ -22,14 +24,34 @@ type SelectedItemsProps< ItemType > = {
};
export const SelectedItems = < ItemType, >( {
isReadOnly,
items,
getItemLabel,
getItemValue,
getSelectedItemProps,
onRemove,
}: SelectedItemsProps< ItemType > ) => {
const classes = classnames(
'woocommerce-experimental-select-control__selected-items',
{
'is-read-only': isReadOnly,
}
);
if ( isReadOnly ) {
return (
<div className={ classes }>
{ items
.map( ( item ) => {
return getItemLabel( item );
} )
.join( ', ' ) }
</div>
);
}
return (
<>
<div className={ classes }>
{ items.map( ( item, index ) => {
return (
// Disable reason: We prevent the default action to keep the input focused on click.
@ -42,6 +64,9 @@ export const SelectedItems = < ItemType, >( {
selectedItem: item,
index,
} ) }
onMouseDown={ ( event ) => {
event.preventDefault();
} }
onClick={ ( event ) => {
event.preventDefault();
} }
@ -56,6 +81,6 @@ export const SelectedItems = < ItemType, >( {
</div>
);
} ) }
</>
</div>
);
};

View File

@ -22,6 +22,7 @@ import {
} from '../experimental-tree-control';
type MenuProps = {
isEventOutside: ( event: React.FocusEvent ) => boolean;
isOpen: boolean;
isLoading?: boolean;
position?: Popover.Position;
@ -32,6 +33,7 @@ type MenuProps = {
} & Omit< TreeControlProps, 'items' >;
export const SelectTreeMenu = ( {
isEventOutside,
isLoading,
isOpen,
className,
@ -103,8 +105,10 @@ export const SelectTreeMenu = ( {
) }
position={ position }
animate={ false }
onFocusOutside={ () => {
onClose();
onFocusOutside={ ( event ) => {
if ( isEventOutside( event ) ) {
onClose();
}
} }
>
{ isOpen && (

View File

@ -2,9 +2,9 @@
/**
* External dependencies
*/
import { createElement, useState } from '@wordpress/element';
import { chevronDown } from '@wordpress/icons';
import classNames from 'classnames';
import { search } from '@wordpress/icons';
import { createElement, useState } from '@wordpress/element';
import { useInstanceId } from '@wordpress/compose';
/**
@ -32,7 +32,7 @@ export const SelectTree = function SelectTree( {
items,
getSelectedItemProps,
treeRef: ref,
suffix = <SuffixIcon icon={ search } />,
suffix = <SuffixIcon icon={ chevronDown } />,
placeholder,
isLoading,
onInputChange,
@ -40,24 +40,37 @@ export const SelectTree = function SelectTree( {
...props
}: SelectTreeProps ) {
const linkedTree = useLinkedTree( items );
const selectTreeInstanceId = useInstanceId(
SelectTree,
'woocommerce-experimental-select-tree-control__dropdown'
);
const menuInstanceId = useInstanceId(
SelectTree,
'woocommerce-select-tree-control__menu'
);
const isEventOutside = ( event: React.FocusEvent ) => {
return ! document
.querySelector( '.' + selectTreeInstanceId )
?.contains( event.relatedTarget );
};
const [ isFocused, setIsFocused ] = useState( false );
const [ isOpen, setIsOpen ] = useState( false );
const isReadOnly = ! isOpen && ! isFocused;
return (
<div
className="woocommerce-experimental-select-tree-control__dropdown"
className={ `woocommerce-experimental-select-tree-control__dropdown ${ selectTreeInstanceId }` }
tabIndex={ -1 }
>
<div
className={ classNames(
'woocommerce-experimental-select-control',
{
'is-read-only': isReadOnly,
'is-focused': isFocused,
'is-multiple': props.multiple,
'has-selected-items': props.selected?.length,
}
) }
>
@ -93,12 +106,7 @@ export const SelectTree = function SelectTree( {
},
onBlur: ( event ) => {
// if blurring to an element inside the dropdown, don't close it
if (
isOpen &&
! document
.querySelector( '.' + menuInstanceId )
?.contains( event.relatedTarget )
) {
if ( isEventOutside( event ) ) {
setIsOpen( false );
}
setIsFocused( false );
@ -126,13 +134,13 @@ export const SelectTree = function SelectTree( {
suffix={ suffix }
>
<SelectedItems
isReadOnly={ isReadOnly }
items={ ( props.selected as Item[] ) || [] }
getItemLabel={ ( item ) => item?.label || '' }
getItemValue={ ( item ) => item?.value || '' }
onRemove={ ( item ) => {
if ( ! Array.isArray( item ) && props.onRemove ) {
props.onRemove( item );
setIsOpen( false );
}
} }
getSelectedItemProps={ () => ( {} ) }
@ -144,10 +152,13 @@ export const SelectTree = function SelectTree( {
id={ `${ props.id }-menu` }
className={ menuInstanceId.toString() }
ref={ ref }
isEventOutside={ isEventOutside }
isOpen={ isOpen }
items={ linkedTree }
shouldShowCreateButton={ shouldShowCreateButton }
onClose={ () => setIsOpen( false ) }
onClose={ () => {
setIsOpen( false );
} }
/>
</div>
);

View File

@ -0,0 +1,4 @@
Significance: minor
Type: dev
Fix broken assertion with comma separated list in category select control

View File

@ -74,7 +74,6 @@ describe( 'CategoryField', () => {
</Form>
);
queryByPlaceholderText( 'Search or create category…' )?.focus();
expect( queryAllByText( 'Test' ) ).toHaveLength( 2 );
expect( queryAllByText( 'Clothing' ) ).toHaveLength( 2 );
expect( queryAllByText( 'Test, Clothing' ) ).toHaveLength( 1 );
} );
} );