@spark-ui/components
Version:
Spark (Leboncoin design system) components.
1,270 lines (1,240 loc) • 41 kB
JavaScript
import {
Popover
} from "../chunk-QS2FHLSL.mjs";
import {
IconButton
} from "../chunk-QLOIAU3C.mjs";
import "../chunk-USSL4UZ5.mjs";
import {
Spinner
} from "../chunk-MUNDKRAE.mjs";
import {
Icon
} from "../chunk-AESXFMCC.mjs";
import {
VisuallyHidden
} from "../chunk-NBZKMCHF.mjs";
import "../chunk-4F5DOL57.mjs";
// src/combobox/ComboboxContext.tsx
import { useFormFieldControl } from "@spark-ui/components/form-field";
import { useCombinedState } from "@spark-ui/hooks/use-combined-state";
import { useCombobox as useCombobox3, useMultipleSelection } from "downshift";
import {
createContext,
Fragment,
useContext,
useEffect,
useId,
useRef,
useState
} from "react";
// src/combobox/useCombobox/multipleSelectionReducer.ts
import { useCombobox } from "downshift";
// src/combobox/utils/index.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) === "Combobox.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 findNestedItemText = (children) => {
if (!children) return "";
for (const child of Children.toArray(children)) {
if (isValidElement(child)) {
const childElement = child;
if (getElementDisplayName(childElement) === "Combobox.ItemText") {
return childElement.props.children;
}
const foundText = findNestedItemText(childElement.props.children);
if (foundText) return foundText;
}
}
return "";
};
var getItemText = (children) => {
return typeof children === "string" ? children : findNestedItemText(children);
};
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;
});
};
var findElement = (children, value) => {
return Children.toArray(children).filter(isValidElement).find((child) => value === getElementDisplayName(child) || "");
};
// src/combobox/useCombobox/multipleSelectionReducer.ts
var multipleSelectionReducer = ({
multiselect,
selectedItems,
allowCustomValue = false,
setSelectedItems,
triggerAreaRef,
items
}) => {
const reducer = (_, { changes, type }) => {
const isFocusInsideTriggerArea = triggerAreaRef.current?.contains?.(document.activeElement);
switch (type) {
case useCombobox.stateChangeTypes.InputClick:
return {
...changes,
isOpen: true
// keep menu opened
};
case useCombobox.stateChangeTypes.InputKeyDownEnter:
case useCombobox.stateChangeTypes.ItemClick: {
const newState = { ...changes };
if (changes.selectedItem != null) {
newState.inputValue = "";
newState.isOpen = true;
const highlightedIndex = getIndexByKey(items, changes.selectedItem.value);
newState.highlightedIndex = highlightedIndex;
const isAlreadySelected = multiselect.selectedItems.some(
(selectedItem) => selectedItem.value === changes.selectedItem?.value
);
const updatedItems = isAlreadySelected ? selectedItems.filter((item) => item.value !== changes.selectedItem?.value) : [...selectedItems, changes.selectedItem];
setSelectedItems(updatedItems);
}
return newState;
}
case useCombobox.stateChangeTypes.ToggleButtonClick:
return {
...changes,
inputValue: allowCustomValue ? changes.inputValue : ""
};
case useCombobox.stateChangeTypes.InputChange:
return {
...changes,
selectedItem: changes.highlightedIndex === -1 ? null : changes.selectedItem
};
case useCombobox.stateChangeTypes.InputBlur:
return {
...changes,
inputValue: allowCustomValue ? changes.inputValue : "",
isOpen: isFocusInsideTriggerArea
};
default:
return changes;
}
};
return reducer;
};
// src/combobox/useCombobox/singleSelectionReducer.ts
import { useCombobox as useCombobox2 } from "downshift";
var singleSelectionReducer = ({
filteredItems,
allowCustomValue = false,
setSelectedItem
}) => {
const reducer = (state, { changes, type }) => {
const exactMatch = filteredItems.find(
(item) => item.text.toLowerCase() === state.inputValue.toLowerCase()
);
switch (type) {
case useCombobox2.stateChangeTypes.InputKeyDownEscape:
if (!changes.selectedItem) {
setSelectedItem(null);
}
return changes;
case useCombobox2.stateChangeTypes.ItemClick:
case useCombobox2.stateChangeTypes.InputKeyDownEnter:
if (changes.selectedItem) {
setSelectedItem(changes.selectedItem);
}
return changes;
case useCombobox2.stateChangeTypes.InputClick:
return { ...changes, isOpen: true };
case useCombobox2.stateChangeTypes.ToggleButtonClick:
case useCombobox2.stateChangeTypes.InputBlur:
if (allowCustomValue) return changes;
if (state.inputValue === "") {
setSelectedItem(null);
return { ...changes, selectedItem: null };
}
if (exactMatch) {
setSelectedItem(exactMatch);
return { ...changes, selectedItem: exactMatch, inputValue: exactMatch.text };
}
if (state.selectedItem) {
return { ...changes, inputValue: state.selectedItem.text };
}
return { ...changes, inputValue: "" };
default:
return changes;
}
};
return reducer;
};
// src/combobox/ComboboxContext.tsx
import { jsx } from "react/jsx-runtime";
var ComboboxContext = createContext(null);
var getFilteredItemsMap = (map, inputValue) => {
if (!inputValue) return map;
return new Map(
Array.from(map).filter(([_, { text }]) => text.toLowerCase().includes(inputValue.toLowerCase()))
);
};
var ID_PREFIX = ":combobox";
var ComboboxProvider = ({
children,
state: stateProp,
allowCustomValue = false,
filtering = "auto",
disabled: disabledProp = false,
multiple = false,
readOnly: readOnlyProp = false,
wrap = true,
// Value
value: controlledValue,
defaultValue,
onValueChange,
// Open
open: controlledOpen,
defaultOpen,
onOpenChange,
isLoading
}) => {
const isMounted = useRef(false);
const [inputValue, setInputValue] = useState("");
const [isTyping, setIsTyping] = useState(filtering === "strict");
const triggerAreaRef = useRef(null);
const innerInputRef = useRef(null);
const [onInputValueChange, setOnInputValueChange] = useState(null);
const [comboboxValue] = useCombinedState(controlledValue, defaultValue);
const shouldFilterItems = filtering === "strict" || filtering === "auto" && isTyping;
const [itemsMap, setItemsMap] = useState(getItemsFromChildren(children));
const [filteredItemsMap, setFilteredItems] = useState(
shouldFilterItems ? getFilteredItemsMap(itemsMap, inputValue) : itemsMap
);
const [selectedItem, setSelectedItem] = useState(
itemsMap.get(comboboxValue) || null
);
const [selectedItems, setSelectedItems] = useState(
comboboxValue ? [...itemsMap.values()].filter((item) => comboboxValue.includes(item.value)) : []
);
const onInternalSelectedItemChange = (item) => {
setIsTyping(false);
if (item?.value !== selectedItem?.value) {
setSelectedItem(item);
setTimeout(() => {
onValueChange?.(item?.value);
}, 0);
}
};
const onInternalSelectedItemsChange = (items) => {
setSelectedItems(items);
setTimeout(() => {
onValueChange?.(items.map((i) => i.value));
}, 0);
};
useEffect(() => {
if (!isMounted.current) {
isMounted.current = true;
return;
}
if (multiple) {
const newSelectedItems = comboboxValue.reduce(
(accum, value) => {
const match = itemsMap.get(value);
return match ? [...accum, match] : accum;
},
[]
);
setSelectedItems(comboboxValue ? newSelectedItems : []);
} else {
setSelectedItem(itemsMap.get(comboboxValue) || null);
}
}, [multiple ? JSON.stringify(comboboxValue) : comboboxValue]);
const field = useFormFieldControl();
const internalFieldLabelID = `${ID_PREFIX}-label-${useId()}`;
const internalFieldID = `${ID_PREFIX}-field-${useId()}`;
const id = field.id || internalFieldID;
const labelId = field.labelId || internalFieldLabelID;
const state = field.state || stateProp;
const disabled = field.disabled ?? disabledProp;
const readOnly = field.readOnly ?? readOnlyProp;
const [hasPopover, setHasPopover] = useState(
hasChildComponent(children, "Combobox.Popover")
);
const [lastInteractionType, setLastInteractionType] = useState("mouse");
useEffect(() => {
setFilteredItems(shouldFilterItems ? getFilteredItemsMap(itemsMap, inputValue) : itemsMap);
}, [inputValue, itemsMap]);
const multiselect = useMultipleSelection({
selectedItems,
stateReducer: (state2, { type, changes }) => {
const types = useMultipleSelection.stateChangeTypes;
switch (type) {
case types.SelectedItemKeyDownBackspace:
case types.SelectedItemKeyDownDelete: {
onInternalSelectedItemsChange(changes.selectedItems || []);
let activeIndex;
if (type === types.SelectedItemKeyDownDelete) {
const isLastItem = state2?.activeIndex === changes.selectedItems?.length;
activeIndex = isLastItem ? -1 : state2.activeIndex;
} else {
const hasItemBefore = (changes?.activeIndex || 0) - 1 >= 0;
activeIndex = hasItemBefore ? state2.activeIndex - 1 : changes?.activeIndex;
}
return {
...changes,
activeIndex
};
}
case types.SelectedItemClick:
if (innerInputRef.current) {
innerInputRef.current.focus();
}
return {
...changes,
activeIndex: -1
// the focus will remain on the input
};
case types.FunctionRemoveSelectedItem:
return {
...changes,
activeIndex: -1
// the focus will remain on the input
};
case types.DropdownKeyDownNavigationPrevious:
downshift.closeMenu();
return changes;
default:
return changes;
}
}
});
const filteredItems = Array.from(filteredItemsMap.values());
useEffect(() => {
onInputValueChange?.(inputValue || "");
}, [inputValue]);
const downshift = useCombobox3({
inputId: id,
items: filteredItems,
selectedItem: multiple ? void 0 : selectedItem,
id,
labelId,
// Input
inputValue,
onInputValueChange: ({ inputValue: newInputValue }) => {
setInputValue(newInputValue);
if (shouldFilterItems) {
const filtered = getFilteredItemsMap(itemsMap, newInputValue || "");
setFilteredItems(filtered);
}
},
// Open
initialIsOpen: defaultOpen,
...controlledOpen != null && { isOpen: controlledOpen },
onIsOpenChange: (changes) => {
if (changes.isOpen != null) {
onOpenChange?.(changes.isOpen);
}
},
// Custom Spark item object parsing
itemToString: (item) => {
return item?.text;
},
isItemDisabled: (item) => {
const isFilteredOut = !!inputValue && !filteredItems.some((filteredItem) => {
return item.value === filteredItem.value;
});
return item.disabled || isFilteredOut;
},
// Main reducer
stateReducer: multiple ? multipleSelectionReducer({
multiselect,
selectedItems,
allowCustomValue,
setSelectedItems: onInternalSelectedItemsChange,
triggerAreaRef,
items: itemsMap
}) : singleSelectionReducer({
allowCustomValue,
setSelectedItem: onInternalSelectedItemChange,
filteredItems: [...filteredItemsMap.values()]
}),
/**
* 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;
}
});
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(
ComboboxContext.Provider,
{
value: {
// Data
itemsMap,
filteredItemsMap,
highlightedItem: getElementByIndex(filteredItemsMap, downshift.highlightedIndex),
// State
multiple,
disabled,
readOnly,
hasPopover,
setHasPopover,
state,
lastInteractionType,
setLastInteractionType,
wrap,
// Refs
innerInputRef,
triggerAreaRef,
// Downshift state
...downshift,
...multiselect,
setInputValue,
selectItem: onInternalSelectedItemChange,
setSelectedItems: onInternalSelectedItemsChange,
isLoading,
setOnInputValueChange,
isTyping,
setIsTyping
},
children: /* @__PURE__ */ jsx(WrapperComponent, { ...wrapperProps, children })
}
);
};
var useComboboxContext = () => {
const context = useContext(ComboboxContext);
if (!context) {
throw Error("useComboboxContext must be used within a Combobox provider");
}
return context;
};
// src/combobox/Combobox.tsx
import { jsx as jsx2 } from "react/jsx-runtime";
var Combobox = ({ children, ...props }) => {
return /* @__PURE__ */ jsx2(ComboboxProvider, { ...props, children });
};
Combobox.displayName = "Combobox";
// src/combobox/ComboboxClearButton.tsx
import { DeleteOutline } from "@spark-ui/icons/DeleteOutline";
import { cx } from "class-variance-authority";
import { jsx as jsx3 } from "react/jsx-runtime";
var ClearButton = ({
className,
tabIndex = -1,
onClick,
ref,
...others
}) => {
const ctx = useComboboxContext();
const handleClick = (event) => {
event.stopPropagation();
if (ctx.multiple) {
ctx.setSelectedItems([]);
} else {
ctx.selectItem(null);
}
ctx.setInputValue("");
if (ctx.innerInputRef.current) {
ctx.innerInputRef.current.focus();
}
if (onClick) {
onClick(event);
}
};
return /* @__PURE__ */ jsx3(
"button",
{
ref,
className: cx(className, "h-sz-44 text-neutral hover:text-neutral-hovered"),
tabIndex,
onClick: handleClick,
type: "button",
...others,
children: /* @__PURE__ */ jsx3(Icon, { size: "sm", children: /* @__PURE__ */ jsx3(DeleteOutline, {}) })
}
);
};
ClearButton.displayName = "Combobox.ClearButton";
// src/combobox/ComboboxDisclosure.tsx
import { useMergeRefs } from "@spark-ui/hooks/use-merge-refs";
import { ArrowHorizontalDown } from "@spark-ui/icons/ArrowHorizontalDown";
import { cx as cx2 } from "class-variance-authority";
import { jsx as jsx4 } from "react/jsx-runtime";
var Disclosure = ({
className,
closedLabel,
openedLabel,
intent = "neutral",
design = "ghost",
size = "sm",
ref: forwardedRef,
...props
}) => {
const ctx = useComboboxContext();
const { ref: downshiftRef, ...downshiftDisclosureProps } = ctx.getToggleButtonProps({
disabled: ctx.disabled || ctx.readOnly,
onClick: (event) => {
event.stopPropagation();
}
});
const isExpanded = downshiftDisclosureProps["aria-expanded"];
const ref = useMergeRefs(forwardedRef, downshiftRef);
return /* @__PURE__ */ jsx4(
IconButton,
{
ref,
className: cx2(className, "mt-[calc((44px-32px)/2)]"),
intent,
design,
size,
...downshiftDisclosureProps,
...props,
"aria-label": isExpanded ? openedLabel : closedLabel,
disabled: ctx.disabled,
children: /* @__PURE__ */ jsx4(
Icon,
{
className: cx2("shrink-0", "rotate-0 transition duration-100 ease-in", {
"rotate-180": isExpanded
}),
size: "sm",
children: /* @__PURE__ */ jsx4(ArrowHorizontalDown, {})
}
)
}
);
};
Disclosure.displayName = "Combobox.Disclosure";
// src/combobox/ComboboxEmpty.tsx
import { cx as cx3 } from "class-variance-authority";
import { jsx as jsx5 } from "react/jsx-runtime";
var Empty = ({ className, children, ref: forwardedRef }) => {
const ctx = useComboboxContext();
const hasNoItemVisible = ctx.filteredItemsMap.size === 0;
return hasNoItemVisible ? /* @__PURE__ */ jsx5(
"div",
{
ref: forwardedRef,
className: cx3("px-lg py-md text-body-1 text-on-surface/dim-1", className),
children
}
) : null;
};
Empty.displayName = "Combobox.Empty";
// src/combobox/ComboboxGroup.tsx
import { cx as cx4 } from "class-variance-authority";
import { Children as Children2, isValidElement as isValidElement2 } from "react";
// src/combobox/ComboboxItemsGroupContext.tsx
import { createContext as createContext2, useContext as useContext2, useId as useId2 } from "react";
import { jsx as jsx6 } from "react/jsx-runtime";
var ComboboxGroupContext = createContext2(null);
var ComboboxGroupProvider = ({ children }) => {
const groupLabelId = `${ID_PREFIX}-group-label-${useId2()}`;
return /* @__PURE__ */ jsx6(ComboboxGroupContext.Provider, { value: { groupLabelId }, children });
};
var useComboboxGroupContext = () => {
const context = useContext2(ComboboxGroupContext);
if (!context) {
throw Error("useComboboxGroupContext must be used within a ComboboxGroup provider");
}
return context;
};
// src/combobox/ComboboxGroup.tsx
import { jsx as jsx7 } from "react/jsx-runtime";
var Group = ({ children, ref: forwardedRef, ...props }) => {
return /* @__PURE__ */ jsx7(ComboboxGroupProvider, { children: /* @__PURE__ */ jsx7(GroupContent, { ref: forwardedRef, ...props, children }) });
};
var GroupContent = ({ children, className, ref: forwardedRef }) => {
const ctx = useComboboxContext();
const groupCtx = useComboboxGroupContext();
const hasVisibleOptions = Children2.toArray(children).some((child) => {
return isValidElement2(child) && ctx.filteredItemsMap.get(child.props.value);
});
return hasVisibleOptions ? /* @__PURE__ */ jsx7(
"div",
{
ref: forwardedRef,
role: "group",
"aria-labelledby": groupCtx.groupLabelId,
className: cx4(className),
children
}
) : null;
};
Group.displayName = "Combobox.Group";
// src/combobox/ComboboxInput.tsx
import { useFormFieldControl as useFormFieldControl2 } from "@spark-ui/components/form-field";
import { useCombinedState as useCombinedState2 } from "@spark-ui/hooks/use-combined-state";
import { useMergeRefs as useMergeRefs2 } from "@spark-ui/hooks/use-merge-refs";
import { cx as cx5 } from "class-variance-authority";
import {
Fragment as Fragment2,
useEffect as useEffect2
} from "react";
import { Fragment as Fragment3, jsx as jsx8, jsxs } from "react/jsx-runtime";
var Input = ({
"aria-label": ariaLabel,
className,
placeholder,
value,
defaultValue,
onValueChange,
ref: forwardedRef,
...props
}) => {
const ctx = useComboboxContext();
const field = useFormFieldControl2();
const [inputValue] = useCombinedState2(value, defaultValue);
const { isInvalid, description } = field;
useEffect2(() => {
if (inputValue != null) {
ctx.setInputValue(inputValue);
}
}, [inputValue]);
useEffect2(() => {
if (onValueChange) {
ctx.setOnInputValueChange(() => onValueChange);
}
if (!ctx.multiple && ctx.selectedItem) {
ctx.setInputValue(ctx.selectedItem.text);
}
}, []);
const [PopoverTrigger, popoverTriggerProps] = ctx.hasPopover ? [Popover.Trigger, { asChild: true, type: void 0 }] : [Fragment2, {}];
const multiselectInputProps = ctx.getDropdownProps();
const inputRef = useMergeRefs2(forwardedRef, ctx.innerInputRef, multiselectInputProps.ref);
const downshiftInputProps = ctx.getInputProps({
disabled: ctx.disabled || ctx.readOnly,
...multiselectInputProps,
onKeyDown: (event) => {
multiselectInputProps.onKeyDown?.(event);
ctx.setLastInteractionType("keyboard");
ctx.setIsTyping(true);
},
/**
*
* Important:
* - without this, the input cursor is moved to the end after every change.
* @see https://github.com/downshift-js/downshift/issues/1108#issuecomment-674180157
*/
onChange: (e) => {
ctx.setInputValue(e.target.value);
},
ref: inputRef
});
const hasPlaceholder = ctx.multiple ? ctx.selectedItems.length === 0 : ctx.selectedItem === null;
function mergeHandlers(handlerA, handlerB) {
return (event) => {
handlerA?.(event);
handlerB?.(event);
};
}
const mergedEventProps = {
onBlur: mergeHandlers(props.onBlur, downshiftInputProps.onBlur),
onChange: mergeHandlers(props.onChange, downshiftInputProps.onChange),
onClick: mergeHandlers(props.onClick, downshiftInputProps.onClick),
onKeyDown: mergeHandlers(props.onKeyDown, downshiftInputProps.onKeyDown)
};
return /* @__PURE__ */ jsxs(Fragment3, { children: [
ariaLabel && /* @__PURE__ */ jsx8(VisuallyHidden, { children: /* @__PURE__ */ jsx8("label", { ...ctx.getLabelProps(), children: ariaLabel }) }),
/* @__PURE__ */ jsx8(PopoverTrigger, { ...popoverTriggerProps, children: /* @__PURE__ */ jsx8(
"input",
{
"data-spark-component": "combobox-input",
type: "text",
...hasPlaceholder && { placeholder },
className: cx5(
"max-w-full shrink-0 grow basis-[80px]",
"h-sz-28 bg-surface px-sm text-body-1 text-ellipsis outline-hidden",
"disabled:text-on-surface/dim-3 disabled:cursor-not-allowed disabled:bg-transparent",
"read-only:text-on-surface read-only:cursor-default read-only:bg-transparent",
className
),
...props,
...downshiftInputProps,
...mergedEventProps,
value: ctx.inputValue,
"aria-label": ariaLabel,
disabled: ctx.disabled,
readOnly: ctx.readOnly,
"aria-invalid": isInvalid,
"aria-describedby": description
}
) })
] });
};
Input.displayName = "Combobox.Input";
// src/combobox/ComboboxItem.tsx
import { useMergeRefs as useMergeRefs3 } from "@spark-ui/hooks/use-merge-refs";
import { cva, cx as cx6 } from "class-variance-authority";
// src/combobox/ComboboxItemContext.tsx
import {
createContext as createContext3,
useContext as useContext3,
useState as useState2
} from "react";
import { jsx as jsx9 } from "react/jsx-runtime";
var ComboboxItemContext = createContext3(null);
var ComboboxItemProvider = ({
value,
disabled = false,
children
}) => {
const ctx = useComboboxContext();
const [textId, setTextId] = useState2(void 0);
const index = getIndexByKey(ctx.filteredItemsMap, value);
const itemData = { disabled, value, text: getItemText(children) };
const isSelected = ctx.multiple ? ctx.selectedItems.some((selectedItem) => selectedItem.value === value) : ctx.selectedItem?.value === value;
return /* @__PURE__ */ jsx9(
ComboboxItemContext.Provider,
{
value: { textId, setTextId, isSelected, itemData, index, disabled },
children
}
);
};
var useComboboxItemContext = () => {
const context = useContext3(ComboboxItemContext);
if (!context) {
throw Error("useComboboxItemContext must be used within a ComboboxItem provider");
}
return context;
};
// src/combobox/ComboboxItem.tsx
import { jsx as jsx10 } from "react/jsx-runtime";
var Item = ({ children, ref: forwardedRef, ...props }) => {
const { value, disabled } = props;
return /* @__PURE__ */ jsx10(ComboboxItemProvider, { value, disabled, children: /* @__PURE__ */ jsx10(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 ctx = useComboboxContext();
const itemCtx = useComboboxItemContext();
const isVisible = !!ctx.filteredItemsMap.get(value);
const { ref: downshiftRef, ...downshiftItemProps } = ctx.getItemProps({
item: itemCtx.itemData,
index: itemCtx.index
});
const ref = useMergeRefs3(forwardedRef, downshiftRef);
if (!isVisible) return null;
return /* @__PURE__ */ jsx10(
"li",
{
ref,
className: cx6(
styles({
selected: itemCtx.isSelected,
disabled,
highlighted: ctx.highlightedItem?.value === value,
interactionType: ctx.lastInteractionType,
className
})
),
...downshiftItemProps,
"aria-selected": itemCtx.isSelected,
"aria-labelledby": itemCtx.textId,
children
},
value
);
};
Item.displayName = "Combobox.Item";
// src/combobox/ComboboxItemIndicator.tsx
import { Check } from "@spark-ui/icons/Check";
import { cx as cx7 } from "class-variance-authority";
import { jsx as jsx11 } from "react/jsx-runtime";
var ItemIndicator = ({
className,
children,
label,
ref: forwardedRef
}) => {
const { disabled, isSelected } = useComboboxItemContext();
const childElement = children || /* @__PURE__ */ jsx11(Icon, { size: "sm", children: /* @__PURE__ */ jsx11(Check, { "aria-label": label }) });
return /* @__PURE__ */ jsx11(
"span",
{
ref: forwardedRef,
className: cx7("min-h-sz-16 min-w-sz-16 flex", disabled && "opacity-dim-3", className),
children: isSelected && childElement
}
);
};
ItemIndicator.displayName = "Combobox.ItemIndicator";
// src/combobox/ComboboxItems.tsx
import { useMergeRefs as useMergeRefs4 } from "@spark-ui/hooks/use-merge-refs";
import { cx as cx8 } from "class-variance-authority";
import { useLayoutEffect, useRef as useRef2 } from "react";
import { jsx as jsx12 } from "react/jsx-runtime";
var Items = ({ children, className, ref: forwardedRef, ...props }) => {
const ctx = useComboboxContext();
const { ref: downshiftRef, ...downshiftMenuProps } = ctx.getMenuProps({
onMouseMove: () => {
ctx.setLastInteractionType("mouse");
}
});
const innerRef = useRef2(null);
const ref = useMergeRefs4(forwardedRef, downshiftRef, innerRef);
const isOpen = ctx.hasPopover ? ctx.isOpen : true;
const isPointerEventsDisabled = ctx.hasPopover && !isOpen;
useLayoutEffect(() => {
if (innerRef.current?.parentElement) {
innerRef.current.parentElement.style.pointerEvents = isPointerEventsDisabled ? "none" : "";
innerRef.current.style.pointerEvents = isPointerEventsDisabled ? "none" : "";
}
}, [isPointerEventsDisabled]);
return /* @__PURE__ */ jsx12(
"ul",
{
ref,
className: cx8(
className,
"flex flex-col",
isOpen ? "block" : "pointer-events-none invisible opacity-0",
ctx.hasPopover && "p-lg",
ctx.isLoading && "items-center overflow-y-auto"
),
...props,
...downshiftMenuProps,
"aria-busy": ctx.isLoading,
"data-spark-component": "combobox-items",
children: ctx.isLoading ? /* @__PURE__ */ jsx12(Spinner, { size: "sm" }) : children
}
);
};
Items.displayName = "Combobox.Items";
// src/combobox/ComboboxItemText.tsx
import { cx as cx9 } from "class-variance-authority";
import { useEffect as useEffect3, useId as useId3 } from "react";
import { jsx as jsx13 } from "react/jsx-runtime";
var ItemText = ({ children, className, ref: forwardedRef }) => {
const id = `${ID_PREFIX}-item-text-${useId3()}`;
const { setTextId } = useComboboxItemContext();
useEffect3(() => {
setTextId(id);
return () => setTextId(void 0);
});
return /* @__PURE__ */ jsx13("span", { id, className: cx9("inline", className), ref: forwardedRef, children });
};
ItemText.displayName = "Combobox.ItemText";
// src/combobox/ComboboxLabel.tsx
import { cx as cx10 } from "class-variance-authority";
import { jsx as jsx14 } from "react/jsx-runtime";
var Label = ({ children, className, ref: forwardedRef }) => {
const groupCtx = useComboboxGroupContext();
return /* @__PURE__ */ jsx14(
"div",
{
ref: forwardedRef,
id: groupCtx.groupLabelId,
className: cx10("px-md py-sm text-body-2 text-neutral italic", className),
children
}
);
};
Label.displayName = "Combobox.Label";
// src/combobox/ComboboxLeadingIcon.tsx
import { jsx as jsx15 } from "react/jsx-runtime";
var LeadingIcon = ({ children }) => {
return /* @__PURE__ */ jsx15(Icon, { size: "sm", className: "h-sz-44 shrink-0", children });
};
LeadingIcon.displayName = "Combobox.LeadingIcon";
// src/combobox/ComboboxPopover.tsx
import { cx as cx11 } from "class-variance-authority";
import { useEffect as useEffect4 } from "react";
import { jsx as jsx16 } from "react/jsx-runtime";
var Popover2 = ({
children,
matchTriggerWidth = true,
sideOffset = 4,
className,
ref: forwardedRef,
...props
}) => {
const ctx = useComboboxContext();
useEffect4(() => {
ctx.setHasPopover(true);
return () => ctx.setHasPopover(false);
}, []);
return /* @__PURE__ */ jsx16(
Popover.Content,
{
ref: forwardedRef,
inset: true,
asChild: true,
matchTriggerWidth,
className: cx11("z-dropdown! relative", className),
sideOffset,
onOpenAutoFocus: (e) => {
e.preventDefault();
},
...props,
"data-spark-component": "combobox-popover",
children
}
);
};
Popover2.displayName = "Combobox.Popover";
// src/combobox/ComboboxPortal.tsx
import { jsx as jsx17 } from "react/jsx-runtime";
var Portal = ({ children, ...rest }) => /* @__PURE__ */ jsx17(Popover.Portal, { ...rest, children });
Portal.displayName = "Combobox.Portal";
// src/combobox/ComboboxSelectedItems.tsx
import { DeleteOutline as DeleteOutline2 } from "@spark-ui/icons/DeleteOutline";
import { cx as cx12 } from "class-variance-authority";
import { Fragment as Fragment4, jsx as jsx18, jsxs as jsxs2 } from "react/jsx-runtime";
var SelectedItem = ({ item: selectedItem, index }) => {
const ctx = useComboboxContext();
const isCleanable = !ctx.disabled && !ctx.readOnly;
const handleFocus = (e) => {
const element = e.target;
if (ctx.lastInteractionType === "keyboard") {
element.scrollIntoView({
behavior: "smooth",
block: "nearest",
inline: "nearest"
});
}
};
const { disabled, ...selectedItemProps } = ctx.getSelectedItemProps({
disabled: ctx.disabled || ctx.readOnly,
selectedItem,
index
});
const Element = disabled ? "button" : "span";
return /* @__PURE__ */ jsxs2(
Element,
{
role: "presentation",
"data-spark-component": "combobox-selected-item",
className: cx12(
"h-sz-28 bg-neutral-container flex items-center rounded-md align-middle",
"text-body-2 text-on-neutral-container",
"disabled:opacity-dim-3 disabled:cursor-not-allowed",
"focus-visible:u-outline-inset outline-hidden",
{ "px-md": !isCleanable, "pl-md": isCleanable }
),
...selectedItemProps,
tabIndex: -1,
...disabled && { disabled: true },
onFocus: handleFocus,
children: [
/* @__PURE__ */ jsx18(
"span",
{
className: cx12("line-clamp-1 overflow-x-hidden leading-normal break-all text-ellipsis", {
"w-max": !ctx.wrap
}),
children: selectedItem.text
}
),
ctx.disabled,
isCleanable && /* @__PURE__ */ jsx18(
"button",
{
type: "button",
tabIndex: -1,
"aria-hidden": true,
className: "px-md h-full cursor-pointer",
onClick: (e) => {
e.stopPropagation();
const updatedSelectedItems = ctx.selectedItems.filter(
(item) => item.value !== selectedItem.value
);
ctx.setSelectedItems(updatedSelectedItems);
if (ctx.innerInputRef.current) {
ctx.innerInputRef.current.focus({ preventScroll: true });
}
},
children: /* @__PURE__ */ jsx18(Icon, { size: "sm", children: /* @__PURE__ */ jsx18(DeleteOutline2, {}) })
}
)
]
},
`selected-item-${index}`
);
};
var SelectedItems = () => {
const ctx = useComboboxContext();
return ctx.multiple && ctx.selectedItems.length ? /* @__PURE__ */ jsx18(Fragment4, { children: ctx.selectedItems.map((item, index) => /* @__PURE__ */ jsx18(SelectedItem, { item, index }, item.value)) }) : null;
};
SelectedItems.displayName = "Combobox.SelectedItems";
// src/combobox/ComboboxTrigger.tsx
import { useFormFieldControl as useFormFieldControl3 } from "@spark-ui/components/form-field";
import { useMergeRefs as useMergeRefs5 } from "@spark-ui/hooks/use-merge-refs";
import { cx as cx13 } from "class-variance-authority";
import { Fragment as Fragment5, useEffect as useEffect6, useRef as useRef4 } from "react";
// src/combobox/ComboboxTrigger.styles.tsx
import { cva as cva2 } from "class-variance-authority";
var styles2 = cva2(
[
"flex items-start gap-md min-h-sz-44 text-body-1",
"h-fit rounded-lg px-lg",
// outline styles
"ring-1 outline-hidden ring-inset focus-within:ring-2"
],
{
variants: {
allowWrap: {
true: "",
false: "h-sz-44"
},
state: {
undefined: "ring-outline focus-within:ring-outline-high",
error: "ring-error",
alert: "ring-alert",
success: "ring-success"
},
disabled: {
true: "cursor-not-allowed border-outline bg-on-surface/dim-5 text-on-surface/dim-3"
},
readOnly: {
true: "cursor-default bg-on-surface/dim-5 text-on-surface"
}
},
compoundVariants: [
{
disabled: false,
state: void 0,
class: "hover:ring-outline-high"
},
{
disabled: false,
readOnly: false,
class: "bg-surface text-on-surface cursor-text"
}
],
defaultVariants: {
state: void 0,
disabled: false,
readOnly: false
}
}
);
// src/combobox/utils/useWidthIncreaseCallback.ts
import { useEffect as useEffect5, useRef as useRef3 } from "react";
var useWidthIncreaseCallback = (elementRef, callback) => {
const prevWidthRef = useRef3(null);
useEffect5(() => {
const checkWidthIncrease = () => {
const currentWidth = elementRef.current?.scrollWidth || null;
if (prevWidthRef.current && currentWidth && currentWidth > prevWidthRef.current) {
callback();
}
prevWidthRef.current = currentWidth;
requestAnimationFrame(checkWidthIncrease);
};
const interval = requestAnimationFrame(checkWidthIncrease);
return () => cancelAnimationFrame(interval);
}, [elementRef]);
};
// src/combobox/ComboboxTrigger.tsx
import { Fragment as Fragment6, jsx as jsx19, jsxs as jsxs3 } from "react/jsx-runtime";
var Trigger = ({ className, children, ref: forwardedRef }) => {
const ctx = useComboboxContext();
const field = useFormFieldControl3();
const leadingIcon = findElement(children, "Combobox.LeadingIcon");
const selectedItems = findElement(children, "Combobox.SelectedItems");
const input = findElement(children, "Combobox.Input");
const clearButton = findElement(children, "Combobox.ClearButton");
const disclosure = findElement(children, "Combobox.Disclosure");
const [PopoverAnchor, popoverAnchorProps] = ctx.hasPopover ? [Popover.Anchor, { asChild: true, type: void 0 }] : [Fragment5, {}];
const ref = useMergeRefs5(forwardedRef, ctx.triggerAreaRef);
const scrollableAreaRef = useRef4(null);
const disabled = field.disabled || ctx.disabled;
const readOnly = field.readOnly || ctx.readOnly;
const hasClearButton = !!clearButton && !disabled && !readOnly;
const scrollToRight = () => {
if (scrollableAreaRef.current && !ctx.wrap) {
const { scrollWidth, clientWidth } = scrollableAreaRef.current;
scrollableAreaRef.current.scrollLeft = scrollWidth - clientWidth;
}
};
useWidthIncreaseCallback(scrollableAreaRef, scrollToRight);
useEffect6(() => {
const resizeObserver = new ResizeObserver(scrollToRight);
if (scrollableAreaRef.current) {
resizeObserver.observe(scrollableAreaRef.current);
}
return () => {
resizeObserver.disconnect();
};
}, []);
return /* @__PURE__ */ jsx19(Fragment6, { children: /* @__PURE__ */ jsx19(PopoverAnchor, { ...popoverAnchorProps, children: /* @__PURE__ */ jsxs3(
"div",
{
ref,
className: styles2({
className,
state: ctx.state,
disabled,
readOnly,
allowWrap: ctx.wrap
}),
onClick: () => {
if (!ctx.isOpen && !disabled && !readOnly) {
ctx.openMenu();
if (ctx.innerInputRef.current) {
ctx.innerInputRef.current.focus();
}
}
},
children: [
leadingIcon,
/* @__PURE__ */ jsxs3(
"div",
{
ref: scrollableAreaRef,
className: cx13(
"min-w-none gap-sm py-md inline-flex grow items-start",
ctx.wrap ? "flex-wrap" : "u-no-scrollbar overflow-x-auto p-[2px]"
),
children: [
selectedItems,
input
]
}
),
hasClearButton && clearButton,
disclosure
]
}
) }) });
};
Trigger.displayName = "Combobox.Trigger";
// src/combobox/index.ts
var Combobox2 = Object.assign(Combobox, {
Group,
Item,
Items,
ItemText,
ItemIndicator,
Label,
Popover: Popover2,
Trigger,
LeadingIcon,
Empty,
Input,
Disclosure,
SelectedItems,
ClearButton,
Portal
});
Combobox2.displayName = "Combobox";
Group.displayName = "Combobox.Group";
Items.displayName = "Combobox.Items";
Item.displayName = "Combobox.Item";
ItemText.displayName = "Combobox.ItemText";
ItemIndicator.displayName = "Combobox.ItemIndicator";
Label.displayName = "Combobox.Label";
Popover2.displayName = "Combobox.Popover";
Trigger.displayName = "Combobox.Trigger";
LeadingIcon.displayName = "Combobox.LeadingIcon";
Empty.displayName = "Combobox.Empty";
Input.displayName = "Combobox.Input";
Disclosure.displayName = "Combobox.Disclosure";
SelectedItems.displayName = "Combobox.SelectedItems";
ClearButton.displayName = "Combobox.ClearButton";
Portal.displayName = "Combobox.Portal";
export {
Combobox2 as Combobox,
ComboboxProvider,
useComboboxContext
};
//# sourceMappingURL=index.mjs.map