UNPKG

@spark-ui/dropdown

Version:

Displays a list of options for the user to pick from—triggered by a button. Differs from Select in that it offers multiple select and its list is not native.

172 lines (171 loc) • 13.9 kB
import l, { isValidElement as B, createContext as G, useState as O, useId as k, useEffect as A, useContext as F, Fragment as se, useRef as Me, useLayoutEffect as $e } from "react"; import { useFormFieldControl as ze } from "@spark-ui/form-field"; import { Popover as V } from "@spark-ui/popover"; import { useMultipleSelection as He, useSelect as P } from "downshift"; import { cx as h, cva as ie } from "class-variance-authority"; import { useMergeRefs as _ } from "@spark-ui/use-merge-refs"; import { Icon as j } from "@spark-ui/icon"; import { VisuallyHidden as Be } from "@spark-ui/visually-hidden"; const Ge = (e, a) => { const t = ((n, r) => { let i = 0; for (const s of n.keys()) { if (i === r) return s; i++; } })(e, a); return t !== void 0 ? e.get(t) : void 0; }, K = (e) => e ? e.type.displayName : "", de = (e, a = []) => (l.Children.forEach(e, (t) => { if (B(t)) { if (K(t) === "Dropdown.Item") { const n = t.props; a.push({ value: n.value, disabled: !!n.disabled, text: R(n.children) }); } t.props.children && de(t.props.children, a); } }), a), R = (e, a = "") => typeof e == "string" ? e : (l.Children.forEach(e, (t) => { B(t) && (K(t) === "Dropdown.ItemText" && (a = t.props.children), t.props.children && R(t.props.children, a)); }), a), oe = (e) => { const a = /* @__PURE__ */ new Map(); return de(e).forEach((t) => { a.set(t.value, t); }), a; }, ce = (e, a) => l.Children.toArray(e).some((t) => !!B(t) && (K(t) === a || !!t.props.children && ce(t.props.children, a))), me = G(null), S = ":dropdown", Ae = ({ children: e, defaultValue: a, value: t, onValueChange: n, open: r, onOpenChange: i, defaultOpen: s, multiple: d = !1, disabled: c = !1, readOnly: m = !1, state: p }) => { const [u, g] = O(oe(e)), [y, I] = O(ce(e, "Dropdown.Popover")), [b, L] = O("mouse"), N = ze(), we = N.state || p, ye = `${S}-label-${k()}`, Ie = `${S}-input-${k()}`, be = N.id || Ie, xe = N.labelId || ye, Ne = N.disabled ?? c, De = N.readOnly ?? m, re = (({ itemsMap: D, defaultValue: x, value: f, onValueChange: C, open: T, onOpenChange: M, defaultOpen: $, multiple: v, id: Te, labelId: Pe }) => { const z = [...D.values()], H = He({ selectedItems: f != null && v ? z.filter((o) => v ? f.includes(o.value) : f === o.value) : void 0, initialSelectedItems: x != null && v ? z.filter((o) => v ? x.includes(o.value) : x === o.value) : void 0, onSelectedItemsChange: ({ selectedItems: o }) => { o != null && v && C?.(o.map((w) => w.value)); } }); return { ...P({ items: z, isItemDisabled: (o) => o.disabled, itemToString: (o) => o ? o.text : "", id: Te, labelId: Pe, isOpen: T, onIsOpenChange: ({ isOpen: o }) => { o != null && M?.(o); }, initialIsOpen: $ ?? !1, stateReducer: (o, { changes: w, type: Oe }) => { if (!v) return w; const { selectedItems: ke, removeSelectedItem: Se, addSelectedItem: Ve } = H; switch (Oe) { case P.stateChangeTypes.ToggleButtonKeyDownEnter: case P.stateChangeTypes.ToggleButtonKeyDownSpaceButton: case P.stateChangeTypes.ItemClick: return w.selectedItem != null && (ke.some((Le) => Le.value === w.selectedItem?.value) ? Se(w.selectedItem) : Ve(w.selectedItem)), { ...w, isOpen: !0, highlightedIndex: o.highlightedIndex }; default: return w; } }, selectedItem: f == null || v ? void 0 : D.get(f) || null, initialSelectedItem: x == null && f == null || v ? void 0 : D.get(x) || null, onSelectedItemChange: ({ selectedItem: o }) => { o?.value == null || v || C?.(o?.value); }, scrollIntoView: (o) => { o && o.scrollIntoView({ block: "nearest" }); } }), ...H, selectedItems: [...new Set(H.selectedItems)] }; })({ itemsMap: u, defaultValue: a, value: t, onValueChange: n, open: r, onOpenChange: i, defaultOpen: s, multiple: d, id: be, labelId: xe }); A(() => { const D = oe(e), x = [...u.values()], f = [...D.values()]; (x.length !== f.length || x.some((C, T) => { const M = C.value !== f[T]?.value, $ = C.text !== f[T]?.text; return M || $; })) && g(D); }, [e]); const [Ee, Ce] = y ? [V, { open: !0 }] : [se, {}]; return l.createElement(me.Provider, { value: { multiple: d, disabled: Ne, readOnly: De, ...re, itemsMap: u, highlightedItem: Ge(u, re.highlightedIndex), hasPopover: y, setHasPopover: I, state: we, lastInteractionType: b, setLastInteractionType: L } }, l.createElement(Ee, { ...Ce }, e)); }, E = () => { const e = F(me); if (!e) throw Error("useDropdownContext must be used within a Dropdown provider"); return e; }, pe = ({ children: e, ...a }) => l.createElement(Ae, { ...a }, e); pe.displayName = "Dropdown"; const W = ({ className: e, ref: a }) => l.createElement("div", { ref: a, className: h("my-md border-b-sm border-outline", e) }); W.displayName = "Dropdown.Divider"; const ue = G(null), Fe = ({ children: e }) => { const a = `${S}-group-label-${k()}`; return l.createElement(ue.Provider, { value: { labelId: a } }, e); }, he = () => { const e = F(ue); if (!e) throw Error("useDropdownGroupContext must be used within a DropdownGroup provider"); return e; }, Z = ({ children: e, ref: a, ...t }) => l.createElement(Fe, null, l.createElement(_e, { ref: a, ...t }, e)), _e = ({ children: e, className: a, ref: t }) => { const { labelId: n } = he(); return l.createElement("div", { ref: t, role: "group", "aria-labelledby": n, className: h(a) }, e); }; Z.displayName = "Dropdown.Group"; const fe = G(null), je = ({ value: e, disabled: a = !1, children: t }) => { const { multiple: n, itemsMap: r, selectedItem: i, selectedItems: s } = E(), [d, c] = O(void 0), m = function(g, y) { let I = 0; for (const [b] of g.entries()) { if (b === y) return I; I++; } return -1; }(r, e), p = { disabled: a, value: e, text: R(t) }, u = n ? s.some((g) => g.value === e) : i?.value === e; return l.createElement(fe.Provider, { value: { textId: d, setTextId: c, isSelected: u, itemData: p, index: m, disabled: a } }, t); }, q = () => { const e = F(fe); if (!e) throw Error("useDropdownItemContext must be used within a DropdownItem provider"); return e; }, J = ({ children: e, ref: a, ...t }) => { const { value: n, disabled: r } = t; return l.createElement(je, { value: n, disabled: r }, l.createElement(Re, { ref: a, ...t }, e)); }, Ke = ie("px-lg py-md text-body-1", { variants: { selected: { true: "font-bold" }, disabled: { true: "opacity-dim-3 cursor-not-allowed", false: "cursor-pointer" }, highlighted: { true: "" }, interactionType: { mouse: "", keyboard: "" } }, compoundVariants: [{ highlighted: !0, interactionType: "mouse", class: "bg-surface-hovered" }, { highlighted: !0, interactionType: "keyboard", class: "u-outline" }] }), Re = ({ className: e, disabled: a = !1, value: t, children: n, ref: r }) => { const { getItemProps: i, highlightedItem: s, lastInteractionType: d } = E(), { textId: c, index: m, itemData: p, isSelected: u } = q(), g = s?.value === t, { ref: y, ...I } = i({ item: p, index: m }), b = _(r, y); return l.createElement("li", { ref: b, className: h(Ke({ selected: u, disabled: a, highlighted: g, interactionType: d, className: e })), key: t, ...I, "aria-selected": u, "aria-labelledby": c }, n); }; J.displayName = "Dropdown.Item"; const ge = ({ title: e, fill: a = "currentColor", stroke: t = "none", ref: n, ...r }) => l.createElement("svg", { ref: n, viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", "data-title": "Check", ...e && { "data-title": e }, fill: a, stroke: t, ...r, dangerouslySetInnerHTML: { __html: (e === void 0 ? "" : `<title>${e}</title>`) + '<path d="m8.92,19.08c-.18,0-.36-.03-.53-.1s-.33-.17-.47-.31l-5.49-5.34c-.28-.28-.42-.61-.42-1s.14-.73.42-1c.28-.28.62-.41,1.02-.41s.74.14,1.05.41l4.43,4.3,10.62-10.29c.28-.28.62-.42,1.02-.43.39,0,.73.13,1.02.43.28.28.42.61.42,1s-.14.73-.42,1l-11.65,11.32c-.14.14-.3.24-.47.31-.17.07-.35.1-.53.1Z"/>' } }); ge.displayName = "Check"; const Q = ({ className: e, children: a, label: t, ref: n }) => { const { disabled: r, isSelected: i } = q(), s = a || l.createElement(j, { size: "sm" }, l.createElement(ge, { "aria-label": t })); return l.createElement("span", { ref: n, className: h("min-h-sz-16 min-w-sz-16 flex", r && "opacity-dim-3", e) }, i && s); }; Q.displayName = "Dropdown.ItemIndicator"; const U = ({ children: e, className: a, ref: t, ...n }) => { const { isOpen: r, getMenuProps: i, hasPopover: s, setLastInteractionType: d } = E(), { ref: c, ...m } = i({ onMouseMove: () => { d("mouse"); } }), p = Me(null), u = _(t, c, p); return $e(() => { s && p.current && p.current.parentElement && (p.current.parentElement.style.pointerEvents = r ? "" : "none", p.current.style.pointerEvents = r ? "" : "none"); }, [r, s]), l.createElement("ul", { ref: u, className: h(a, "flex flex-col", r ? "pointer-events-auto! block" : "pointer-events-none invisible absolute opacity-0", s && "p-lg"), ...n, ...m, "data-spark-component": "dropdown-items" }, e); }; U.displayName = "Dropdown.Items"; const X = ({ children: e, ref: a }) => { const t = `${S}-item-text-${k()}`, { setTextId: n } = q(); return A(() => (n(t), () => n(void 0))), l.createElement("span", { id: t, className: h("inline"), ref: a }, e); }; X.displayName = "Dropdown.ItemText"; const Y = ({ children: e, className: a, ref: t }) => { const { labelId: n } = he(); return l.createElement("div", { ref: t, id: n, className: h("px-md py-sm text-body-2 text-neutral italic", a) }, e); }; Y.displayName = "Dropdown.Label"; const ee = ({ children: e }) => l.createElement(j, { size: "sm", className: "shrink-0" }, e); ee.displayName = "Dropdown.LeadingIcon"; const te = ({ children: e, matchTriggerWidth: a = !0, sideOffset: t = 4, className: n, elevation: r = "dropdown", ref: i, ...s }) => { const d = E(); return A(() => (d.setHasPopover(!0), () => d.setHasPopover(!1)), []), l.createElement(V.Content, { ref: i, inset: !0, asChild: !0, matchTriggerWidth: a, elevation: r, className: h("relative", n), sideOffset: t, onOpenAutoFocus: (c) => { c.preventDefault(); }, ...s, "data-spark-component": "dropdown-popover" }, e); }; te.displayName = "Dropdown.Popover"; const ae = ({ children: e, ...a }) => l.createElement(V.Portal, { ...a }, e); ae.displayName = "Dropdown.Portal"; const ve = ({ title: e, fill: a = "currentColor", stroke: t = "none", ref: n, ...r }) => l.createElement("svg", { ref: n, viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", "data-title": "ArrowHorizontalDown", ...e && { "data-title": e }, fill: a, stroke: t, ...r, dangerouslySetInnerHTML: { __html: (e === void 0 ? "" : `<title>${e}</title>`) + '<path fill-rule="evenodd" d="m2.33,7.3c.43-.4,1.14-.4,1.57,0l8.1,7.48,8.1-7.48c.43-.4,1.14-.4,1.57,0,.43.4.43,1.06,0,1.47l-8.34,7.7c-.17.17-.37.3-.6.39-.23.09-.48.14-.73.14s-.5-.05-.73-.14c-.23-.09-.43-.22-.6-.39L2.33,8.77c-.43-.4-.43-1.06,0-1.47Z"/>' } }); ve.displayName = "ArrowHorizontalDown"; const We = ie(["flex w-full items-center justify-between", "min-h-sz-44 rounded-lg bg-surface text-on-surface px-lg", "text-body-1", "ring-1 outline-hidden ring-inset focus:ring-2"], { variants: { state: { undefined: "ring-outline focus:ring-outline-high", error: "ring-error", alert: "ring-alert", success: "ring-success" }, disabled: { true: "disabled:bg-on-surface/dim-5 cursor-not-allowed text-on-surface/dim-3" }, readOnly: { true: "disabled:bg-on-surface/dim-5 cursor-not-allowed text-on-surface/dim-3" } }, compoundVariants: [{ disabled: !1, state: void 0, class: "hover:ring-outline-high" }] }), le = ({ "aria-label": e, children: a, className: t, ref: n }) => { const { getToggleButtonProps: r, getDropdownProps: i, getLabelProps: s, hasPopover: d, disabled: c, readOnly: m, state: p, setLastInteractionType: u } = E(), [g, y] = d ? [V.Trigger, { asChild: !0 }] : [se, {}], { ref: I, ...b } = r({ ...i(), onKeyDown: () => { u("keyboard"); } }), L = b["aria-expanded"], N = _(n, I); return l.createElement(l.Fragment, null, e && l.createElement(Be, null, l.createElement("label", { ...s() }, e)), l.createElement(g, { ...y }, l.createElement("button", { type: "button", ref: N, disabled: c || m, className: We({ className: t, state: p, disabled: c, readOnly: m }), ...b, "data-spark-component": "dropdown-trigger" }, l.createElement("span", { className: "gap-md flex items-center justify-start" }, a), l.createElement(j, { className: h("ml-md shrink-0 rotate-0 transition duration-100 ease-in", { "rotate-180": L }), size: "sm" }, l.createElement(ve, null))))); }; le.displayName = "Dropdown.Trigger"; const ne = ({ children: e, className: a, placeholder: t, ref: n }) => { const { selectedItem: r, multiple: i, selectedItems: s } = E(), d = !!(i ? s.length : r), c = i ? s[0]?.text : r?.text, m = s.length > 1 ? ", +" + (s.length - 1) : ""; return l.createElement("span", { ref: n, className: h("flex shrink items-center text-left", a) }, l.createElement("span", { className: h("line-clamp-1 flex-1 overflow-hidden break-all text-ellipsis", !d && "text-on-surface/dim-1") }, d ? e || c : t), m && l.createElement("span", null, m)); }; ne.displayName = "Dropdown.Value"; const Ze = Object.assign(pe, { Group: Z, Item: J, Items: U, ItemText: X, ItemIndicator: Q, Label: Y, Popover: te, Divider: W, Trigger: le, Value: ne, LeadingIcon: ee, Portal: ae }); Ze.displayName = "Dropdown", Z.displayName = "Dropdown.Group", U.displayName = "Dropdown.Items", J.displayName = "Dropdown.Item", X.displayName = "Dropdown.ItemText", Q.displayName = "Dropdown.ItemIndicator", Y.displayName = "Dropdown.Label", te.displayName = "Dropdown.Popover", W.displayName = "Dropdown.Divider", le.displayName = "Dropdown.Trigger", ne.displayName = "Dropdown.Value", ee.displayName = "Dropdown.LeadingIcon", ae.displayName = "Dropdown.Portal"; export { Ze as Dropdown, Ae as DropdownProvider, E as useDropdownContext };