@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
JavaScript
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
};