UNPKG

@project44-manifest/react

Version:

Manifest Design System react components

160 lines (139 loc) 4.45 kB
import * as React from 'react'; import { useCollator } from '@react-aria/i18n'; import { setInteractionModality } from '@react-aria/interactions'; import { useField } from '@react-aria/label'; import { useMenuTrigger } from '@react-aria/menu'; import type { SelectAria } from '@react-aria/select'; import { ListKeyboardDelegate, useTypeSelect } from '@react-aria/selection'; import { chain, filterDOMProps, mergeProps, useId } from '@react-aria/utils'; import type { KeyboardDelegate } from '@react-types/shared'; import type { AriaMultiSelectProps, MultiSelectState } from '../../types'; export interface AriaMultiSelectOptions<T> extends AriaMultiSelectProps<T> { /** * An optional keyboard delegate implementation for type to select, * to override the default. */ keyboardDelegate?: KeyboardDelegate; } export function useMultiSelect<T>( props: AriaMultiSelectOptions<T>, state: MultiSelectState<T>, ref: React.RefObject<HTMLElement>, ): SelectAria<T> { const { keyboardDelegate, isDisabled } = props; const collator = useCollator({ usage: 'search', sensitivity: 'base' }); const delegate = React.useMemo( () => keyboardDelegate ?? new ListKeyboardDelegate(state.collection, state.disabledKeys, null as never, collator), [keyboardDelegate, state.collection, state.disabledKeys, collator], ); const { menuTriggerProps, menuProps } = useMenuTrigger<T>( { isDisabled, type: 'listbox', }, state, ref, ); const { typeSelectProps } = useTypeSelect({ keyboardDelegate: delegate, selectionManager: state.selectionManager, onTypeSelect: (key) => { if (state.selectedKeys === 'all') { state.setSelectedKeys( new Set([...state.collection].map((item) => item.key).filter((k) => k !== key)), ); } else { state.setSelectedKeys(state.selectedKeys.add(key)); } }, }); const { labelProps, fieldProps, descriptionProps, errorMessageProps } = useField({ ...props, labelElementType: 'span', }); typeSelectProps.onKeyDown = typeSelectProps.onKeyDownCapture; delete typeSelectProps.onKeyDownCapture; const domProps = filterDOMProps(props, { labelable: true }); const triggerProps = mergeProps(typeSelectProps, menuTriggerProps, fieldProps); const valueId = useId(); return { labelProps: { ...labelProps, onClick: () => { if (!props.isDisabled) { ref.current?.focus(); // Show the focus ring so the user knows where focus went setInteractionModality('keyboard'); } }, }, triggerProps: mergeProps(domProps, { ...triggerProps, onKeyDown: chain(triggerProps.onKeyDown, props.onKeyDown), onKeyUp: props.onKeyUp, 'aria-labelledby': [ triggerProps['aria-labelledby'], triggerProps['aria-label'] && !triggerProps['aria-labelledby'] ? triggerProps.id : null, valueId, ] .filter(Boolean) .join(' '), onFocus(event: React.FocusEvent) { if (state.isFocused) { return; } if (props.onFocus) { props.onFocus(event); } if (props.onFocusChange) { props.onFocusChange(true); } state.setFocused(true); }, onBlur(event: React.FocusEvent) { if (state.isOpen) { return; } if (props.onBlur) { props.onBlur(event); } if (props.onFocusChange) { props.onFocusChange(false); } state.setFocused(false); }, }), valueProps: { id: valueId, }, menuProps: { ...menuProps, autoFocus: state.focusStrategy || true, shouldSelectOnPressUp: true, shouldFocusOnHover: true, disallowEmptySelection: true, onBlur: (event: React.FocusEvent) => { if (event.currentTarget.contains(event.relatedTarget as Node)) { return; } if (props.onBlur) { props.onBlur(event); } if (props.onFocusChange) { props.onFocusChange(false); } state.setFocused(false); }, 'aria-labelledby': [ fieldProps['aria-labelledby'], triggerProps['aria-label'] && !fieldProps['aria-labelledby'] ? triggerProps.id : null, ] .filter(Boolean) .join(' '), }, descriptionProps, errorMessageProps, }; }