@wordpress/components
Version:
UI components for WordPress.
269 lines (252 loc) • 7.23 kB
JavaScript
// @ts-nocheck
/**
* External dependencies
*/
import { useSelect } from 'downshift';
import classnames from 'classnames';
/**
* WordPress dependencies
*/
import { Icon, check } from '@wordpress/icons';
import { __, sprintf } from '@wordpress/i18n';
import { useCallback, useState } from '@wordpress/element';
import deprecated from '@wordpress/deprecated';
/**
* Internal dependencies
*/
import { VisuallyHidden } from '../visually-hidden';
import { Select as SelectControlSelect } from '../select-control/styles/select-control-styles';
import SelectControlChevronDown from '../select-control/chevron-down';
import { InputBaseWithBackCompatMinWidth } from './styles';
import { StyledLabel } from '../base-control/styles/base-control-styles';
const itemToString = ( item ) => item?.name;
// This is needed so that in Windows, where
// the menu does not necessarily open on
// key up/down, you can still switch between
// options with the menu closed.
const stateReducer = (
{ selectedItem },
{ type, changes, props: { items } }
) => {
switch ( type ) {
case useSelect.stateChangeTypes.ToggleButtonKeyDownArrowDown:
// If we already have a selected item, try to select the next one,
// without circular navigation. Otherwise, select the first item.
return {
selectedItem:
items[
selectedItem
? Math.min(
items.indexOf( selectedItem ) + 1,
items.length - 1
)
: 0
],
};
case useSelect.stateChangeTypes.ToggleButtonKeyDownArrowUp:
// If we already have a selected item, try to select the previous one,
// without circular navigation. Otherwise, select the last item.
return {
selectedItem:
items[
selectedItem
? Math.max( items.indexOf( selectedItem ) - 1, 0 )
: items.length - 1
],
};
default:
return changes;
}
};
export default function CustomSelectControl( props ) {
const {
/** Start opting into the larger default height that will become the default size in a future version. */
__next36pxDefaultSize = false,
/** Start opting into the unconstrained width that will become the default in a future version. */
__nextUnconstrainedWidth = false,
className,
hideLabelFromVision,
label,
describedBy,
options: items,
onChange: onSelectedItemChange,
/** @type {import('../select-control/types').SelectControlProps.size} */
size = 'default',
value: _selectedItem,
onMouseOver,
onMouseOut,
onFocus,
onBlur,
__experimentalShowSelectedHint = false,
} = props;
const {
getLabelProps,
getToggleButtonProps,
getMenuProps,
getItemProps,
isOpen,
highlightedIndex,
selectedItem,
} = useSelect( {
initialSelectedItem: items[ 0 ],
items,
itemToString,
onSelectedItemChange,
...( typeof _selectedItem !== 'undefined' && _selectedItem !== null
? { selectedItem: _selectedItem }
: undefined ),
stateReducer,
} );
const [ isFocused, setIsFocused ] = useState( false );
function handleOnFocus( e ) {
setIsFocused( true );
onFocus?.( e );
}
function handleOnBlur( e ) {
setIsFocused( false );
onBlur?.( e );
}
if ( ! __nextUnconstrainedWidth ) {
deprecated(
'Constrained width styles for wp.components.CustomSelectControl',
{
since: '6.1',
version: '6.4',
hint: 'Set the `__nextUnconstrainedWidth` prop to true to start opting into the new styles, which will become the default in a future version',
}
);
}
function getDescribedBy() {
if ( describedBy ) {
return describedBy;
}
if ( ! selectedItem ) {
return __( 'No selection' );
}
// translators: %s: The selected option.
return sprintf( __( 'Currently selected: %s' ), selectedItem.name );
}
const menuProps = getMenuProps( {
className: 'components-custom-select-control__menu',
'aria-hidden': ! isOpen,
} );
const onKeyDownHandler = useCallback(
( e ) => {
e.stopPropagation();
menuProps?.onKeyDown?.( e );
},
[ menuProps ]
);
// We need this here, because the null active descendant is not fully ARIA compliant.
if (
menuProps[ 'aria-activedescendant' ]?.startsWith( 'downshift-null' )
) {
delete menuProps[ 'aria-activedescendant' ];
}
return (
<div
className={ classnames(
'components-custom-select-control',
className
) }
>
{ hideLabelFromVision ? (
<VisuallyHidden as="label" { ...getLabelProps() }>
{ label }
</VisuallyHidden>
) : (
/* eslint-disable-next-line jsx-a11y/label-has-associated-control, jsx-a11y/label-has-for */
<StyledLabel
{ ...getLabelProps( {
className: 'components-custom-select-control__label',
} ) }
>
{ label }
</StyledLabel>
) }
<InputBaseWithBackCompatMinWidth
__next36pxDefaultSize={ __next36pxDefaultSize }
__nextUnconstrainedWidth={ __nextUnconstrainedWidth }
isFocused={ isOpen || isFocused }
__unstableInputWidth={
__nextUnconstrainedWidth ? undefined : 'auto'
}
labelPosition={ __nextUnconstrainedWidth ? undefined : 'top' }
size={ size }
suffix={ <SelectControlChevronDown /> }
>
<SelectControlSelect
onMouseOver={ onMouseOver }
onMouseOut={ onMouseOut }
as="button"
onFocus={ handleOnFocus }
onBlur={ handleOnBlur }
selectSize={ size }
__next36pxDefaultSize={ __next36pxDefaultSize }
{ ...getToggleButtonProps( {
// This is needed because some speech recognition software don't support `aria-labelledby`.
'aria-label': label,
'aria-labelledby': undefined,
className: 'components-custom-select-control__button',
describedBy: getDescribedBy(),
} ) }
>
{ itemToString( selectedItem ) }
{ __experimentalShowSelectedHint &&
selectedItem.__experimentalHint && (
<span className="components-custom-select-control__hint">
{ selectedItem.__experimentalHint }
</span>
) }
</SelectControlSelect>
</InputBaseWithBackCompatMinWidth>
{ /* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions */ }
<ul { ...menuProps } onKeyDown={ onKeyDownHandler }>
{ isOpen &&
items.map( ( item, index ) => (
// eslint-disable-next-line react/jsx-key
<li
{ ...getItemProps( {
item,
index,
key: item.key,
className: classnames(
item.className,
'components-custom-select-control__item',
{
'is-highlighted':
index === highlightedIndex,
'has-hint': !! item.__experimentalHint,
'is-next-36px-default-size':
__next36pxDefaultSize,
}
),
style: item.style,
} ) }
>
{ item.name }
{ item.__experimentalHint && (
<span className="components-custom-select-control__item-hint">
{ item.__experimentalHint }
</span>
) }
{ item === selectedItem && (
<Icon
icon={ check }
className="components-custom-select-control__item-icon"
/>
) }
</li>
) ) }
</ul>
</div>
);
}
export function StableCustomSelectControl( props ) {
return (
<CustomSelectControl
{ ...props }
__experimentalShowSelectedHint={ false }
/>
);
}