@tabula/ui-selector
Version:
Selector allow users to choose a single option from a collapsible list of options when space is limited
661 lines (626 loc) • 21 kB
JavaScript
// 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({
className,
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, className),
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,
popupClassName,
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,
{
className: popupClassName,
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