UNPKG

@heroui/select

Version:

A select displays a collapsible list of options and allows a user to select one of them.

501 lines (498 loc) 16.7 kB
"use client"; // src/use-select.ts import { mapPropsVariants, useLabelPlacement, useProviderContext } from "@heroui/system"; import { select } from "@heroui/theme"; import { useDOMRef, filterDOMProps } from "@heroui/react-utils"; import { useMemo, useCallback, useRef, useEffect } from "react"; import { useAriaButton } from "@heroui/use-aria-button"; import { useFocusRing } from "@react-aria/focus"; import { clsx, dataAttr, objectToDeps } from "@heroui/shared-utils"; import { mergeProps } from "@react-aria/utils"; import { useHover } from "@react-aria/interactions"; import { useMultiSelect, useMultiSelectState } from "@heroui/use-aria-multiselect"; import { useSafeLayoutEffect } from "@heroui/use-safe-layout-effect"; import { ariaShouldCloseOnInteractOutside } from "@heroui/aria-utils"; import { FormContext, useSlottedContext } from "@heroui/form"; import { usePreventScroll } from "@react-aria/overlays"; var selectData = /* @__PURE__ */ new WeakMap(); function useSelect(originalProps) { var _a, _b, _c, _d, _e, _f; const globalContext = useProviderContext(); const { validationBehavior: formValidationBehavior } = useSlottedContext(FormContext) || {}; const [props, variantProps] = mapPropsVariants(originalProps, select.variantKeys); const disableAnimation = (_b = (_a = originalProps.disableAnimation) != null ? _a : globalContext == null ? void 0 : globalContext.disableAnimation) != null ? _b : false; const { ref, as, label, name, isLoading, selectorIcon, isOpen, defaultOpen, onOpenChange, startContent, endContent, description, renderValue, onSelectionChange, placeholder, isVirtualized, itemHeight = 36, maxListboxHeight = 256, children, disallowEmptySelection = false, selectionMode = "single", spinnerRef, scrollRef: scrollRefProp, popoverProps = {}, scrollShadowProps = {}, listboxProps = {}, spinnerProps = {}, validationState, onChange, onClose, className, classNames, validationBehavior = (_c = formValidationBehavior != null ? formValidationBehavior : globalContext == null ? void 0 : globalContext.validationBehavior) != null ? _c : "native", hideEmptyContent = false, ...otherProps } = props; const scrollShadowRef = useDOMRef(scrollRefProp); const slotsProps = { popoverProps: mergeProps( { placement: "bottom", triggerScaleOnOpen: false, offset: 5, disableAnimation }, popoverProps ), scrollShadowProps: mergeProps( { ref: scrollShadowRef, isEnabled: (_d = originalProps.showScrollIndicators) != null ? _d : true, hideScrollBar: true, offset: 15 }, scrollShadowProps ), listboxProps: mergeProps( { disableAnimation }, listboxProps ) }; const Component = as || "button"; const shouldFilterDOMProps = typeof Component === "string"; const domRef = useDOMRef(ref); const triggerRef = useRef(null); const listBoxRef = useRef(null); const popoverRef = useRef(null); let state = useMultiSelectState({ ...props, isOpen, selectionMode, disallowEmptySelection, validationBehavior, children, isRequired: originalProps.isRequired, isDisabled: originalProps.isDisabled, isInvalid: originalProps.isInvalid, defaultOpen, hideEmptyContent, onOpenChange: (open) => { onOpenChange == null ? void 0 : onOpenChange(open); if (!open) { onClose == null ? void 0 : onClose(); } }, onSelectionChange: (keys) => { onSelectionChange == null ? void 0 : onSelectionChange(keys); if (onChange && typeof onChange === "function") { onChange({ target: { ...domRef.current && { ...domRef.current, name: domRef.current.name }, value: Array.from(keys).join(",") } }); } state.commitValidation(); } }); state = { ...state, ...originalProps.isDisabled && { disabledKeys: /* @__PURE__ */ new Set([...state.collection.getKeys()]) } }; useSafeLayoutEffect(() => { var _a2; if (!((_a2 = domRef.current) == null ? void 0 : _a2.value)) return; state.setSelectedKeys(/* @__PURE__ */ new Set([...state.selectedKeys, domRef.current.value])); }, [domRef.current]); const { labelProps, triggerProps, valueProps, menuProps, descriptionProps, errorMessageProps, isInvalid: isAriaInvalid, validationErrors, validationDetails } = useMultiSelect( { ...props, disallowEmptySelection, isDisabled: originalProps.isDisabled }, state, triggerRef ); const isInvalid = originalProps.isInvalid || validationState === "invalid" || isAriaInvalid; const { isPressed, buttonProps } = useAriaButton(triggerProps, triggerRef); const { focusProps, isFocused, isFocusVisible } = useFocusRing(); const { isHovered, hoverProps } = useHover({ isDisabled: originalProps.isDisabled }); const labelPlacement = useLabelPlacement({ labelPlacement: originalProps.labelPlacement, label }); const hasPlaceholder = !!placeholder; const shouldLabelBeOutside = labelPlacement === "outside-left" || labelPlacement === "outside"; const shouldLabelBeInside = labelPlacement === "inside"; const isOutsideLeft = labelPlacement === "outside-left"; const isFilled = state.isOpen || hasPlaceholder || !!((_e = state.selectedItems) == null ? void 0 : _e.length) || !!startContent || !!endContent || !!originalProps.isMultiline; const hasValue = !!((_f = state.selectedItems) == null ? void 0 : _f.length); const hasLabel = !!label; const hasLabelOutside = hasLabel && (isOutsideLeft || shouldLabelBeOutside && hasPlaceholder); const baseStyles = clsx(classNames == null ? void 0 : classNames.base, className); const slots = useMemo( () => select({ ...variantProps, isInvalid, labelPlacement, disableAnimation }), [objectToDeps(variantProps), isInvalid, labelPlacement, disableAnimation] ); usePreventScroll({ isDisabled: !state.isOpen }); const errorMessage = typeof props.errorMessage === "function" ? props.errorMessage({ isInvalid, validationErrors, validationDetails }) : props.errorMessage || (validationErrors == null ? void 0 : validationErrors.join(" ")); const hasHelper = !!description || !!errorMessage; useEffect(() => { if (state.isOpen && popoverRef.current && triggerRef.current) { let selectRect = triggerRef.current.getBoundingClientRect(); let popover = popoverRef.current; popover.style.width = selectRect.width + "px"; } }, [state.isOpen]); const getBaseProps = useCallback( (props2 = {}) => ({ "data-slot": "base", "data-filled": dataAttr(isFilled), "data-has-value": dataAttr(hasValue), "data-has-label": dataAttr(hasLabel), "data-has-helper": dataAttr(hasHelper), "data-invalid": dataAttr(isInvalid), "data-has-label-outside": dataAttr(hasLabelOutside), className: slots.base({ class: clsx(baseStyles, props2.className) }), ...props2 }), [slots, hasHelper, hasValue, hasLabel, hasLabelOutside, isFilled, baseStyles] ); const getTriggerProps = useCallback( (props2 = {}) => { return { ref: triggerRef, "data-slot": "trigger", "data-open": dataAttr(state.isOpen), "data-disabled": dataAttr(originalProps == null ? void 0 : originalProps.isDisabled), "data-focus": dataAttr(isFocused), "data-pressed": dataAttr(isPressed), "data-focus-visible": dataAttr(isFocusVisible), "data-hover": dataAttr(isHovered), className: slots.trigger({ class: classNames == null ? void 0 : classNames.trigger }), ...mergeProps( buttonProps, focusProps, hoverProps, filterDOMProps(otherProps, { enabled: shouldFilterDOMProps }), filterDOMProps(props2) ) }; }, [ slots, triggerRef, state.isOpen, classNames == null ? void 0 : classNames.trigger, originalProps == null ? void 0 : originalProps.isDisabled, isFocused, isPressed, isFocusVisible, isHovered, buttonProps, focusProps, hoverProps, otherProps, shouldFilterDOMProps ] ); const getHiddenSelectProps = useCallback( (props2 = {}) => ({ state, triggerRef, selectRef: domRef, selectionMode, label: originalProps == null ? void 0 : originalProps.label, name: originalProps == null ? void 0 : originalProps.name, isRequired: originalProps == null ? void 0 : originalProps.isRequired, autoComplete: originalProps == null ? void 0 : originalProps.autoComplete, isDisabled: originalProps == null ? void 0 : originalProps.isDisabled, form: originalProps == null ? void 0 : originalProps.form, onChange, ...props2 }), [ state, selectionMode, originalProps == null ? void 0 : originalProps.label, originalProps == null ? void 0 : originalProps.autoComplete, originalProps == null ? void 0 : originalProps.name, originalProps == null ? void 0 : originalProps.isDisabled, triggerRef ] ); const getLabelProps = useCallback( (props2 = {}) => ({ "data-slot": "label", className: slots.label({ class: clsx(classNames == null ? void 0 : classNames.label, props2.className) }), ...labelProps, ...props2 }), [slots, classNames == null ? void 0 : classNames.label, labelProps] ); const getValueProps = useCallback( (props2 = {}) => ({ "data-slot": "value", className: slots.value({ class: clsx(classNames == null ? void 0 : classNames.value, props2.className) }), ...valueProps, ...props2 }), [slots, classNames == null ? void 0 : classNames.value, valueProps] ); const getListboxWrapperProps = useCallback( (props2 = {}) => ({ "data-slot": "listboxWrapper", className: slots.listboxWrapper({ class: clsx(classNames == null ? void 0 : classNames.listboxWrapper, props2 == null ? void 0 : props2.className) }), style: { maxHeight: maxListboxHeight != null ? maxListboxHeight : 256, ...props2.style }, ...mergeProps(slotsProps.scrollShadowProps, props2) }), [ slots.listboxWrapper, classNames == null ? void 0 : classNames.listboxWrapper, slotsProps.scrollShadowProps, maxListboxHeight ] ); const getListboxProps = (props2 = {}) => { const shouldVirtualize = isVirtualized != null ? isVirtualized : state.collection.size > 50; return { state, ref: listBoxRef, isVirtualized: shouldVirtualize, virtualization: shouldVirtualize ? { maxListboxHeight, itemHeight } : void 0, "data-slot": "listbox", className: slots.listbox({ class: clsx(classNames == null ? void 0 : classNames.listbox, props2 == null ? void 0 : props2.className) }), scrollShadowProps: slotsProps.scrollShadowProps, ...mergeProps(slotsProps.listboxProps, props2, menuProps) }; }; const getPopoverProps = useCallback( (props2 = {}) => { var _a2, _b2; const popoverProps2 = mergeProps(slotsProps.popoverProps, props2); return { state, triggerRef, ref: popoverRef, "data-slot": "popover", scrollRef: listBoxRef, triggerType: "listbox", classNames: { content: slots.popoverContent({ class: clsx(classNames == null ? void 0 : classNames.popoverContent, props2.className) }) }, ...popoverProps2, offset: state.selectedItems && state.selectedItems.length > 0 ? ( // forces the popover to update its position when the selected items change state.selectedItems.length * 1e-8 + (((_a2 = slotsProps.popoverProps) == null ? void 0 : _a2.offset) || 0) ) : (_b2 = slotsProps.popoverProps) == null ? void 0 : _b2.offset, shouldCloseOnInteractOutside: (popoverProps2 == null ? void 0 : popoverProps2.shouldCloseOnInteractOutside) ? popoverProps2.shouldCloseOnInteractOutside : (element) => ariaShouldCloseOnInteractOutside(element, domRef, state) }; }, [ slots, classNames == null ? void 0 : classNames.popoverContent, slotsProps.popoverProps, triggerRef, state, state.selectedItems ] ); const getSelectorIconProps = useCallback( () => ({ "data-slot": "selectorIcon", "aria-hidden": dataAttr(true), "data-open": dataAttr(state.isOpen), className: slots.selectorIcon({ class: classNames == null ? void 0 : classNames.selectorIcon }) }), [slots, classNames == null ? void 0 : classNames.selectorIcon, state.isOpen] ); const getInnerWrapperProps = useCallback( (props2 = {}) => { return { ...props2, "data-slot": "innerWrapper", className: slots.innerWrapper({ class: clsx(classNames == null ? void 0 : classNames.innerWrapper, props2 == null ? void 0 : props2.className) }) }; }, [slots, classNames == null ? void 0 : classNames.innerWrapper] ); const getHelperWrapperProps = useCallback( (props2 = {}) => { return { ...props2, "data-slot": "helperWrapper", className: slots.helperWrapper({ class: clsx(classNames == null ? void 0 : classNames.helperWrapper, props2 == null ? void 0 : props2.className) }) }; }, [slots, classNames == null ? void 0 : classNames.helperWrapper] ); const getDescriptionProps = useCallback( (props2 = {}) => { return { ...props2, ...descriptionProps, "data-slot": "description", className: slots.description({ class: clsx(classNames == null ? void 0 : classNames.description, props2 == null ? void 0 : props2.className) }) }; }, [slots, classNames == null ? void 0 : classNames.description] ); const getMainWrapperProps = useCallback( (props2 = {}) => { return { ...props2, "data-slot": "mainWrapper", className: slots.mainWrapper({ class: clsx(classNames == null ? void 0 : classNames.mainWrapper, props2 == null ? void 0 : props2.className) }) }; }, [slots, classNames == null ? void 0 : classNames.mainWrapper] ); const getErrorMessageProps = useCallback( (props2 = {}) => { return { ...props2, ...errorMessageProps, "data-slot": "error-message", className: slots.errorMessage({ class: clsx(classNames == null ? void 0 : classNames.errorMessage, props2 == null ? void 0 : props2.className) }) }; }, [slots, errorMessageProps, classNames == null ? void 0 : classNames.errorMessage] ); const getSpinnerProps = useCallback( (props2 = {}) => { return { "aria-hidden": dataAttr(true), "data-slot": "spinner", color: "current", size: "sm", ...spinnerProps, ...props2, ref: spinnerRef, className: slots.spinner({ class: clsx(classNames == null ? void 0 : classNames.spinner, props2 == null ? void 0 : props2.className) }) }; }, [slots, spinnerRef, spinnerProps, classNames == null ? void 0 : classNames.spinner] ); selectData.set(state, { isDisabled: originalProps == null ? void 0 : originalProps.isDisabled, isRequired: originalProps == null ? void 0 : originalProps.isRequired, name: originalProps == null ? void 0 : originalProps.name, isInvalid, validationBehavior }); return { Component, domRef, state, label, name, triggerRef, isLoading, placeholder, startContent, endContent, description, selectorIcon, hasHelper, labelPlacement, hasPlaceholder, renderValue, selectionMode, disableAnimation, isOutsideLeft, shouldLabelBeOutside, shouldLabelBeInside, isInvalid, errorMessage, getBaseProps, getTriggerProps, getLabelProps, getValueProps, getListboxProps, getPopoverProps, getSpinnerProps, getMainWrapperProps, getListboxWrapperProps, getHiddenSelectProps, getInnerWrapperProps, getHelperWrapperProps, getDescriptionProps, getErrorMessageProps, getSelectorIconProps }; } export { selectData, useSelect };