UNPKG

@spark-ui/components

Version:

Spark (Leboncoin design system) components.

738 lines (714 loc) 23.3 kB
import { Popover } from "../chunk-QS2FHLSL.mjs"; import "../chunk-QLOIAU3C.mjs"; import "../chunk-USSL4UZ5.mjs"; import "../chunk-MUNDKRAE.mjs"; import { Icon } from "../chunk-AESXFMCC.mjs"; import { VisuallyHidden } from "../chunk-NBZKMCHF.mjs"; import "../chunk-4F5DOL57.mjs"; // src/dropdown/DropdownContext.tsx import { useFormFieldControl } from "@spark-ui/components/form-field"; import { createContext, Fragment, useContext, useEffect, useId, useState } from "react"; // src/dropdown/useDropdown.ts import { useMultipleSelection, useSelect } from "downshift"; var useDropdown = ({ itemsMap, defaultValue, value, onValueChange, open, onOpenChange, defaultOpen, multiple, id, labelId }) => { const items = [...itemsMap.values()]; const downshiftMultipleSelection = useMultipleSelection({ selectedItems: value != null && multiple ? items.filter( (item) => multiple ? value.includes(item.value) : value === item.value ) : void 0, initialSelectedItems: defaultValue != null && multiple ? items.filter( (item) => multiple ? defaultValue.includes(item.value) : defaultValue === item.value ) : void 0, onSelectedItemsChange: ({ selectedItems }) => { if (selectedItems != null && multiple) { onValueChange?.(selectedItems.map((item) => item.value)); } } }); const stateReducer = (state, { changes, type }) => { if (!multiple) return changes; const { selectedItems, removeSelectedItem, addSelectedItem } = downshiftMultipleSelection; switch (type) { case useSelect.stateChangeTypes.ToggleButtonKeyDownEnter: case useSelect.stateChangeTypes.ToggleButtonKeyDownSpaceButton: case useSelect.stateChangeTypes.ItemClick: if (changes.selectedItem != null) { const isAlreadySelected = selectedItems.some( (selectedItem) => selectedItem.value === changes.selectedItem?.value ); if (isAlreadySelected) removeSelectedItem(changes.selectedItem); else addSelectedItem(changes.selectedItem); } return { ...changes, isOpen: true, // keep the menu open after selection. highlightedIndex: state.highlightedIndex // preserve highlighted index position }; default: return changes; } }; const downshift = useSelect({ items, isItemDisabled: (item) => item.disabled, itemToString: (item) => item ? item.text : "", // a11y attributes id, labelId, // Controlled open state isOpen: open, // undefined must be passed for stateful behaviour (uncontrolled) onIsOpenChange: ({ isOpen }) => { if (isOpen != null) onOpenChange?.(isOpen); }, initialIsOpen: defaultOpen ?? false, stateReducer, // Controlled mode (single selection) selectedItem: value != null && !multiple ? itemsMap.get(value) || null : void 0, initialSelectedItem: (defaultValue != null || value != null) && !multiple ? itemsMap.get(defaultValue) || null : void 0, onSelectedItemChange: ({ selectedItem }) => { if (selectedItem?.value != null && !multiple) { onValueChange?.(selectedItem?.value); } }, /** * 1. Downshift default behaviour is to scroll into view the highlighted item when the dropdown opens. This behaviour is not stable and scrolls the dropdown to the bottom of the screen. */ scrollIntoView: (node) => { if (node) { node.scrollIntoView({ block: "nearest" }); } return void 0; } }); return { ...downshift, ...downshiftMultipleSelection, /** There is a Downshift bug in React 19, it duplicates some selectedItems */ selectedItems: [...new Set(downshiftMultipleSelection.selectedItems)] }; }; // src/dropdown/utils.ts import { isValidElement, Children } from "react"; function getIndexByKey(map, targetKey) { let index = 0; for (const [key] of map.entries()) { if (key === targetKey) { return index; } index++; } return -1; } var getKeyAtIndex = (map, index) => { let i = 0; for (const key of map.keys()) { if (i === index) return key; i++; } return void 0; }; var getElementByIndex = (map, index) => { const key = getKeyAtIndex(map, index); return key !== void 0 ? map.get(key) : void 0; }; var getElementDisplayName = (element) => { return element ? element.type.displayName : ""; }; var getOrderedItems = (children, result = []) => { Children.forEach(children, (child) => { if (!isValidElement(child)) return; if (getElementDisplayName(child) === "Dropdown.Item") { const childProps = child.props; result.push({ value: childProps.value, disabled: !!childProps.disabled, text: getItemText(childProps.children) }); } if (child.props.children) { getOrderedItems(child.props.children, result); } }); return result; }; var getItemText = (children, itemText = "") => { if (typeof children === "string") { return children; } Children.forEach(children, (child) => { if (!isValidElement(child)) return; if (getElementDisplayName(child) === "Dropdown.ItemText") { itemText = child.props.children; } if (child.props.children) { getItemText(child.props.children, itemText); } }); return itemText; }; var getItemsFromChildren = (children) => { const newMap = /* @__PURE__ */ new Map(); getOrderedItems(children).forEach((itemData) => { newMap.set(itemData.value, itemData); }); return newMap; }; var hasChildComponent = (children, displayName) => { return Children.toArray(children).some((child) => { if (!isValidElement(child)) return false; if (getElementDisplayName(child) === displayName) { return true; } else if (child.props.children) { return hasChildComponent(child.props.children, displayName); } return false; }); }; // src/dropdown/DropdownContext.tsx import { jsx } from "react/jsx-runtime"; var DropdownContext = createContext(null); var ID_PREFIX = ":dropdown"; var DropdownProvider = ({ children, defaultValue, value, onValueChange, open, onOpenChange, defaultOpen, multiple = false, disabled: disabledProp = false, readOnly: readOnlyProp = false, state: stateProp }) => { const [itemsMap, setItemsMap] = useState(getItemsFromChildren(children)); const [hasPopover, setHasPopover] = useState( hasChildComponent(children, "Dropdown.Popover") ); const [lastInteractionType, setLastInteractionType] = useState("mouse"); const field = useFormFieldControl(); const state = field.state || stateProp; const internalFieldLabelID = `${ID_PREFIX}-label-${useId()}`; const internalFieldID = `${ID_PREFIX}-input-${useId()}`; const id = field.id || internalFieldID; const labelId = field.labelId || internalFieldLabelID; const disabled = field.disabled ?? disabledProp; const readOnly = field.readOnly ?? readOnlyProp; const dropdownState = useDropdown({ itemsMap, defaultValue, value, onValueChange, open, onOpenChange, defaultOpen, multiple, id, labelId }); useEffect(() => { const newMap = getItemsFromChildren(children); const previousItems = [...itemsMap.values()]; const newItems = [...newMap.values()]; const hasItemsChanges = previousItems.length !== newItems.length || previousItems.some((item, index) => { const hasUpdatedValue = item.value !== newItems[index]?.value; const hasUpdatedText = item.text !== newItems[index]?.text; return hasUpdatedValue || hasUpdatedText; }); if (hasItemsChanges) { setItemsMap(newMap); } }, [children]); const [WrapperComponent, wrapperProps] = hasPopover ? [Popover, { open: true }] : [Fragment, {}]; return /* @__PURE__ */ jsx( DropdownContext.Provider, { value: { multiple, disabled, readOnly, ...dropdownState, itemsMap, highlightedItem: getElementByIndex(itemsMap, dropdownState.highlightedIndex), hasPopover, setHasPopover, state, lastInteractionType, setLastInteractionType }, children: /* @__PURE__ */ jsx(WrapperComponent, { ...wrapperProps, children }) } ); }; var useDropdownContext = () => { const context = useContext(DropdownContext); if (!context) { throw Error("useDropdownContext must be used within a Dropdown provider"); } return context; }; // src/dropdown/Dropdown.tsx import { jsx as jsx2 } from "react/jsx-runtime"; var Dropdown = ({ children, ...props }) => { return /* @__PURE__ */ jsx2(DropdownProvider, { ...props, children }); }; Dropdown.displayName = "Dropdown"; // src/dropdown/DropdownDivider.tsx import { cx } from "class-variance-authority"; import { jsx as jsx3 } from "react/jsx-runtime"; var Divider = ({ className, ref: forwardedRef }) => { return /* @__PURE__ */ jsx3("div", { ref: forwardedRef, className: cx("my-md border-b-sm border-outline", className) }); }; Divider.displayName = "Dropdown.Divider"; // src/dropdown/DropdownGroup.tsx import { cx as cx2 } from "class-variance-authority"; // src/dropdown/DropdownItemsGroupContext.tsx import { createContext as createContext2, useContext as useContext2, useId as useId2 } from "react"; import { jsx as jsx4 } from "react/jsx-runtime"; var DropdownGroupContext = createContext2(null); var DropdownGroupProvider = ({ children }) => { const labelId = `${ID_PREFIX}-group-label-${useId2()}`; return /* @__PURE__ */ jsx4(DropdownGroupContext.Provider, { value: { labelId }, children }); }; var useDropdownGroupContext = () => { const context = useContext2(DropdownGroupContext); if (!context) { throw Error("useDropdownGroupContext must be used within a DropdownGroup provider"); } return context; }; // src/dropdown/DropdownGroup.tsx import { jsx as jsx5 } from "react/jsx-runtime"; var Group = ({ children, ref: forwardedRef, ...props }) => { return /* @__PURE__ */ jsx5(DropdownGroupProvider, { children: /* @__PURE__ */ jsx5(GroupContent, { ref: forwardedRef, ...props, children }) }); }; var GroupContent = ({ children, className, ref: forwardedRef }) => { const { labelId } = useDropdownGroupContext(); return /* @__PURE__ */ jsx5("div", { ref: forwardedRef, role: "group", "aria-labelledby": labelId, className: cx2(className), children }); }; Group.displayName = "Dropdown.Group"; // src/dropdown/DropdownItem.tsx import { useMergeRefs } from "@spark-ui/hooks/use-merge-refs"; import { cva, cx as cx3 } from "class-variance-authority"; // src/dropdown/DropdownItemContext.tsx import { createContext as createContext3, useContext as useContext3, useState as useState2 } from "react"; import { jsx as jsx6 } from "react/jsx-runtime"; var DropdownItemContext = createContext3(null); var DropdownItemProvider = ({ value, disabled = false, children }) => { const { multiple, itemsMap, selectedItem, selectedItems } = useDropdownContext(); const [textId, setTextId] = useState2(void 0); const index = getIndexByKey(itemsMap, value); const itemData = { disabled, value, text: getItemText(children) }; const isSelected = multiple ? selectedItems.some((selectedItem2) => selectedItem2.value === value) : selectedItem?.value === value; return /* @__PURE__ */ jsx6( DropdownItemContext.Provider, { value: { textId, setTextId, isSelected, itemData, index, disabled }, children } ); }; var useDropdownItemContext = () => { const context = useContext3(DropdownItemContext); if (!context) { throw Error("useDropdownItemContext must be used within a DropdownItem provider"); } return context; }; // src/dropdown/DropdownItem.tsx import { jsx as jsx7 } from "react/jsx-runtime"; var Item = ({ children, ref: forwardedRef, ...props }) => { const { value, disabled } = props; return /* @__PURE__ */ jsx7(DropdownItemProvider, { value, disabled, children: /* @__PURE__ */ jsx7(ItemContent, { ref: forwardedRef, ...props, children }) }); }; var styles = cva("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: true, interactionType: "mouse", class: "bg-surface-hovered" }, { highlighted: true, interactionType: "keyboard", class: "u-outline" } ] }); var ItemContent = ({ className, disabled = false, value, children, ref: forwardedRef }) => { const { getItemProps, highlightedItem, lastInteractionType } = useDropdownContext(); const { textId, index, itemData, isSelected } = useDropdownItemContext(); const isHighlighted = highlightedItem?.value === value; const { ref: downshiftRef, ...downshiftItemProps } = getItemProps({ item: itemData, index }); const ref = useMergeRefs(forwardedRef, downshiftRef); return /* @__PURE__ */ jsx7( "li", { ref, className: cx3( styles({ selected: isSelected, disabled, highlighted: isHighlighted, interactionType: lastInteractionType, className }) ), ...downshiftItemProps, "aria-selected": isSelected, "aria-labelledby": textId, children }, value ); }; Item.displayName = "Dropdown.Item"; // src/dropdown/DropdownItemIndicator.tsx import { Check } from "@spark-ui/icons/Check"; import { cx as cx4 } from "class-variance-authority"; import { jsx as jsx8 } from "react/jsx-runtime"; var ItemIndicator = ({ className, children, label, ref: forwardedRef }) => { const { disabled, isSelected } = useDropdownItemContext(); const childElement = children || /* @__PURE__ */ jsx8(Icon, { size: "sm", children: /* @__PURE__ */ jsx8(Check, { "aria-label": label }) }); return /* @__PURE__ */ jsx8( "span", { ref: forwardedRef, className: cx4("min-h-sz-16 min-w-sz-16 flex", disabled && "opacity-dim-3", className), children: isSelected && childElement } ); }; ItemIndicator.displayName = "Dropdown.ItemIndicator"; // src/dropdown/DropdownItems.tsx import { useMergeRefs as useMergeRefs2 } from "@spark-ui/hooks/use-merge-refs"; import { cx as cx5 } from "class-variance-authority"; import { useLayoutEffect, useRef } from "react"; import { jsx as jsx9 } from "react/jsx-runtime"; var Items = ({ children, className, ref: forwardedRef, ...props }) => { const { isOpen, getMenuProps, hasPopover, setLastInteractionType } = useDropdownContext(); const { ref: downshiftRef, ...downshiftMenuProps } = getMenuProps({ onMouseMove: () => { setLastInteractionType("mouse"); } }); const innerRef = useRef(null); const ref = useMergeRefs2(forwardedRef, downshiftRef, innerRef); useLayoutEffect(() => { if (!hasPopover) return; if (!innerRef.current) return; if (innerRef.current.parentElement) { innerRef.current.parentElement.style.pointerEvents = isOpen ? "" : "none"; innerRef.current.style.pointerEvents = isOpen ? "" : "none"; } }, [isOpen, hasPopover]); return /* @__PURE__ */ jsx9( "ul", { ref, className: cx5( className, "flex flex-col", isOpen ? "pointer-events-auto! block" : "pointer-events-none invisible absolute opacity-0", hasPopover && "p-lg" ), ...props, ...downshiftMenuProps, "data-spark-component": "dropdown-items", children } ); }; Items.displayName = "Dropdown.Items"; // src/dropdown/DropdownItemText.tsx import { cx as cx6 } from "class-variance-authority"; import { useEffect as useEffect2, useId as useId3 } from "react"; import { jsx as jsx10 } from "react/jsx-runtime"; var ItemText = ({ children, ref: forwardedRef }) => { const id = `${ID_PREFIX}-item-text-${useId3()}`; const { setTextId } = useDropdownItemContext(); useEffect2(() => { setTextId(id); return () => setTextId(void 0); }); return /* @__PURE__ */ jsx10("span", { id, className: cx6("inline"), ref: forwardedRef, children }); }; ItemText.displayName = "Dropdown.ItemText"; // src/dropdown/DropdownLabel.tsx import { cx as cx7 } from "class-variance-authority"; import { jsx as jsx11 } from "react/jsx-runtime"; var Label = ({ children, className, ref: forwardedRef }) => { const { labelId } = useDropdownGroupContext(); return /* @__PURE__ */ jsx11( "div", { ref: forwardedRef, id: labelId, className: cx7("px-md py-sm text-body-2 text-neutral italic", className), children } ); }; Label.displayName = "Dropdown.Label"; // src/dropdown/DropdownLeadingIcon.tsx import { jsx as jsx12 } from "react/jsx-runtime"; var LeadingIcon = ({ children }) => { return /* @__PURE__ */ jsx12(Icon, { size: "sm", className: "shrink-0", children }); }; LeadingIcon.displayName = "Dropdown.LeadingIcon"; // src/dropdown/DropdownPopover.tsx import { cx as cx8 } from "class-variance-authority"; import { useEffect as useEffect3 } from "react"; import { jsx as jsx13 } from "react/jsx-runtime"; var Popover2 = ({ children, matchTriggerWidth = true, sideOffset = 4, className, elevation = "dropdown", ref: forwardedRef, ...props }) => { const ctx = useDropdownContext(); useEffect3(() => { ctx.setHasPopover(true); return () => ctx.setHasPopover(false); }, []); return /* @__PURE__ */ jsx13( Popover.Content, { ref: forwardedRef, inset: true, asChild: true, matchTriggerWidth, elevation, className: cx8("relative", className), sideOffset, onOpenAutoFocus: (e) => { e.preventDefault(); }, ...props, "data-spark-component": "dropdown-popover", children } ); }; Popover2.displayName = "Dropdown.Popover"; // src/dropdown/DropdownPortal.tsx import { jsx as jsx14 } from "react/jsx-runtime"; var Portal = ({ children, ...rest }) => /* @__PURE__ */ jsx14(Popover.Portal, { ...rest, children }); Portal.displayName = "Dropdown.Portal"; // src/dropdown/DropdownTrigger.tsx import { useMergeRefs as useMergeRefs3 } from "@spark-ui/hooks/use-merge-refs"; import { ArrowHorizontalDown } from "@spark-ui/icons/ArrowHorizontalDown"; import { cx as cx9 } from "class-variance-authority"; import { Fragment as Fragment2 } from "react"; // src/dropdown/DropdownTrigger.styles.tsx import { cva as cva2 } from "class-variance-authority"; var styles2 = cva2( [ "flex w-full items-center justify-between", "min-h-sz-44 rounded-lg bg-surface text-on-surface px-lg", "text-body-1", // outline styles "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: false, state: void 0, class: "hover:ring-outline-high" } ] } ); // src/dropdown/DropdownTrigger.tsx import { Fragment as Fragment3, jsx as jsx15, jsxs } from "react/jsx-runtime"; var Trigger = ({ "aria-label": ariaLabel, children, className, ref: forwardedRef }) => { const { getToggleButtonProps, getDropdownProps, getLabelProps, hasPopover, disabled, readOnly, state, setLastInteractionType } = useDropdownContext(); const [WrapperComponent, wrapperProps] = hasPopover ? [Popover.Trigger, { asChild: true }] : [Fragment2, {}]; const { ref: downshiftRef, ...downshiftTriggerProps } = getToggleButtonProps({ ...getDropdownProps(), onKeyDown: () => { setLastInteractionType("keyboard"); } }); const isExpanded = downshiftTriggerProps["aria-expanded"]; const ref = useMergeRefs3(forwardedRef, downshiftRef); return /* @__PURE__ */ jsxs(Fragment3, { children: [ ariaLabel && /* @__PURE__ */ jsx15(VisuallyHidden, { children: /* @__PURE__ */ jsx15("label", { ...getLabelProps(), children: ariaLabel }) }), /* @__PURE__ */ jsx15(WrapperComponent, { ...wrapperProps, children: /* @__PURE__ */ jsxs( "button", { type: "button", ref, disabled: disabled || readOnly, className: styles2({ className, state, disabled, readOnly }), ...downshiftTriggerProps, "data-spark-component": "dropdown-trigger", children: [ /* @__PURE__ */ jsx15("span", { className: "gap-md flex items-center justify-start", children }), /* @__PURE__ */ jsx15( Icon, { className: cx9("ml-md shrink-0 rotate-0 transition duration-100 ease-in", { "rotate-180": isExpanded }), size: "sm", children: /* @__PURE__ */ jsx15(ArrowHorizontalDown, {}) } ) ] } ) }) ] }); }; Trigger.displayName = "Dropdown.Trigger"; // src/dropdown/DropdownValue.tsx import { cx as cx10 } from "class-variance-authority"; import { jsx as jsx16, jsxs as jsxs2 } from "react/jsx-runtime"; var Value = ({ children, className, placeholder, ref: forwardedRef }) => { const { selectedItem, multiple, selectedItems } = useDropdownContext(); const hasSelectedItems = !!(multiple ? selectedItems.length : selectedItem); const text = multiple ? selectedItems[0]?.text : selectedItem?.text; const suffix = selectedItems.length > 1 ? `, +${selectedItems.length - 1}` : ""; return /* @__PURE__ */ jsxs2("span", { ref: forwardedRef, className: cx10("flex shrink items-center text-left", className), children: [ /* @__PURE__ */ jsx16( "span", { className: cx10( "line-clamp-1 flex-1 overflow-hidden break-all text-ellipsis", !hasSelectedItems && "text-on-surface/dim-1" ), children: !hasSelectedItems ? placeholder : children || text } ), suffix && /* @__PURE__ */ jsx16("span", { children: suffix }) ] }); }; Value.displayName = "Dropdown.Value"; // src/dropdown/index.ts var Dropdown2 = Object.assign(Dropdown, { Group, Item, Items, ItemText, ItemIndicator, Label, Popover: Popover2, Divider, Trigger, Value, LeadingIcon, Portal }); Dropdown2.displayName = "Dropdown"; Group.displayName = "Dropdown.Group"; Items.displayName = "Dropdown.Items"; Item.displayName = "Dropdown.Item"; ItemText.displayName = "Dropdown.ItemText"; ItemIndicator.displayName = "Dropdown.ItemIndicator"; Label.displayName = "Dropdown.Label"; Popover2.displayName = "Dropdown.Popover"; Divider.displayName = "Dropdown.Divider"; Trigger.displayName = "Dropdown.Trigger"; Value.displayName = "Dropdown.Value"; LeadingIcon.displayName = "Dropdown.LeadingIcon"; Portal.displayName = "Dropdown.Portal"; export { Dropdown2 as Dropdown, DropdownProvider, useDropdownContext }; //# sourceMappingURL=index.mjs.map