/**
* WordPress dependencies
*/
import { useEffect, useRef, useState } from '@safe-wordpress/element';
import { FormTokenField } from '@safe-wordpress/components';
import { _x, sprintf } from '@safe-wordpress/i18n';
import type { RefObject } from 'react';
import type { TokenItem } from '@wordpress/components/build-types/form-token-field/types';
/**
* External dependencies
*/
import { noop, isString } from 'lodash';
/**
* Internal dependencies
*/
import './style.scss';
type Item = {
readonly value: string;
readonly label: string;
};
export type MultipleSelectControlProps = {
readonly values: ReadonlyArray< string >;
readonly options: ReadonlyArray< Item >;
readonly placeholder?: string;
readonly onChange: ( value: ReadonlyArray< string > ) => void;
readonly disabled?: boolean;
};
export const MultipleSelectControl = ( {
values = [],
options,
disabled,
placeholder = _x( 'Select…', 'user', 'nelio-session-recordings' ),
onChange,
}: MultipleSelectControlProps ): JSX.Element => {
const [ autoExpand, setAutoExpand ] = useState( false ); // NOTE. Workaround.
const ref = useRef< HTMLDivElement >( null );
useEffectOnFocusAndBlur( ref, setAutoExpand );
const onSelectionChange = (
selection: ReadonlyArray< string | TokenItem >
): void => {
const str = selection.find( isString ) ?? '';
const item = findByLabel( str, options );
onChange(
[ ...selection, { itemId: item?.value } ]
.filter( hasItemId )
.map( ( s ) => s.itemId )
);
};
return (
itemToFormValue( value, options )
) }
disabled={ disabled }
suggestions={ options
.filter( ( o ) => ! values.includes( o.value ) )
.map( ( o ) => o.label ) }
onChange={ onSelectionChange }
{ ...{
label: '',
__experimentalExpandOnFocus: autoExpand,
placeholder,
} }
/>
);
};
// =======
// HELPERS
// =======
const findByLabel = (
label: string,
items: ReadonlyArray< Item >
): Item | undefined =>
items.find( ( item ) => item.label.toLowerCase() === label.toLowerCase() );
const itemToFormValue = (
value: string,
options: ReadonlyArray< Item >
): string | TokenItem =>
( {
itemId: value,
value:
options.find( ( o ) => o.value === value )?.label ??
sprintf(
/* translators: item value */
_x( 'Missing item %s', 'text', 'nelio-session-recordings' ),
value
),
} ) as string | TokenItem;
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
const hasItemId = ( p: any ): p is { itemId: string } => !! p.itemId;
// =====
// HOOKS
// =====
const useEffectOnFocusAndBlur = (
ref: RefObject< HTMLDivElement >,
callback: ( focus: boolean ) => void = noop
) =>
useEffect( () => {
const onFocus = () => callback( true );
const onBlur = () => callback( false );
const opts = { capture: true };
ref.current?.addEventListener( 'focus', onFocus, opts );
ref.current?.addEventListener( 'blur', onBlur, opts );
return () => {
ref.current?.removeEventListener( 'focus', onFocus, opts );
// eslint-disable-next-line react-hooks/exhaustive-deps
ref.current?.removeEventListener( 'blur', onBlur, opts );
};
}, [ callback, ref ] );