@spark-ui/components
Version:
Spark (Leboncoin design system) components.
738 lines (714 loc) • 23.3 kB
JavaScript
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