UNPKG

@kobalte/core

Version:

Unstyled components and primitives for building accessible web apps and design systems with SolidJS.

710 lines (704 loc) 23.8 kB
import { HiddenSelectBase } from './R6P3BGDT.js'; import { ListboxItem, ListboxItemDescription, ListboxItemIndicator, ListboxItemLabel, ListboxSection, ListboxRoot } from './MJSKFKQK.js'; import { PopperArrow, Popper } from './LMWVDFW6.js'; import { ListKeyboardDelegate } from './GLKC2QFF.js'; import { createTypeSelect, createListState, Selection } from './H6DSIDEC.js'; import { createCollator } from './XHJPQEZP.js'; import { createFocusScope } from './ISKHZMHS.js'; import { createHideOutside } from './455ZADO2.js'; import { DismissableLayer } from './BASJUNIE.js'; import { createDisclosureState } from './7LCANGHD.js'; import { ButtonRoot } from './7OVKXYPU.js'; import { FORM_CONTROL_FIELD_PROP_NAMES, createFormControlField } from './HYP2U57X.js'; import { FormControlLabel } from './7ZHN3PYD.js'; import { createFormResetListener } from './ANN3A2QM.js'; import { FormControlErrorMessage } from './ICNSTULC.js'; import { FormControlDescription, useFormControlContext, FORM_CONTROL_PROP_NAMES, createFormControl, FormControlContext } from './YKGT7A57.js'; import { createRegisterId } from './E4R2EMM4.js'; import { Polymorphic } from './6Y7B2NEO.js'; import { __export } from './5ZKAE4VZ.js'; import { createComponent, mergeProps, memo, Portal } from 'solid-js/web'; import { focusWithoutScrolling, mergeRefs, mergeDefaultProps, isFunction, callHandler, access, createGenerateId } from '@kobalte/utils'; import { createContext, useContext, splitProps, Show, createEffect, onCleanup, createMemo, children, createUniqueId, createSignal, on } from 'solid-js'; import { combineStyle } from '@solid-primitives/props'; import createPreventScroll from 'solid-prevent-scroll'; import createPresence from 'solid-presence'; // src/select/index.tsx var select_exports = {}; __export(select_exports, { Arrow: () => PopperArrow, Content: () => SelectContent, Description: () => FormControlDescription, ErrorMessage: () => FormControlErrorMessage, HiddenSelect: () => SelectHiddenSelect, Icon: () => SelectIcon, Item: () => ListboxItem, ItemDescription: () => ListboxItemDescription, ItemIndicator: () => ListboxItemIndicator, ItemLabel: () => ListboxItemLabel, Label: () => SelectLabel, Listbox: () => SelectListbox, Portal: () => SelectPortal, Root: () => SelectRoot, Section: () => ListboxSection, Select: () => Select, Trigger: () => SelectTrigger, Value: () => SelectValue, useSelectContext: () => useSelectContext }); var SelectContext = createContext(); function useSelectContext() { const context = useContext(SelectContext); if (context === void 0) { throw new Error("[kobalte]: `useSelectContext` must be used within a `Select` component"); } return context; } // src/select/select-content.tsx function SelectContent(props) { let ref; const context = useSelectContext(); const [local, others] = splitProps(props, ["ref", "style", "onCloseAutoFocus", "onFocusOutside"]); const onEscapeKeyDown = (e) => { context.close(); }; const onFocusOutside = (e) => { local.onFocusOutside?.(e); if (context.isOpen() && context.isModal()) { e.preventDefault(); } }; createHideOutside({ isDisabled: () => !(context.isOpen() && context.isModal()), targets: () => ref ? [ref] : [] }); createPreventScroll({ element: () => ref ?? null, enabled: () => context.contentPresent() && context.preventScroll() }); createFocusScope({ trapFocus: () => context.isOpen() && context.isModal(), onMountAutoFocus: (e) => { e.preventDefault(); }, onUnmountAutoFocus: (e) => { local.onCloseAutoFocus?.(e); if (!e.defaultPrevented) { focusWithoutScrolling(context.triggerRef()); e.preventDefault(); } } }, () => ref); return createComponent(Show, { get when() { return context.contentPresent(); }, get children() { return createComponent(Popper.Positioner, { get children() { return createComponent(DismissableLayer, mergeProps({ ref(r$) { const _ref$ = mergeRefs((el) => { context.setContentRef(el); ref = el; }, local.ref); typeof _ref$ === "function" && _ref$(r$); }, get disableOutsidePointerEvents() { return memo(() => !!context.isModal())() && context.isOpen(); }, get excludedElements() { return [context.triggerRef]; }, get style() { return combineStyle({ "--kb-select-content-transform-origin": "var(--kb-popper-content-transform-origin)", position: "relative" }, local.style); }, onEscapeKeyDown, onFocusOutside, get onDismiss() { return context.close; } }, () => context.dataset(), others)); } }); } }); } function SelectHiddenSelect(props) { const context = useSelectContext(); return createComponent(HiddenSelectBase, mergeProps({ get collection() { return context.listState().collection(); }, get selectionManager() { return context.listState().selectionManager(); }, get isOpen() { return context.isOpen(); }, get isMultiple() { return context.isMultiple(); }, get isVirtualized() { return context.isVirtualized(); }, focusTrigger: () => context.triggerRef()?.focus() }, props)); } function SelectIcon(props) { const context = useSelectContext(); const mergedProps = mergeDefaultProps({ children: "\u25BC" }, props); return createComponent(Polymorphic, mergeProps({ as: "span", "aria-hidden": "true" }, () => context.dataset(), mergedProps)); } function SelectLabel(props) { const context = useSelectContext(); const [local, others] = splitProps(props, ["onClick"]); const onClick = (e) => { callHandler(e, local.onClick); if (!context.isDisabled()) { context.triggerRef()?.focus(); } }; return createComponent(FormControlLabel, mergeProps({ as: "span", onClick }, others)); } function SelectListbox(props) { const context = useSelectContext(); const mergedProps = mergeDefaultProps({ id: context.generateId("listbox") }, props); const [local, others] = splitProps(mergedProps, ["ref", "id", "onKeyDown"]); createEffect(() => onCleanup(context.registerListboxId(local.id))); const onKeyDown = (e) => { callHandler(e, local.onKeyDown); if (e.key === "Escape") { e.preventDefault(); } }; return createComponent(ListboxRoot, mergeProps({ ref(r$) { const _ref$ = mergeRefs(context.setListboxRef, local.ref); typeof _ref$ === "function" && _ref$(r$); }, get id() { return local.id; }, get state() { return context.listState(); }, get virtualized() { return context.isVirtualized(); }, get autoFocus() { return context.autoFocus(); }, shouldSelectOnPressUp: true, shouldFocusOnHover: true, get shouldFocusWrap() { return context.shouldFocusWrap(); }, get disallowTypeAhead() { return context.disallowTypeAhead(); }, get ["aria-labelledby"]() { return context.listboxAriaLabelledBy(); }, get renderItem() { return context.renderItem; }, get renderSection() { return context.renderSection; }, onKeyDown }, others)); } function SelectPortal(props) { const context = useSelectContext(); return createComponent(Show, { get when() { return context.contentPresent(); }, get children() { return createComponent(Portal, props); } }); } function SelectBase(props) { const defaultId = `select-${createUniqueId()}`; const mergedProps = mergeDefaultProps({ id: defaultId, selectionMode: "single", disallowEmptySelection: false, closeOnSelection: props.selectionMode === "single", allowDuplicateSelectionEvents: true, gutter: 8, sameWidth: true, modal: false }, props); const [local, popperProps, formControlProps, others] = splitProps(mergedProps, ["itemComponent", "sectionComponent", "open", "defaultOpen", "onOpenChange", "value", "defaultValue", "onChange", "placeholder", "options", "optionValue", "optionTextValue", "optionDisabled", "optionGroupChildren", "keyboardDelegate", "allowDuplicateSelectionEvents", "disallowEmptySelection", "closeOnSelection", "disallowTypeAhead", "shouldFocusWrap", "selectionBehavior", "selectionMode", "virtualized", "modal", "preventScroll", "forceMount"], ["getAnchorRect", "placement", "gutter", "shift", "flip", "slide", "overlap", "sameWidth", "fitViewport", "hideWhenDetached", "detachedPadding", "arrowPadding", "overflowPadding"], FORM_CONTROL_PROP_NAMES); const [triggerId, setTriggerId] = createSignal(); const [valueId, setValueId] = createSignal(); const [listboxId, setListboxId] = createSignal(); const [triggerRef, setTriggerRef] = createSignal(); const [contentRef, setContentRef] = createSignal(); const [listboxRef, setListboxRef] = createSignal(); const [listboxAriaLabelledBy, setListboxAriaLabelledBy] = createSignal(); const [focusStrategy, setFocusStrategy] = createSignal(true); const getOptionValue = (option) => { const optionValue = local.optionValue; if (optionValue == null) { return String(option); } return String(isFunction(optionValue) ? optionValue(option) : option[optionValue]); }; const flattenOptions = createMemo(() => { const optionGroupChildren = local.optionGroupChildren; if (optionGroupChildren == null) { return local.options; } return local.options.flatMap((item) => item[optionGroupChildren] ?? item); }); const flattenOptionKeys = createMemo(() => { return flattenOptions().map((option) => getOptionValue(option)); }); const getOptionsFromValues = (values) => { return [...values].map((value) => flattenOptions().find((option) => getOptionValue(option) === value)).filter((option) => option != null); }; const disclosureState = createDisclosureState({ open: () => local.open, defaultOpen: () => local.defaultOpen, onOpenChange: (isOpen) => local.onOpenChange?.(isOpen) }); const listState = createListState({ selectedKeys: () => { if (local.value != null) { return local.value.map(getOptionValue); } return local.value; }, defaultSelectedKeys: () => { if (local.defaultValue != null) { return local.defaultValue.map(getOptionValue); } return local.defaultValue; }, onSelectionChange: (selectedKeys) => { local.onChange?.(getOptionsFromValues(selectedKeys)); if (local.closeOnSelection) { close(); } }, allowDuplicateSelectionEvents: () => access(local.allowDuplicateSelectionEvents), disallowEmptySelection: () => access(local.disallowEmptySelection), selectionBehavior: () => access(local.selectionBehavior), selectionMode: () => local.selectionMode, dataSource: () => local.options ?? [], getKey: () => local.optionValue, getTextValue: () => local.optionTextValue, getDisabled: () => local.optionDisabled, getSectionChildren: () => local.optionGroupChildren }); const selectedOptions = createMemo(() => { return getOptionsFromValues(listState.selectionManager().selectedKeys()); }); const removeOptionFromSelection = (option) => { listState.selectionManager().toggleSelection(getOptionValue(option)); }; const { present: contentPresent } = createPresence({ show: () => local.forceMount || disclosureState.isOpen(), element: () => contentRef() ?? null }); const focusListbox = () => { const listboxEl = listboxRef(); if (listboxEl) { focusWithoutScrolling(listboxEl); } }; const open = (focusStrategy2) => { if (local.options.length <= 0) { return; } setFocusStrategy(focusStrategy2); disclosureState.open(); let focusedKey = listState.selectionManager().firstSelectedKey(); if (focusedKey == null) { if (focusStrategy2 === "first") { focusedKey = listState.collection().getFirstKey(); } else if (focusStrategy2 === "last") { focusedKey = listState.collection().getLastKey(); } } focusListbox(); listState.selectionManager().setFocused(true); listState.selectionManager().setFocusedKey(focusedKey); }; const close = () => { disclosureState.close(); listState.selectionManager().setFocused(false); listState.selectionManager().setFocusedKey(void 0); }; const toggle = (focusStrategy2) => { if (disclosureState.isOpen()) { close(); } else { open(focusStrategy2); } }; const { formControlContext } = createFormControl(formControlProps); createFormResetListener(triggerRef, () => { const defaultSelectedKeys = local.defaultValue ? [...local.defaultValue].map(getOptionValue) : new Selection(); listState.selectionManager().setSelectedKeys(defaultSelectedKeys); }); const collator = createCollator({ usage: "search", sensitivity: "base" }); const delegate = createMemo(() => { const keyboardDelegate = access(local.keyboardDelegate); if (keyboardDelegate) { return keyboardDelegate; } return new ListKeyboardDelegate(listState.collection, void 0, collator); }); const renderItem = (item) => { return local.itemComponent?.({ item }); }; const renderSection = (section) => { return local.sectionComponent?.({ section }); }; createEffect(on([flattenOptionKeys], ([flattenOptionKeys2]) => { const currentSelectedKeys = [...listState.selectionManager().selectedKeys()]; const keysToKeep = currentSelectedKeys.filter((key) => flattenOptionKeys2.includes(key)); listState.selectionManager().setSelectedKeys(keysToKeep); }, { defer: true })); const dataset = createMemo(() => ({ "data-expanded": disclosureState.isOpen() ? "" : void 0, "data-closed": !disclosureState.isOpen() ? "" : void 0 })); const context = { dataset, isOpen: disclosureState.isOpen, isDisabled: () => formControlContext.isDisabled() ?? false, isMultiple: () => access(local.selectionMode) === "multiple", isVirtualized: () => local.virtualized ?? false, isModal: () => local.modal ?? false, preventScroll: () => local.preventScroll ?? context.isModal(), disallowTypeAhead: () => local.disallowTypeAhead ?? false, shouldFocusWrap: () => local.shouldFocusWrap ?? false, selectedOptions, contentPresent, autoFocus: focusStrategy, triggerRef, listState: () => listState, keyboardDelegate: delegate, triggerId, valueId, listboxId, listboxAriaLabelledBy, setListboxAriaLabelledBy, setTriggerRef, setContentRef, setListboxRef, open, close, toggle, placeholder: () => local.placeholder, renderItem, renderSection, removeOptionFromSelection, generateId: createGenerateId(() => access(formControlProps.id)), registerTriggerId: createRegisterId(setTriggerId), registerValueId: createRegisterId(setValueId), registerListboxId: createRegisterId(setListboxId) }; return createComponent(FormControlContext.Provider, { value: formControlContext, get children() { return createComponent(SelectContext.Provider, { value: context, get children() { return createComponent(Popper, mergeProps({ anchorRef: triggerRef, contentRef }, popperProps, { get children() { return createComponent(Polymorphic, mergeProps({ as: "div", role: "group", get id() { return access(formControlProps.id); } }, () => formControlContext.dataset(), dataset, others)); } })); } }); } }); } // src/select/select-root.tsx function SelectRoot(props) { const [local, others] = splitProps(props, ["value", "defaultValue", "onChange", "multiple"]); const value = createMemo(() => { if (local.value != null) { return local.multiple ? local.value : [local.value]; } return local.value; }); const defaultValue = createMemo(() => { if (local.defaultValue != null) { return local.multiple ? local.defaultValue : [local.defaultValue]; } return local.defaultValue; }); const onChange = (value2) => { if (local.multiple) { local.onChange?.(value2 ?? []); } else { local.onChange?.(value2[0] ?? null); } }; return createComponent(SelectBase, mergeProps({ get value() { return value(); }, get defaultValue() { return defaultValue(); }, onChange, get selectionMode() { return local.multiple ? "multiple" : "single"; } }, others)); } function SelectTrigger(props) { const formControlContext = useFormControlContext(); const context = useSelectContext(); const mergedProps = mergeDefaultProps({ id: context.generateId("trigger") }, props); const [local, formControlFieldProps, others] = splitProps(mergedProps, ["ref", "disabled", "onPointerDown", "onClick", "onKeyDown", "onFocus", "onBlur"], FORM_CONTROL_FIELD_PROP_NAMES); const selectionManager = () => context.listState().selectionManager(); const keyboardDelegate = () => context.keyboardDelegate(); const isDisabled = () => local.disabled || context.isDisabled(); const { fieldProps } = createFormControlField(formControlFieldProps); const { typeSelectHandlers } = createTypeSelect({ keyboardDelegate, selectionManager, onTypeSelect: (key) => selectionManager().select(key) }); const ariaLabelledBy = () => { return [context.listboxAriaLabelledBy(), context.valueId()].filter(Boolean).join(" ") || void 0; }; const onPointerDown = (e) => { callHandler(e, local.onPointerDown); e.currentTarget.dataset.pointerType = e.pointerType; if (!isDisabled() && e.pointerType !== "touch" && e.button === 0) { e.preventDefault(); context.toggle(true); } }; const onClick = (e) => { callHandler(e, local.onClick); if (!isDisabled() && e.currentTarget.dataset.pointerType === "touch") { context.toggle(true); } }; const onKeyDown = (e) => { callHandler(e, local.onKeyDown); if (isDisabled()) { return; } callHandler(e, typeSelectHandlers.onKeyDown); switch (e.key) { case "Enter": case " ": case "ArrowDown": e.stopPropagation(); e.preventDefault(); context.toggle("first"); break; case "ArrowUp": e.stopPropagation(); e.preventDefault(); context.toggle("last"); break; case "ArrowLeft": { e.preventDefault(); if (context.isMultiple()) { return; } const firstSelectedKey = selectionManager().firstSelectedKey(); const key = firstSelectedKey != null ? keyboardDelegate().getKeyAbove?.(firstSelectedKey) : keyboardDelegate().getFirstKey?.(); if (key != null) { selectionManager().select(key); } break; } case "ArrowRight": { e.preventDefault(); if (context.isMultiple()) { return; } const firstSelectedKey = selectionManager().firstSelectedKey(); const key = firstSelectedKey != null ? keyboardDelegate().getKeyBelow?.(firstSelectedKey) : keyboardDelegate().getFirstKey?.(); if (key != null) { selectionManager().select(key); } break; } } }; const onFocus = (e) => { callHandler(e, local.onFocus); if (selectionManager().isFocused()) { return; } selectionManager().setFocused(true); }; const onBlur = (e) => { callHandler(e, local.onBlur); if (context.isOpen()) { return; } selectionManager().setFocused(false); }; createEffect(() => onCleanup(context.registerTriggerId(fieldProps.id()))); createEffect(() => { context.setListboxAriaLabelledBy([fieldProps.ariaLabelledBy(), fieldProps.ariaLabel() && !fieldProps.ariaLabelledBy() ? fieldProps.id() : null].filter(Boolean).join(" ") || void 0); }); return createComponent(ButtonRoot, mergeProps({ ref(r$) { const _ref$ = mergeRefs(context.setTriggerRef, local.ref); typeof _ref$ === "function" && _ref$(r$); }, get id() { return fieldProps.id(); }, get disabled() { return isDisabled(); }, "aria-haspopup": "listbox", get ["aria-expanded"]() { return context.isOpen(); }, get ["aria-controls"]() { return memo(() => !!context.isOpen())() ? context.listboxId() : void 0; }, get ["aria-label"]() { return fieldProps.ariaLabel(); }, get ["aria-labelledby"]() { return ariaLabelledBy(); }, get ["aria-describedby"]() { return fieldProps.ariaDescribedBy(); }, onPointerDown, onClick, onKeyDown, onFocus, onBlur }, () => context.dataset(), () => formControlContext.dataset(), others)); } function SelectValue(props) { const formControlContext = useFormControlContext(); const context = useSelectContext(); const mergedProps = mergeDefaultProps({ id: context.generateId("value") }, props); const [local, others] = splitProps(mergedProps, ["id", "children"]); const selectionManager = () => context.listState().selectionManager(); const isSelectionEmpty = () => { const selectedKeys = selectionManager().selectedKeys(); if (selectedKeys.size === 1 && selectedKeys.has("")) { return true; } return selectionManager().isEmpty(); }; createEffect(() => onCleanup(context.registerValueId(local.id))); return createComponent(Polymorphic, mergeProps({ as: "span", get id() { return local.id; }, get ["data-placeholder-shown"]() { return isSelectionEmpty() ? "" : void 0; } }, () => formControlContext.dataset(), others, { get children() { return createComponent(Show, { get when() { return !isSelectionEmpty(); }, get fallback() { return context.placeholder(); }, get children() { return createComponent(SelectValueChild, { state: { selectedOption: () => context.selectedOptions()[0], selectedOptions: () => context.selectedOptions(), remove: (option) => context.removeOptionFromSelection(option), clear: () => selectionManager().clearSelection() }, get children() { return local.children; } }); } }); } })); } function SelectValueChild(props) { const resolvedChildren = children(() => { const body = props.children; return isFunction(body) ? body(props.state) : body; }); return memo(resolvedChildren); } // src/select/index.tsx var Select = Object.assign(SelectRoot, { Arrow: PopperArrow, Content: SelectContent, Description: FormControlDescription, ErrorMessage: FormControlErrorMessage, HiddenSelect: SelectHiddenSelect, Icon: SelectIcon, Item: ListboxItem, ItemDescription: ListboxItemDescription, ItemIndicator: ListboxItemIndicator, ItemLabel: ListboxItemLabel, Label: SelectLabel, Listbox: SelectListbox, Portal: SelectPortal, Section: ListboxSection, Trigger: SelectTrigger, Value: SelectValue }); export { Select, SelectContent, SelectHiddenSelect, SelectIcon, SelectLabel, SelectListbox, SelectPortal, SelectRoot, SelectTrigger, SelectValue, select_exports, useSelectContext };