import React, { useEffect, useMemo, useRef } from 'react'; import { useObjectRef } from 'react-aria'; import { Dialog, Popover } from 'react-aria-components'; import type { AriaLabelingProps } from '@react-types/shared'; import { useTranslations } from '@shoptet/ui-core-web'; import { useScreenSize } from '../../../hooks/useScreenSize'; import { forwardRef } from '../../../utils/forwardRef'; import { InputDialog } from '../../Fields/InputDialog/InputDialog'; import { useMultiSelect } from './hooks/useMultiSelect'; import type { MultiSelectSelectors } from './hooks/useMultiSelectState'; import { extractAllItemKeys, useMultiSelectState } from './hooks/useMultiSelectState'; import { MultiSelectList } from './MultiSelectList/MultiSelectList'; import { dictionary } from './dictionary'; import { MultiSelectContent } from './MultiSelectContent'; import { MultiSelectFilter } from './MultiSelectFilter'; import type { MultiSelectInputProps } from './MultiSelectInput'; import { MultiSelectInput } from './MultiSelectInput'; import { MultiSelectToggleAll } from './MultiSelectToggleAll'; function useFocusTriggerOnCloseSafariFix( isOpen: boolean, triggerRef: React.RefObject ) { const focusTriggerOnCloseActive = useRef(false); const shouldFocusTriggerOnClose = focusTriggerOnCloseActive.current && !isOpen; useEffect(() => { if (shouldFocusTriggerOnClose && triggerRef.current) { triggerRef.current.focus(); } focusTriggerOnCloseActive.current = isOpen; }, [isOpen, shouldFocusTriggerOnClose, triggerRef]); } /** @inheritdoc */ export type MultiSelectProps = MultiSelectSelectors & Pick, 'error' | 'warning' | 'width' | 'placeholder' | 'label' | keyof AriaLabelingProps> & { /** The unique identifier of the input */ id?: string; /** Whether the element should receive focus on render. */ autoFocus?: boolean; /** The options to render in the list. */ options: T[]; /** Whether the field is disabled. */ disabled?: boolean; /** Handler that is called when the selection changes. */ onChange?: (selection: string[]) => void; /** The default query to search for (uncontrolled). */ defaultQuery?: string; /** The query to search for (controlled). */ query?: string; /** Handler that is called when the search query text changes. */ onQueryChange?: (value: string) => void; /** The currently selected keys in the collection (controlled). */ value?: string[]; /** The initial selected keys in the collection (uncontrolled). */ defaultValue?: string[]; /** The item keys that are disabled. These items cannot be selected, focused, or otherwise interacted with. */ disabledValues?: string[]; /** Whether the input shows options in a dropdown or directly. */ mode?: 'dropdown' | 'content'; /** Whether the select all button should be hidden. */ hideSelectAll?: boolean; }; export const MultiSelect = forwardRef(function MultiSelect( props: MultiSelectProps, forwardedRef: React.ForwardedRef ) { const { id, disabled: isDisabled, label, error, warning, width = 'md', mode = 'dropdown', onChange, value, defaultValue, options, getOptionValue, getGroupOptions, placeholder, hideSelectAll = false, } = props; const triggerRef = useObjectRef(forwardedRef); const popoverRef = useRef(null); const filterRef = useRef(null); const listRef = useRef(null); const disabledValues = useMemo( () => isDisabled && mode === 'content' ? extractAllItemKeys(options ?? [], getOptionValue, getGroupOptions) : props.disabledValues, [getGroupOptions, getOptionValue, mode, options, isDisabled, props.disabledValues] ); const state = useMultiSelectState({ ...props, items: options, onSelectionChange: onChange, selectedKeys: value, defaultSelectedKeys: defaultValue, disabledKeys: disabledValues, }); const translations = useTranslations(dictionary); useFocusTriggerOnCloseSafariFix(state.isOpen, triggerRef); const { inputProps, popoverProps, dialogProps, filterProps, toggleAllProps, listProps } = useMultiSelect( { ...props, isDisabled, triggerRef, popoverRef, filterRef, listRef, }, state ); const screenSize = useScreenSize(); const showToggleAll = !hideSelectAll && state.collection.size > 0; if (mode === 'content') { return ( {/* Select all for controlled options not implemented */} {showToggleAll && ( <> {translations.toggleAll}
)} ); } const input = ( { if (e.pointerType !== 'touch' && screenSize === 'large') { inputProps.onPressStart?.(e); } }} onPress={e => { if (e.pointerType === 'touch') { inputProps.onPress?.(e); } else if (screenSize !== 'large') { inputProps.onPressStart?.(e); } }} /> ); const search = ( ); // Select all for controlled options not implemented const toggleAll = showToggleAll && ( <>
{translations.toggleAll} ); const list = ( ); if (screenSize === 'large') { return ( <> {input} !open && state.close()} className='multiSelectField__popover' triggerRef={triggerRef} style={{ width: triggerRef.current?.offsetWidth }} // We set the offset via CSS offset={0} shouldCloseOnInteractOutside={ state.isOpen ? element => element !== null && triggerRef.current !== null && element !== triggerRef.current : undefined } {...popoverProps} >
{search}
{toggleAll}
{list}
); } return ( !open && state.close()} {...popoverProps} {...dialogProps} >
{search}
{toggleAll}
{list}
); });