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:
parent
3f0219b1bc
commit
c4806c3ac8
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: update
|
||||
|
||||
Show comma separated list in ready only mode of select tree control
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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 && (
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: dev
|
||||
|
||||
Fix broken assertion with comma separated list in category select control
|
|
@ -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 );
|
||||
} );
|
||||
} );
|
||||
|
|
Loading…
Reference in New Issue