mirror of
https://github.com/nextui-org/nextui.git
synced 2025-12-08 19:26:11 +00:00
* chore(select): update select deps * fix(select): select scroll content will close immediately when popover on click * chore(select): add .changeset file * chore(changeset): add issue number --------- Co-authored-by: աӄա <wingkwong.code@gmail.com>
754 lines
21 KiB
TypeScript
754 lines
21 KiB
TypeScript
import type {SelectSlots, SelectVariantProps, SlotsToClasses} from "@heroui/theme";
|
|
import type {HiddenSelectProps} from "./hidden-select";
|
|
|
|
import {
|
|
DOMAttributes,
|
|
HTMLHeroUIProps,
|
|
mapPropsVariants,
|
|
PropGetter,
|
|
SharedSelection,
|
|
useLabelPlacement,
|
|
useProviderContext,
|
|
} from "@heroui/system";
|
|
import {select} from "@heroui/theme";
|
|
import {ReactRef, useDOMRef, filterDOMProps} from "@heroui/react-utils";
|
|
import {useMemo, useCallback, useRef, Key, ReactNode, useEffect} from "react";
|
|
import {ListboxProps} from "@heroui/listbox";
|
|
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 {PopoverProps} from "@heroui/popover";
|
|
import {ScrollShadowProps} from "@heroui/scroll-shadow";
|
|
import {
|
|
MultiSelectProps,
|
|
MultiSelectState,
|
|
useMultiSelect,
|
|
useMultiSelectState,
|
|
} from "@heroui/use-aria-multiselect";
|
|
import {SpinnerProps} from "@heroui/spinner";
|
|
import {useSafeLayoutEffect} from "@heroui/use-safe-layout-effect";
|
|
import {ariaShouldCloseOnInteractOutside} from "@heroui/aria-utils";
|
|
import {CollectionChildren, ValidationError} from "@react-types/shared";
|
|
import {FormContext, useSlottedContext} from "@heroui/form";
|
|
import {usePreventScroll} from "@react-aria/overlays";
|
|
|
|
export type SelectedItemProps<T = object> = {
|
|
/** A unique key for the item. */
|
|
key?: Key;
|
|
/** The props passed to the item. */
|
|
props?: Record<string, any>;
|
|
/** The item data. */
|
|
data?: T | null;
|
|
/** An accessibility label for this item. */
|
|
"aria-label"?: string;
|
|
/** The rendered contents of this item (e.g. JSX). */
|
|
rendered?: ReactNode;
|
|
/** A string value for this item, used for features like typeahead. */
|
|
textValue?: string;
|
|
/** The type of item this item represents. */
|
|
type?: string;
|
|
};
|
|
|
|
export type SelectedItems<T = object> = Array<SelectedItemProps<T>>;
|
|
|
|
interface Props<T> extends Omit<HTMLHeroUIProps<"select">, keyof SelectVariantProps> {
|
|
/**
|
|
* Ref to the DOM node.
|
|
*/
|
|
ref?: ReactRef<HTMLSelectElement | null>;
|
|
/**
|
|
* The ref to the scroll element. Useful when having async loading of items.
|
|
*/
|
|
scrollRef?: ReactRef<HTMLElement | null>;
|
|
/**
|
|
* The ref to the spinner element.
|
|
*/
|
|
spinnerRef?: ReactRef<HTMLElement | null>;
|
|
/**
|
|
* Whether the select is required.
|
|
* @default false
|
|
*/
|
|
isRequired?: boolean;
|
|
/**
|
|
* The icon that represents the select open state. Usually a chevron icon.
|
|
*/
|
|
selectorIcon?: ReactNode;
|
|
/**
|
|
* Element to be rendered in the left side of the select.
|
|
*/
|
|
startContent?: React.ReactNode;
|
|
/**
|
|
* Element to be rendered in the right side of the select.
|
|
*/
|
|
endContent?: ReactNode;
|
|
/**
|
|
* The placeholder for the select to display when no option is selected.
|
|
* @default "Select an option"
|
|
*/
|
|
placeholder?: string;
|
|
/**
|
|
* Whether to display a top and bottom arrow indicators when the listbox is scrollable.
|
|
* @default true
|
|
*/
|
|
showScrollIndicators?: boolean;
|
|
/**
|
|
* Props to be passed to the popover component.
|
|
*
|
|
* @default { placement: "bottom", triggerScaleOnOpen: false, offset: 5 }
|
|
*/
|
|
popoverProps?: Partial<PopoverProps>;
|
|
/**
|
|
* Props to be passed to the listbox component.
|
|
*
|
|
* @default { disableAnimation: false }
|
|
*/
|
|
listboxProps?: Partial<ListboxProps>;
|
|
/**
|
|
* Props to be passed to the scroll shadow component. This component
|
|
* adds a shadow to the top and bottom of the listbox when it is scrollable.
|
|
*
|
|
* @default { hideScrollBar: true, offset: 15 }
|
|
*/
|
|
scrollShadowProps?: Partial<ScrollShadowProps>;
|
|
/**
|
|
* Props to be passed to the spinner component.
|
|
*
|
|
* @default { size: "sm" , color: "current" }
|
|
*/
|
|
spinnerProps?: Partial<SpinnerProps>;
|
|
/**
|
|
* Function to render the value of the select. It renders the selected item by default.
|
|
* @param value
|
|
* @returns
|
|
*/
|
|
renderValue?: (items: SelectedItems<T>) => ReactNode;
|
|
/**
|
|
* Callback fired when the select menu is closed.
|
|
*/
|
|
onClose?: () => void;
|
|
/**
|
|
* Classes object to style the select and its children.
|
|
*/
|
|
classNames?: SlotsToClasses<SelectSlots>;
|
|
/**
|
|
* Handler that is called when the selection changes.
|
|
*/
|
|
onSelectionChange?: (keys: SharedSelection) => void;
|
|
/**
|
|
* A function that returns an error message if a given value is invalid.
|
|
* Validation errors are displayed to the user when the form is submitted
|
|
* if `validationBehavior="native"`. For realtime validation, use the `isInvalid`
|
|
* prop instead.
|
|
*/
|
|
validate?: (value: string | string[]) => ValidationError | true | null | undefined;
|
|
}
|
|
|
|
interface SelectData {
|
|
isDisabled?: boolean;
|
|
isRequired?: boolean;
|
|
isInvalid?: boolean;
|
|
name?: string;
|
|
validationBehavior?: "aria" | "native";
|
|
}
|
|
|
|
export const selectData = new WeakMap<MultiSelectState<any>, SelectData>();
|
|
|
|
export type UseSelectProps<T> = Omit<
|
|
Props<T>,
|
|
keyof Omit<MultiSelectProps<T>, "onSelectionChange">
|
|
> &
|
|
Omit<MultiSelectProps<T>, "onSelectionChange"> &
|
|
SelectVariantProps & {
|
|
/**
|
|
* The height of each item in the listbox.
|
|
* For dataset with sections, the itemHeight must be the height of each item (including padding, border, margin).
|
|
* This is required for virtualized listboxes to calculate the height of each item.
|
|
* @default 36
|
|
*/
|
|
itemHeight?: number;
|
|
/**
|
|
* The max height of the listbox (which will be rendered in a popover).
|
|
* This is required for virtualized listboxes to set the maximum height of the listbox.
|
|
*/
|
|
maxListboxHeight?: number;
|
|
/**
|
|
* Whether to enable virtualization of the listbox items.
|
|
* By default, virtualization is automatically enabled when the number of items is greater than 50.
|
|
* @default undefined
|
|
*/
|
|
isVirtualized?: boolean;
|
|
/**
|
|
* Whether the listbox will be prevented from opening when there are no items.
|
|
* @default false
|
|
*/
|
|
hideEmptyContent?: boolean;
|
|
};
|
|
|
|
export function useSelect<T extends object>(originalProps: UseSelectProps<T>) {
|
|
const globalContext = useProviderContext();
|
|
const {validationBehavior: formValidationBehavior} = useSlottedContext(FormContext) || {};
|
|
|
|
const [props, variantProps] = mapPropsVariants(originalProps, select.variantKeys);
|
|
|
|
const disableAnimation =
|
|
originalProps.disableAnimation ?? globalContext?.disableAnimation ?? 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 = formValidationBehavior ?? globalContext?.validationBehavior ?? "native",
|
|
hideEmptyContent = false,
|
|
...otherProps
|
|
} = props;
|
|
|
|
const scrollShadowRef = useDOMRef(scrollRefProp);
|
|
|
|
const slotsProps: {
|
|
popoverProps: UseSelectProps<T>["popoverProps"];
|
|
scrollShadowProps: UseSelectProps<T>["scrollShadowProps"];
|
|
listboxProps: UseSelectProps<T>["listboxProps"];
|
|
} = {
|
|
popoverProps: mergeProps(
|
|
{
|
|
placement: "bottom",
|
|
triggerScaleOnOpen: false,
|
|
offset: 5,
|
|
disableAnimation,
|
|
},
|
|
popoverProps,
|
|
),
|
|
scrollShadowProps: mergeProps(
|
|
{
|
|
ref: scrollShadowRef,
|
|
isEnabled: originalProps.showScrollIndicators ?? 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<HTMLElement>(null);
|
|
const listBoxRef = useRef<HTMLUListElement>(null);
|
|
const popoverRef = useRef<HTMLDivElement>(null);
|
|
|
|
let state = useMultiSelectState<T>({
|
|
...props,
|
|
isOpen,
|
|
selectionMode,
|
|
disallowEmptySelection,
|
|
validationBehavior,
|
|
children: children as CollectionChildren<T>,
|
|
isRequired: originalProps.isRequired,
|
|
isDisabled: originalProps.isDisabled,
|
|
isInvalid: originalProps.isInvalid,
|
|
defaultOpen,
|
|
hideEmptyContent,
|
|
onOpenChange: (open) => {
|
|
onOpenChange?.(open);
|
|
if (!open) {
|
|
onClose?.();
|
|
}
|
|
},
|
|
onSelectionChange: (keys) => {
|
|
onSelectionChange?.(keys);
|
|
if (onChange && typeof onChange === "function") {
|
|
onChange({
|
|
target: {
|
|
...(domRef.current && {
|
|
...domRef.current,
|
|
name: domRef.current.name,
|
|
}),
|
|
value: Array.from(keys).join(","),
|
|
},
|
|
} as React.ChangeEvent<HTMLSelectElement>);
|
|
}
|
|
state.commitValidation();
|
|
},
|
|
});
|
|
|
|
state = {
|
|
...state,
|
|
...(originalProps.isDisabled && {
|
|
disabledKeys: new Set([...state.collection.getKeys()]),
|
|
}),
|
|
};
|
|
|
|
// if we use `react-hook-form`, it will set the native select value using the ref in register
|
|
// i.e. setting ref.current.value to something which is uncontrolled
|
|
// hence, sync the state with `ref.current.value`
|
|
useSafeLayoutEffect(() => {
|
|
if (!domRef.current?.value) return;
|
|
|
|
state.setSelectedKeys(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" &&
|
|
(!(hasPlaceholder || !!description) || !!originalProps.isMultiline));
|
|
const shouldLabelBeInside = labelPlacement === "inside";
|
|
const isOutsideLeft = labelPlacement === "outside-left";
|
|
|
|
const isFilled =
|
|
state.isOpen ||
|
|
hasPlaceholder ||
|
|
!!state.selectedItems?.length ||
|
|
!!startContent ||
|
|
!!endContent ||
|
|
!!originalProps.isMultiline;
|
|
const hasValue = !!state.selectedItems?.length;
|
|
const hasLabel = !!label;
|
|
|
|
const baseStyles = clsx(classNames?.base, className);
|
|
|
|
const slots = useMemo(
|
|
() =>
|
|
select({
|
|
...variantProps,
|
|
isInvalid,
|
|
labelPlacement,
|
|
disableAnimation,
|
|
}),
|
|
[objectToDeps(variantProps), isInvalid, labelPlacement, disableAnimation],
|
|
);
|
|
|
|
// scroll the listbox to the selected item
|
|
useEffect(() => {
|
|
if (state.isOpen && popoverRef.current && listBoxRef.current) {
|
|
let selectedItem = listBoxRef.current.querySelector("[aria-selected=true] [data-label=true]");
|
|
let scrollShadow = scrollShadowRef.current;
|
|
|
|
// scroll the listbox to the selected item
|
|
if (selectedItem && scrollShadow && selectedItem.parentElement) {
|
|
let scrollShadowRect = scrollShadow?.getBoundingClientRect();
|
|
let scrollShadowHeight = scrollShadowRect.height;
|
|
|
|
scrollShadow.scrollTop =
|
|
selectedItem.parentElement.offsetTop -
|
|
scrollShadowHeight / 2 +
|
|
selectedItem.parentElement.clientHeight / 2;
|
|
}
|
|
}
|
|
}, [state.isOpen, disableAnimation]);
|
|
|
|
usePreventScroll({
|
|
isDisabled: !state.isOpen,
|
|
});
|
|
|
|
const errorMessage =
|
|
typeof props.errorMessage === "function"
|
|
? props.errorMessage({isInvalid, validationErrors, validationDetails})
|
|
: props.errorMessage || validationErrors?.join(" ");
|
|
|
|
const hasHelper = !!description || !!errorMessage;
|
|
|
|
// apply the same with to the popover as the select
|
|
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: PropGetter = useCallback(
|
|
(props = {}) => ({
|
|
"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),
|
|
className: slots.base({
|
|
class: clsx(baseStyles, props.className),
|
|
}),
|
|
...props,
|
|
}),
|
|
[slots, hasHelper, hasValue, hasLabel, isFilled, baseStyles],
|
|
);
|
|
|
|
const getTriggerProps: PropGetter = useCallback(
|
|
(props = {}) => {
|
|
return {
|
|
ref: triggerRef,
|
|
"data-slot": "trigger",
|
|
"data-open": dataAttr(state.isOpen),
|
|
"data-disabled": dataAttr(originalProps?.isDisabled),
|
|
"data-focus": dataAttr(isFocused),
|
|
"data-pressed": dataAttr(isPressed),
|
|
"data-focus-visible": dataAttr(isFocusVisible),
|
|
"data-hover": dataAttr(isHovered),
|
|
className: slots.trigger({class: classNames?.trigger}),
|
|
...mergeProps(
|
|
buttonProps,
|
|
focusProps,
|
|
hoverProps,
|
|
filterDOMProps(otherProps, {
|
|
enabled: shouldFilterDOMProps,
|
|
}),
|
|
filterDOMProps(props),
|
|
),
|
|
};
|
|
},
|
|
[
|
|
slots,
|
|
triggerRef,
|
|
state.isOpen,
|
|
classNames?.trigger,
|
|
originalProps?.isDisabled,
|
|
isFocused,
|
|
isPressed,
|
|
isFocusVisible,
|
|
isHovered,
|
|
buttonProps,
|
|
focusProps,
|
|
hoverProps,
|
|
otherProps,
|
|
shouldFilterDOMProps,
|
|
],
|
|
);
|
|
|
|
const getHiddenSelectProps = useCallback(
|
|
(props = {}) =>
|
|
({
|
|
state,
|
|
triggerRef,
|
|
selectRef: domRef,
|
|
selectionMode,
|
|
label: originalProps?.label,
|
|
name: originalProps?.name,
|
|
isRequired: originalProps?.isRequired,
|
|
autoComplete: originalProps?.autoComplete,
|
|
isDisabled: originalProps?.isDisabled,
|
|
onChange,
|
|
...props,
|
|
} as HiddenSelectProps<T>),
|
|
[
|
|
state,
|
|
selectionMode,
|
|
originalProps?.label,
|
|
originalProps?.autoComplete,
|
|
originalProps?.name,
|
|
originalProps?.isDisabled,
|
|
triggerRef,
|
|
],
|
|
);
|
|
|
|
const getLabelProps: PropGetter = useCallback(
|
|
(props = {}) => ({
|
|
"data-slot": "label",
|
|
className: slots.label({
|
|
class: clsx(classNames?.label, props.className),
|
|
}),
|
|
...labelProps,
|
|
...props,
|
|
}),
|
|
[slots, classNames?.label, labelProps],
|
|
);
|
|
|
|
const getValueProps: PropGetter = useCallback(
|
|
(props = {}) => ({
|
|
"data-slot": "value",
|
|
className: slots.value({
|
|
class: clsx(classNames?.value, props.className),
|
|
}),
|
|
...valueProps,
|
|
...props,
|
|
}),
|
|
[slots, classNames?.value, valueProps],
|
|
);
|
|
|
|
const getListboxWrapperProps: PropGetter = useCallback(
|
|
(props = {}) => ({
|
|
"data-slot": "listboxWrapper",
|
|
className: slots.listboxWrapper({
|
|
class: clsx(classNames?.listboxWrapper, props?.className),
|
|
}),
|
|
style: {
|
|
maxHeight: maxListboxHeight ?? 256,
|
|
...props.style,
|
|
},
|
|
...mergeProps(slotsProps.scrollShadowProps, props),
|
|
}),
|
|
[
|
|
slots.listboxWrapper,
|
|
classNames?.listboxWrapper,
|
|
slotsProps.scrollShadowProps,
|
|
maxListboxHeight,
|
|
],
|
|
);
|
|
|
|
const getListboxProps = (props: any = {}) => {
|
|
const shouldVirtualize = isVirtualized ?? state.collection.size > 50;
|
|
|
|
return {
|
|
state,
|
|
ref: listBoxRef,
|
|
isVirtualized: shouldVirtualize,
|
|
virtualization: shouldVirtualize
|
|
? {
|
|
maxListboxHeight,
|
|
itemHeight,
|
|
}
|
|
: undefined,
|
|
"data-slot": "listbox",
|
|
className: slots.listbox({
|
|
class: clsx(classNames?.listbox, props?.className),
|
|
}),
|
|
scrollShadowProps: slotsProps.scrollShadowProps,
|
|
...mergeProps(slotsProps.listboxProps, props, menuProps),
|
|
} as ListboxProps;
|
|
};
|
|
|
|
const getPopoverProps = useCallback(
|
|
(props: DOMAttributes = {}) => {
|
|
const popoverProps = mergeProps(slotsProps.popoverProps, props);
|
|
|
|
return {
|
|
state,
|
|
triggerRef,
|
|
ref: popoverRef,
|
|
"data-slot": "popover",
|
|
scrollRef: listBoxRef,
|
|
triggerType: "listbox",
|
|
classNames: {
|
|
content: slots.popoverContent({
|
|
class: clsx(classNames?.popoverContent, props.className),
|
|
}),
|
|
},
|
|
...popoverProps,
|
|
offset:
|
|
state.selectedItems && state.selectedItems.length > 0
|
|
? // forces the popover to update its position when the selected items change
|
|
state.selectedItems.length * 0.00000001 + (slotsProps.popoverProps?.offset || 0)
|
|
: slotsProps.popoverProps?.offset,
|
|
shouldCloseOnInteractOutside: popoverProps?.shouldCloseOnInteractOutside
|
|
? popoverProps.shouldCloseOnInteractOutside
|
|
: (element: Element) => ariaShouldCloseOnInteractOutside(element, domRef, state),
|
|
} as PopoverProps;
|
|
},
|
|
[
|
|
slots,
|
|
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?.selectorIcon}),
|
|
}),
|
|
[slots, classNames?.selectorIcon, state.isOpen],
|
|
);
|
|
|
|
const getInnerWrapperProps: PropGetter = useCallback(
|
|
(props = {}) => {
|
|
return {
|
|
...props,
|
|
"data-slot": "innerWrapper",
|
|
className: slots.innerWrapper({
|
|
class: clsx(classNames?.innerWrapper, props?.className),
|
|
}),
|
|
};
|
|
},
|
|
[slots, classNames?.innerWrapper],
|
|
);
|
|
|
|
const getHelperWrapperProps: PropGetter = useCallback(
|
|
(props = {}) => {
|
|
return {
|
|
...props,
|
|
"data-slot": "helperWrapper",
|
|
className: slots.helperWrapper({
|
|
class: clsx(classNames?.helperWrapper, props?.className),
|
|
}),
|
|
};
|
|
},
|
|
[slots, classNames?.helperWrapper],
|
|
);
|
|
|
|
const getDescriptionProps: PropGetter = useCallback(
|
|
(props = {}) => {
|
|
return {
|
|
...props,
|
|
...descriptionProps,
|
|
"data-slot": "description",
|
|
className: slots.description({class: clsx(classNames?.description, props?.className)}),
|
|
};
|
|
},
|
|
[slots, classNames?.description],
|
|
);
|
|
|
|
const getMainWrapperProps: PropGetter = useCallback(
|
|
(props = {}) => {
|
|
return {
|
|
...props,
|
|
"data-slot": "mainWrapper",
|
|
className: slots.mainWrapper({
|
|
class: clsx(classNames?.mainWrapper, props?.className),
|
|
}),
|
|
};
|
|
},
|
|
[slots, classNames?.mainWrapper],
|
|
);
|
|
|
|
const getErrorMessageProps: PropGetter = useCallback(
|
|
(props = {}) => {
|
|
return {
|
|
...props,
|
|
...errorMessageProps,
|
|
"data-slot": "error-message",
|
|
className: slots.errorMessage({class: clsx(classNames?.errorMessage, props?.className)}),
|
|
};
|
|
},
|
|
[slots, errorMessageProps, classNames?.errorMessage],
|
|
);
|
|
|
|
const getSpinnerProps: PropGetter = useCallback(
|
|
(props = {}) => {
|
|
return {
|
|
"aria-hidden": dataAttr(true),
|
|
"data-slot": "spinner",
|
|
color: "current",
|
|
size: "sm",
|
|
...spinnerProps,
|
|
...props,
|
|
ref: spinnerRef,
|
|
className: slots.spinner({class: clsx(classNames?.spinner, props?.className)}),
|
|
};
|
|
},
|
|
[slots, spinnerRef, spinnerProps, classNames?.spinner],
|
|
);
|
|
|
|
// store the data to be used in useHiddenSelect
|
|
selectData.set(state, {
|
|
isDisabled: originalProps?.isDisabled,
|
|
isRequired: originalProps?.isRequired,
|
|
name: 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 type UseSelectReturn = ReturnType<typeof useSelect>;
|