UNPKG

@tabula/ui-selector

Version:

Selector allow users to choose a single option from a collapsible list of options when space is limited

658 lines (623 loc) 21 kB
// src/Selector.types.ts var VisibleKind = /* @__PURE__ */ ((VisibleKind2) => { VisibleKind2[VisibleKind2["Outside"] = 0] = "Outside"; VisibleKind2[VisibleKind2["Trigger"] = 1] = "Trigger"; VisibleKind2[VisibleKind2["Select"] = 2] = "Select"; return VisibleKind2; })(VisibleKind || {}); // src/useSelector/useAsyncSelector.ts import { useCallback as useCallback4, useState as useState2 } from "react"; import { useAsyncState } from "@tabula/use-async-state"; import { usePreviousValue as usePreviousValue2 } from "@tabula/use-previous-value"; // src/useSelector/useSyncSelector.ts import { useMemo as useMemo4 } from "react"; // src/hooks/useClosePopupOnOutsideEvents.ts import { useCallback } from "react"; import { useCloseOnOutsideEvents } from "@tabula/use-close-on-outside-events"; function useClosePopupOnOutsideEvents({ refs, isVisible: isVisible2, onChangeVisible }) { const onOutsideClick = useCallback( (event) => { if (!(event.target instanceof Element)) { throw new TypeError("Target must be an Element"); } const { current: trigger } = refs.reference; if (trigger == null) { return; } if (!(trigger instanceof Element)) { return; } if (!trigger.contains(event.target)) { onChangeVisible(false, 0 /* Outside */); } }, [refs.reference, onChangeVisible] ); useCloseOnOutsideEvents({ ref: refs.floating, isOpen: isVisible2, listenEvents: ["mousedown"], listener: onOutsideClick }); } // src/hooks/useConfig.ts import { useMemo } from "react"; // src/skeleton.css.ts var content = "tabula_ui_selector__1loqjno1"; var root = "tabula_ui_selector__1loqjno0"; // src/hooks/useConfig.ts var LOADING_CONFIG = [ { id: "loading-1", content: "", className: root, contentClassName: content }, { id: "loading-2", content: "", className: root, contentClassName: content } ]; function useConfig({ outerConfig, defaultItem, searchValue, showSearchField, loading }) { const isMatch = useMemo(() => { const pattern = searchValue.trim().toLowerCase(); return (target) => target.trim().toLowerCase().includes(pattern); }, [searchValue]); return useMemo(() => { if (loading) { return LOADING_CONFIG; } if (showSearchField && searchValue !== "") { return outerConfig.reduce((acc, item) => { if ("divider" in item || "menuTitle" in item) { return acc; } const { denyFilter, searchKeys, ...restItem } = item; if (denyFilter || searchKeys?.some((key) => isMatch(key)) || isMatch(item.id)) { acc.push(restItem); } return acc; }, []).sort((left, right) => left.id.localeCompare(right.id)); } const menuConfig = outerConfig.map((item) => { if ("divider" in item || "menuTitle" in item) { return item; } const { denyFilter, searchKeys, ...restItem } = item; return restItem; }); if (defaultItem != null) { menuConfig.unshift(defaultItem, { id: "default-item-divider", divider: true }); } return menuConfig; }, [loading, showSearchField, searchValue, defaultItem, outerConfig, isMatch]); } // src/hooks/usePopup.ts import { useEffect, useMemo as useMemo2 } from "react"; import { autoUpdate, flip, hide, offset, shift, useFloating } from "@floating-ui/react"; import { usePreviousValue } from "@tabula/use-previous-value"; var DEFAULT_OFFSET = 4; function usePopup({ offset: offsetOptions = DEFAULT_OFFSET, isVisible: isVisible2, onChangeVisible }) { const prevVisible = usePreviousValue(isVisible2); const middleware = useMemo2( () => [offset(offsetOptions), shift(), flip({ fallbackStrategy: "initialPlacement" }), hide()], [offsetOptions] ); const { x, y, refs, update, strategy, middlewareData } = useFloating({ placement: "bottom-start", middleware }); useEffect(() => { const { current: reference } = refs.reference; const { current: floating } = refs.floating; if (reference == null || floating == null) { return; } return autoUpdate(reference, floating, update); }, [refs.reference, refs.floating, update]); useEffect(() => { if (!prevVisible && isVisible2) { update(); } }, [isVisible2, prevVisible, update]); useClosePopupOnOutsideEvents({ refs, isVisible: isVisible2, onChangeVisible }); const isReferenceHidden = middlewareData.hide?.referenceHidden; const popupStyle = useMemo2(() => { const innerStyles = { position: strategy }; if (!isVisible2) { return innerStyles; } if (isReferenceHidden) { innerStyles.visibility = "hidden"; return innerStyles; } innerStyles.transform = `translate(${Math.round(x)}px, ${Math.round(y)}px)`; const { current: trigger } = refs.reference; if (trigger == null) { return innerStyles; } innerStyles.width = "clientWidth" in trigger ? trigger.clientWidth : trigger.getBoundingClientRect().width; return innerStyles; }, [isVisible2, isReferenceHidden, refs.reference, strategy, x, y]); return { reference: refs.setReference, floating: refs.setFloating, popupStyle }; } // src/hooks/useTriggerRenderer.tsx import { useCallback as useCallback2 } from "react"; // src/TriggerContent/TriggerContent.tsx import clsx from "clsx"; // src/TriggerContent/TriggerContent.css.ts var contentWrapper = "tabula_ui_selector__1v4zfic8"; var icon = "tabula_ui_selector__1v4zfic1"; var iconVariants = { left: "tabula_ui_selector__1v4zfic2", right: "tabula_ui_selector__1v4zfic3" }; var lines = { single: "tabula_ui_selector__1v4zfic6", multiple: "tabula_ui_selector__1v4zfic7" }; var root2 = "tabula_ui_selector__1v4zfic0"; var text = "tabula_ui_selector__1v4zfic5"; var title = "tabula_ui_selector__1v4zfic4"; // src/TriggerContent/TriggerContent.tsx import { jsx, jsxs } from "react/jsx-runtime"; function TriggerContent({ content: content3, contentClassName, leftIcon: LeftIcon, rightIcon: RightIcon, skipLeftIcon, skipRightIcon, title: title2 }) { return /* @__PURE__ */ jsxs("div", { className: clsx(root2, title2 == null ? lines.single : lines.multiple), children: [ LeftIcon && !skipLeftIcon && /* @__PURE__ */ jsx(LeftIcon, { className: clsx(icon, iconVariants.left) }), /* @__PURE__ */ jsxs("span", { className: clsx(contentWrapper, contentClassName), children: [ title2 && /* @__PURE__ */ jsx("b", { className: title, children: title2 }), /* @__PURE__ */ jsx("span", { className: text, children: content3 }) ] }), RightIcon && !skipRightIcon && /* @__PURE__ */ jsx(RightIcon, { className: clsx(icon, iconVariants.right) }) ] }); } if (import.meta.env.DEV) { TriggerContent.displayName = "UiSelector(TriggerContent)"; } // src/hooks/useTriggerRenderer.tsx import { jsx as jsx2 } from "react/jsx-runtime"; function useTriggerRenderer(params) { return useCallback2(() => { if ("value" in params) { const { value: item, options, itemConfigGetter } = params; const itemConfig = itemConfigGetter({ item, isTrigger: true, options }); if (itemConfig == null) { return null; } return /* @__PURE__ */ jsx2(TriggerContent, { ...itemConfig }); } return /* @__PURE__ */ jsx2(TriggerContent, { ...params.itemConfig }); }, [params]); } // src/hooks/useVisibility.ts import { useCallback as useCallback3, useMemo as useMemo3, useState } from "react"; import { isButtonTarget } from "@tabula/dom-utils"; function useVisibility({ disabled, outerVisible, onChangeOuterVisible, onClearSearch }) { const [innerVisible, setInnerVisible] = useState(false); const onChangeVisible = useCallback3( (visible, kind) => { if (outerVisible == null) { setInnerVisible(visible); } onChangeOuterVisible?.(visible, kind); if (!visible) { onClearSearch(); } }, [outerVisible, onChangeOuterVisible, onClearSearch] ); const isVisible2 = useMemo3(() => outerVisible ?? innerVisible, [innerVisible, outerVisible]); const onTriggerClick = useCallback3(() => { if (disabled) { return; } onChangeVisible(!isVisible2, 1 /* Trigger */); }, [onChangeVisible, isVisible2, disabled]); const onPopupClick = useCallback3( (event) => { if (isButtonTarget(event)) { onChangeVisible(false, 2 /* Select */); } }, [onChangeVisible] ); return { isVisible: isVisible2, onChangeVisible, onTriggerClick, onPopupClick }; } // src/useSelector/useSyncSelector.ts function useSyncSelector({ itemConfigGetter, minItemsForSearch, onChange, options, value }) { const triggerRenderer = useTriggerRenderer({ value, options, itemConfigGetter }); const config = useMemo4( () => options.reduce((acc, item) => { if (typeof item === "object" && item != null && ("divider" in item || "menuTitle" in item)) { acc.push(item); return acc; } const itemConfig = itemConfigGetter({ item, options }); if (itemConfig != null) { acc.push({ ...itemConfig, onClick() { onChange(item); } }); } return acc; }, []), [onChange, options, itemConfigGetter] ); const showSearchField = useMemo4(() => { if (minItemsForSearch == null) { return; } return options.length > minItemsForSearch; }, [minItemsForSearch, options]); return { config, showSearchField, triggerRenderer }; } // src/useSelector/useAsyncSelector.ts function useAsyncSelector({ loadOnHidden = false, optionsGetter, refresh, ...params }) { const [isVisible2, setIsVisible] = useState2(false); const prevVisible = usePreviousValue2(isVisible2); const skipLoading = useCallback4( () => !loadOnHidden && (prevVisible ?? !isVisible2), [loadOnHidden, prevVisible, isVisible2] ); const skipWaiting = useCallback4( () => Boolean(prevVisible && !isVisible2), [prevVisible, isVisible2] ); const [options, loading, refreshing, onRefresh] = useAsyncState({ initialLoading: false, initialState: [], promise: optionsGetter, refresh, skipLoading, skipWaiting }); const syncSelector = useSyncSelector({ ...params, options }); return { ...syncSelector, isVisible: isVisible2, loading, onChangeVisibleTo: setIsVisible, onRefresh, refreshing }; } // src/UiSelector/UiSelector.tsx import { useCallback as useCallback6 } from "react"; import clsx5 from "clsx"; // src/UiSelector/UiSelector.css.ts var search = "tabula_ui_selector__3mtwgw1"; var searchInput = "tabula_ui_selector__3mtwgw2"; var triggerContainer = "tabula_ui_selector__3mtwgw0"; // src/Popup/Popup.tsx import { createPortal } from "react-dom"; import clsx2 from "clsx"; import { portalRootFor } from "@tabula/portal-root-for"; import { UiMenu } from "@tabula/ui-menu"; // src/Popup/Popup.css.ts var isVisible = "tabula_ui_selector__1gn5jfw0"; var menu = "tabula_ui_selector__1gn5jfw2"; var root3 = "tabula_ui_selector__1gn5jfw1"; // src/Popup/Popup.tsx import { jsx as jsx3 } from "react/jsx-runtime"; var portalRoot = portalRootFor({ id: "ui-selector" }); function Popup({ config, emptyContent, isVisible: isVisible2, onClick, setRef, style }) { return createPortal( // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions /* @__PURE__ */ jsx3( "div", { className: clsx2(root3, isVisible2 && isVisible), onClick, ref: setRef, style, children: /* @__PURE__ */ jsx3(UiMenu, { className: menu, config, emptyContent, size: "medium" }) } ), portalRoot ); } if (import.meta.env.DEV) { Popup.displayName = "UiSelector(Popup)"; } // src/Search/Search.tsx import clsx3 from "clsx"; // src/assets/clear.svg?svgr import * as React from "react"; import { memo } from "react"; import { jsx as jsx4 } from "react/jsx-runtime"; var SvgClear = (props) => /* @__PURE__ */ jsx4("svg", { width: 16, height: 16, fill: "none", xmlns: "http://www.w3.org/2000/svg", ...props, children: /* @__PURE__ */ jsx4("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M4.146 4.146a.5.5 0 0 1 .708 0L8 7.293l3.146-3.147a.5.5 0 0 1 .708.708L8.707 8l3.147 3.146a.5.5 0 0 1-.708.708L8 8.707l-3.146 3.147a.5.5 0 0 1-.708-.708L7.293 8 4.146 4.854a.5.5 0 0 1 0-.708Z", fill: "currentColor" }) }); var Memo = memo(SvgClear); // src/Search/Search.css.ts var clear = "tabula_ui_selector__15h12tp8"; var input = "tabula_ui_selector__15h12tp7"; var inputWrapper = "tabula_ui_selector__15h12tp6"; var root4 = "tabula_ui_selector__15h12tp0"; // src/Search/Search.tsx import { jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime"; function Search({ autoFocus, className, forwardedRef, inputClassName, onChange, onClear, onClick, placeholder: placeholder2, showClearControl = true, value }) { return /* @__PURE__ */ jsx5("div", { className, children: /* @__PURE__ */ jsxs2("div", { className: root4, children: [ /* @__PURE__ */ jsx5("div", { className: inputWrapper, children: /* @__PURE__ */ jsx5( "input", { autoFocus, className: clsx3(input, inputClassName), onChange, onClick, placeholder: placeholder2, ref: forwardedRef, type: "text", value } ) }), showClearControl && value !== "" && /* @__PURE__ */ jsx5("button", { className: clear, type: "button", onClick: onClear, children: /* @__PURE__ */ jsx5(Memo, {}) }) ] }) }); } if (import.meta.env.DEV) { Search.displayName = `UiSelector(SearchInput)`; } // src/Search/Search.hooks.ts import { useCallback as useCallback5, useRef, useState as useState3 } from "react"; function useSearch() { const [value, setValue] = useState3(""); const ref = useRef(null); const changeSearchHandler = useCallback5((event) => { setValue(event.target.value); }, []); const clearSearchHandler = useCallback5((event, focusToField) => { if (event) { event.stopPropagation(); } if (focusToField && ref.current !== null) { ref.current.focus(); } setValue(""); }, []); return [value, changeSearchHandler, clearSearchHandler, ref]; } // src/Trigger/Trigger.tsx import { useMemo as useMemo5 } from "react"; import clsx4 from "clsx"; // src/assets/chevronDown.svg?svgr import * as React2 from "react"; import { memo as memo2 } from "react"; import { jsx as jsx6 } from "react/jsx-runtime"; var SvgChevronDown = (props) => /* @__PURE__ */ jsx6("svg", { width: 12, height: 12, viewBox: "0 0 12 12", fill: "none", xmlns: "http://www.w3.org/2000/svg", ...props, children: /* @__PURE__ */ jsx6("path", { d: "M1.646 3.646a.5.5 0 0 1 .708 0L6 7.293l3.646-3.647a.5.5 0 1 1 .708.708l-4 4a.5.5 0 0 1-.708 0l-4-4a.5.5 0 0 1 0-.708Z", fill: "currentColor" }) }); var Memo2 = memo2(SvgChevronDown); // src/assets/chevronUp.svg?svgr import * as React3 from "react"; import { memo as memo3 } from "react"; import { jsx as jsx7 } from "react/jsx-runtime"; var SvgChevronUp = (props) => /* @__PURE__ */ jsx7("svg", { width: 12, height: 12, viewBox: "0 0 12 12", fill: "none", xmlns: "http://www.w3.org/2000/svg", ...props, children: /* @__PURE__ */ jsx7("path", { d: "M1.646 8.354a.5.5 0 0 0 .708 0L6 4.707l3.646 3.647a.5.5 0 1 0 .708-.708l-4-4a.5.5 0 0 0-.708 0l-4 4a.5.5 0 0 0 0 .708Z", fill: "currentColor" }) }); var Memo3 = memo3(SvgChevronUp); // src/assets/running.svg?svgr import * as React4 from "react"; import { memo as memo4 } from "react"; import { jsx as jsx8 } from "react/jsx-runtime"; var SvgRunning = (props) => /* @__PURE__ */ jsx8("svg", { width: 16, height: 16, viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", ...props, children: /* @__PURE__ */ jsx8("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M4.667 12.989a6 6 0 0 0 3.326 1.01L8 14a.5.5 0 0 0 0-1v-.005A4.995 4.995 0 1 1 12.995 8H13a.5.5 0 0 0 1 0 6 6 0 1 0-9.333 4.989Z", fill: "currentColor", children: /* @__PURE__ */ jsx8("animateTransform", { attributeName: "transform", attributeType: "XML", type: "rotate", from: "0 8 8", to: "360 8 8", dur: "700ms", repeatCount: "indefinite" }) }) }); var Memo4 = memo4(SvgRunning); // src/Trigger/Trigger.css.ts var arrow = "tabula_ui_selector__tw5abi5"; var content2 = "tabula_ui_selector__tw5abi6"; var placeholder = "tabula_ui_selector__tw5abi7"; var root5 = "tabula_ui_selector__tw5abi0"; var states = { isVisible: "tabula_ui_selector__tw5abi1", isWarning: "tabula_ui_selector__tw5abi2", isInvalid: "tabula_ui_selector__tw5abi3", isDisabled: "tabula_ui_selector__tw5abi4" }; // src/Trigger/Trigger.tsx import { jsx as jsx9, jsxs as jsxs3 } from "react/jsx-runtime"; function Trigger({ className, disabled, isInvalid, isVisible: isVisible2, isWarning, loading, onRenderTrigger, placeholder: placeholder2, showSearchField }) { const triggerContent = useMemo5(() => { if (typeof onRenderTrigger === "function") { const renderedTrigger = onRenderTrigger(); if (renderedTrigger) { return renderedTrigger; } } return /* @__PURE__ */ jsx9("span", { className: placeholder, children: placeholder2 }); }, [onRenderTrigger, placeholder2]); const triggerIcon = useMemo5(() => { if (loading) { return /* @__PURE__ */ jsx9(Memo4, { className: arrow }); } if (showSearchField && isVisible2) { return null; } return isVisible2 ? /* @__PURE__ */ jsx9(Memo3, { className: arrow }) : /* @__PURE__ */ jsx9(Memo2, { className: arrow }); }, [isVisible2, loading, showSearchField]); return /* @__PURE__ */ jsxs3( "div", { className: clsx4( root5, className, isVisible2 && states.isVisible, isWarning && states.isWarning, isInvalid && states.isInvalid, disabled && states.isDisabled ), children: [ /* @__PURE__ */ jsx9("div", { className: content2, children: triggerContent }), triggerIcon ] } ); } if (import.meta.env.DEV) { Trigger.displayName = "UiSelector(Trigger)"; } // src/UiSelector/UiSelector.tsx import { Fragment, jsx as jsx10, jsxs as jsxs4 } from "react/jsx-runtime"; function UiSelector({ children, config: outerConfig, defaultItem, emptyContent, isInvalid, isVisible: outerVisible, isWarning, loading, offset: offset2, onChangeVisible: onChangeOuterVisible, onRenderTrigger, placeholder: placeholder2, readOnly, searchClassName, showSearchClear, showSearchField, triggerClassName, triggerContainerClassName }) { const [searchValue, onChangeSearch, onClearSearch, searchRef] = useSearch(); const onClickSearch = useCallback6((event) => { event.stopPropagation(); }, []); const { isVisible: isVisible2, onChangeVisible, onTriggerClick, onPopupClick } = useVisibility({ disabled: readOnly, onChangeOuterVisible, onClearSearch, outerVisible }); const config = useConfig({ outerConfig, defaultItem, searchValue, showSearchField, loading }); const { reference, floating, popupStyle } = usePopup({ isVisible: isVisible2, offset: offset2, onChangeVisible }); const trigger = children ?? /* @__PURE__ */ jsx10( Trigger, { className: triggerClassName, disabled: readOnly, isInvalid, isVisible: isVisible2, loading, onRenderTrigger, placeholder: placeholder2, showSearchField, isWarning } ); return /* @__PURE__ */ jsxs4(Fragment, { children: [ /* @__PURE__ */ jsxs4( "div", { className: clsx5(triggerContainer, triggerContainerClassName), onClick: onTriggerClick, ref: reference, children: [ trigger, showSearchField && isVisible2 && /* @__PURE__ */ jsx10( Search, { autoFocus: true, className: clsx5(search, searchClassName), forwardedRef: searchRef, inputClassName: searchInput, onChange: onChangeSearch, onClear: onClearSearch, onClick: onClickSearch, placeholder: placeholder2, showClearControl: showSearchClear, value: searchValue } ) ] } ), /* @__PURE__ */ jsx10( Popup, { config, emptyContent, isVisible: isVisible2, onClick: onPopupClick, setRef: floating, style: popupStyle } ) ] }); } if (import.meta.env.DEV) { UiSelector.displayName = "ui-selector(UiSelector)"; } export { UiSelector, TriggerContent as UiSelectorTriggerContent, VisibleKind, useAsyncSelector, usePopup, useSyncSelector, useTriggerRenderer }; // post-build: auto import bundled styles import "./index.css"; //# sourceMappingURL=index.js.map