import React, { useId, useRef, type HTMLAttributes } from 'react' import { mergeRefs } from '@react-aria/utils' import classnames from 'classnames' import { type ReactFocusOnProps } from 'react-focus-on/dist/es5/types' import { FieldMessage, type FieldMessageProps } from '~components/FieldMessage' import { Heading } from '~components/Heading' import { type OverrideClassName } from '~components/types/OverrideClassName' import { MultiSelectOptions, type MultiSelectOptionsProps, } from './subcomponents/MultiSelectOptions' import { MultiSelectToggle } from './subcomponents/MultiSelectToggle' import { Popover, useFloating } from './subcomponents/Popover' import { type MultiSelectOption, type ValidationMessage } from './types' import styles from './MultiSelect.module.scss' export type MultiSelectProps = { inputRef?: React.Ref label: string items: MultiSelectOptionsProps['options'] selectedValues: Set /** * A description that provides context for the field */ description?: FieldMessageProps['message'] onSelectedValuesChange: MultiSelectOptionsProps['onChange'] isOpen: boolean onOpenChange: (isOpen: boolean) => void /** A status and message to provide context to the validation issue */ validationMessage?: ValidationMessage } & OverrideClassName> export const MultiSelect = ({ id: propsId, label, items, selectedValues, description, onSelectedValuesChange, isOpen, onOpenChange, classNameOverride, validationMessage, inputRef, ...restProps }: MultiSelectProps): JSX.Element => { const fallbackId = useId() const id = propsId ?? fallbackId const descriptionId = `${id}-description` const validationId = `${id}-validation-message` const toggleButtonRef = useRef(null) const { refs } = useFloating() const handleToggleClick = (): void => onOpenChange(!isOpen) const handleClose = (): void => onOpenChange(false) const onClickOutside: ReactFocusOnProps['onClickOutside'] = (e) => { const toggle = refs.reference.current as Node const isInToggle = toggle.contains(e.target as HTMLElement) if (!isInToggle) { e.stopPropagation() handleClose() } } const itemsMap = items.reduce( (acc, item) => { acc[item.value] = item return acc }, {} as Record, ) const handleOnRemoveOption = (optionValue: MultiSelectOption['value']): void => { const newValues = new Set(selectedValues.values()) newValues.delete(optionValue) onSelectedValuesChange(newValues) } const handleRemoveAllOptions = (): void => { const newValues = new Set([]) onSelectedValuesChange(newValues) } return (
{label}
itemsMap[value])} status={validationMessage?.status} onRemoveOption={handleOnRemoveOption} onRemoveAllOptions={handleRemoveAllOptions} />
{validationMessage && } {description && } {isOpen && ( refs.floating?.current?.focus(), }} classNameOverride={styles.popover} > )}
) } MultiSelect.displayName = 'MultiSelect'