/**
* External dependencies
*/
import { __, sprintf } from '@wordpress/i18n';
import {
Button,
FormTokenField,
Spinner,
TextControl,
withSpokenMessages,
} from '@wordpress/components';
import {
useState,
useMemo,
useEffect,
useCallback,
Fragment,
} from '@wordpress/element';
import { Icon, info } from '@wordpress/icons';
import classnames from 'classnames';
import { useInstanceId } from '@wordpress/compose';
/**
* Internal dependencies
*/
import { getFilteredList, defaultMessages } from './utils';
import SearchListItem from './item';
import Tag from '../tag';
import type {
SearchListItem as SearchListItemProps,
SearchListControlProps,
SearchListMessages,
renderItemArgs,
ListItemsProps,
SearchListItemsContainerProps,
} from './types';
const defaultRenderListItem = ( args: renderItemArgs ): JSX.Element => {
return ;
};
const ListItems = ( props: ListItemsProps ): JSX.Element | null => {
const {
list,
selected,
renderItem,
depth = 0,
onSelect,
instanceId,
isSingle,
search,
useExpandedPanelId,
} = props;
const [ expandedPanelId ] = useExpandedPanelId;
if ( ! list ) {
return null;
}
return (
<>
{ list.map( ( item ) => {
const isSelected =
item.children?.length && ! isSingle
? item.children.every( ( { id } ) =>
selected.find(
( selectedItem ) => selectedItem.id === id
)
)
: !! selected.find( ( { id } ) => id === item.id );
const isExpanded =
item.children?.length && expandedPanelId === item.id;
return (
{ renderItem( {
item,
isSelected,
onSelect,
isSingle,
selected,
search,
depth,
useExpandedPanelId,
controlId: instanceId,
} ) }
{ isExpanded ? (
) : null }
);
} ) }
>
);
};
const SelectedListItems = ( {
isLoading,
isSingle,
selected,
messages,
onChange,
onRemove,
}: SearchListControlProps & {
messages: SearchListMessages;
onRemove: ( itemId: string | number ) => () => void;
} ) => {
if ( isLoading || isSingle || ! selected ) {
return null;
}
const selectedCount = selected.length;
return (
{ messages.selected( selectedCount ) }
{ selectedCount > 0 ? (
) : null }
{ selectedCount > 0 ? (
{ selected.map( ( item, i ) => (
-
) ) }
) : null }
);
};
const ListItemsContainer = ( {
filteredList,
search,
onSelect,
instanceId,
useExpandedPanelId,
...props
}: SearchListItemsContainerProps ) => {
const { messages, renderItem, selected, isSingle } = props;
const renderItemCallback = renderItem || defaultRenderListItem;
if ( filteredList.length === 0 ) {
return (
{ search
? // eslint-disable-next-line @wordpress/valid-sprintf
sprintf( messages.noResults, search )
: messages.noItems }
);
}
return (
);
};
/**
* Component to display a searchable, selectable list of items.
*/
export const SearchListControl = ( props: SearchListControlProps ) => {
const {
className = '',
isCompact,
isHierarchical,
isLoading,
isSingle,
list,
messages: customMessages = defaultMessages,
onChange,
onSearch,
selected,
type = 'text',
debouncedSpeak,
} = props;
const [ search, setSearch ] = useState( '' );
const useExpandedPanelId = useState< number >( -1 );
const instanceId = useInstanceId( SearchListControl );
const messages = useMemo(
() => ( { ...defaultMessages, ...customMessages } ),
[ customMessages ]
);
const filteredList = useMemo( () => {
return getFilteredList( list, search, isHierarchical );
}, [ list, search, isHierarchical ] );
useEffect( () => {
if ( debouncedSpeak ) {
debouncedSpeak( messages.updated );
}
}, [ debouncedSpeak, messages ] );
useEffect( () => {
if ( typeof onSearch === 'function' ) {
onSearch( search );
}
}, [ search, onSearch ] );
const onRemove = useCallback(
( itemId: string | number ) => () => {
if ( isSingle ) {
onChange( [] );
}
const i = selected.findIndex(
( { id: selectedId } ) => selectedId === itemId
);
onChange( [
...selected.slice( 0, i ),
...selected.slice( i + 1 ),
] );
},
[ isSingle, selected, onChange ]
);
const onSelect = useCallback(
( item: SearchListItemProps | SearchListItemProps[] ) => () => {
if ( Array.isArray( item ) ) {
onChange( item );
return;
}
if ( selected.findIndex( ( { id } ) => id === item.id ) !== -1 ) {
onRemove( item.id )();
return;
}
if ( isSingle ) {
onChange( [ item ] );
} else {
onChange( [ ...selected, item ] );
}
},
[ isSingle, onRemove, onChange, selected ]
);
const onRemoveToken = useCallback(
( tokens: Array< SearchListItemProps & { value: string } > ) => {
const [ removedItem ] = selected.filter(
( item ) => ! tokens.find( ( token ) => item.id === token.id )
);
onRemove( removedItem.id )();
},
[ onRemove, selected ]
);
return (
{ type === 'text' && (
) }
{ type === 'text' ? (
setSearch( value ) }
/>
) : (
setSearch( value ) }
suggestions={ [] }
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - Ignoring because `__experimentalValidateInput` is not yet in the type definitions.
__experimentalValidateInput={ () => false }
value={
isLoading
? [
__(
'Loading…',
'woo-gutenberg-products-block'
),
]
: selected.map( ( token ) => ( {
...token,
value: token.name,
} ) )
}
__experimentalShowHowTo={ false }
/>
) }
{ isLoading ? (
) : (
) }
);
};
export default withSpokenMessages( SearchListControl );