@stackone/malachite 2.45.0 → 2.46.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/COMPONENTS.md +1 -1
- package/dist/components/ActionMenuTagHost.js +11 -11
- package/dist/components/ActionMenuTagHost.js.map +1 -1
- package/dist/components/Dropdown.js +290 -286
- package/dist/components/Dropdown.js.map +1 -1
- package/dist/components/MultiSelect.js +132 -131
- package/dist/components/MultiSelect.js.map +1 -1
- package/dist/components/floatingMenuGeometry.js +51 -50
- package/dist/components/floatingMenuGeometry.js.map +1 -1
- package/dist/components/hasBadge.js +5 -0
- package/dist/components/hasBadge.js.map +1 -0
- package/dist/index.d.ts +8 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MultiSelect.js","sources":["../../src/components/MultiSelect.tsx"],"sourcesContent":["import React, {\n useCallback,\n useEffect,\n useId,\n useLayoutEffect,\n useInsertionEffect,\n useMemo,\n useRef,\n useState,\n} from 'react';\nimport { createPortal } from 'react-dom';\nimport type { Control, FieldPath, FieldValues } from 'react-hook-form';\nimport BaseFormComponent, {\n type RequiredIndicator,\n type RightSideContent,\n useFormFieldIds,\n} from './BaseFormComponent';\nimport CaretDownIcon from './CustomIcons/CaretDownIcon';\nimport InfoFilledIcon from './CustomIcons/InfoFilledIcon';\nimport SearchIcon from './CustomIcons/SearchIcon';\nimport LoadingInput from './LoadingInput';\nimport Spinner from './Spinner';\nimport Tag from './Tag';\nimport Tooltip from './Tooltip';\nimport { getDeepActiveElement } from './activeElement';\nimport { CONTROL_TEXT_SIZE, CONTROL_TEXT_SIZE_CLASS } from './controlTextSize';\nimport { controlWidthStyle } from './controlWidth';\nimport { DROPDOWN_TRIGGER_ATTR, getTriggerVisual } from './dropdownTriggerStyle';\nimport { pushEscapeLayer } from './escapeLayers';\nimport {\n MENU_MAX_HEIGHT_PX,\n MENU_SPECS,\n type MenuGeometry,\n measureMenuGeometry,\n} from './floatingMenuGeometry';\nimport { FOCUS_RING, FOCUS_RING_WITHIN_INPUT } from './focusRing';\nimport { ensureMultiSelectPanelStyles } from './optionRowStyles';\nimport { usePopupLayer } from './overlayLayers';\nimport { useTopLayer } from './topLayer';\nimport { isClamped, isCut, tooltipLines } from './truncatedLines';\n\nconst EMPTY_MESSAGE_STYLE: React.CSSProperties = {\n padding: '8px 10px',\n color: 'var(--malachite-text-secondary, rgb(123, 123, 123))',\n};\n\nexport type MultiSelectSize = 'small' | 'medium' | 'large';\n\n// Mirrors Dropdown, which is what Figma specs a select as.\nconst TRIGGER_SIZE_HEIGHT: Record<MultiSelectSize, string> = {\n small: 'h-[var(--malachite-control-height-small,28px)]',\n medium: 'h-[var(--malachite-control-height-medium,32px)]',\n large: 'h-[var(--malachite-control-height-large,42px)]',\n};\n\nconst TRIGGER_SIZE_CLASSES: Record<MultiSelectSize, string> = {\n small: `${TRIGGER_SIZE_HEIGHT.small} gap-[6px] px-2.5 ${CONTROL_TEXT_SIZE_CLASS.small} rounded-[var(--malachite-radius-10,10px)]`,\n medium: `${TRIGGER_SIZE_HEIGHT.medium} gap-[8px] px-3 ${CONTROL_TEXT_SIZE_CLASS.medium} rounded-xl`,\n large: `${TRIGGER_SIZE_HEIGHT.large} gap-[10px] px-4 ${CONTROL_TEXT_SIZE_CLASS.large} rounded-[var(--malachite-radius-xxl,14px)]`,\n};\n\nexport interface MultiSelectOption {\n value: string;\n label: React.ReactNode;\n searchText?: string;\n /** Prevent toggling or removing this option — used for mandatory/locked selections */\n disabled?: boolean;\n /** Right-aligned badge shown in the dropdown only (not in chips) */\n badge?: React.ReactNode;\n /** Description text for this option */\n description?: string;\n /** How description renders — 'icon': info icon with tooltip, 'subtitle': text below label, 'hover': tooltip on label. Default: 'icon' */\n descriptionMode?: 'icon' | 'subtitle' | 'hover';\n /** Custom icon for 'icon' mode. Default: built-in ⓘ */\n descriptionIcon?: React.ReactNode;\n /** Truncate subtitle after N lines. Only applies to 'subtitle' mode */\n descriptionMaxLines?: number;\n}\n\ninterface MultiSelectBaseProps {\n options: MultiSelectOption[];\n /** Controlled value. When provided, component is fully controlled. */\n value?: string[];\n /** Initial value for uncontrolled usage (like Dropdown's defaultValue). */\n defaultValue?: string[];\n onChange?: (next: string[]) => void;\n placeholder?: string;\n renderTrigger?: (selected: MultiSelectOption[]) => React.ReactNode;\n searchable?: boolean;\n /** 'large' (42px) for form inputs; 'medium' (32px) for toolbar chips; 'small' (28px) beside a title. */\n size?: MultiSelectSize;\n width?: string | number;\n minWidth?: number;\n disabled?: boolean;\n error?: boolean;\n loading?: boolean;\n label?: string;\n description?: string;\n tooltip?: string;\n hint?: string;\n rightSideContent?: RightSideContent;\n required?: boolean;\n requiredIndicator?: RequiredIndicator;\n /** Show selected values as chips instead of \"N selected\" text */\n chips?: boolean;\n /** Show × button on each chip to remove it (only when chips=true) */\n chipsRemovable?: boolean;\n /** Max visible chips. 'auto' = show all chips. Number = hard limit with \"+N\" badge */\n chipsMax?: number | 'auto';\n /** Search input inside the trigger field instead of external search bar */\n inlineSearch?: boolean;\n /**\n * Called with the query as typed, and with `''` whenever the component clears it — on\n * select, and on close — so a consumer's request stays in step with what is on screen.\n *\n * For a list too large to load in full: fire the request here and pass the results back\n * as `options`. Pair it with `localFilter={false}`, or the component filters the answer\n * the server already filtered. Debouncing is left to the caller, who is the only one who\n * knows what a request costs.\n */\n onSearchChange?: (query: string) => void;\n /**\n * Whether this component filters `options` itself. Default true.\n *\n * Set false when the options already reflect the query. A server commonly matches on\n * fields the label does not contain — an account found by its owner's email, say — and\n * filtering again here would hide precisely those matches.\n */\n localFilter?: boolean;\n className?: string;\n style?: React.CSSProperties;\n /** Names the trigger from the light DOM, where `htmlFor` cannot reach into the shadow root. */\n id?: string;\n 'aria-label'?: string;\n 'aria-labelledby'?: string;\n}\n\n// When formControl is provided, name is required for form registration\nexport type MultiSelectProps =\n | (MultiSelectBaseProps & {\n formControl: Control<FieldValues, unknown>;\n name: string;\n })\n | (MultiSelectBaseProps & {\n formControl?: undefined;\n name?: string;\n });\n\nconst AUTO_SEARCHABLE_THRESHOLD = 10;\n\nconst KEY_ARROW_DOWN = 'ArrowDown';\nconst KEY_ARROW_UP = 'ArrowUp';\nconst KEY_ENTER = 'Enter';\nconst KEY_BACKSPACE = 'Backspace';\nconst NAV_KEYS = new Set([KEY_ARROW_DOWN, KEY_ARROW_UP, KEY_ENTER]);\n\n// Nesting interactive elements inside <button> is invalid HTML —\n// combobox for text input (inline search), group for chip buttons, button otherwise\ninterface TriggerElementProps extends React.HTMLAttributes<HTMLElement> {\n triggerRole: 'combobox' | 'group' | 'button';\n disabled: boolean;\n children: React.ReactNode;\n}\n\nconst TriggerElement = React.forwardRef<HTMLButtonElement | HTMLDivElement, TriggerElementProps>(\n ({ triggerRole, disabled, children, ...props }, ref) => {\n if (triggerRole !== 'button') {\n return (\n <div\n ref={ref as React.Ref<HTMLDivElement>}\n role={triggerRole}\n tabIndex={disabled ? -1 : 0}\n {...(props as React.HTMLAttributes<HTMLDivElement>)}\n >\n {children}\n </div>\n );\n }\n return (\n <button\n ref={ref as React.Ref<HTMLButtonElement>}\n type=\"button\"\n disabled={disabled}\n {...(props as React.ButtonHTMLAttributes<HTMLButtonElement>)}\n >\n {children}\n </button>\n );\n },\n);\nTriggerElement.displayName = 'TriggerElement';\n\nconst OPTION_TOOLTIP_DELAY = 0.6;\nconst OPTION_TOOLTIP_MAX_WIDTH = 420;\n\nconst MultiSelectOptionContent: React.FC<{ option: MultiSelectOption }> = ({ option }) => {\n const labelRef = useRef<HTMLSpanElement>(null);\n const subtitleRef = useRef<HTMLSpanElement>(null);\n const mode = option.description ? (option.descriptionMode ?? 'icon') : null;\n const isSubtitle = mode === 'subtitle';\n\n // Seeded all-cut: `Tooltip` skips `shouldShow` when its text is empty.\n const [cutLines, setCutLines] = useState({ label: true, subtitle: true });\n const measureLines = () => {\n const label = isCut(labelRef.current);\n const subtitle = isClamped(subtitleRef.current);\n if (!label && !subtitle && mode !== 'hover') {\n return false;\n }\n setCutLines({ label, subtitle });\n return true;\n };\n const showsDescription = mode === 'hover' || (isSubtitle && cutLines.subtitle);\n\n const withTooltip = (children: React.ReactNode) => (\n <Tooltip\n text={tooltipLines([\n ['label', cutLines.label && option.label],\n ['description', showsDescription && option.description],\n ])}\n shouldShow={measureLines}\n delay={OPTION_TOOLTIP_DELAY}\n maxWidth={OPTION_TOOLTIP_MAX_WIDTH}\n triggerAs=\"span\"\n isShrinkable\n describesTrigger={mode === 'hover'}\n >\n {children}\n </Tooltip>\n );\n\n const labelEl = (\n <span\n ref={labelRef}\n style={{\n display: 'block',\n overflow: 'hidden',\n textOverflow: 'ellipsis',\n whiteSpace: 'nowrap',\n }}\n >\n {option.label}\n </span>\n );\n\n if (isSubtitle) {\n return withTooltip(\n <span\n style={{\n flex: 1,\n minWidth: 0,\n display: 'flex',\n flexDirection: 'column',\n alignItems: 'stretch',\n gap: 2,\n }}\n >\n {labelEl}\n <span\n ref={subtitleRef}\n style={{\n fontSize: 12,\n color: 'var(--malachite-text-secondary, rgb(123, 123, 123))',\n whiteSpace: 'normal',\n lineHeight: 1.3,\n ...(option.descriptionMaxLines\n ? {\n display: '-webkit-box',\n WebkitLineClamp: option.descriptionMaxLines,\n WebkitBoxOrient: 'vertical' as const,\n overflow: 'hidden',\n }\n : {}),\n }}\n >\n {option.description}\n </span>\n </span>,\n );\n }\n\n return (\n <span\n style={{\n flex: 1,\n minWidth: 0,\n display: 'flex',\n alignItems: 'center',\n gap: 6,\n }}\n >\n {withTooltip(labelEl)}\n {mode === 'icon' && (\n <Tooltip\n text={option.description}\n delay={OPTION_TOOLTIP_DELAY}\n maxWidth={OPTION_TOOLTIP_MAX_WIDTH}\n >\n <span\n style={{\n cursor: 'help',\n color: 'var(--malachite-text-placeholder, rgb(187, 187, 187))',\n }}\n aria-label=\"More information\"\n onClick={(e) => e.stopPropagation()}\n >\n {option.descriptionIcon ?? <InfoFilledIcon size={14} />}\n </span>\n </Tooltip>\n )}\n </span>\n );\n};\n\nconst MultiSelect: React.FC<MultiSelectProps> = ({\n options,\n value: controlledValue,\n defaultValue,\n onChange,\n placeholder = 'Select',\n renderTrigger,\n searchable,\n size = 'large',\n width = 'auto',\n minWidth,\n disabled = false,\n error = false,\n loading = false,\n label,\n name,\n description,\n tooltip,\n hint,\n rightSideContent,\n required = false,\n requiredIndicator,\n formControl,\n chips = false,\n chipsRemovable = true,\n chipsMax = 'auto',\n inlineSearch = false,\n onSearchChange,\n localFilter = true,\n className = '',\n style,\n id,\n 'aria-label': ariaLabel,\n 'aria-labelledby': ariaLabelledBy,\n}) => {\n // Stable IDs for aria-activedescendant and aria-controls\n const instanceId = useId();\n const listboxId = `${instanceId}-listbox`;\n const getOptionId = (index: number) => `${instanceId}-option-${index}`;\n const { controlId, labelId, descriptionId, hintId, describedBy } = useFormFieldIds({\n id,\n label,\n description,\n hint,\n });\n\n // Mount-only initial value — matches native <input defaultValue> semantics\n const [internalValue, setInternalValue] = useState<string[]>(() => defaultValue ?? []);\n const isControlled = controlledValue !== undefined;\n const value = isControlled ? controlledValue : internalValue;\n\n const [open, setOpen] = useState(false);\n const [position, setPosition] = useState<MenuGeometry | null>(null);\n const [search, setSearch] = useState('');\n const [highlightedIndex, setHighlightedIndex] = useState(-1);\n const triggerRef = useRef<HTMLButtonElement>(null);\n const inlineInputRef = useRef<HTMLInputElement>(null);\n const popoverRef = useRef<HTMLDivElement>(null);\n const listRef = useRef<HTMLDivElement>(null);\n\n useTopLayer(popoverRef, open);\n const popupIdentity = usePopupLayer('multiselect', open, popoverRef);\n\n const optionRefs = useRef<(HTMLButtonElement | null)[]>([]);\n\n // Set lookup — `value.includes(...)` inside a filter or a per-row render is O(n),\n // making selection checks O(options x value) on every pass.\n const selectedValues = useMemo(() => new Set(value), [value]);\n\n const selectedOptions = useMemo(\n () => options.filter((o) => selectedValues.has(o.value)),\n [options, selectedValues],\n );\n\n const maxVisibleChips = typeof chipsMax === 'number' ? chipsMax : undefined;\n\n const filteredOptions = useMemo(() => {\n // The options already answer the query when the consumer is fetching them.\n if (!localFilter) return options;\n const query = search.trim().toLowerCase();\n if (!query) return options;\n return options.filter((o) => {\n const text = o.searchText ?? (typeof o.label === 'string' ? o.label : '');\n return text.toLowerCase().includes(query);\n });\n }, [options, search, localFilter]);\n\n // Programmatic search clears (after Enter) should preserve highlight position\n const isUserSearching = useRef(false);\n\n /**\n * Every change to the query goes through here, including the programmatic clears on\n * select and on close, so `onSearchChange` cannot fall out of step with what the list is\n * showing: a consumer left holding the previous query would keep serving results for a\n * search the component has already forgotten.\n *\n * The ref mirrors the state so a repeated clear does not fire the callback again — the\n * close effect runs on every close, whether or not anything was typed.\n */\n const searchRef = useRef('');\n const updateSearch = useCallback(\n (next: string, fromUser: boolean) => {\n if (searchRef.current === next) return;\n searchRef.current = next;\n if (fromUser) isUserSearching.current = true;\n setSearch(next);\n onSearchChange?.(next);\n },\n [onSearchChange],\n );\n\n const handleSearchChange = useCallback(\n (newSearch: string) => updateSearch(newSearch, true),\n [updateSearch],\n );\n\n const clearSearch = useCallback(() => updateSearch('', false), [updateSearch]);\n // biome-ignore lint/correctness/useExhaustiveDependencies: search triggers the effect, ref gates it\n useEffect(() => {\n if (isUserSearching.current) {\n setHighlightedIndex(-1);\n isUserSearching.current = false;\n }\n }, [search]);\n\n // A consumer fetching its options always needs the input, however few results came\n // back: auto-hiding it on a short list would take the search away mid-typing, exactly\n // when the query has narrowed the list to a handful.\n const isSearchable =\n searchable ?? (onSearchChange !== undefined || options.length > AUTO_SEARCHABLE_THRESHOLD);\n\n // Inline search replaces the external search bar\n const showSearch = inlineSearch ? false : isSearchable;\n\n const dismiss = useCallback(() => {\n setOpen(false);\n if (popoverRef.current?.contains(getDeepActiveElement())) {\n triggerRef.current?.focus();\n }\n }, []);\n\n const updatePosition = useCallback(() => {\n const trigger = triggerRef.current;\n if (!trigger) return;\n\n const geometry = measureMenuGeometry(popoverRef.current, {\n triggerRect: trigger.getBoundingClientRect(),\n preset: 'standard',\n minWidth,\n viewportWidth: window.innerWidth,\n viewportHeight: window.innerHeight,\n });\n\n setPosition(geometry);\n }, [minWidth]);\n\n useEffect(() => {\n if (!open) {\n clearSearch();\n setHighlightedIndex(-1);\n setPosition(null);\n return;\n }\n updatePosition();\n // Auto-focus inline search so user can type immediately\n if (inlineSearch) inlineInputRef.current?.focus();\n\n const handleScrollOrResize = () => updatePosition();\n const handleClickOutside = (e: MouseEvent) => {\n const path = e.composedPath();\n const trigger = triggerRef.current;\n const popover = popoverRef.current;\n if ((!trigger || !path.includes(trigger)) && (!popover || !path.includes(popover))) {\n dismiss();\n }\n };\n // ESC via the shared layer stack: closes only the topmost overlay.\n const popEscapeLayer = pushEscapeLayer(dismiss);\n window.addEventListener('scroll', handleScrollOrResize, true);\n window.addEventListener('resize', handleScrollOrResize);\n document.addEventListener('mousedown', handleClickOutside);\n return () => {\n window.removeEventListener('scroll', handleScrollOrResize, true);\n window.removeEventListener('resize', handleScrollOrResize);\n document.removeEventListener('mousedown', handleClickOutside);\n popEscapeLayer();\n };\n }, [open, updatePosition, inlineSearch, clearSearch, dismiss]);\n\n useLayoutEffect(() => {\n if (!open) {\n return;\n }\n updatePosition();\n\n const popover = popoverRef.current;\n if (!popover || typeof ResizeObserver === 'undefined') {\n return;\n }\n const observer = new ResizeObserver(() => updatePosition());\n observer.observe(popover);\n return () => observer.disconnect();\n }, [open, updatePosition]);\n\n const updateValue = useCallback(\n (next: string[]) => {\n if (!isControlled) setInternalValue(next);\n onChange?.(next);\n },\n [isControlled, onChange],\n );\n\n const isOptionDisabled = useCallback(\n (optionValue: string) => {\n return options.some((o) => o.value === optionValue && o.disabled);\n },\n [options],\n );\n\n const toggle = useCallback(\n (optionValue: string) => {\n if (isOptionDisabled(optionValue)) return;\n if (value.includes(optionValue)) {\n updateValue(value.filter((v) => v !== optionValue));\n } else {\n updateValue([...value, optionValue]);\n }\n },\n [value, updateValue, isOptionDisabled],\n );\n\n const disabledValues = useMemo(() => {\n const locked = new Set<string>();\n for (const option of options) {\n if (option.disabled) {\n locked.add(option.value);\n }\n }\n return locked;\n }, [options]);\n\n const clearAll = useCallback(\n (e: React.MouseEvent) => {\n e.stopPropagation();\n // Keep disabled (locked) options when clearing\n const locked = value.filter((v) => disabledValues.has(v));\n updateValue(locked);\n },\n [updateValue, value, disabledValues],\n );\n\n const removeChip = useCallback(\n (optionValue: string, e: React.MouseEvent) => {\n e.stopPropagation();\n if (isOptionDisabled(optionValue)) return;\n updateValue(value.filter((v) => v !== optionValue));\n },\n [value, updateValue, isOptionDisabled],\n );\n\n // Inline input lives inside trigger button — stopPropagation prevents\n // the keypress from firing twice (input → button bubble)\n const handleOptionNavigation = useCallback(\n (e: React.KeyboardEvent) => {\n if (!NAV_KEYS.has(e.key)) return;\n\n e.preventDefault();\n e.stopPropagation();\n\n if (!open) {\n setOpen(true);\n return;\n }\n\n const optionCount = filteredOptions.length;\n if (optionCount === 0) return;\n\n // Manual scroll within the list container — scrollIntoView can scroll the page\n const scrollToOption = (index: number) => {\n requestAnimationFrame(() => {\n const el = optionRefs.current[index];\n const container = listRef.current;\n if (!el || !container) return;\n\n const elTop = el.offsetTop;\n const elBottom = elTop + el.offsetHeight;\n const containerTop = container.scrollTop;\n const containerBottom = containerTop + container.clientHeight;\n\n if (elBottom > containerBottom) {\n container.scrollTo({\n top: elBottom - container.clientHeight,\n behavior: 'smooth',\n });\n } else if (elTop < containerTop) {\n container.scrollTo({ top: elTop, behavior: 'smooth' });\n }\n });\n };\n\n const moveHighlight = (direction: 1 | -1) => {\n let nextIndex = highlightedIndex;\n for (let i = 0; i < optionCount; i++) {\n nextIndex =\n direction === 1\n ? nextIndex < optionCount - 1\n ? nextIndex + 1\n : 0\n : nextIndex > 0\n ? nextIndex - 1\n : optionCount - 1;\n if (!filteredOptions[nextIndex]?.disabled) {\n scrollToOption(nextIndex);\n setHighlightedIndex(nextIndex);\n return;\n }\n }\n };\n\n const selectHighlighted = () => {\n if (highlightedIndex < 0 || highlightedIndex >= optionCount) {\n return;\n }\n if (filteredOptions[highlightedIndex]?.disabled) return;\n const selectedValue = filteredOptions[highlightedIndex].value;\n toggle(selectedValue);\n clearSearch();\n // After clearing search, filteredOptions resets to the full list —\n // re-locate the selected option so the highlight stays on it\n const newIndex = options.findIndex((o) => o.value === selectedValue);\n setHighlightedIndex(newIndex >= 0 ? newIndex : -1);\n scrollToOption(newIndex >= 0 ? newIndex : 0);\n requestAnimationFrame(() => inlineInputRef.current?.focus());\n };\n\n switch (e.key) {\n case KEY_ARROW_DOWN:\n moveHighlight(1);\n break;\n case KEY_ARROW_UP:\n moveHighlight(-1);\n break;\n case KEY_ENTER:\n selectHighlighted();\n break;\n }\n },\n [open, filteredOptions, highlightedIndex, toggle, options, clearSearch],\n );\n\n const handleSearchKeyDown = useCallback(\n (e: React.KeyboardEvent<HTMLInputElement>) => {\n // Empty backspace removes last chip for quick corrections\n if (e.key === KEY_BACKSPACE && search === '' && value.length > 0) {\n e.preventDefault();\n updateValue(value.slice(0, -1));\n // Keep focus on input so user stays in the dropdown context\n requestAnimationFrame(() => inlineInputRef.current?.focus());\n return;\n }\n handleOptionNavigation(e);\n },\n [handleOptionNavigation, search, value, updateValue],\n );\n\n const clearable = selectedOptions.length > 0 && !disabled && !loading;\n\n const triggerVisual = getTriggerVisual({\n disabled,\n error,\n open,\n loading,\n hasSelection: selectedOptions.length > 0,\n });\n\n // Inline search input shared between chip mode and empty state\n const inlineSearchInput = useMemo(() => {\n if (!inlineSearch) return null;\n return (\n <input\n ref={inlineInputRef}\n type=\"text\"\n value={search}\n onChange={(e) => handleSearchChange(e.target.value)}\n onKeyDown={handleSearchKeyDown}\n placeholder={selectedOptions.length === 0 ? placeholder : ''}\n // Inherits rather than naming a rung: an input does not take the parent's\n // font by default, and the trigger already sets the one its tier calls for.\n className=\"flex-1 w-full min-w-[40px] border-none outline-none bg-transparent text-[length:inherit] p-0 text-[var(--malachite-text-primary,rgb(10,10,10))]\"\n onMouseDown={(e) => {\n // Prevent trigger blur while keeping dropdown open\n e.preventDefault();\n }}\n />\n );\n }, [\n inlineSearch,\n search,\n handleSearchChange,\n handleSearchKeyDown,\n selectedOptions.length,\n placeholder,\n ]);\n\n const triggerLabel = useMemo(() => {\n if (renderTrigger && selectedOptions.length > 0) {\n return renderTrigger(selectedOptions);\n }\n\n // Chips give users direct visibility and removal of selected values\n if (chips && selectedOptions.length > 0) {\n const visible =\n maxVisibleChips != null\n ? selectedOptions.slice(0, maxVisibleChips)\n : selectedOptions;\n const remaining = selectedOptions.length - visible.length;\n return (\n <div\n className=\"flex items-center gap-1 overflow-hidden min-w-0 flex-1\"\n style={{ flexWrap: 'nowrap' }}\n >\n {visible.map((opt) => {\n const optDisabled = disabledValues.has(opt.value);\n return (\n <Tag\n key={opt.value}\n value={opt.label}\n type=\"default\"\n textTransform=\"none\"\n onRemove={\n chipsRemovable && !disabled && !optDisabled\n ? (e) => removeChip(opt.value, e)\n : undefined\n }\n className=\"shrink-0\"\n />\n );\n })}\n {remaining > 0 && (\n <span className=\"shrink-0 text-xs text-[var(--malachite-text-secondary,rgb(123,123,123))]\">\n +{remaining}\n </span>\n )}\n {inlineSearchInput}\n </div>\n );\n }\n\n if (inlineSearch && selectedOptions.length === 0) {\n return inlineSearchInput;\n }\n\n if (selectedOptions.length === 0) {\n return <span className=\"text-[var(--malachite-text-placeholder)]\">{placeholder}</span>;\n }\n if (selectedOptions.length === 1) {\n return <span>{selectedOptions[0].label}</span>;\n }\n return <span>{`${selectedOptions.length} selected`}</span>;\n }, [\n selectedOptions,\n renderTrigger,\n chips,\n maxVisibleChips,\n chipsRemovable,\n disabled,\n removeChip,\n inlineSearch,\n inlineSearchInput,\n placeholder,\n disabledValues,\n ]);\n\n const emptyMessage = options.length === 0 ? 'No options available' : 'No results found';\n\n useInsertionEffect(() => {\n if (open) {\n ensureMultiSelectPanelStyles();\n }\n }, [open]);\n\n const popoverStyle: React.CSSProperties = {\n position: 'fixed',\n top: position?.top ?? 0,\n left: position?.left ?? 0,\n width: position?.width,\n background: MENU_SPECS.standard.background,\n border: MENU_SPECS.standard.border,\n borderRadius: MENU_SPECS.standard.radius,\n boxShadow: MENU_SPECS.standard.shadow,\n padding: MENU_SPECS.standard.padding,\n display: 'flex',\n flexDirection: 'column',\n gap: 4,\n maxHeight: position?.maxHeight ?? MENU_MAX_HEIGHT_PX,\n fontSize: CONTROL_TEXT_SIZE[size],\n visibility: position ? 'visible' : 'hidden',\n opacity: position ? 1 : 0,\n };\n\n // Form wrapper only when form-specific props are provided (backwards compatible).\n // `rightSideContent` renders inside it, so omitting it here would drop the link silently.\n const hasFormWrapper = !!(\n description ||\n tooltip ||\n hint ||\n rightSideContent ||\n name ||\n formControl ||\n required\n );\n\n // Only the form wrapper renders a <label>, so aria-labelledby has a target only when it\n // is present; without it the trigger falls back to aria-label or it would have no name.\n const hasLabelElement = hasFormWrapper && !!label;\n\n const coreComponent = (\n <>\n <div className=\"relative\" style={controlWidthStyle(width)}>\n {/* Nesting <input> inside <button> is invalid HTML — use div[role=combobox] for inline search */}\n <TriggerElement\n ref={triggerRef}\n id={controlId}\n triggerRole={\n inlineSearch ? 'combobox' : chips && chipsRemovable ? 'group' : 'button'\n }\n disabled={disabled || loading}\n {...{ [DROPDOWN_TRIGGER_ATTR]: '' }}\n aria-haspopup=\"listbox\"\n aria-expanded={open}\n aria-controls={open ? listboxId : undefined}\n aria-activedescendant={\n inlineSearch && open && highlightedIndex >= 0\n ? getOptionId(highlightedIndex)\n : undefined\n }\n // aria-labelledby outranks aria-label in the accessible-name algorithm, so\n // the generated one has to be dropped for a caller's aria-label to apply.\n aria-labelledby={\n ariaLabelledBy ?? (hasLabelElement && !ariaLabel ? labelId : undefined)\n }\n aria-label={ariaLabel ?? (hasLabelElement ? undefined : label)}\n aria-invalid={error || undefined}\n aria-required={required || undefined}\n aria-describedby={describedBy}\n aria-busy={loading || undefined}\n onClick={() => {\n if (!(disabled || loading)) setOpen((v) => !v);\n }}\n onKeyDown={handleOptionNavigation}\n style={{\n transition: 'border-color 0.15s ease, box-shadow 0.15s ease',\n ...triggerVisual,\n ...style,\n }}\n className={`w-full ${TRIGGER_SIZE_CLASSES[size]} border border-solid flex items-center font-normal text-[var(--malachite-text-primary)] ${disabled || loading ? 'cursor-not-allowed' : 'cursor-pointer'} ${FOCUS_RING} ${FOCUS_RING_WITHIN_INPUT} ${className}`}\n >\n <span\n className={`flex-1 min-w-0 text-left ${inlineSearch ? 'flex' : 'truncate'} ${clearable ? 'pr-[18px]' : ''}`}\n >\n {triggerLabel}\n </span>\n {loading ? (\n <span className=\"flex items-center justify-center shrink-0 size-[14px]\">\n <Spinner size=\"xxxsmall\" />\n </span>\n ) : (\n <CaretDownIcon\n size={13}\n className=\"shrink-0\"\n style={{\n color: open\n ? 'var(--malachite-text-primary, rgb(10, 10, 10))'\n : 'var(--malachite-text-placeholder, rgb(187, 187, 187))',\n transition: 'transform 0.2s ease, color 0.2s ease',\n transform: open ? 'rotate(180deg)' : 'rotate(0deg)',\n }}\n />\n )}\n </TriggerElement>\n {clearable && (\n <button\n type=\"button\"\n aria-label=\"Clear selection\"\n onClick={clearAll}\n className=\"absolute top-0 bottom-0 right-[28px] flex items-center justify-center px-[2px] leading-none bg-transparent border-none text-[var(--malachite-text-placeholder)] hover:text-[var(--malachite-text-primary)] cursor-pointer\"\n >\n ×\n </button>\n )}\n </div>\n {open &&\n createPortal(\n <div\n ref={popoverRef}\n id={listboxId}\n role=\"listbox\"\n aria-multiselectable=\"true\"\n {...popupIdentity}\n style={popoverStyle}\n >\n {/* Inline styles, not Tailwind: this portals into document.body, outside the\n Shadow DOM the malachite stylesheet is adopted into. */}\n {showSearch && (\n <div\n style={{\n display: 'flex',\n alignItems: 'center',\n gap: 8,\n background: 'var(--malachite-tag-default-background, #f1f1f1)',\n borderRadius: 8,\n height: 32,\n flexShrink: 0,\n padding: '0 12px',\n }}\n >\n <SearchIcon\n size={13}\n style={{\n color: 'var(--malachite-text-placeholder, rgb(187, 187, 187))',\n flexShrink: 0,\n }}\n />\n <input\n type=\"text\"\n value={search}\n onChange={(e) => handleSearchChange(e.target.value)}\n onKeyDown={handleOptionNavigation}\n placeholder=\"Search\"\n aria-label=\"Search options\"\n autoFocus\n style={{\n flex: 1,\n background: 'transparent',\n border: 'none',\n outline: 'none',\n font: 'inherit',\n color: 'var(--malachite-text-primary, rgb(10, 10, 10))',\n minWidth: 0,\n }}\n />\n </div>\n )}\n {showSearch && (\n <div\n aria-hidden=\"true\"\n style={{\n height: 1,\n width: '100%',\n background: 'var(--malachite-card-border, rgb(232, 232, 232))',\n flexShrink: 0,\n }}\n />\n )}\n <div\n ref={listRef}\n style={{\n display: 'flex',\n flexDirection: 'column',\n gap: 2,\n overflowY: 'auto',\n overscrollBehavior: 'contain',\n minHeight: 0,\n flex: 1,\n scrollbarWidth: 'thin',\n scrollbarColor:\n 'var(--malachite-text-placeholder, rgb(187, 187, 187)) transparent',\n }}\n >\n {filteredOptions.length === 0 ? (\n // `role=\"listbox\"` admits only option/group children.\n <div role=\"presentation\" style={EMPTY_MESSAGE_STYLE}>\n {emptyMessage}\n </div>\n ) : (\n filteredOptions.map((option, index) => {\n const checked = selectedValues.has(option.value);\n const highlighted = index === highlightedIndex;\n const optLocked = !!option.disabled;\n return (\n <button\n key={option.value}\n id={getOptionId(index)}\n ref={(el) => {\n optionRefs.current[index] = el;\n }}\n type=\"button\"\n role=\"option\"\n aria-selected={checked}\n aria-disabled={optLocked || undefined}\n onClick={() => {\n toggle(option.value);\n clearSearch();\n inlineInputRef.current?.focus();\n }}\n // Exception to \"no JS for styling\": the highlight IS\n // the keyboard cursor, so a bare :hover rule would\n // light a second row the keyboard has left.\n onMouseEnter={() => setHighlightedIndex(index)}\n onMouseLeave={() => {\n if (highlightedIndex === index)\n setHighlightedIndex(-1);\n }}\n className=\"malachite-multiselect-option\"\n data-checked={checked}\n data-highlighted={highlighted}\n data-disabled={optLocked}\n style={{\n display: 'flex',\n alignItems: 'center',\n width: '100%',\n // The list scrolls; unpinned, rows compress instead.\n flexShrink: 0,\n border: 'none',\n textAlign: 'left',\n borderRadius: 8,\n padding: '8px 10px',\n font: 'inherit',\n cursor: optLocked ? 'not-allowed' : 'pointer',\n opacity: optLocked ? 0.6 : 1,\n color: checked\n ? 'var(--malachite-accent-foreground, rgb(2, 122, 66))'\n : 'var(--malachite-text-primary, rgb(10, 10, 10))',\n }}\n >\n <MultiSelectOptionContent option={option} />\n {option.badge && (\n <span\n style={{\n marginLeft: 'auto',\n flexShrink: 0,\n paddingLeft: 8,\n }}\n >\n {option.badge}\n </span>\n )}\n </button>\n );\n })\n )}\n </div>\n </div>,\n document.body,\n )}\n </>\n );\n\n if (!hasFormWrapper) {\n return coreComponent;\n }\n\n return (\n <BaseFormComponent\n label={label}\n hint={hint}\n rightSideContent={rightSideContent}\n tooltip={tooltip}\n name={(name ?? '') as FieldPath<FieldValues>}\n required={required}\n requiredIndicator={requiredIndicator}\n loading={loading}\n formControl={formControl}\n description={description}\n controlId={controlId}\n labelId={labelId}\n descriptionId={descriptionId}\n hintId={hintId}\n loadingComponent={\n width === undefined ? (\n <LoadingInput heightClass={TRIGGER_SIZE_HEIGHT[size]} />\n ) : (\n <div style={controlWidthStyle(width)}>\n <LoadingInput heightClass={TRIGGER_SIZE_HEIGHT[size]} />\n </div>\n )\n }\n >\n {coreComponent}\n </BaseFormComponent>\n );\n};\n\nexport default MultiSelect;\n"],"names":["EMPTY_MESSAGE_STYLE","TRIGGER_SIZE_HEIGHT","TRIGGER_SIZE_CLASSES","CONTROL_TEXT_SIZE_CLASS","AUTO_SEARCHABLE_THRESHOLD","KEY_ARROW_DOWN","KEY_ARROW_UP","KEY_ENTER","KEY_BACKSPACE","NAV_KEYS","TriggerElement","React","triggerRole","disabled","children","props","ref","jsx","OPTION_TOOLTIP_DELAY","OPTION_TOOLTIP_MAX_WIDTH","MultiSelectOptionContent","option","labelRef","useRef","subtitleRef","mode","isSubtitle","cutLines","setCutLines","useState","measureLines","label","isCut","subtitle","isClamped","showsDescription","withTooltip","Tooltip","tooltipLines","labelEl","jsxs","e","InfoFilledIcon","MultiSelect","options","controlledValue","defaultValue","onChange","placeholder","renderTrigger","searchable","size","width","minWidth","error","loading","name","description","tooltip","hint","rightSideContent","required","requiredIndicator","formControl","chips","chipsRemovable","chipsMax","inlineSearch","onSearchChange","localFilter","className","style","id","ariaLabel","ariaLabelledBy","instanceId","useId","listboxId","getOptionId","index","controlId","labelId","descriptionId","hintId","describedBy","useFormFieldIds","internalValue","setInternalValue","isControlled","value","open","setOpen","position","setPosition","search","setSearch","highlightedIndex","setHighlightedIndex","triggerRef","inlineInputRef","popoverRef","listRef","useTopLayer","popupIdentity","usePopupLayer","optionRefs","selectedValues","useMemo","selectedOptions","o","maxVisibleChips","filteredOptions","query","isUserSearching","searchRef","updateSearch","useCallback","next","fromUser","handleSearchChange","newSearch","clearSearch","useEffect","isSearchable","showSearch","dismiss","_a","getDeepActiveElement","_b","updatePosition","trigger","geometry","measureMenuGeometry","handleScrollOrResize","handleClickOutside","path","popover","popEscapeLayer","pushEscapeLayer","useLayoutEffect","observer","updateValue","isOptionDisabled","optionValue","toggle","v","disabledValues","locked","clearAll","removeChip","handleOptionNavigation","optionCount","scrollToOption","el","container","elTop","elBottom","containerTop","containerBottom","moveHighlight","direction","nextIndex","i","selectHighlighted","selectedValue","newIndex","handleSearchKeyDown","clearable","triggerVisual","getTriggerVisual","inlineSearchInput","triggerLabel","visible","remaining","opt","optDisabled","Tag","emptyMessage","useInsertionEffect","ensureMultiSelectPanelStyles","popoverStyle","MENU_SPECS","MENU_MAX_HEIGHT_PX","CONTROL_TEXT_SIZE","hasFormWrapper","hasLabelElement","coreComponent","Fragment","controlWidthStyle","DROPDOWN_TRIGGER_ATTR","FOCUS_RING","FOCUS_RING_WITHIN_INPUT","Spinner","CaretDownIcon","createPortal","SearchIcon","checked","highlighted","optLocked","BaseFormComponent","LoadingInput"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"MultiSelect.js","sources":["../../src/components/MultiSelect.tsx"],"sourcesContent":["import React, {\n useCallback,\n useEffect,\n useId,\n useLayoutEffect,\n useInsertionEffect,\n useMemo,\n useRef,\n useState,\n} from 'react';\nimport { createPortal } from 'react-dom';\nimport type { Control, FieldPath, FieldValues } from 'react-hook-form';\nimport BaseFormComponent, {\n type RequiredIndicator,\n type RightSideContent,\n useFormFieldIds,\n} from './BaseFormComponent';\nimport CaretDownIcon from './CustomIcons/CaretDownIcon';\nimport InfoFilledIcon from './CustomIcons/InfoFilledIcon';\nimport SearchIcon from './CustomIcons/SearchIcon';\nimport LoadingInput from './LoadingInput';\nimport Spinner from './Spinner';\nimport Tag from './Tag';\nimport Tooltip from './Tooltip';\nimport { getDeepActiveElement } from './activeElement';\nimport { CONTROL_TEXT_SIZE, CONTROL_TEXT_SIZE_CLASS } from './controlTextSize';\nimport { controlWidthStyle } from './controlWidth';\nimport { DROPDOWN_TRIGGER_ATTR, getTriggerVisual } from './dropdownTriggerStyle';\nimport { pushEscapeLayer } from './escapeLayers';\nimport {\n MENU_MAX_HEIGHT_PX,\n MENU_SPECS,\n type MenuGeometry,\n measureMenuGeometry,\n} from './floatingMenuGeometry';\nimport { FOCUS_RING, FOCUS_RING_WITHIN_INPUT } from './focusRing';\nimport { hasBadge } from './hasBadge';\nimport { ensureMultiSelectPanelStyles } from './optionRowStyles';\nimport { usePopupLayer } from './overlayLayers';\nimport { useTopLayer } from './topLayer';\nimport { isClamped, isCut, tooltipLines } from './truncatedLines';\n\n// The menu portals into document.body, outside any shadow root, so a Tailwind-styled badge (a Tag)\n// needs a shadow host of its own; loaded only when a badge is passed.\nconst BadgeHost = React.lazy(() => import('./ActionMenuTagHost'));\n\nconst EMPTY_MESSAGE_STYLE: React.CSSProperties = {\n padding: '8px 10px',\n color: 'var(--malachite-text-secondary, rgb(123, 123, 123))',\n};\n\nexport type MultiSelectSize = 'small' | 'medium' | 'large';\n\n// Mirrors Dropdown, which is what Figma specs a select as.\nconst TRIGGER_SIZE_HEIGHT: Record<MultiSelectSize, string> = {\n small: 'h-[var(--malachite-control-height-small,28px)]',\n medium: 'h-[var(--malachite-control-height-medium,32px)]',\n large: 'h-[var(--malachite-control-height-large,42px)]',\n};\n\nconst TRIGGER_SIZE_CLASSES: Record<MultiSelectSize, string> = {\n small: `${TRIGGER_SIZE_HEIGHT.small} gap-[6px] px-2.5 ${CONTROL_TEXT_SIZE_CLASS.small} rounded-[var(--malachite-radius-10,10px)]`,\n medium: `${TRIGGER_SIZE_HEIGHT.medium} gap-[8px] px-3 ${CONTROL_TEXT_SIZE_CLASS.medium} rounded-xl`,\n large: `${TRIGGER_SIZE_HEIGHT.large} gap-[10px] px-4 ${CONTROL_TEXT_SIZE_CLASS.large} rounded-[var(--malachite-radius-xxl,14px)]`,\n};\n\nexport interface MultiSelectOption {\n value: string;\n label: React.ReactNode;\n searchText?: string;\n /** Prevent toggling or removing this option — used for mandatory/locked selections */\n disabled?: boolean;\n /** Right-aligned badge shown in the dropdown only (not in chips) */\n badge?: React.ReactNode;\n /** Description text for this option */\n description?: string;\n /** How description renders — 'icon': info icon with tooltip, 'subtitle': text below label, 'hover': tooltip on label. Default: 'icon' */\n descriptionMode?: 'icon' | 'subtitle' | 'hover';\n /** Custom icon for 'icon' mode. Default: built-in ⓘ */\n descriptionIcon?: React.ReactNode;\n /** Truncate subtitle after N lines. Only applies to 'subtitle' mode */\n descriptionMaxLines?: number;\n}\n\ninterface MultiSelectBaseProps {\n options: MultiSelectOption[];\n /** Controlled value. When provided, component is fully controlled. */\n value?: string[];\n /** Initial value for uncontrolled usage (like Dropdown's defaultValue). */\n defaultValue?: string[];\n onChange?: (next: string[]) => void;\n placeholder?: string;\n renderTrigger?: (selected: MultiSelectOption[]) => React.ReactNode;\n searchable?: boolean;\n /** 'large' (42px) for form inputs; 'medium' (32px) for toolbar chips; 'small' (28px) beside a title. */\n size?: MultiSelectSize;\n width?: string | number;\n minWidth?: number;\n disabled?: boolean;\n error?: boolean;\n loading?: boolean;\n label?: string;\n description?: string;\n tooltip?: string;\n hint?: string;\n rightSideContent?: RightSideContent;\n required?: boolean;\n requiredIndicator?: RequiredIndicator;\n /** Show selected values as chips instead of \"N selected\" text */\n chips?: boolean;\n /** Show × button on each chip to remove it (only when chips=true) */\n chipsRemovable?: boolean;\n /** Max visible chips. 'auto' = show all chips. Number = hard limit with \"+N\" badge */\n chipsMax?: number | 'auto';\n /** Search input inside the trigger field instead of external search bar */\n inlineSearch?: boolean;\n /**\n * Called with the query as typed, and with `''` whenever the component clears it — on\n * select, and on close — so a consumer's request stays in step with what is on screen.\n *\n * For a list too large to load in full: fire the request here and pass the results back\n * as `options`. Pair it with `localFilter={false}`, or the component filters the answer\n * the server already filtered. Debouncing is left to the caller, who is the only one who\n * knows what a request costs.\n */\n onSearchChange?: (query: string) => void;\n /**\n * Whether this component filters `options` itself. Default true.\n *\n * Set false when the options already reflect the query. A server commonly matches on\n * fields the label does not contain — an account found by its owner's email, say — and\n * filtering again here would hide precisely those matches.\n */\n localFilter?: boolean;\n className?: string;\n style?: React.CSSProperties;\n /** Names the trigger from the light DOM, where `htmlFor` cannot reach into the shadow root. */\n id?: string;\n 'aria-label'?: string;\n 'aria-labelledby'?: string;\n}\n\n// When formControl is provided, name is required for form registration\nexport type MultiSelectProps =\n | (MultiSelectBaseProps & {\n formControl: Control<FieldValues, unknown>;\n name: string;\n })\n | (MultiSelectBaseProps & {\n formControl?: undefined;\n name?: string;\n });\n\nconst AUTO_SEARCHABLE_THRESHOLD = 10;\n\nconst KEY_ARROW_DOWN = 'ArrowDown';\nconst KEY_ARROW_UP = 'ArrowUp';\nconst KEY_ENTER = 'Enter';\nconst KEY_BACKSPACE = 'Backspace';\nconst NAV_KEYS = new Set([KEY_ARROW_DOWN, KEY_ARROW_UP, KEY_ENTER]);\n\n// Nesting interactive elements inside <button> is invalid HTML —\n// combobox for text input (inline search), group for chip buttons, button otherwise\ninterface TriggerElementProps extends React.HTMLAttributes<HTMLElement> {\n triggerRole: 'combobox' | 'group' | 'button';\n disabled: boolean;\n children: React.ReactNode;\n}\n\nconst TriggerElement = React.forwardRef<HTMLButtonElement | HTMLDivElement, TriggerElementProps>(\n ({ triggerRole, disabled, children, ...props }, ref) => {\n if (triggerRole !== 'button') {\n return (\n <div\n ref={ref as React.Ref<HTMLDivElement>}\n role={triggerRole}\n tabIndex={disabled ? -1 : 0}\n {...(props as React.HTMLAttributes<HTMLDivElement>)}\n >\n {children}\n </div>\n );\n }\n return (\n <button\n ref={ref as React.Ref<HTMLButtonElement>}\n type=\"button\"\n disabled={disabled}\n {...(props as React.ButtonHTMLAttributes<HTMLButtonElement>)}\n >\n {children}\n </button>\n );\n },\n);\nTriggerElement.displayName = 'TriggerElement';\n\nconst OPTION_TOOLTIP_DELAY = 0.6;\nconst OPTION_TOOLTIP_MAX_WIDTH = 420;\n\nconst MultiSelectOptionContent: React.FC<{ option: MultiSelectOption }> = ({ option }) => {\n const labelRef = useRef<HTMLSpanElement>(null);\n const subtitleRef = useRef<HTMLSpanElement>(null);\n const mode = option.description ? (option.descriptionMode ?? 'icon') : null;\n const isSubtitle = mode === 'subtitle';\n\n // Seeded all-cut: `Tooltip` skips `shouldShow` when its text is empty.\n const [cutLines, setCutLines] = useState({ label: true, subtitle: true });\n const measureLines = () => {\n const label = isCut(labelRef.current);\n const subtitle = isClamped(subtitleRef.current);\n if (!label && !subtitle && mode !== 'hover') {\n return false;\n }\n setCutLines({ label, subtitle });\n return true;\n };\n const showsDescription = mode === 'hover' || (isSubtitle && cutLines.subtitle);\n\n const withTooltip = (children: React.ReactNode) => (\n <Tooltip\n text={tooltipLines([\n ['label', cutLines.label && option.label],\n ['description', showsDescription && option.description],\n ])}\n shouldShow={measureLines}\n delay={OPTION_TOOLTIP_DELAY}\n maxWidth={OPTION_TOOLTIP_MAX_WIDTH}\n triggerAs=\"span\"\n isShrinkable\n describesTrigger={mode === 'hover'}\n >\n {children}\n </Tooltip>\n );\n\n const labelEl = (\n <span\n ref={labelRef}\n style={{\n display: 'block',\n overflow: 'hidden',\n textOverflow: 'ellipsis',\n whiteSpace: 'nowrap',\n }}\n >\n {option.label}\n </span>\n );\n\n if (isSubtitle) {\n return withTooltip(\n <span\n style={{\n flex: 1,\n minWidth: 0,\n display: 'flex',\n flexDirection: 'column',\n alignItems: 'stretch',\n gap: 2,\n }}\n >\n {labelEl}\n <span\n ref={subtitleRef}\n style={{\n fontSize: 12,\n color: 'var(--malachite-text-secondary, rgb(123, 123, 123))',\n whiteSpace: 'normal',\n lineHeight: 1.3,\n ...(option.descriptionMaxLines\n ? {\n display: '-webkit-box',\n WebkitLineClamp: option.descriptionMaxLines,\n WebkitBoxOrient: 'vertical' as const,\n overflow: 'hidden',\n }\n : {}),\n }}\n >\n {option.description}\n </span>\n </span>,\n );\n }\n\n return (\n <span\n style={{\n flex: 1,\n minWidth: 0,\n display: 'flex',\n alignItems: 'center',\n gap: 6,\n }}\n >\n {withTooltip(labelEl)}\n {mode === 'icon' && (\n <Tooltip\n text={option.description}\n delay={OPTION_TOOLTIP_DELAY}\n maxWidth={OPTION_TOOLTIP_MAX_WIDTH}\n >\n <span\n style={{\n cursor: 'help',\n color: 'var(--malachite-text-placeholder, rgb(187, 187, 187))',\n }}\n aria-label=\"More information\"\n onClick={(e) => e.stopPropagation()}\n >\n {option.descriptionIcon ?? <InfoFilledIcon size={14} />}\n </span>\n </Tooltip>\n )}\n </span>\n );\n};\n\nconst MultiSelect: React.FC<MultiSelectProps> = ({\n options,\n value: controlledValue,\n defaultValue,\n onChange,\n placeholder = 'Select',\n renderTrigger,\n searchable,\n size = 'large',\n width = 'auto',\n minWidth,\n disabled = false,\n error = false,\n loading = false,\n label,\n name,\n description,\n tooltip,\n hint,\n rightSideContent,\n required = false,\n requiredIndicator,\n formControl,\n chips = false,\n chipsRemovable = true,\n chipsMax = 'auto',\n inlineSearch = false,\n onSearchChange,\n localFilter = true,\n className = '',\n style,\n id,\n 'aria-label': ariaLabel,\n 'aria-labelledby': ariaLabelledBy,\n}) => {\n // Stable IDs for aria-activedescendant and aria-controls\n const instanceId = useId();\n const listboxId = `${instanceId}-listbox`;\n const getOptionId = (index: number) => `${instanceId}-option-${index}`;\n const { controlId, labelId, descriptionId, hintId, describedBy } = useFormFieldIds({\n id,\n label,\n description,\n hint,\n });\n\n // Mount-only initial value — matches native <input defaultValue> semantics\n const [internalValue, setInternalValue] = useState<string[]>(() => defaultValue ?? []);\n const isControlled = controlledValue !== undefined;\n const value = isControlled ? controlledValue : internalValue;\n\n const [open, setOpen] = useState(false);\n const [position, setPosition] = useState<MenuGeometry | null>(null);\n const [search, setSearch] = useState('');\n const [highlightedIndex, setHighlightedIndex] = useState(-1);\n const triggerRef = useRef<HTMLButtonElement>(null);\n const inlineInputRef = useRef<HTMLInputElement>(null);\n const popoverRef = useRef<HTMLDivElement>(null);\n const listRef = useRef<HTMLDivElement>(null);\n\n useTopLayer(popoverRef, open);\n const popupIdentity = usePopupLayer('multiselect', open, popoverRef);\n\n const optionRefs = useRef<(HTMLButtonElement | null)[]>([]);\n\n // Set lookup — `value.includes(...)` inside a filter or a per-row render is O(n),\n // making selection checks O(options x value) on every pass.\n const selectedValues = useMemo(() => new Set(value), [value]);\n\n const selectedOptions = useMemo(\n () => options.filter((o) => selectedValues.has(o.value)),\n [options, selectedValues],\n );\n\n const maxVisibleChips = typeof chipsMax === 'number' ? chipsMax : undefined;\n\n const filteredOptions = useMemo(() => {\n // The options already answer the query when the consumer is fetching them.\n if (!localFilter) return options;\n const query = search.trim().toLowerCase();\n if (!query) return options;\n return options.filter((o) => {\n const text = o.searchText ?? (typeof o.label === 'string' ? o.label : '');\n return text.toLowerCase().includes(query);\n });\n }, [options, search, localFilter]);\n\n // Programmatic search clears (after Enter) should preserve highlight position\n const isUserSearching = useRef(false);\n\n /**\n * Every change to the query goes through here, including the programmatic clears on\n * select and on close, so `onSearchChange` cannot fall out of step with what the list is\n * showing: a consumer left holding the previous query would keep serving results for a\n * search the component has already forgotten.\n *\n * The ref mirrors the state so a repeated clear does not fire the callback again — the\n * close effect runs on every close, whether or not anything was typed.\n */\n const searchRef = useRef('');\n const updateSearch = useCallback(\n (next: string, fromUser: boolean) => {\n if (searchRef.current === next) return;\n searchRef.current = next;\n if (fromUser) isUserSearching.current = true;\n setSearch(next);\n onSearchChange?.(next);\n },\n [onSearchChange],\n );\n\n const handleSearchChange = useCallback(\n (newSearch: string) => updateSearch(newSearch, true),\n [updateSearch],\n );\n\n const clearSearch = useCallback(() => updateSearch('', false), [updateSearch]);\n // biome-ignore lint/correctness/useExhaustiveDependencies: search triggers the effect, ref gates it\n useEffect(() => {\n if (isUserSearching.current) {\n setHighlightedIndex(-1);\n isUserSearching.current = false;\n }\n }, [search]);\n\n // A consumer fetching its options always needs the input, however few results came\n // back: auto-hiding it on a short list would take the search away mid-typing, exactly\n // when the query has narrowed the list to a handful.\n const isSearchable =\n searchable ?? (onSearchChange !== undefined || options.length > AUTO_SEARCHABLE_THRESHOLD);\n\n // Inline search replaces the external search bar\n const showSearch = inlineSearch ? false : isSearchable;\n\n const dismiss = useCallback(() => {\n setOpen(false);\n if (popoverRef.current?.contains(getDeepActiveElement())) {\n triggerRef.current?.focus();\n }\n }, []);\n\n const updatePosition = useCallback(() => {\n const trigger = triggerRef.current;\n if (!trigger) return;\n\n const geometry = measureMenuGeometry(popoverRef.current, {\n triggerRect: trigger.getBoundingClientRect(),\n preset: 'standard',\n minWidth,\n viewportWidth: window.innerWidth,\n viewportHeight: window.innerHeight,\n });\n\n setPosition(geometry);\n }, [minWidth]);\n\n useEffect(() => {\n if (!open) {\n clearSearch();\n setHighlightedIndex(-1);\n setPosition(null);\n return;\n }\n updatePosition();\n // Auto-focus inline search so user can type immediately\n if (inlineSearch) inlineInputRef.current?.focus();\n\n const handleScrollOrResize = () => updatePosition();\n const handleClickOutside = (e: MouseEvent) => {\n const path = e.composedPath();\n const trigger = triggerRef.current;\n const popover = popoverRef.current;\n if ((!trigger || !path.includes(trigger)) && (!popover || !path.includes(popover))) {\n dismiss();\n }\n };\n // ESC via the shared layer stack: closes only the topmost overlay.\n const popEscapeLayer = pushEscapeLayer(dismiss);\n window.addEventListener('scroll', handleScrollOrResize, true);\n window.addEventListener('resize', handleScrollOrResize);\n document.addEventListener('mousedown', handleClickOutside);\n return () => {\n window.removeEventListener('scroll', handleScrollOrResize, true);\n window.removeEventListener('resize', handleScrollOrResize);\n document.removeEventListener('mousedown', handleClickOutside);\n popEscapeLayer();\n };\n }, [open, updatePosition, inlineSearch, clearSearch, dismiss]);\n\n useLayoutEffect(() => {\n if (!open) {\n return;\n }\n updatePosition();\n\n const popover = popoverRef.current;\n if (!popover || typeof ResizeObserver === 'undefined') {\n return;\n }\n const observer = new ResizeObserver(() => updatePosition());\n observer.observe(popover);\n return () => observer.disconnect();\n }, [open, updatePosition]);\n\n const updateValue = useCallback(\n (next: string[]) => {\n if (!isControlled) setInternalValue(next);\n onChange?.(next);\n },\n [isControlled, onChange],\n );\n\n const isOptionDisabled = useCallback(\n (optionValue: string) => {\n return options.some((o) => o.value === optionValue && o.disabled);\n },\n [options],\n );\n\n const toggle = useCallback(\n (optionValue: string) => {\n if (isOptionDisabled(optionValue)) return;\n if (value.includes(optionValue)) {\n updateValue(value.filter((v) => v !== optionValue));\n } else {\n updateValue([...value, optionValue]);\n }\n },\n [value, updateValue, isOptionDisabled],\n );\n\n const disabledValues = useMemo(() => {\n const locked = new Set<string>();\n for (const option of options) {\n if (option.disabled) {\n locked.add(option.value);\n }\n }\n return locked;\n }, [options]);\n\n const clearAll = useCallback(\n (e: React.MouseEvent) => {\n e.stopPropagation();\n // Keep disabled (locked) options when clearing\n const locked = value.filter((v) => disabledValues.has(v));\n updateValue(locked);\n },\n [updateValue, value, disabledValues],\n );\n\n const removeChip = useCallback(\n (optionValue: string, e: React.MouseEvent) => {\n e.stopPropagation();\n if (isOptionDisabled(optionValue)) return;\n updateValue(value.filter((v) => v !== optionValue));\n },\n [value, updateValue, isOptionDisabled],\n );\n\n // Inline input lives inside trigger button — stopPropagation prevents\n // the keypress from firing twice (input → button bubble)\n const handleOptionNavigation = useCallback(\n (e: React.KeyboardEvent) => {\n if (!NAV_KEYS.has(e.key)) return;\n\n e.preventDefault();\n e.stopPropagation();\n\n if (!open) {\n setOpen(true);\n return;\n }\n\n const optionCount = filteredOptions.length;\n if (optionCount === 0) return;\n\n // Manual scroll within the list container — scrollIntoView can scroll the page\n const scrollToOption = (index: number) => {\n requestAnimationFrame(() => {\n const el = optionRefs.current[index];\n const container = listRef.current;\n if (!el || !container) return;\n\n const elTop = el.offsetTop;\n const elBottom = elTop + el.offsetHeight;\n const containerTop = container.scrollTop;\n const containerBottom = containerTop + container.clientHeight;\n\n if (elBottom > containerBottom) {\n container.scrollTo({\n top: elBottom - container.clientHeight,\n behavior: 'smooth',\n });\n } else if (elTop < containerTop) {\n container.scrollTo({ top: elTop, behavior: 'smooth' });\n }\n });\n };\n\n const moveHighlight = (direction: 1 | -1) => {\n let nextIndex = highlightedIndex;\n for (let i = 0; i < optionCount; i++) {\n nextIndex =\n direction === 1\n ? nextIndex < optionCount - 1\n ? nextIndex + 1\n : 0\n : nextIndex > 0\n ? nextIndex - 1\n : optionCount - 1;\n if (!filteredOptions[nextIndex]?.disabled) {\n scrollToOption(nextIndex);\n setHighlightedIndex(nextIndex);\n return;\n }\n }\n };\n\n const selectHighlighted = () => {\n if (highlightedIndex < 0 || highlightedIndex >= optionCount) {\n return;\n }\n if (filteredOptions[highlightedIndex]?.disabled) return;\n const selectedValue = filteredOptions[highlightedIndex].value;\n toggle(selectedValue);\n clearSearch();\n // After clearing search, filteredOptions resets to the full list —\n // re-locate the selected option so the highlight stays on it\n const newIndex = options.findIndex((o) => o.value === selectedValue);\n setHighlightedIndex(newIndex >= 0 ? newIndex : -1);\n scrollToOption(newIndex >= 0 ? newIndex : 0);\n requestAnimationFrame(() => inlineInputRef.current?.focus());\n };\n\n switch (e.key) {\n case KEY_ARROW_DOWN:\n moveHighlight(1);\n break;\n case KEY_ARROW_UP:\n moveHighlight(-1);\n break;\n case KEY_ENTER:\n selectHighlighted();\n break;\n }\n },\n [open, filteredOptions, highlightedIndex, toggle, options, clearSearch],\n );\n\n const handleSearchKeyDown = useCallback(\n (e: React.KeyboardEvent<HTMLInputElement>) => {\n // Empty backspace removes last chip for quick corrections\n if (e.key === KEY_BACKSPACE && search === '' && value.length > 0) {\n e.preventDefault();\n updateValue(value.slice(0, -1));\n // Keep focus on input so user stays in the dropdown context\n requestAnimationFrame(() => inlineInputRef.current?.focus());\n return;\n }\n handleOptionNavigation(e);\n },\n [handleOptionNavigation, search, value, updateValue],\n );\n\n const clearable = selectedOptions.length > 0 && !disabled && !loading;\n\n const triggerVisual = getTriggerVisual({\n disabled,\n error,\n open,\n loading,\n hasSelection: selectedOptions.length > 0,\n });\n\n // Inline search input shared between chip mode and empty state\n const inlineSearchInput = useMemo(() => {\n if (!inlineSearch) return null;\n return (\n <input\n ref={inlineInputRef}\n type=\"text\"\n value={search}\n onChange={(e) => handleSearchChange(e.target.value)}\n onKeyDown={handleSearchKeyDown}\n placeholder={selectedOptions.length === 0 ? placeholder : ''}\n // Inherits rather than naming a rung: an input does not take the parent's\n // font by default, and the trigger already sets the one its tier calls for.\n className=\"flex-1 w-full min-w-[40px] border-none outline-none bg-transparent text-[length:inherit] p-0 text-[var(--malachite-text-primary,rgb(10,10,10))]\"\n onMouseDown={(e) => {\n // Prevent trigger blur while keeping dropdown open\n e.preventDefault();\n }}\n />\n );\n }, [\n inlineSearch,\n search,\n handleSearchChange,\n handleSearchKeyDown,\n selectedOptions.length,\n placeholder,\n ]);\n\n const triggerLabel = useMemo(() => {\n if (renderTrigger && selectedOptions.length > 0) {\n return renderTrigger(selectedOptions);\n }\n\n // Chips give users direct visibility and removal of selected values\n if (chips && selectedOptions.length > 0) {\n const visible =\n maxVisibleChips != null\n ? selectedOptions.slice(0, maxVisibleChips)\n : selectedOptions;\n const remaining = selectedOptions.length - visible.length;\n return (\n <div\n className=\"flex items-center gap-1 overflow-hidden min-w-0 flex-1\"\n style={{ flexWrap: 'nowrap' }}\n >\n {visible.map((opt) => {\n const optDisabled = disabledValues.has(opt.value);\n return (\n <Tag\n key={opt.value}\n value={opt.label}\n type=\"default\"\n textTransform=\"none\"\n onRemove={\n chipsRemovable && !disabled && !optDisabled\n ? (e) => removeChip(opt.value, e)\n : undefined\n }\n className=\"shrink-0\"\n />\n );\n })}\n {remaining > 0 && (\n <span className=\"shrink-0 text-xs text-[var(--malachite-text-secondary,rgb(123,123,123))]\">\n +{remaining}\n </span>\n )}\n {inlineSearchInput}\n </div>\n );\n }\n\n if (inlineSearch && selectedOptions.length === 0) {\n return inlineSearchInput;\n }\n\n if (selectedOptions.length === 0) {\n return <span className=\"text-[var(--malachite-text-placeholder)]\">{placeholder}</span>;\n }\n if (selectedOptions.length === 1) {\n return <span>{selectedOptions[0].label}</span>;\n }\n return <span>{`${selectedOptions.length} selected`}</span>;\n }, [\n selectedOptions,\n renderTrigger,\n chips,\n maxVisibleChips,\n chipsRemovable,\n disabled,\n removeChip,\n inlineSearch,\n inlineSearchInput,\n placeholder,\n disabledValues,\n ]);\n\n const emptyMessage = options.length === 0 ? 'No options available' : 'No results found';\n\n useInsertionEffect(() => {\n if (open) {\n ensureMultiSelectPanelStyles();\n }\n }, [open]);\n\n const popoverStyle: React.CSSProperties = {\n position: 'fixed',\n top: position?.top ?? 0,\n left: position?.left ?? 0,\n width: position?.width,\n background: MENU_SPECS.standard.background,\n border: MENU_SPECS.standard.border,\n borderRadius: MENU_SPECS.standard.radius,\n boxShadow: MENU_SPECS.standard.shadow,\n padding: MENU_SPECS.standard.padding,\n display: 'flex',\n flexDirection: 'column',\n gap: 4,\n maxHeight: position?.maxHeight ?? MENU_MAX_HEIGHT_PX,\n fontSize: CONTROL_TEXT_SIZE[size],\n visibility: position ? 'visible' : 'hidden',\n opacity: position ? 1 : 0,\n };\n\n // Form wrapper only when form-specific props are provided (backwards compatible).\n // `rightSideContent` renders inside it, so omitting it here would drop the link silently.\n const hasFormWrapper = !!(\n description ||\n tooltip ||\n hint ||\n rightSideContent ||\n name ||\n formControl ||\n required\n );\n\n // Only the form wrapper renders a <label>, so aria-labelledby has a target only when it\n // is present; without it the trigger falls back to aria-label or it would have no name.\n const hasLabelElement = hasFormWrapper && !!label;\n\n const coreComponent = (\n <>\n <div className=\"relative\" style={controlWidthStyle(width)}>\n {/* Nesting <input> inside <button> is invalid HTML — use div[role=combobox] for inline search */}\n <TriggerElement\n ref={triggerRef}\n id={controlId}\n triggerRole={\n inlineSearch ? 'combobox' : chips && chipsRemovable ? 'group' : 'button'\n }\n disabled={disabled || loading}\n {...{ [DROPDOWN_TRIGGER_ATTR]: '' }}\n aria-haspopup=\"listbox\"\n aria-expanded={open}\n aria-controls={open ? listboxId : undefined}\n aria-activedescendant={\n inlineSearch && open && highlightedIndex >= 0\n ? getOptionId(highlightedIndex)\n : undefined\n }\n // aria-labelledby outranks aria-label in the accessible-name algorithm, so\n // the generated one has to be dropped for a caller's aria-label to apply.\n aria-labelledby={\n ariaLabelledBy ?? (hasLabelElement && !ariaLabel ? labelId : undefined)\n }\n aria-label={ariaLabel ?? (hasLabelElement ? undefined : label)}\n aria-invalid={error || undefined}\n aria-required={required || undefined}\n aria-describedby={describedBy}\n aria-busy={loading || undefined}\n onClick={() => {\n if (!(disabled || loading)) setOpen((v) => !v);\n }}\n onKeyDown={handleOptionNavigation}\n style={{\n transition: 'border-color 0.15s ease, box-shadow 0.15s ease',\n ...triggerVisual,\n ...style,\n }}\n className={`w-full ${TRIGGER_SIZE_CLASSES[size]} border border-solid flex items-center font-normal text-[var(--malachite-text-primary)] ${disabled || loading ? 'cursor-not-allowed' : 'cursor-pointer'} ${FOCUS_RING} ${FOCUS_RING_WITHIN_INPUT} ${className}`}\n >\n <span\n className={`flex-1 min-w-0 text-left ${inlineSearch ? 'flex' : 'truncate'} ${clearable ? 'pr-[18px]' : ''}`}\n >\n {triggerLabel}\n </span>\n {loading ? (\n <span className=\"flex items-center justify-center shrink-0 size-[14px]\">\n <Spinner size=\"xxxsmall\" />\n </span>\n ) : (\n <CaretDownIcon\n size={13}\n className=\"shrink-0\"\n style={{\n color: open\n ? 'var(--malachite-text-primary, rgb(10, 10, 10))'\n : 'var(--malachite-text-placeholder, rgb(187, 187, 187))',\n transition: 'transform 0.2s ease, color 0.2s ease',\n transform: open ? 'rotate(180deg)' : 'rotate(0deg)',\n }}\n />\n )}\n </TriggerElement>\n {clearable && (\n <button\n type=\"button\"\n aria-label=\"Clear selection\"\n onClick={clearAll}\n className=\"absolute top-0 bottom-0 right-[28px] flex items-center justify-center px-[2px] leading-none bg-transparent border-none text-[var(--malachite-text-placeholder)] hover:text-[var(--malachite-text-primary)] cursor-pointer\"\n >\n ×\n </button>\n )}\n </div>\n {open &&\n createPortal(\n <div\n ref={popoverRef}\n id={listboxId}\n role=\"listbox\"\n aria-multiselectable=\"true\"\n {...popupIdentity}\n style={popoverStyle}\n >\n {/* Inline styles, not Tailwind: this portals into document.body, outside the\n Shadow DOM the malachite stylesheet is adopted into. */}\n {showSearch && (\n <div\n style={{\n display: 'flex',\n alignItems: 'center',\n gap: 8,\n background: 'var(--malachite-tag-default-background, #f1f1f1)',\n borderRadius: 8,\n height: 32,\n flexShrink: 0,\n padding: '0 12px',\n }}\n >\n <SearchIcon\n size={13}\n style={{\n color: 'var(--malachite-text-placeholder, rgb(187, 187, 187))',\n flexShrink: 0,\n }}\n />\n <input\n type=\"text\"\n value={search}\n onChange={(e) => handleSearchChange(e.target.value)}\n onKeyDown={handleOptionNavigation}\n placeholder=\"Search\"\n aria-label=\"Search options\"\n autoFocus\n style={{\n flex: 1,\n background: 'transparent',\n border: 'none',\n outline: 'none',\n font: 'inherit',\n color: 'var(--malachite-text-primary, rgb(10, 10, 10))',\n minWidth: 0,\n }}\n />\n </div>\n )}\n {showSearch && (\n <div\n aria-hidden=\"true\"\n style={{\n height: 1,\n width: '100%',\n background: 'var(--malachite-card-border, rgb(232, 232, 232))',\n flexShrink: 0,\n }}\n />\n )}\n <div\n ref={listRef}\n style={{\n display: 'flex',\n flexDirection: 'column',\n gap: 2,\n overflowY: 'auto',\n overscrollBehavior: 'contain',\n minHeight: 0,\n flex: 1,\n scrollbarWidth: 'thin',\n scrollbarColor:\n 'var(--malachite-text-placeholder, rgb(187, 187, 187)) transparent',\n }}\n >\n {filteredOptions.length === 0 ? (\n // `role=\"listbox\"` admits only option/group children.\n <div role=\"presentation\" style={EMPTY_MESSAGE_STYLE}>\n {emptyMessage}\n </div>\n ) : (\n filteredOptions.map((option, index) => {\n const checked = selectedValues.has(option.value);\n const highlighted = index === highlightedIndex;\n const optLocked = !!option.disabled;\n return (\n <button\n key={option.value}\n id={getOptionId(index)}\n ref={(el) => {\n optionRefs.current[index] = el;\n }}\n type=\"button\"\n role=\"option\"\n aria-selected={checked}\n aria-disabled={optLocked || undefined}\n onClick={() => {\n toggle(option.value);\n clearSearch();\n inlineInputRef.current?.focus();\n }}\n // Exception to \"no JS for styling\": the highlight IS\n // the keyboard cursor, so a bare :hover rule would\n // light a second row the keyboard has left.\n onMouseEnter={() => setHighlightedIndex(index)}\n onMouseLeave={() => {\n if (highlightedIndex === index)\n setHighlightedIndex(-1);\n }}\n className=\"malachite-multiselect-option\"\n data-checked={checked}\n data-highlighted={highlighted}\n data-disabled={optLocked}\n style={{\n display: 'flex',\n alignItems: 'center',\n width: '100%',\n // The list scrolls; unpinned, rows compress instead.\n flexShrink: 0,\n border: 'none',\n textAlign: 'left',\n borderRadius: 8,\n padding: '8px 10px',\n font: 'inherit',\n cursor: optLocked ? 'not-allowed' : 'pointer',\n opacity: optLocked ? 0.6 : 1,\n color: checked\n ? 'var(--malachite-accent-foreground, rgb(2, 122, 66))'\n : 'var(--malachite-text-primary, rgb(10, 10, 10))',\n }}\n >\n <MultiSelectOptionContent option={option} />\n {hasBadge(option.badge) && (\n <span\n style={{\n marginLeft: 'auto',\n flexShrink: 0,\n paddingLeft: 8,\n }}\n >\n <React.Suspense fallback={null}>\n <BadgeHost>{option.badge}</BadgeHost>\n </React.Suspense>\n </span>\n )}\n </button>\n );\n })\n )}\n </div>\n </div>,\n document.body,\n )}\n </>\n );\n\n if (!hasFormWrapper) {\n return coreComponent;\n }\n\n return (\n <BaseFormComponent\n label={label}\n hint={hint}\n rightSideContent={rightSideContent}\n tooltip={tooltip}\n name={(name ?? '') as FieldPath<FieldValues>}\n required={required}\n requiredIndicator={requiredIndicator}\n loading={loading}\n formControl={formControl}\n description={description}\n controlId={controlId}\n labelId={labelId}\n descriptionId={descriptionId}\n hintId={hintId}\n loadingComponent={\n width === undefined ? (\n <LoadingInput heightClass={TRIGGER_SIZE_HEIGHT[size]} />\n ) : (\n <div style={controlWidthStyle(width)}>\n <LoadingInput heightClass={TRIGGER_SIZE_HEIGHT[size]} />\n </div>\n )\n }\n >\n {coreComponent}\n </BaseFormComponent>\n );\n};\n\nexport default MultiSelect;\n"],"names":["BadgeHost","React","EMPTY_MESSAGE_STYLE","TRIGGER_SIZE_HEIGHT","TRIGGER_SIZE_CLASSES","CONTROL_TEXT_SIZE_CLASS","AUTO_SEARCHABLE_THRESHOLD","KEY_ARROW_DOWN","KEY_ARROW_UP","KEY_ENTER","KEY_BACKSPACE","NAV_KEYS","TriggerElement","triggerRole","disabled","children","props","ref","jsx","OPTION_TOOLTIP_DELAY","OPTION_TOOLTIP_MAX_WIDTH","MultiSelectOptionContent","option","labelRef","useRef","subtitleRef","mode","isSubtitle","cutLines","setCutLines","useState","measureLines","label","isCut","subtitle","isClamped","showsDescription","withTooltip","Tooltip","tooltipLines","labelEl","jsxs","e","InfoFilledIcon","MultiSelect","options","controlledValue","defaultValue","onChange","placeholder","renderTrigger","searchable","size","width","minWidth","error","loading","name","description","tooltip","hint","rightSideContent","required","requiredIndicator","formControl","chips","chipsRemovable","chipsMax","inlineSearch","onSearchChange","localFilter","className","style","id","ariaLabel","ariaLabelledBy","instanceId","useId","listboxId","getOptionId","index","controlId","labelId","descriptionId","hintId","describedBy","useFormFieldIds","internalValue","setInternalValue","isControlled","value","open","setOpen","position","setPosition","search","setSearch","highlightedIndex","setHighlightedIndex","triggerRef","inlineInputRef","popoverRef","listRef","useTopLayer","popupIdentity","usePopupLayer","optionRefs","selectedValues","useMemo","selectedOptions","o","maxVisibleChips","filteredOptions","query","isUserSearching","searchRef","updateSearch","useCallback","next","fromUser","handleSearchChange","newSearch","clearSearch","useEffect","isSearchable","showSearch","dismiss","_a","getDeepActiveElement","_b","updatePosition","trigger","geometry","measureMenuGeometry","handleScrollOrResize","handleClickOutside","path","popover","popEscapeLayer","pushEscapeLayer","useLayoutEffect","observer","updateValue","isOptionDisabled","optionValue","toggle","v","disabledValues","locked","clearAll","removeChip","handleOptionNavigation","optionCount","scrollToOption","el","container","elTop","elBottom","containerTop","containerBottom","moveHighlight","direction","nextIndex","i","selectHighlighted","selectedValue","newIndex","handleSearchKeyDown","clearable","triggerVisual","getTriggerVisual","inlineSearchInput","triggerLabel","visible","remaining","opt","optDisabled","Tag","emptyMessage","useInsertionEffect","ensureMultiSelectPanelStyles","popoverStyle","MENU_SPECS","MENU_MAX_HEIGHT_PX","CONTROL_TEXT_SIZE","hasFormWrapper","hasLabelElement","coreComponent","Fragment","controlWidthStyle","DROPDOWN_TRIGGER_ATTR","FOCUS_RING","FOCUS_RING_WITHIN_INPUT","Spinner","CaretDownIcon","createPortal","SearchIcon","checked","highlighted","optLocked","hasBadge","BaseFormComponent","LoadingInput"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const S = 8, y = 160, F = 160, $ = 180,
|
|
1
|
+
const S = 8, y = 160, F = 160, $ = 180, g = {
|
|
2
2
|
standard: {
|
|
3
3
|
width: "trigger",
|
|
4
4
|
minWidth: 160,
|
|
@@ -22,85 +22,86 @@ const S = 8, y = 160, F = 160, $ = 180, L = {
|
|
|
22
22
|
background: "var(--malachite-card-background, rgb(255, 255, 255))",
|
|
23
23
|
border: "1px solid var(--malachite-card-border, rgb(232, 232, 232))"
|
|
24
24
|
}
|
|
25
|
-
},
|
|
25
|
+
}, L = 400, O = ({
|
|
26
26
|
triggerRect: t,
|
|
27
|
-
preset:
|
|
27
|
+
preset: n,
|
|
28
28
|
minWidth: o,
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
align: e,
|
|
30
|
+
contentWidth: s,
|
|
31
|
+
menuHeight: c,
|
|
32
|
+
viewportWidth: m,
|
|
32
33
|
viewportHeight: h
|
|
33
34
|
}) => {
|
|
34
|
-
const
|
|
35
|
-
let
|
|
36
|
-
E(
|
|
37
|
-
const l = h - t.bottom -
|
|
35
|
+
const i = g[n], a = m - 16, f = Math.max(i.minWidth, o ?? 0), P = Math.min(a, Math.max(i.maxWidth ?? a, f)), T = i.width === "trigger" ? t.width : s ?? P, _ = Math.max(0, Math.min(Math.max(T, f), P)), r = e ?? i.align, d = r === "right" ? t.right - _ : t.left, I = r === "right" ? t.left : t.right - _, N = 8, b = m - _ - 8, E = (D) => D >= N && D <= b;
|
|
36
|
+
let x;
|
|
37
|
+
E(d) ? x = d : E(I) ? x = I : x = Math.max(N, Math.min(d, b));
|
|
38
|
+
const l = h - t.bottom - i.gap - 8, u = t.top - i.gap - 8, M = c > 0 && c > l && u > l, X = Math.max(0, Math.min(L, M ? u : l)), p = c > 0 ? Math.min(c, X) : 0;
|
|
38
39
|
return { top: Math.max(
|
|
39
40
|
8,
|
|
40
|
-
M ? t.top -
|
|
41
|
-
), left:
|
|
41
|
+
M ? t.top - i.gap - p : t.bottom + i.gap
|
|
42
|
+
), left: x, width: _, maxHeight: X, openUp: M };
|
|
42
43
|
}, k = (t) => {
|
|
43
44
|
if (!t || t.style.width !== "")
|
|
44
45
|
return;
|
|
45
|
-
const
|
|
46
|
+
const n = t.querySelectorAll('button, [role="menuitem"]');
|
|
46
47
|
let o = 0;
|
|
47
|
-
if (
|
|
48
|
-
o = Math.max(o,
|
|
48
|
+
if (n.forEach((m) => {
|
|
49
|
+
o = Math.max(o, m.scrollWidth);
|
|
49
50
|
}), o === 0)
|
|
50
51
|
return;
|
|
51
|
-
const
|
|
52
|
-
return Math.ceil(o +
|
|
53
|
-
}, B = (t,
|
|
52
|
+
const e = getComputedStyle(t), s = (m) => Number.parseFloat(m) || 0, c = s(e.paddingLeft) + s(e.paddingRight) + s(e.borderLeftWidth) + s(e.borderRightWidth);
|
|
53
|
+
return Math.ceil(o + c);
|
|
54
|
+
}, B = (t, n) => {
|
|
54
55
|
if (!t)
|
|
55
|
-
return
|
|
56
|
-
const o = `${
|
|
57
|
-
return t.style.width !== o && (t.style.width = o),
|
|
58
|
-
}, C = (t,
|
|
56
|
+
return O({ ...n, menuHeight: 0 });
|
|
57
|
+
const o = `${O({ ...n, menuHeight: 0 }).width}px`;
|
|
58
|
+
return t.style.width !== o && (t.style.width = o), O({ ...n, menuHeight: t.offsetHeight });
|
|
59
|
+
}, C = (t, n, o) => t.bottom < 0 || t.top > o || t.right < 0 || t.left > n, v = (t) => `${t.top},${t.left},${t.width},${t.height}`, V = (t, n, o) => t === "end" ? n.right - o : t === "center" ? (n.left + n.right - o) / 2 : n.left, q = (t) => t.scrollHeight + t.offsetHeight - t.clientHeight, j = { small: 8, medium: 12, large: 14 }, A = (t, n, o) => Math.max(8, Math.min(t, o - n - 8)), z = ({
|
|
59
60
|
triggerRect: t,
|
|
60
|
-
placement:
|
|
61
|
+
placement: n,
|
|
61
62
|
panelWidth: o,
|
|
62
|
-
panelHeight:
|
|
63
|
-
gap:
|
|
64
|
-
viewportWidth:
|
|
65
|
-
viewportHeight:
|
|
63
|
+
panelHeight: e,
|
|
64
|
+
gap: s,
|
|
65
|
+
viewportWidth: c,
|
|
66
|
+
viewportHeight: m
|
|
66
67
|
}) => {
|
|
67
|
-
const [
|
|
68
|
-
if (
|
|
69
|
-
const
|
|
68
|
+
const [h, i] = n.split("-"), a = 8;
|
|
69
|
+
if (h === "top" || h === "bottom") {
|
|
70
|
+
const l = m - t.bottom - s - a, u = t.top - s - a, M = h === "bottom" ? l : u, X = h === "bottom" ? u : l, p = e > M && X > M ? h === "bottom" ? "top" : "bottom" : h, G = Math.max(a, t.bottom + s), D = Math.min(m - a, t.top - s), U = Math.max(
|
|
70
71
|
0,
|
|
71
|
-
|
|
72
|
-
), W = Math.min(
|
|
73
|
-
|
|
72
|
+
p === "bottom" ? m - G - a : D - a
|
|
73
|
+
), W = Math.min(e, U), w = p === "bottom" ? G : Math.max(a, D - W), H = A(
|
|
74
|
+
V(i, t, o),
|
|
74
75
|
o,
|
|
75
|
-
|
|
76
|
+
c
|
|
76
77
|
);
|
|
77
|
-
return { top: w, left: H, maxHeight: U, side:
|
|
78
|
+
return { top: w, left: H, maxHeight: U, side: p };
|
|
78
79
|
}
|
|
79
|
-
const
|
|
80
|
-
if (
|
|
80
|
+
const f = c - t.right - s - a, P = t.left - s - a, T = h === "right" ? f : P, _ = h === "right" ? P : f, r = o > T && _ > T ? h === "right" ? "left" : "right" : h, d = r === "right" ? t.right + s : t.left - s - o, I = Math.max(a, t.top), N = m - I - a;
|
|
81
|
+
if (i === "start" && N >= Math.min(e, 160))
|
|
81
82
|
return {
|
|
82
|
-
top:
|
|
83
|
-
left:
|
|
84
|
-
maxHeight:
|
|
85
|
-
side:
|
|
83
|
+
top: I,
|
|
84
|
+
left: A(d, o, c),
|
|
85
|
+
maxHeight: N,
|
|
86
|
+
side: r
|
|
86
87
|
};
|
|
87
|
-
const
|
|
88
|
-
return { top:
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
), left:
|
|
88
|
+
const b = Math.max(0, m - a * 2), E = Math.min(e, b);
|
|
89
|
+
return { top: A(
|
|
90
|
+
i === "end" ? t.bottom - E : t.top,
|
|
91
|
+
E,
|
|
92
|
+
m
|
|
93
|
+
), left: A(d, o, c), maxHeight: b, side: r };
|
|
93
94
|
};
|
|
94
95
|
export {
|
|
95
96
|
j as FLOATING_OFFSET_PX,
|
|
96
|
-
|
|
97
|
+
L as MENU_MAX_HEIGHT_PX,
|
|
97
98
|
F as MENU_MIN_WIDTH_PX,
|
|
98
99
|
$ as MENU_OPTIONS_MIN_WIDTH_PX,
|
|
99
|
-
|
|
100
|
+
g as MENU_SPECS,
|
|
100
101
|
S as MENU_VIEWPORT_PADDING_PX,
|
|
101
102
|
y as SIDE_PANEL_MIN_HEIGHT_PX,
|
|
102
103
|
z as computeFloatingGeometry,
|
|
103
|
-
|
|
104
|
+
O as computeMenuGeometry,
|
|
104
105
|
q as fullPanelHeight,
|
|
105
106
|
C as isTriggerOffscreen,
|
|
106
107
|
k as measureContentWidth,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"floatingMenuGeometry.js","sources":["../../src/components/floatingMenuGeometry.ts"],"sourcesContent":["// Figma: `Dropdown Component - Item List` fills its trigger, 10px below — 444 (68:2789),\n// 200 (2343:2201), 414 (1437:2986). `Dropdown Component - Options List` (213:4320) is a\n// fixed 280px, 14px below. Figma declares no trigger width and no floor; 160 and 400 are ours.\n\nexport const MENU_VIEWPORT_PADDING_PX = 8;\n\nexport const SIDE_PANEL_MIN_HEIGHT_PX = 160;\n\nexport const MENU_MIN_WIDTH_PX = 160;\n\n// Figma draws the Options menu at a flat 280. Taken as a ceiling rather than a width: a kebab\n// of four short verbs then carries 130px of air, and every kebab in a consuming app is the same\n// width whatever it holds. 180 is the floor those menus sized themselves to before this preset.\nexport const MENU_OPTIONS_MIN_WIDTH_PX = 180;\n\nexport type MenuPreset = 'standard' | 'options';\n\nexport interface MenuSpec {\n /** `'trigger'` fills the control that opened it; `'content'` sizes to the widest item. */\n width: 'trigger' | 'content';\n /** Where this preset stops shrinking. */\n minWidth: number;\n /** Where a content-sized one stops growing. A caller's `minWidth` still raises past it. */\n maxWidth?: number;\n align: 'left' | 'right';\n gap: number;\n radius: number;\n padding: number;\n shadow: string;\n background: string;\n border: string;\n}\n\nexport const MENU_SPECS: Record<MenuPreset, MenuSpec> = {\n standard: {\n width: 'trigger',\n minWidth: MENU_MIN_WIDTH_PX,\n align: 'left',\n gap: 10,\n radius: 12,\n padding: 4,\n shadow: '0px 2px 5px 0px rgba(0, 0, 0, 0.1)',\n background: 'var(--malachite-card-background, rgb(255, 255, 255))',\n border: '1px solid var(--malachite-card-border, rgb(232, 232, 232))',\n },\n options: {\n width: 'content',\n minWidth: MENU_OPTIONS_MIN_WIDTH_PX,\n maxWidth: 280,\n align: 'right',\n gap: 14,\n radius: 12,\n padding: 8,\n shadow: '0px 4px 10px 0px rgba(0, 0, 0, 0.1)',\n background: 'var(--malachite-card-background, rgb(255, 255, 255))',\n border: '1px solid var(--malachite-card-border, rgb(232, 232, 232))',\n },\n};\n\nexport const MENU_MAX_HEIGHT_PX = 400;\n\nexport interface MenuGeometry {\n top: number;\n left: number;\n width: number;\n maxHeight: number;\n openUp: boolean;\n}\n\nexport interface MenuGeometryInput {\n triggerRect: Pick<DOMRect, 'top' | 'left' | 'right' | 'bottom' | 'width'>;\n preset: MenuPreset;\n minWidth?: number;\n /** The widest item, for a content-sized preset. Absent until the menu has been laid out. */\n contentWidth?: number;\n menuHeight: number;\n viewportWidth: number;\n viewportHeight: number;\n}\n\nexport const computeMenuGeometry = ({\n triggerRect,\n preset,\n minWidth,\n contentWidth,\n menuHeight,\n viewportWidth,\n viewportHeight,\n}: MenuGeometryInput): MenuGeometry => {\n const spec = MENU_SPECS[preset];\n const available = viewportWidth - MENU_VIEWPORT_PADDING_PX * 2;\n\n const floor = Math.max(spec.minWidth, minWidth ?? 0);\n const ceiling = Math.min(available, Math.max(spec.maxWidth ?? available, floor));\n const natural = spec.width === 'trigger' ? triggerRect.width : (contentWidth ?? ceiling);\n const width = Math.max(0, Math.min(Math.max(natural, floor), ceiling));\n\n const preferredLeft = spec.align === 'right' ? triggerRect.right - width : triggerRect.left;\n const oppositeLeft = spec.align === 'right' ? triggerRect.left : triggerRect.right - width;\n const minLeft = MENU_VIEWPORT_PADDING_PX;\n const maxLeft = viewportWidth - width - MENU_VIEWPORT_PADDING_PX;\n const fits = (candidate: number) => candidate >= minLeft && candidate <= maxLeft;\n\n let left: number;\n if (fits(preferredLeft)) {\n left = preferredLeft;\n } else if (fits(oppositeLeft)) {\n left = oppositeLeft;\n } else {\n left = Math.max(minLeft, Math.min(preferredLeft, maxLeft));\n }\n\n // Which side, then how much of it. \"More room above\" is not \"enough room above\", so the\n // height is clamped to whichever side won. Clamping `top` instead keeps the menu on screen\n // by painting it over its own trigger, which also swallows the click that closes it.\n const roomBelow = viewportHeight - triggerRect.bottom - spec.gap - MENU_VIEWPORT_PADDING_PX;\n const roomAbove = triggerRect.top - spec.gap - MENU_VIEWPORT_PADDING_PX;\n const openUp = menuHeight > 0 && menuHeight > roomBelow && roomAbove > roomBelow;\n\n const maxHeight = Math.max(0, Math.min(MENU_MAX_HEIGHT_PX, openUp ? roomAbove : roomBelow));\n const height = menuHeight > 0 ? Math.min(menuHeight, maxHeight) : 0;\n\n const top = Math.max(\n MENU_VIEWPORT_PADDING_PX,\n openUp ? triggerRect.top - spec.gap - height : triggerRect.bottom + spec.gap,\n );\n\n return { top, left, width, maxHeight, openUp };\n};\n\n// The height decides whether the menu opens upwards, and it is only meaningful at the width the\n// menu will actually have — so on the pass that first sizes the panel the width is written\n// before the height is read. The guard is what keeps that out of the steady state: every later\n// call finds the width already set and writes nothing, so a `ResizeObserver` callback does not\n// resize what it is observing (Chrome's \"undelivered notifications\" error) and a per-frame\n// reposition does not force a reflow. After the first pass this is a pure read.\n// A content-sized preset needs the width the panel chose for itself, which is only readable\n// while no width has been written to it — i.e. on the pass that opens the menu. The caller keeps\n// it from there, because after the first write `offsetWidth` reports what we wrote, not what the\n// items want.\nexport const measureContentWidth = (panel: HTMLElement | null): number | undefined => {\n if (!panel || panel.style.width !== '') {\n return undefined;\n }\n\n // The rows, not the panel. Each label carries `min-width: 0` and `overflow: hidden` so it\n // can ellipsise, which also means it contributes nothing to the panel's intrinsic width —\n // measuring the panel itself returns a box the size of its own padding. `scrollWidth` on a\n // clipped row is the width the row wanted.\n const rows = panel.querySelectorAll<HTMLElement>('button, [role=\"menuitem\"]');\n let widest = 0;\n rows.forEach((row) => {\n widest = Math.max(widest, row.scrollWidth);\n });\n\n if (widest === 0) {\n return undefined;\n }\n\n // The panel is border-box, so the width we hand back has to carry its own padding and rule.\n // Each read is defaulted: a computed style can come back empty — jsdom never expands the\n // `border` shorthand — and one NaN here would make the whole width NaN, which CSS drops,\n // leaving the panel unsized with no sign of why.\n const styles = getComputedStyle(panel);\n const edge = (value: string) => Number.parseFloat(value) || 0;\n const chrome =\n edge(styles.paddingLeft) +\n edge(styles.paddingRight) +\n edge(styles.borderLeftWidth) +\n edge(styles.borderRightWidth);\n\n return Math.ceil(widest + chrome);\n};\n\nexport const measureMenuGeometry = (\n panel: HTMLElement | null,\n input: Omit<MenuGeometryInput, 'menuHeight'>,\n): MenuGeometry => {\n if (!panel) {\n return computeMenuGeometry({ ...input, menuHeight: 0 });\n }\n\n const cssWidth = `${computeMenuGeometry({ ...input, menuHeight: 0 }).width}px`;\n if (panel.style.width !== cssWidth) {\n panel.style.width = cssWidth;\n }\n\n return computeMenuGeometry({ ...input, menuHeight: panel.offsetHeight });\n};\n\nexport const isTriggerOffscreen = (\n rect: Pick<DOMRect, 'top' | 'left' | 'right' | 'bottom'>,\n viewportWidth: number,\n viewportHeight: number,\n): boolean =>\n rect.bottom < 0 || rect.top > viewportHeight || rect.right < 0 || rect.left > viewportWidth;\n\nexport const rectSignature = (rect: Pick<DOMRect, 'top' | 'left' | 'width' | 'height'>): string =>\n `${rect.top},${rect.left},${rect.width},${rect.height}`;\n\nexport type FloatingPlacement =\n | 'bottom-start'\n | 'bottom-end'\n | 'top-start'\n | 'top-center'\n | 'right-start'\n | 'right-end'\n | 'left-start';\n\nconst alignedLeft = (\n align: 'start' | 'end' | 'center',\n triggerRect: Pick<DOMRect, 'left' | 'right'>,\n panelWidth: number,\n): number => {\n if (align === 'end') {\n return triggerRect.right - panelWidth;\n }\n if (align === 'center') {\n return (triggerRect.left + triggerRect.right - panelWidth) / 2;\n }\n return triggerRect.left;\n};\n\nexport const fullPanelHeight = (panel: HTMLElement): number =>\n panel.scrollHeight + panel.offsetHeight - panel.clientHeight;\n\nexport const FLOATING_OFFSET_PX = { small: 8, medium: 12, large: 14 } as const;\n\nexport type FloatingOffset = keyof typeof FLOATING_OFFSET_PX;\n\nexport interface FloatingGeometryInput {\n triggerRect: Pick<DOMRect, 'top' | 'left' | 'right' | 'bottom'>;\n placement: FloatingPlacement;\n panelWidth: number;\n panelHeight: number;\n gap: number;\n viewportWidth: number;\n viewportHeight: number;\n}\n\nexport interface FloatingGeometry {\n top: number;\n left: number;\n maxHeight: number;\n side: 'top' | 'bottom' | 'left' | 'right';\n}\n\nconst clampToViewport = (value: number, size: number, viewport: number): number =>\n Math.max(MENU_VIEWPORT_PADDING_PX, Math.min(value, viewport - size - MENU_VIEWPORT_PADDING_PX));\n\nexport const computeFloatingGeometry = ({\n triggerRect,\n placement,\n panelWidth,\n panelHeight,\n gap,\n viewportWidth,\n viewportHeight,\n}: FloatingGeometryInput): FloatingGeometry => {\n const [preferred, align] = placement.split('-') as [\n FloatingGeometry['side'],\n 'start' | 'end' | 'center',\n ];\n const pad = MENU_VIEWPORT_PADDING_PX;\n\n if (preferred === 'top' || preferred === 'bottom') {\n const roomBelow = viewportHeight - triggerRect.bottom - gap - pad;\n const roomAbove = triggerRect.top - gap - pad;\n const wanted = preferred === 'bottom' ? roomBelow : roomAbove;\n const opposite = preferred === 'bottom' ? roomAbove : roomBelow;\n const side: FloatingGeometry['side'] =\n panelHeight > wanted && opposite > wanted\n ? preferred === 'bottom'\n ? 'top'\n : 'bottom'\n : preferred;\n const belowTop = Math.max(pad, triggerRect.bottom + gap);\n const aboveBottom = Math.min(viewportHeight - pad, triggerRect.top - gap);\n const maxHeight = Math.max(\n 0,\n side === 'bottom' ? viewportHeight - belowTop - pad : aboveBottom - pad,\n );\n const height = Math.min(panelHeight, maxHeight);\n const top = side === 'bottom' ? belowTop : Math.max(pad, aboveBottom - height);\n const left = clampToViewport(\n alignedLeft(align, triggerRect, panelWidth),\n panelWidth,\n viewportWidth,\n );\n return { top, left, maxHeight, side };\n }\n\n const roomRight = viewportWidth - triggerRect.right - gap - pad;\n const roomLeft = triggerRect.left - gap - pad;\n const wanted = preferred === 'right' ? roomRight : roomLeft;\n const opposite = preferred === 'right' ? roomLeft : roomRight;\n const side: FloatingGeometry['side'] =\n panelWidth > wanted && opposite > wanted\n ? preferred === 'right'\n ? 'left'\n : 'right'\n : preferred;\n const left = side === 'right' ? triggerRect.right + gap : triggerRect.left - gap - panelWidth;\n const levelTop = Math.max(pad, triggerRect.top);\n const roomBelow = viewportHeight - levelTop - pad;\n if (align === 'start' && roomBelow >= Math.min(panelHeight, SIDE_PANEL_MIN_HEIGHT_PX)) {\n return {\n top: levelTop,\n left: clampToViewport(left, panelWidth, viewportWidth),\n maxHeight: roomBelow,\n side,\n };\n }\n const maxHeight = Math.max(0, viewportHeight - pad * 2);\n const height = Math.min(panelHeight, maxHeight);\n const top = clampToViewport(\n align === 'end' ? triggerRect.bottom - height : triggerRect.top,\n height,\n viewportHeight,\n );\n return { top, left: clampToViewport(left, panelWidth, viewportWidth), maxHeight, side };\n};\n"],"names":["MENU_VIEWPORT_PADDING_PX","SIDE_PANEL_MIN_HEIGHT_PX","MENU_MIN_WIDTH_PX","MENU_OPTIONS_MIN_WIDTH_PX","MENU_SPECS","MENU_MAX_HEIGHT_PX","computeMenuGeometry","triggerRect","preset","minWidth","contentWidth","menuHeight","viewportWidth","viewportHeight","spec","available","floor","ceiling","natural","width","preferredLeft","oppositeLeft","minLeft","maxLeft","fits","candidate","left","roomBelow","roomAbove","openUp","maxHeight","height","measureContentWidth","panel","rows","widest","row","styles","edge","value","chrome","measureMenuGeometry","input","cssWidth","isTriggerOffscreen","rect","rectSignature","alignedLeft","align","panelWidth","fullPanelHeight","FLOATING_OFFSET_PX","clampToViewport","size","viewport","computeFloatingGeometry","placement","panelHeight","gap","preferred","pad","wanted","opposite","side","belowTop","aboveBottom","top","roomRight","roomLeft","levelTop"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"floatingMenuGeometry.js","sources":["../../src/components/floatingMenuGeometry.ts"],"sourcesContent":["// Figma: `Dropdown Component - Item List` fills its trigger, 10px below — 444 (68:2789),\n// 200 (2343:2201), 414 (1437:2986). `Dropdown Component - Options List` (213:4320) is a\n// fixed 280px, 14px below. Figma declares no trigger width and no floor; 160 and 400 are ours.\n\nexport const MENU_VIEWPORT_PADDING_PX = 8;\n\nexport const SIDE_PANEL_MIN_HEIGHT_PX = 160;\n\nexport const MENU_MIN_WIDTH_PX = 160;\n\n// Figma draws the Options menu at a flat 280. Taken as a ceiling rather than a width: a kebab\n// of four short verbs then carries 130px of air, and every kebab in a consuming app is the same\n// width whatever it holds. 180 is the floor those menus sized themselves to before this preset.\nexport const MENU_OPTIONS_MIN_WIDTH_PX = 180;\n\nexport type MenuPreset = 'standard' | 'options';\n\nexport interface MenuSpec {\n /** `'trigger'` fills the control that opened it; `'content'` sizes to the widest item. */\n width: 'trigger' | 'content';\n /** Where this preset stops shrinking. */\n minWidth: number;\n /** Where a content-sized one stops growing. A caller's `minWidth` still raises past it. */\n maxWidth?: number;\n align: 'left' | 'right';\n gap: number;\n radius: number;\n padding: number;\n shadow: string;\n background: string;\n border: string;\n}\n\nexport const MENU_SPECS: Record<MenuPreset, MenuSpec> = {\n standard: {\n width: 'trigger',\n minWidth: MENU_MIN_WIDTH_PX,\n align: 'left',\n gap: 10,\n radius: 12,\n padding: 4,\n shadow: '0px 2px 5px 0px rgba(0, 0, 0, 0.1)',\n background: 'var(--malachite-card-background, rgb(255, 255, 255))',\n border: '1px solid var(--malachite-card-border, rgb(232, 232, 232))',\n },\n options: {\n width: 'content',\n minWidth: MENU_OPTIONS_MIN_WIDTH_PX,\n maxWidth: 280,\n align: 'right',\n gap: 14,\n radius: 12,\n padding: 8,\n shadow: '0px 4px 10px 0px rgba(0, 0, 0, 0.1)',\n background: 'var(--malachite-card-background, rgb(255, 255, 255))',\n border: '1px solid var(--malachite-card-border, rgb(232, 232, 232))',\n },\n};\n\nexport const MENU_MAX_HEIGHT_PX = 400;\n\nexport interface MenuGeometry {\n top: number;\n left: number;\n width: number;\n maxHeight: number;\n openUp: boolean;\n}\n\nexport interface MenuGeometryInput {\n triggerRect: Pick<DOMRect, 'top' | 'left' | 'right' | 'bottom' | 'width'>;\n preset: MenuPreset;\n minWidth?: number;\n /** Which trigger edge the menu prefers to share; defaults to the preset's. */\n align?: 'left' | 'right';\n /** The widest item, for a content-sized preset. Absent until the menu has been laid out. */\n contentWidth?: number;\n menuHeight: number;\n viewportWidth: number;\n viewportHeight: number;\n}\n\nexport const computeMenuGeometry = ({\n triggerRect,\n preset,\n minWidth,\n align: alignOverride,\n contentWidth,\n menuHeight,\n viewportWidth,\n viewportHeight,\n}: MenuGeometryInput): MenuGeometry => {\n const spec = MENU_SPECS[preset];\n const available = viewportWidth - MENU_VIEWPORT_PADDING_PX * 2;\n\n const floor = Math.max(spec.minWidth, minWidth ?? 0);\n const ceiling = Math.min(available, Math.max(spec.maxWidth ?? available, floor));\n const natural = spec.width === 'trigger' ? triggerRect.width : (contentWidth ?? ceiling);\n const width = Math.max(0, Math.min(Math.max(natural, floor), ceiling));\n\n const align = alignOverride ?? spec.align;\n const preferredLeft = align === 'right' ? triggerRect.right - width : triggerRect.left;\n const oppositeLeft = align === 'right' ? triggerRect.left : triggerRect.right - width;\n const minLeft = MENU_VIEWPORT_PADDING_PX;\n const maxLeft = viewportWidth - width - MENU_VIEWPORT_PADDING_PX;\n const fits = (candidate: number) => candidate >= minLeft && candidate <= maxLeft;\n\n let left: number;\n if (fits(preferredLeft)) {\n left = preferredLeft;\n } else if (fits(oppositeLeft)) {\n left = oppositeLeft;\n } else {\n left = Math.max(minLeft, Math.min(preferredLeft, maxLeft));\n }\n\n // Which side, then how much of it. \"More room above\" is not \"enough room above\", so the\n // height is clamped to whichever side won. Clamping `top` instead keeps the menu on screen\n // by painting it over its own trigger, which also swallows the click that closes it.\n const roomBelow = viewportHeight - triggerRect.bottom - spec.gap - MENU_VIEWPORT_PADDING_PX;\n const roomAbove = triggerRect.top - spec.gap - MENU_VIEWPORT_PADDING_PX;\n const openUp = menuHeight > 0 && menuHeight > roomBelow && roomAbove > roomBelow;\n\n const maxHeight = Math.max(0, Math.min(MENU_MAX_HEIGHT_PX, openUp ? roomAbove : roomBelow));\n const height = menuHeight > 0 ? Math.min(menuHeight, maxHeight) : 0;\n\n const top = Math.max(\n MENU_VIEWPORT_PADDING_PX,\n openUp ? triggerRect.top - spec.gap - height : triggerRect.bottom + spec.gap,\n );\n\n return { top, left, width, maxHeight, openUp };\n};\n\n// The height decides whether the menu opens upwards, and it is only meaningful at the width the\n// menu will actually have — so on the pass that first sizes the panel the width is written\n// before the height is read. The guard is what keeps that out of the steady state: every later\n// call finds the width already set and writes nothing, so a `ResizeObserver` callback does not\n// resize what it is observing (Chrome's \"undelivered notifications\" error) and a per-frame\n// reposition does not force a reflow. After the first pass this is a pure read.\n// A content-sized preset needs the width the panel chose for itself, which is only readable\n// while no width has been written to it — i.e. on the pass that opens the menu. The caller keeps\n// it from there, because after the first write `offsetWidth` reports what we wrote, not what the\n// items want.\nexport const measureContentWidth = (panel: HTMLElement | null): number | undefined => {\n if (!panel || panel.style.width !== '') {\n return undefined;\n }\n\n // The rows, not the panel. Each label carries `min-width: 0` and `overflow: hidden` so it\n // can ellipsise, which also means it contributes nothing to the panel's intrinsic width —\n // measuring the panel itself returns a box the size of its own padding. `scrollWidth` on a\n // clipped row is the width the row wanted.\n const rows = panel.querySelectorAll<HTMLElement>('button, [role=\"menuitem\"]');\n let widest = 0;\n rows.forEach((row) => {\n widest = Math.max(widest, row.scrollWidth);\n });\n\n if (widest === 0) {\n return undefined;\n }\n\n // The panel is border-box, so the width we hand back has to carry its own padding and rule.\n // Each read is defaulted: a computed style can come back empty — jsdom never expands the\n // `border` shorthand — and one NaN here would make the whole width NaN, which CSS drops,\n // leaving the panel unsized with no sign of why.\n const styles = getComputedStyle(panel);\n const edge = (value: string) => Number.parseFloat(value) || 0;\n const chrome =\n edge(styles.paddingLeft) +\n edge(styles.paddingRight) +\n edge(styles.borderLeftWidth) +\n edge(styles.borderRightWidth);\n\n return Math.ceil(widest + chrome);\n};\n\nexport const measureMenuGeometry = (\n panel: HTMLElement | null,\n input: Omit<MenuGeometryInput, 'menuHeight'>,\n): MenuGeometry => {\n if (!panel) {\n return computeMenuGeometry({ ...input, menuHeight: 0 });\n }\n\n const cssWidth = `${computeMenuGeometry({ ...input, menuHeight: 0 }).width}px`;\n if (panel.style.width !== cssWidth) {\n panel.style.width = cssWidth;\n }\n\n return computeMenuGeometry({ ...input, menuHeight: panel.offsetHeight });\n};\n\nexport const isTriggerOffscreen = (\n rect: Pick<DOMRect, 'top' | 'left' | 'right' | 'bottom'>,\n viewportWidth: number,\n viewportHeight: number,\n): boolean =>\n rect.bottom < 0 || rect.top > viewportHeight || rect.right < 0 || rect.left > viewportWidth;\n\nexport const rectSignature = (rect: Pick<DOMRect, 'top' | 'left' | 'width' | 'height'>): string =>\n `${rect.top},${rect.left},${rect.width},${rect.height}`;\n\nexport type FloatingPlacement =\n | 'bottom-start'\n | 'bottom-end'\n | 'top-start'\n | 'top-center'\n | 'right-start'\n | 'right-end'\n | 'left-start';\n\nconst alignedLeft = (\n align: 'start' | 'end' | 'center',\n triggerRect: Pick<DOMRect, 'left' | 'right'>,\n panelWidth: number,\n): number => {\n if (align === 'end') {\n return triggerRect.right - panelWidth;\n }\n if (align === 'center') {\n return (triggerRect.left + triggerRect.right - panelWidth) / 2;\n }\n return triggerRect.left;\n};\n\nexport const fullPanelHeight = (panel: HTMLElement): number =>\n panel.scrollHeight + panel.offsetHeight - panel.clientHeight;\n\nexport const FLOATING_OFFSET_PX = { small: 8, medium: 12, large: 14 } as const;\n\nexport type FloatingOffset = keyof typeof FLOATING_OFFSET_PX;\n\nexport interface FloatingGeometryInput {\n triggerRect: Pick<DOMRect, 'top' | 'left' | 'right' | 'bottom'>;\n placement: FloatingPlacement;\n panelWidth: number;\n panelHeight: number;\n gap: number;\n viewportWidth: number;\n viewportHeight: number;\n}\n\nexport interface FloatingGeometry {\n top: number;\n left: number;\n maxHeight: number;\n side: 'top' | 'bottom' | 'left' | 'right';\n}\n\nconst clampToViewport = (value: number, size: number, viewport: number): number =>\n Math.max(MENU_VIEWPORT_PADDING_PX, Math.min(value, viewport - size - MENU_VIEWPORT_PADDING_PX));\n\nexport const computeFloatingGeometry = ({\n triggerRect,\n placement,\n panelWidth,\n panelHeight,\n gap,\n viewportWidth,\n viewportHeight,\n}: FloatingGeometryInput): FloatingGeometry => {\n const [preferred, align] = placement.split('-') as [\n FloatingGeometry['side'],\n 'start' | 'end' | 'center',\n ];\n const pad = MENU_VIEWPORT_PADDING_PX;\n\n if (preferred === 'top' || preferred === 'bottom') {\n const roomBelow = viewportHeight - triggerRect.bottom - gap - pad;\n const roomAbove = triggerRect.top - gap - pad;\n const wanted = preferred === 'bottom' ? roomBelow : roomAbove;\n const opposite = preferred === 'bottom' ? roomAbove : roomBelow;\n const side: FloatingGeometry['side'] =\n panelHeight > wanted && opposite > wanted\n ? preferred === 'bottom'\n ? 'top'\n : 'bottom'\n : preferred;\n const belowTop = Math.max(pad, triggerRect.bottom + gap);\n const aboveBottom = Math.min(viewportHeight - pad, triggerRect.top - gap);\n const maxHeight = Math.max(\n 0,\n side === 'bottom' ? viewportHeight - belowTop - pad : aboveBottom - pad,\n );\n const height = Math.min(panelHeight, maxHeight);\n const top = side === 'bottom' ? belowTop : Math.max(pad, aboveBottom - height);\n const left = clampToViewport(\n alignedLeft(align, triggerRect, panelWidth),\n panelWidth,\n viewportWidth,\n );\n return { top, left, maxHeight, side };\n }\n\n const roomRight = viewportWidth - triggerRect.right - gap - pad;\n const roomLeft = triggerRect.left - gap - pad;\n const wanted = preferred === 'right' ? roomRight : roomLeft;\n const opposite = preferred === 'right' ? roomLeft : roomRight;\n const side: FloatingGeometry['side'] =\n panelWidth > wanted && opposite > wanted\n ? preferred === 'right'\n ? 'left'\n : 'right'\n : preferred;\n const left = side === 'right' ? triggerRect.right + gap : triggerRect.left - gap - panelWidth;\n const levelTop = Math.max(pad, triggerRect.top);\n const roomBelow = viewportHeight - levelTop - pad;\n if (align === 'start' && roomBelow >= Math.min(panelHeight, SIDE_PANEL_MIN_HEIGHT_PX)) {\n return {\n top: levelTop,\n left: clampToViewport(left, panelWidth, viewportWidth),\n maxHeight: roomBelow,\n side,\n };\n }\n const maxHeight = Math.max(0, viewportHeight - pad * 2);\n const height = Math.min(panelHeight, maxHeight);\n const top = clampToViewport(\n align === 'end' ? triggerRect.bottom - height : triggerRect.top,\n height,\n viewportHeight,\n );\n return { top, left: clampToViewport(left, panelWidth, viewportWidth), maxHeight, side };\n};\n"],"names":["MENU_VIEWPORT_PADDING_PX","SIDE_PANEL_MIN_HEIGHT_PX","MENU_MIN_WIDTH_PX","MENU_OPTIONS_MIN_WIDTH_PX","MENU_SPECS","MENU_MAX_HEIGHT_PX","computeMenuGeometry","triggerRect","preset","minWidth","alignOverride","contentWidth","menuHeight","viewportWidth","viewportHeight","spec","available","floor","ceiling","natural","width","align","preferredLeft","oppositeLeft","minLeft","maxLeft","fits","candidate","left","roomBelow","roomAbove","openUp","maxHeight","height","measureContentWidth","panel","rows","widest","row","styles","edge","value","chrome","measureMenuGeometry","input","cssWidth","isTriggerOffscreen","rect","rectSignature","alignedLeft","panelWidth","fullPanelHeight","FLOATING_OFFSET_PX","clampToViewport","size","viewport","computeFloatingGeometry","placement","panelHeight","gap","preferred","pad","wanted","opposite","side","belowTop","aboveBottom","top","roomRight","roomLeft","levelTop"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hasBadge.js","sources":["../../src/components/hasBadge.ts"],"sourcesContent":["import type React from 'react';\n\n// React renders 0 and '' as text, so only the values it skips mean \"no badge\".\nexport const hasBadge = (badge: React.ReactNode): boolean =>\n badge !== undefined && badge !== null && badge !== false;\n"],"names":["hasBadge","badge"],"mappings":"AAGO,MAAMA,IAAW,CAACC,MACEA,KAAU,QAAQA,MAAU;"}
|