tempest-react-sdk 0.66.0 → 0.67.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/dist/components/BottomSheet/BottomSheet.cjs +1 -1
- package/dist/components/BottomSheet/BottomSheet.cjs.map +1 -1
- package/dist/components/BottomSheet/BottomSheet.js +31 -40
- package/dist/components/BottomSheet/BottomSheet.js.map +1 -1
- package/dist/components/Combobox/Combobox.cjs +1 -1
- package/dist/components/Combobox/Combobox.cjs.map +1 -1
- package/dist/components/Combobox/Combobox.js +94 -73
- package/dist/components/Combobox/Combobox.js.map +1 -1
- package/dist/components/Combobox/Combobox.module.cjs +1 -1
- package/dist/components/Combobox/Combobox.module.cjs.map +1 -1
- package/dist/components/Combobox/Combobox.module.js +4 -3
- package/dist/components/Combobox/Combobox.module.js.map +1 -1
- package/dist/components/ContextMenu/ContextMenu.cjs +1 -1
- package/dist/components/ContextMenu/ContextMenu.cjs.map +1 -1
- package/dist/components/ContextMenu/ContextMenu.js +80 -83
- package/dist/components/ContextMenu/ContextMenu.js.map +1 -1
- package/dist/components/Drawer/Drawer.cjs +1 -1
- package/dist/components/Drawer/Drawer.cjs.map +1 -1
- package/dist/components/Drawer/Drawer.js +43 -46
- package/dist/components/Drawer/Drawer.js.map +1 -1
- package/dist/components/DropdownMenu/DropdownMenu.cjs +1 -1
- package/dist/components/DropdownMenu/DropdownMenu.cjs.map +1 -1
- package/dist/components/DropdownMenu/DropdownMenu.js +129 -105
- package/dist/components/DropdownMenu/DropdownMenu.js.map +1 -1
- package/dist/components/DropdownMenu/DropdownMenu.module.cjs +1 -1
- package/dist/components/DropdownMenu/DropdownMenu.module.cjs.map +1 -1
- package/dist/components/DropdownMenu/DropdownMenu.module.js +12 -11
- package/dist/components/DropdownMenu/DropdownMenu.module.js.map +1 -1
- package/dist/components/HoverCard/HoverCard.cjs +1 -1
- package/dist/components/HoverCard/HoverCard.cjs.map +1 -1
- package/dist/components/HoverCard/HoverCard.js +54 -29
- package/dist/components/HoverCard/HoverCard.js.map +1 -1
- package/dist/components/HoverCard/HoverCard.module.cjs +1 -1
- package/dist/components/HoverCard/HoverCard.module.cjs.map +1 -1
- package/dist/components/HoverCard/HoverCard.module.js +4 -3
- package/dist/components/HoverCard/HoverCard.module.js.map +1 -1
- package/dist/components/Lightbox/Lightbox.cjs +1 -1
- package/dist/components/Lightbox/Lightbox.cjs.map +1 -1
- package/dist/components/Lightbox/Lightbox.js +86 -89
- package/dist/components/Lightbox/Lightbox.js.map +1 -1
- package/dist/components/Menubar/Menubar.cjs +1 -1
- package/dist/components/Menubar/Menubar.cjs.map +1 -1
- package/dist/components/Menubar/Menubar.js +86 -65
- package/dist/components/Menubar/Menubar.js.map +1 -1
- package/dist/components/Menubar/Menubar.module.cjs +1 -1
- package/dist/components/Menubar/Menubar.module.cjs.map +1 -1
- package/dist/components/Menubar/Menubar.module.js +4 -3
- package/dist/components/Menubar/Menubar.module.js.map +1 -1
- package/dist/components/Modal/Modal.cjs +1 -1
- package/dist/components/Modal/Modal.cjs.map +1 -1
- package/dist/components/Modal/Modal.js +44 -51
- package/dist/components/Modal/Modal.js.map +1 -1
- package/dist/components/MultiSelect/MultiSelect.cjs +1 -1
- package/dist/components/MultiSelect/MultiSelect.cjs.map +1 -1
- package/dist/components/MultiSelect/MultiSelect.js +113 -96
- package/dist/components/MultiSelect/MultiSelect.js.map +1 -1
- package/dist/components/MultiSelect/MultiSelect.module.cjs +1 -1
- package/dist/components/MultiSelect/MultiSelect.module.cjs.map +1 -1
- package/dist/components/MultiSelect/MultiSelect.module.js +4 -3
- package/dist/components/MultiSelect/MultiSelect.module.js.map +1 -1
- package/dist/components/NavigationMenu/NavigationMenu.cjs +1 -1
- package/dist/components/NavigationMenu/NavigationMenu.cjs.map +1 -1
- package/dist/components/NavigationMenu/NavigationMenu.js +83 -61
- package/dist/components/NavigationMenu/NavigationMenu.js.map +1 -1
- package/dist/components/NavigationMenu/NavigationMenu.module.cjs +1 -1
- package/dist/components/NavigationMenu/NavigationMenu.module.cjs.map +1 -1
- package/dist/components/NavigationMenu/NavigationMenu.module.js +4 -3
- package/dist/components/NavigationMenu/NavigationMenu.module.js.map +1 -1
- package/dist/components/Popover/Popover.cjs +1 -1
- package/dist/components/Popover/Popover.cjs.map +1 -1
- package/dist/components/Popover/Popover.js +57 -36
- package/dist/components/Popover/Popover.js.map +1 -1
- package/dist/components/Popover/Popover.module.cjs +1 -1
- package/dist/components/Popover/Popover.module.cjs.map +1 -1
- package/dist/components/Popover/Popover.module.js +7 -6
- package/dist/components/Popover/Popover.module.js.map +1 -1
- package/dist/components/Portal/anchor-position.cjs +2 -0
- package/dist/components/Portal/anchor-position.cjs.map +1 -0
- package/dist/components/Portal/anchor-position.js +99 -0
- package/dist/components/Portal/anchor-position.js.map +1 -0
- package/dist/components/Portal/escape-layer.cjs +2 -0
- package/dist/components/Portal/escape-layer.cjs.map +1 -0
- package/dist/components/Portal/escape-layer.js +30 -0
- package/dist/components/Portal/escape-layer.js.map +1 -0
- package/dist/components/Portal/tab-bridge.cjs +2 -0
- package/dist/components/Portal/tab-bridge.cjs.map +1 -0
- package/dist/components/Portal/tab-bridge.js +39 -0
- package/dist/components/Portal/tab-bridge.js.map +1 -0
- package/dist/components/Tooltip/Tooltip.cjs +1 -1
- package/dist/components/Tooltip/Tooltip.cjs.map +1 -1
- package/dist/components/Tooltip/Tooltip.js +43 -29
- package/dist/components/Tooltip/Tooltip.js.map +1 -1
- package/dist/components/Tooltip/Tooltip.module.cjs +1 -1
- package/dist/components/Tooltip/Tooltip.module.cjs.map +1 -1
- package/dist/components/Tooltip/Tooltip.module.js +4 -3
- package/dist/components/Tooltip/Tooltip.module.js.map +1 -1
- package/dist/hooks/use-focus-trap.cjs +1 -1
- package/dist/hooks/use-focus-trap.cjs.map +1 -1
- package/dist/hooks/use-focus-trap.js +2 -2
- package/dist/hooks/use-focus-trap.js.map +1 -1
- package/dist/hooks/use-sortable.cjs +1 -1
- package/dist/hooks/use-sortable.cjs.map +1 -1
- package/dist/hooks/use-sortable.js +3 -3
- package/dist/hooks/use-sortable.js.map +1 -1
- package/dist/http/describe-api-error.cjs +1 -1
- package/dist/http/describe-api-error.cjs.map +1 -1
- package/dist/http/describe-api-error.js +4 -2
- package/dist/http/describe-api-error.js.map +1 -1
- package/dist/styles/actions.css +3 -0
- package/dist/styles/advanced.css +1 -0
- package/dist/styles/component/Combobox.css +1 -0
- package/dist/styles/component/DropdownMenu.css +1 -0
- package/dist/styles/component/HoverCard.css +1 -0
- package/dist/styles/component/Menubar.css +1 -0
- package/dist/styles/component/MultiSelect.css +1 -0
- package/dist/styles/component/NavigationMenu.css +1 -0
- package/dist/styles/component/Popover.css +1 -0
- package/dist/styles/component/Tooltip.css +1 -0
- package/dist/styles/forms.css +2 -0
- package/dist/styles/navigation.css +2 -0
- package/dist/styles.css +1 -1
- package/dist/tempest-react-sdk.cjs +1 -1
- package/dist/tempest-react-sdk.d.ts +147 -13
- package/dist/tempest-react-sdk.js +134 -134
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MultiSelect.cjs","names":[],"sources":["../../../src/components/MultiSelect/MultiSelect.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — options/value/onChange\n * plus the shared field contract, and the three knobs that make it multi: maxItems,\n * filter, emptyMessage. The body is the listbox with chip removal, type-ahead and\n * the ARIA active-descendant wiring reading one active index.\n */\nimport { useCallback, useEffect, useId, useMemo, useRef, useState } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./MultiSelect.module.css\";\n\nexport interface MultiSelectOption {\n value: string;\n label: string;\n disabled?: boolean;\n}\n\nexport interface MultiSelectProps {\n options: MultiSelectOption[];\n /** Currently selected values. */\n value: string[];\n onChange: (value: string[]) => void;\n label?: string;\n placeholder?: string;\n helperText?: string;\n error?: string;\n disabled?: boolean;\n /** Cap the number of selectable items. */\n maxItems?: number;\n /** Custom filter — return true to keep the option. Default: case-insensitive substring on label. */\n filter?: (option: MultiSelectOption, query: string) => boolean;\n /** Message shown when no option matches. */\n emptyMessage?: string;\n className?: string;\n}\n\nfunction defaultFilter(option: MultiSelectOption, query: string): boolean {\n if (!query) return true;\n return option.label.toLowerCase().includes(query.toLowerCase());\n}\n\n/**\n * MultiSelect — a filterable dropdown that selects many options, shown as\n * removable chips inside the field. Selecting toggles a value; Backspace on an\n * empty query removes the last chip.\n *\n * Keyboard: ArrowUp/ArrowDown navigate, Enter toggles the active option, Esc\n * closes, Backspace (empty input) pops the last chip.\n */\nexport function MultiSelect({\n options,\n value,\n onChange,\n label,\n placeholder = \"Selecione\",\n helperText,\n error,\n disabled,\n maxItems,\n filter = defaultFilter,\n emptyMessage = \"Nenhuma opção encontrada\",\n className,\n}: MultiSelectProps) {\n const id = useId();\n const rootRef = useRef<HTMLDivElement>(null);\n const inputRef = useRef<HTMLInputElement>(null);\n const [open, setOpen] = useState(false);\n const [query, setQuery] = useState(\"\");\n const [activeIndex, setActiveIndex] = useState(0);\n\n const selectedSet = useMemo(() => new Set(value), [value]);\n const selectedOptions = useMemo(\n () =>\n value\n .map((v) => options.find((o) => o.value === v))\n .filter((o): o is MultiSelectOption => Boolean(o)),\n [value, options],\n );\n const filtered = useMemo(\n () => options.filter((o) => filter(o, query)),\n [options, filter, query],\n );\n\n const atMax = maxItems !== undefined && value.length >= maxItems;\n\n const toggle = useCallback(\n (option: MultiSelectOption): void => {\n if (option.disabled) return;\n if (selectedSet.has(option.value)) {\n onChange(value.filter((v) => v !== option.value));\n } else {\n if (atMax) return;\n onChange([...value, option.value]);\n }\n setQuery(\"\");\n inputRef.current?.focus();\n },\n [selectedSet, onChange, value, atMax],\n );\n\n const removeAt = useCallback(\n (optionValue: string): void => {\n onChange(value.filter((v) => v !== optionValue));\n },\n [onChange, value],\n );\n\n useEffect(() => {\n if (!open) return;\n const onDown = (event: MouseEvent): void => {\n if (rootRef.current && !rootRef.current.contains(event.target as Node)) {\n setOpen(false);\n setQuery(\"\");\n }\n };\n window.addEventListener(\"mousedown\", onDown);\n return () => window.removeEventListener(\"mousedown\", onDown);\n }, [open]);\n\n const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>): void => {\n if (event.key === \"ArrowDown\") {\n event.preventDefault();\n setOpen(true);\n setActiveIndex((current) => Math.min(filtered.length - 1, current + 1));\n } else if (event.key === \"ArrowUp\") {\n event.preventDefault();\n setActiveIndex((current) => Math.max(0, current - 1));\n } else if (event.key === \"Enter\") {\n event.preventDefault();\n const option = filtered[activeIndex];\n if (option) toggle(option);\n } else if (event.key === \"Escape\") {\n setOpen(false);\n setQuery(\"\");\n } else if (event.key === \"Backspace\" && query === \"\" && value.length > 0) {\n removeAt(value[value.length - 1]);\n }\n };\n\n return (\n <div ref={rootRef} className={cn(styles.wrapper, error && styles.error, className)}>\n {label && (\n <label htmlFor={id} className={styles.label}>\n {label}\n </label>\n )}\n <div\n className={cn(styles.field, disabled && styles.disabled)}\n onMouseDown={(event) => {\n if (disabled) return;\n // Keep focus on the input when clicking empty field space.\n if (event.target === event.currentTarget) {\n event.preventDefault();\n inputRef.current?.focus();\n setOpen(true);\n }\n }}\n >\n {selectedOptions.map((option) => (\n <span key={option.value} className={styles.chip}>\n {option.label}\n <button\n type=\"button\"\n className={styles.chipRemove}\n aria-label={`Remover ${option.label}`}\n disabled={disabled}\n onMouseDown={(event) => {\n event.preventDefault();\n removeAt(option.value);\n }}\n >\n ×\n </button>\n </span>\n ))}\n <input\n id={id}\n ref={inputRef}\n type=\"text\"\n role=\"combobox\"\n aria-expanded={open}\n aria-controls={`${id}-listbox`}\n aria-autocomplete=\"list\"\n className={styles.input}\n placeholder={value.length === 0 ? placeholder : \"\"}\n disabled={disabled}\n value={query}\n onFocus={() => setOpen(true)}\n onChange={(event) => {\n setQuery(event.target.value);\n setOpen(true);\n setActiveIndex(0);\n }}\n onKeyDown={handleKeyDown}\n />\n </div>\n {open && (\n <ul\n id={`${id}-listbox`}\n role=\"listbox\"\n aria-multiselectable\n className={styles.menu}\n >\n {filtered.length === 0 ? (\n <li className={styles.empty}>{emptyMessage}</li>\n ) : (\n filtered.map((option, index) => {\n const isSelected = selectedSet.has(option.value);\n return (\n <li\n key={option.value}\n role=\"option\"\n aria-selected={isSelected}\n aria-disabled={option.disabled || (!isSelected && atMax)}\n className={cn(\n styles.option,\n index === activeIndex && styles.active,\n isSelected && styles.selected,\n )}\n onMouseEnter={() => setActiveIndex(index)}\n onMouseDown={(event) => {\n event.preventDefault();\n toggle(option);\n }}\n >\n <span className={styles.check} aria-hidden>\n {isSelected ? \"✓\" : \"\"}\n </span>\n {option.label}\n </li>\n );\n })\n )}\n </ul>\n )}\n {error ? (\n <span className={styles.errorText}>{error}</span>\n ) : helperText ? (\n <span className={styles.helper}>{helperText}</span>\n ) : null}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"MultiSelect.cjs","names":[],"sources":["../../../src/components/MultiSelect/MultiSelect.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — options/value/onChange\n * plus the shared field contract, the knobs that make it multi (maxItems, filter,\n * emptyMessage) and portal. The body is the listbox with chip removal, type-ahead and\n * the ARIA active-descendant wiring reading one active index.\n */\nimport { useCallback, useEffect, useId, useMemo, useRef, useState } from \"react\";\nimport { Portal } from \"@/components/Portal\";\nimport { useAnchorPosition } from \"@/components/Portal/anchor-position\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./MultiSelect.module.css\";\n\nexport interface MultiSelectOption {\n value: string;\n label: string;\n disabled?: boolean;\n}\n\nexport interface MultiSelectProps {\n options: MultiSelectOption[];\n /** Currently selected values. */\n value: string[];\n onChange: (value: string[]) => void;\n label?: string;\n placeholder?: string;\n helperText?: string;\n error?: string;\n disabled?: boolean;\n /** Cap the number of selectable items. */\n maxItems?: number;\n /** Custom filter — return true to keep the option. Default: case-insensitive substring on label. */\n filter?: (option: MultiSelectOption, query: string) => boolean;\n /** Message shown when no option matches. */\n emptyMessage?: string;\n /**\n * Render the list in a portal, positioned under the field. Default `true`.\n *\n * In flow, an ancestor with `overflow` other than `visible` clips the list —\n * in the last row of a `DataTable` none of it was visible (measured in Chrome\n * at 1440 px). In a portal it escapes the clip, takes the field's width,\n * flips above the field when there is no room below, and follows it on\n * scroll, resize and when the field grows a line of chips.\n */\n portal?: boolean;\n className?: string;\n}\n\nfunction defaultFilter(option: MultiSelectOption, query: string): boolean {\n if (!query) return true;\n return option.label.toLowerCase().includes(query.toLowerCase());\n}\n\n/** Gap between field and list, in px — the in-flow CSS uses the same 4 px. */\nconst LIST_OFFSET = 4;\n\n/**\n * MultiSelect — a filterable dropdown that selects many options, shown as\n * removable chips inside the field. Selecting toggles a value; Backspace on an\n * empty query removes the last chip.\n *\n * Keyboard: ArrowUp/ArrowDown navigate, Enter toggles the active option, Esc\n * closes, Backspace (empty input) pops the last chip.\n */\nexport function MultiSelect({\n options,\n value,\n onChange,\n label,\n placeholder = \"Selecione\",\n helperText,\n error,\n disabled,\n maxItems,\n filter = defaultFilter,\n emptyMessage = \"Nenhuma opção encontrada\",\n portal = true,\n className,\n}: MultiSelectProps) {\n const id = useId();\n const rootRef = useRef<HTMLDivElement>(null);\n const inputRef = useRef<HTMLInputElement>(null);\n const [open, setOpen] = useState(false);\n const [query, setQuery] = useState(\"\");\n const [activeIndex, setActiveIndex] = useState(0);\n const [fieldNode, setFieldNode] = useState<HTMLDivElement | null>(null);\n const [listNode, setListNode] = useState<HTMLUListElement | null>(null);\n const floatingStyle = useAnchorPosition({\n anchor: fieldNode,\n floating: listNode,\n side: \"bottom\",\n align: \"start\",\n offset: LIST_OFFSET,\n enabled: portal && open,\n matchWidth: true,\n });\n\n const selectedSet = useMemo(() => new Set(value), [value]);\n const selectedOptions = useMemo(\n () =>\n value\n .map((v) => options.find((o) => o.value === v))\n .filter((o): o is MultiSelectOption => Boolean(o)),\n [value, options],\n );\n const filtered = useMemo(\n () => options.filter((o) => filter(o, query)),\n [options, filter, query],\n );\n\n const atMax = maxItems !== undefined && value.length >= maxItems;\n\n const toggle = useCallback(\n (option: MultiSelectOption): void => {\n if (option.disabled) return;\n if (selectedSet.has(option.value)) {\n onChange(value.filter((v) => v !== option.value));\n } else {\n if (atMax) return;\n onChange([...value, option.value]);\n }\n setQuery(\"\");\n inputRef.current?.focus();\n },\n [selectedSet, onChange, value, atMax],\n );\n\n const removeAt = useCallback(\n (optionValue: string): void => {\n onChange(value.filter((v) => v !== optionValue));\n },\n [onChange, value],\n );\n\n /**\n * Close on a press outside both the field and the list.\n *\n * The list is checked on its own because in a portal it is not inside the\n * field's wrapper: a press on its scrollbar, or on the empty message, would\n * otherwise count as outside and close it.\n */\n useEffect(() => {\n if (!open) return;\n const onDown = (event: MouseEvent): void => {\n const target = event.target as Node;\n if (rootRef.current?.contains(target) || listNode?.contains(target)) return;\n setOpen(false);\n setQuery(\"\");\n };\n window.addEventListener(\"mousedown\", onDown);\n return () => window.removeEventListener(\"mousedown\", onDown);\n }, [open, listNode]);\n\n /**\n * Open the list and keep focus on the input when the empty part of the field\n * is pressed.\n *\n * Only a press on the field itself counts: a press on a chip or on its remove\n * button has its own handler, and the input focuses itself.\n */\n const handleFieldMouseDown = (event: React.MouseEvent<HTMLDivElement>): void => {\n if (disabled || event.target !== event.currentTarget) return;\n event.preventDefault();\n inputRef.current?.focus();\n setOpen(true);\n };\n\n /**\n * Keyboard model of the input.\n *\n * `Escape` calls `preventDefault()` only while the list is open: that marks the\n * key as consumed, so an enclosing `Modal` or `Drawer` stays open and the next\n * `Escape` reaches it. With the list closed the key is left alone and dismisses\n * the enclosing layer, as the user expects.\n */\n const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>): void => {\n if (event.key === \"ArrowDown\") {\n event.preventDefault();\n setOpen(true);\n setActiveIndex((current) => Math.min(filtered.length - 1, current + 1));\n } else if (event.key === \"ArrowUp\") {\n event.preventDefault();\n setActiveIndex((current) => Math.max(0, current - 1));\n } else if (event.key === \"Enter\") {\n event.preventDefault();\n const option = filtered[activeIndex];\n if (option) toggle(option);\n } else if (event.key === \"Escape\") {\n if (open) event.preventDefault();\n setOpen(false);\n setQuery(\"\");\n } else if (event.key === \"Backspace\" && query === \"\" && value.length > 0) {\n removeAt(value[value.length - 1]);\n }\n };\n\n const list = (\n <ul\n ref={setListNode}\n id={`${id}-listbox`}\n role=\"listbox\"\n aria-multiselectable\n className={cn(styles.menu, portal && styles.portalled)}\n style={floatingStyle}\n >\n {filtered.length === 0 ? (\n <li className={styles.empty}>{emptyMessage}</li>\n ) : (\n filtered.map((option, index) => {\n const isSelected = selectedSet.has(option.value);\n return (\n <li\n key={option.value}\n role=\"option\"\n aria-selected={isSelected}\n aria-disabled={option.disabled || (!isSelected && atMax)}\n className={cn(\n styles.option,\n index === activeIndex && styles.active,\n isSelected && styles.selected,\n )}\n onMouseEnter={() => setActiveIndex(index)}\n onMouseDown={(event) => {\n event.preventDefault();\n toggle(option);\n }}\n >\n <span className={styles.check} aria-hidden>\n {isSelected ? \"✓\" : \"\"}\n </span>\n {option.label}\n </li>\n );\n })\n )}\n </ul>\n );\n\n return (\n <div ref={rootRef} className={cn(styles.wrapper, error && styles.error, className)}>\n {label && (\n <label htmlFor={id} className={styles.label}>\n {label}\n </label>\n )}\n <div\n ref={setFieldNode}\n className={cn(styles.field, disabled && styles.disabled)}\n onMouseDown={handleFieldMouseDown}\n >\n {selectedOptions.map((option) => (\n <span key={option.value} className={styles.chip}>\n {option.label}\n <button\n type=\"button\"\n className={styles.chipRemove}\n aria-label={`Remover ${option.label}`}\n disabled={disabled}\n onMouseDown={(event) => {\n event.preventDefault();\n removeAt(option.value);\n }}\n >\n ×\n </button>\n </span>\n ))}\n <input\n id={id}\n ref={inputRef}\n type=\"text\"\n role=\"combobox\"\n aria-expanded={open}\n aria-controls={`${id}-listbox`}\n aria-autocomplete=\"list\"\n className={styles.input}\n placeholder={value.length === 0 ? placeholder : \"\"}\n disabled={disabled}\n value={query}\n onFocus={() => setOpen(true)}\n onChange={(event) => {\n setQuery(event.target.value);\n setOpen(true);\n setActiveIndex(0);\n }}\n onKeyDown={handleKeyDown}\n />\n </div>\n {open && (portal ? <Portal>{list}</Portal> : list)}\n {error ? (\n <span className={styles.errorText}>{error}</span>\n ) : helperText ? (\n <span className={styles.helper}>{helperText}</span>\n ) : null}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1,130 +1,147 @@
|
|
|
1
1
|
import { cn as e } from "../../utils/cn.js";
|
|
2
|
-
import t from "
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
2
|
+
import { Portal as t } from "../Portal/Portal.js";
|
|
3
|
+
import { useAnchorPosition as n } from "../Portal/anchor-position.js";
|
|
4
|
+
import r from "./MultiSelect.module.js";
|
|
5
|
+
import { useCallback as i, useEffect as a, useId as o, useMemo as s, useRef as c, useState as l } from "react";
|
|
6
|
+
import { jsx as u, jsxs as d } from "react/jsx-runtime";
|
|
5
7
|
//#region src/components/MultiSelect/MultiSelect.tsx
|
|
6
|
-
function
|
|
8
|
+
function f(e, t) {
|
|
7
9
|
return !t || e.label.toLowerCase().includes(t.toLowerCase());
|
|
8
10
|
}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
var p = 4;
|
|
12
|
+
function m({ options: m, value: h, onChange: g, label: _, placeholder: v = "Selecione", helperText: y, error: b, disabled: x, maxItems: S, filter: C = f, emptyMessage: w = "Nenhuma opção encontrada", portal: T = !0, className: E }) {
|
|
13
|
+
let D = o(), O = c(null), k = c(null), [A, j] = l(!1), [M, N] = l(""), [P, F] = l(0), [I, L] = l(null), [R, z] = l(null), B = n({
|
|
14
|
+
anchor: I,
|
|
15
|
+
floating: R,
|
|
16
|
+
side: "bottom",
|
|
17
|
+
align: "start",
|
|
18
|
+
offset: p,
|
|
19
|
+
enabled: T && A,
|
|
20
|
+
matchWidth: !0
|
|
21
|
+
}), V = s(() => new Set(h), [h]), H = s(() => h.map((e) => m.find((t) => t.value === e)).filter((e) => !!e), [h, m]), U = s(() => m.filter((e) => C(e, M)), [
|
|
22
|
+
m,
|
|
23
|
+
C,
|
|
24
|
+
M
|
|
25
|
+
]), W = S !== void 0 && h.length >= S, G = i((e) => {
|
|
15
26
|
if (!e.disabled) {
|
|
16
|
-
if (
|
|
27
|
+
if (V.has(e.value)) g(h.filter((t) => t !== e.value));
|
|
17
28
|
else {
|
|
18
|
-
if (
|
|
19
|
-
|
|
29
|
+
if (W) return;
|
|
30
|
+
g([...h, e.value]);
|
|
20
31
|
}
|
|
21
|
-
|
|
32
|
+
N(""), k.current?.focus();
|
|
22
33
|
}
|
|
23
34
|
}, [
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
]),
|
|
29
|
-
|
|
30
|
-
}, [
|
|
31
|
-
|
|
32
|
-
if (!
|
|
35
|
+
V,
|
|
36
|
+
g,
|
|
37
|
+
h,
|
|
38
|
+
W
|
|
39
|
+
]), K = i((e) => {
|
|
40
|
+
g(h.filter((t) => t !== e));
|
|
41
|
+
}, [g, h]);
|
|
42
|
+
a(() => {
|
|
43
|
+
if (!A) return;
|
|
33
44
|
let e = (e) => {
|
|
34
|
-
|
|
45
|
+
let t = e.target;
|
|
46
|
+
O.current?.contains(t) || R?.contains(t) || (j(!1), N(""));
|
|
35
47
|
};
|
|
36
48
|
return window.addEventListener("mousedown", e), () => window.removeEventListener("mousedown", e);
|
|
37
|
-
}, [
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
49
|
+
}, [A, R]);
|
|
50
|
+
let q = (e) => {
|
|
51
|
+
x || e.target !== e.currentTarget || (e.preventDefault(), k.current?.focus(), j(!0));
|
|
52
|
+
}, J = (e) => {
|
|
53
|
+
if (e.key === "ArrowDown") e.preventDefault(), j(!0), F((e) => Math.min(U.length - 1, e + 1));
|
|
54
|
+
else if (e.key === "ArrowUp") e.preventDefault(), F((e) => Math.max(0, e - 1));
|
|
55
|
+
else if (e.key === "Enter") {
|
|
56
|
+
e.preventDefault();
|
|
57
|
+
let t = U[P];
|
|
58
|
+
t && G(t);
|
|
59
|
+
} else e.key === "Escape" ? (A && e.preventDefault(), j(!1), N("")) : e.key === "Backspace" && M === "" && h.length > 0 && K(h[h.length - 1]);
|
|
60
|
+
}, Y = /* @__PURE__ */ u("ul", {
|
|
61
|
+
ref: z,
|
|
62
|
+
id: `${D}-listbox`,
|
|
63
|
+
role: "listbox",
|
|
64
|
+
"aria-multiselectable": !0,
|
|
65
|
+
className: e(r.menu, T && r.portalled),
|
|
66
|
+
style: B,
|
|
67
|
+
children: U.length === 0 ? /* @__PURE__ */ u("li", {
|
|
68
|
+
className: r.empty,
|
|
69
|
+
children: w
|
|
70
|
+
}) : U.map((t, n) => {
|
|
71
|
+
let i = V.has(t.value);
|
|
72
|
+
return /* @__PURE__ */ d("li", {
|
|
73
|
+
role: "option",
|
|
74
|
+
"aria-selected": i,
|
|
75
|
+
"aria-disabled": t.disabled || !i && W,
|
|
76
|
+
className: e(r.option, n === P && r.active, i && r.selected),
|
|
77
|
+
onMouseEnter: () => F(n),
|
|
48
78
|
onMouseDown: (e) => {
|
|
49
|
-
|
|
79
|
+
e.preventDefault(), G(t);
|
|
50
80
|
},
|
|
51
|
-
children: [
|
|
52
|
-
className:
|
|
53
|
-
|
|
81
|
+
children: [/* @__PURE__ */ u("span", {
|
|
82
|
+
className: r.check,
|
|
83
|
+
"aria-hidden": !0,
|
|
84
|
+
children: i ? "✓" : ""
|
|
85
|
+
}), t.label]
|
|
86
|
+
}, t.value);
|
|
87
|
+
})
|
|
88
|
+
});
|
|
89
|
+
return /* @__PURE__ */ d("div", {
|
|
90
|
+
ref: O,
|
|
91
|
+
className: e(r.wrapper, b && r.error, E),
|
|
92
|
+
children: [
|
|
93
|
+
_ && /* @__PURE__ */ u("label", {
|
|
94
|
+
htmlFor: D,
|
|
95
|
+
className: r.label,
|
|
96
|
+
children: _
|
|
97
|
+
}),
|
|
98
|
+
/* @__PURE__ */ d("div", {
|
|
99
|
+
ref: L,
|
|
100
|
+
className: e(r.field, x && r.disabled),
|
|
101
|
+
onMouseDown: q,
|
|
102
|
+
children: [H.map((e) => /* @__PURE__ */ d("span", {
|
|
103
|
+
className: r.chip,
|
|
104
|
+
children: [e.label, /* @__PURE__ */ u("button", {
|
|
54
105
|
type: "button",
|
|
55
|
-
className:
|
|
106
|
+
className: r.chipRemove,
|
|
56
107
|
"aria-label": `Remover ${e.label}`,
|
|
57
|
-
disabled:
|
|
108
|
+
disabled: x,
|
|
58
109
|
onMouseDown: (t) => {
|
|
59
|
-
t.preventDefault(),
|
|
110
|
+
t.preventDefault(), K(e.value);
|
|
60
111
|
},
|
|
61
112
|
children: "×"
|
|
62
113
|
})]
|
|
63
|
-
}, e.value)), /* @__PURE__ */
|
|
64
|
-
id:
|
|
65
|
-
ref:
|
|
114
|
+
}, e.value)), /* @__PURE__ */ u("input", {
|
|
115
|
+
id: D,
|
|
116
|
+
ref: k,
|
|
66
117
|
type: "text",
|
|
67
118
|
role: "combobox",
|
|
68
|
-
"aria-expanded":
|
|
69
|
-
"aria-controls": `${
|
|
119
|
+
"aria-expanded": A,
|
|
120
|
+
"aria-controls": `${D}-listbox`,
|
|
70
121
|
"aria-autocomplete": "list",
|
|
71
|
-
className:
|
|
72
|
-
placeholder:
|
|
73
|
-
disabled:
|
|
74
|
-
value:
|
|
75
|
-
onFocus: () =>
|
|
122
|
+
className: r.input,
|
|
123
|
+
placeholder: h.length === 0 ? v : "",
|
|
124
|
+
disabled: x,
|
|
125
|
+
value: M,
|
|
126
|
+
onFocus: () => j(!0),
|
|
76
127
|
onChange: (e) => {
|
|
77
|
-
|
|
128
|
+
N(e.target.value), j(!0), F(0);
|
|
78
129
|
},
|
|
79
|
-
onKeyDown:
|
|
80
|
-
if (e.key === "ArrowDown") e.preventDefault(), D(!0), j((e) => Math.min(P.length - 1, e + 1));
|
|
81
|
-
else if (e.key === "ArrowUp") e.preventDefault(), j((e) => Math.max(0, e - 1));
|
|
82
|
-
else if (e.key === "Enter") {
|
|
83
|
-
e.preventDefault();
|
|
84
|
-
let t = P[A];
|
|
85
|
-
t && I(t);
|
|
86
|
-
} else e.key === "Escape" ? (D(!1), k("")) : e.key === "Backspace" && O === "" && f.length > 0 && L(f[f.length - 1]);
|
|
87
|
-
}
|
|
130
|
+
onKeyDown: J
|
|
88
131
|
})]
|
|
89
132
|
}),
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
children: x
|
|
98
|
-
}) : P.map((n, r) => {
|
|
99
|
-
let i = M.has(n.value);
|
|
100
|
-
return /* @__PURE__ */ l("li", {
|
|
101
|
-
role: "option",
|
|
102
|
-
"aria-selected": i,
|
|
103
|
-
"aria-disabled": n.disabled || !i && F,
|
|
104
|
-
className: e(t.option, r === A && t.active, i && t.selected),
|
|
105
|
-
onMouseEnter: () => j(r),
|
|
106
|
-
onMouseDown: (e) => {
|
|
107
|
-
e.preventDefault(), I(n);
|
|
108
|
-
},
|
|
109
|
-
children: [/* @__PURE__ */ c("span", {
|
|
110
|
-
className: t.check,
|
|
111
|
-
"aria-hidden": !0,
|
|
112
|
-
children: i ? "✓" : ""
|
|
113
|
-
}), n.label]
|
|
114
|
-
}, n.value);
|
|
115
|
-
})
|
|
116
|
-
}),
|
|
117
|
-
_ ? /* @__PURE__ */ c("span", {
|
|
118
|
-
className: t.errorText,
|
|
119
|
-
children: _
|
|
120
|
-
}) : g ? /* @__PURE__ */ c("span", {
|
|
121
|
-
className: t.helper,
|
|
122
|
-
children: g
|
|
133
|
+
A && (T ? /* @__PURE__ */ u(t, { children: Y }) : Y),
|
|
134
|
+
b ? /* @__PURE__ */ u("span", {
|
|
135
|
+
className: r.errorText,
|
|
136
|
+
children: b
|
|
137
|
+
}) : y ? /* @__PURE__ */ u("span", {
|
|
138
|
+
className: r.helper,
|
|
139
|
+
children: y
|
|
123
140
|
}) : null
|
|
124
141
|
]
|
|
125
142
|
});
|
|
126
143
|
}
|
|
127
144
|
//#endregion
|
|
128
|
-
export {
|
|
145
|
+
export { m as MultiSelect };
|
|
129
146
|
|
|
130
147
|
//# sourceMappingURL=MultiSelect.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MultiSelect.js","names":[],"sources":["../../../src/components/MultiSelect/MultiSelect.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — options/value/onChange\n * plus the shared field contract, and the three knobs that make it multi: maxItems,\n * filter, emptyMessage. The body is the listbox with chip removal, type-ahead and\n * the ARIA active-descendant wiring reading one active index.\n */\nimport { useCallback, useEffect, useId, useMemo, useRef, useState } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./MultiSelect.module.css\";\n\nexport interface MultiSelectOption {\n value: string;\n label: string;\n disabled?: boolean;\n}\n\nexport interface MultiSelectProps {\n options: MultiSelectOption[];\n /** Currently selected values. */\n value: string[];\n onChange: (value: string[]) => void;\n label?: string;\n placeholder?: string;\n helperText?: string;\n error?: string;\n disabled?: boolean;\n /** Cap the number of selectable items. */\n maxItems?: number;\n /** Custom filter — return true to keep the option. Default: case-insensitive substring on label. */\n filter?: (option: MultiSelectOption, query: string) => boolean;\n /** Message shown when no option matches. */\n emptyMessage?: string;\n className?: string;\n}\n\nfunction defaultFilter(option: MultiSelectOption, query: string): boolean {\n if (!query) return true;\n return option.label.toLowerCase().includes(query.toLowerCase());\n}\n\n/**\n * MultiSelect — a filterable dropdown that selects many options, shown as\n * removable chips inside the field. Selecting toggles a value; Backspace on an\n * empty query removes the last chip.\n *\n * Keyboard: ArrowUp/ArrowDown navigate, Enter toggles the active option, Esc\n * closes, Backspace (empty input) pops the last chip.\n */\nexport function MultiSelect({\n options,\n value,\n onChange,\n label,\n placeholder = \"Selecione\",\n helperText,\n error,\n disabled,\n maxItems,\n filter = defaultFilter,\n emptyMessage = \"Nenhuma opção encontrada\",\n className,\n}: MultiSelectProps) {\n const id = useId();\n const rootRef = useRef<HTMLDivElement>(null);\n const inputRef = useRef<HTMLInputElement>(null);\n const [open, setOpen] = useState(false);\n const [query, setQuery] = useState(\"\");\n const [activeIndex, setActiveIndex] = useState(0);\n\n const selectedSet = useMemo(() => new Set(value), [value]);\n const selectedOptions = useMemo(\n () =>\n value\n .map((v) => options.find((o) => o.value === v))\n .filter((o): o is MultiSelectOption => Boolean(o)),\n [value, options],\n );\n const filtered = useMemo(\n () => options.filter((o) => filter(o, query)),\n [options, filter, query],\n );\n\n const atMax = maxItems !== undefined && value.length >= maxItems;\n\n const toggle = useCallback(\n (option: MultiSelectOption): void => {\n if (option.disabled) return;\n if (selectedSet.has(option.value)) {\n onChange(value.filter((v) => v !== option.value));\n } else {\n if (atMax) return;\n onChange([...value, option.value]);\n }\n setQuery(\"\");\n inputRef.current?.focus();\n },\n [selectedSet, onChange, value, atMax],\n );\n\n const removeAt = useCallback(\n (optionValue: string): void => {\n onChange(value.filter((v) => v !== optionValue));\n },\n [onChange, value],\n );\n\n useEffect(() => {\n if (!open) return;\n const onDown = (event: MouseEvent): void => {\n if (rootRef.current && !rootRef.current.contains(event.target as Node)) {\n setOpen(false);\n setQuery(\"\");\n }\n };\n window.addEventListener(\"mousedown\", onDown);\n return () => window.removeEventListener(\"mousedown\", onDown);\n }, [open]);\n\n const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>): void => {\n if (event.key === \"ArrowDown\") {\n event.preventDefault();\n setOpen(true);\n setActiveIndex((current) => Math.min(filtered.length - 1, current + 1));\n } else if (event.key === \"ArrowUp\") {\n event.preventDefault();\n setActiveIndex((current) => Math.max(0, current - 1));\n } else if (event.key === \"Enter\") {\n event.preventDefault();\n const option = filtered[activeIndex];\n if (option) toggle(option);\n } else if (event.key === \"Escape\") {\n setOpen(false);\n setQuery(\"\");\n } else if (event.key === \"Backspace\" && query === \"\" && value.length > 0) {\n removeAt(value[value.length - 1]);\n }\n };\n\n return (\n <div ref={rootRef} className={cn(styles.wrapper, error && styles.error, className)}>\n {label && (\n <label htmlFor={id} className={styles.label}>\n {label}\n </label>\n )}\n <div\n className={cn(styles.field, disabled && styles.disabled)}\n onMouseDown={(event) => {\n if (disabled) return;\n // Keep focus on the input when clicking empty field space.\n if (event.target === event.currentTarget) {\n event.preventDefault();\n inputRef.current?.focus();\n setOpen(true);\n }\n }}\n >\n {selectedOptions.map((option) => (\n <span key={option.value} className={styles.chip}>\n {option.label}\n <button\n type=\"button\"\n className={styles.chipRemove}\n aria-label={`Remover ${option.label}`}\n disabled={disabled}\n onMouseDown={(event) => {\n event.preventDefault();\n removeAt(option.value);\n }}\n >\n ×\n </button>\n </span>\n ))}\n <input\n id={id}\n ref={inputRef}\n type=\"text\"\n role=\"combobox\"\n aria-expanded={open}\n aria-controls={`${id}-listbox`}\n aria-autocomplete=\"list\"\n className={styles.input}\n placeholder={value.length === 0 ? placeholder : \"\"}\n disabled={disabled}\n value={query}\n onFocus={() => setOpen(true)}\n onChange={(event) => {\n setQuery(event.target.value);\n setOpen(true);\n setActiveIndex(0);\n }}\n onKeyDown={handleKeyDown}\n />\n </div>\n {open && (\n <ul\n id={`${id}-listbox`}\n role=\"listbox\"\n aria-multiselectable\n className={styles.menu}\n >\n {filtered.length === 0 ? (\n <li className={styles.empty}>{emptyMessage}</li>\n ) : (\n filtered.map((option, index) => {\n const isSelected = selectedSet.has(option.value);\n return (\n <li\n key={option.value}\n role=\"option\"\n aria-selected={isSelected}\n aria-disabled={option.disabled || (!isSelected && atMax)}\n className={cn(\n styles.option,\n index === activeIndex && styles.active,\n isSelected && styles.selected,\n )}\n onMouseEnter={() => setActiveIndex(index)}\n onMouseDown={(event) => {\n event.preventDefault();\n toggle(option);\n }}\n >\n <span className={styles.check} aria-hidden>\n {isSelected ? \"✓\" : \"\"}\n </span>\n {option.label}\n </li>\n );\n })\n )}\n </ul>\n )}\n {error ? (\n <span className={styles.errorText}>{error}</span>\n ) : helperText ? (\n <span className={styles.helper}>{helperText}</span>\n ) : null}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"MultiSelect.js","names":[],"sources":["../../../src/components/MultiSelect/MultiSelect.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — options/value/onChange\n * plus the shared field contract, the knobs that make it multi (maxItems, filter,\n * emptyMessage) and portal. The body is the listbox with chip removal, type-ahead and\n * the ARIA active-descendant wiring reading one active index.\n */\nimport { useCallback, useEffect, useId, useMemo, useRef, useState } from \"react\";\nimport { Portal } from \"@/components/Portal\";\nimport { useAnchorPosition } from \"@/components/Portal/anchor-position\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./MultiSelect.module.css\";\n\nexport interface MultiSelectOption {\n value: string;\n label: string;\n disabled?: boolean;\n}\n\nexport interface MultiSelectProps {\n options: MultiSelectOption[];\n /** Currently selected values. */\n value: string[];\n onChange: (value: string[]) => void;\n label?: string;\n placeholder?: string;\n helperText?: string;\n error?: string;\n disabled?: boolean;\n /** Cap the number of selectable items. */\n maxItems?: number;\n /** Custom filter — return true to keep the option. Default: case-insensitive substring on label. */\n filter?: (option: MultiSelectOption, query: string) => boolean;\n /** Message shown when no option matches. */\n emptyMessage?: string;\n /**\n * Render the list in a portal, positioned under the field. Default `true`.\n *\n * In flow, an ancestor with `overflow` other than `visible` clips the list —\n * in the last row of a `DataTable` none of it was visible (measured in Chrome\n * at 1440 px). In a portal it escapes the clip, takes the field's width,\n * flips above the field when there is no room below, and follows it on\n * scroll, resize and when the field grows a line of chips.\n */\n portal?: boolean;\n className?: string;\n}\n\nfunction defaultFilter(option: MultiSelectOption, query: string): boolean {\n if (!query) return true;\n return option.label.toLowerCase().includes(query.toLowerCase());\n}\n\n/** Gap between field and list, in px — the in-flow CSS uses the same 4 px. */\nconst LIST_OFFSET = 4;\n\n/**\n * MultiSelect — a filterable dropdown that selects many options, shown as\n * removable chips inside the field. Selecting toggles a value; Backspace on an\n * empty query removes the last chip.\n *\n * Keyboard: ArrowUp/ArrowDown navigate, Enter toggles the active option, Esc\n * closes, Backspace (empty input) pops the last chip.\n */\nexport function MultiSelect({\n options,\n value,\n onChange,\n label,\n placeholder = \"Selecione\",\n helperText,\n error,\n disabled,\n maxItems,\n filter = defaultFilter,\n emptyMessage = \"Nenhuma opção encontrada\",\n portal = true,\n className,\n}: MultiSelectProps) {\n const id = useId();\n const rootRef = useRef<HTMLDivElement>(null);\n const inputRef = useRef<HTMLInputElement>(null);\n const [open, setOpen] = useState(false);\n const [query, setQuery] = useState(\"\");\n const [activeIndex, setActiveIndex] = useState(0);\n const [fieldNode, setFieldNode] = useState<HTMLDivElement | null>(null);\n const [listNode, setListNode] = useState<HTMLUListElement | null>(null);\n const floatingStyle = useAnchorPosition({\n anchor: fieldNode,\n floating: listNode,\n side: \"bottom\",\n align: \"start\",\n offset: LIST_OFFSET,\n enabled: portal && open,\n matchWidth: true,\n });\n\n const selectedSet = useMemo(() => new Set(value), [value]);\n const selectedOptions = useMemo(\n () =>\n value\n .map((v) => options.find((o) => o.value === v))\n .filter((o): o is MultiSelectOption => Boolean(o)),\n [value, options],\n );\n const filtered = useMemo(\n () => options.filter((o) => filter(o, query)),\n [options, filter, query],\n );\n\n const atMax = maxItems !== undefined && value.length >= maxItems;\n\n const toggle = useCallback(\n (option: MultiSelectOption): void => {\n if (option.disabled) return;\n if (selectedSet.has(option.value)) {\n onChange(value.filter((v) => v !== option.value));\n } else {\n if (atMax) return;\n onChange([...value, option.value]);\n }\n setQuery(\"\");\n inputRef.current?.focus();\n },\n [selectedSet, onChange, value, atMax],\n );\n\n const removeAt = useCallback(\n (optionValue: string): void => {\n onChange(value.filter((v) => v !== optionValue));\n },\n [onChange, value],\n );\n\n /**\n * Close on a press outside both the field and the list.\n *\n * The list is checked on its own because in a portal it is not inside the\n * field's wrapper: a press on its scrollbar, or on the empty message, would\n * otherwise count as outside and close it.\n */\n useEffect(() => {\n if (!open) return;\n const onDown = (event: MouseEvent): void => {\n const target = event.target as Node;\n if (rootRef.current?.contains(target) || listNode?.contains(target)) return;\n setOpen(false);\n setQuery(\"\");\n };\n window.addEventListener(\"mousedown\", onDown);\n return () => window.removeEventListener(\"mousedown\", onDown);\n }, [open, listNode]);\n\n /**\n * Open the list and keep focus on the input when the empty part of the field\n * is pressed.\n *\n * Only a press on the field itself counts: a press on a chip or on its remove\n * button has its own handler, and the input focuses itself.\n */\n const handleFieldMouseDown = (event: React.MouseEvent<HTMLDivElement>): void => {\n if (disabled || event.target !== event.currentTarget) return;\n event.preventDefault();\n inputRef.current?.focus();\n setOpen(true);\n };\n\n /**\n * Keyboard model of the input.\n *\n * `Escape` calls `preventDefault()` only while the list is open: that marks the\n * key as consumed, so an enclosing `Modal` or `Drawer` stays open and the next\n * `Escape` reaches it. With the list closed the key is left alone and dismisses\n * the enclosing layer, as the user expects.\n */\n const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>): void => {\n if (event.key === \"ArrowDown\") {\n event.preventDefault();\n setOpen(true);\n setActiveIndex((current) => Math.min(filtered.length - 1, current + 1));\n } else if (event.key === \"ArrowUp\") {\n event.preventDefault();\n setActiveIndex((current) => Math.max(0, current - 1));\n } else if (event.key === \"Enter\") {\n event.preventDefault();\n const option = filtered[activeIndex];\n if (option) toggle(option);\n } else if (event.key === \"Escape\") {\n if (open) event.preventDefault();\n setOpen(false);\n setQuery(\"\");\n } else if (event.key === \"Backspace\" && query === \"\" && value.length > 0) {\n removeAt(value[value.length - 1]);\n }\n };\n\n const list = (\n <ul\n ref={setListNode}\n id={`${id}-listbox`}\n role=\"listbox\"\n aria-multiselectable\n className={cn(styles.menu, portal && styles.portalled)}\n style={floatingStyle}\n >\n {filtered.length === 0 ? (\n <li className={styles.empty}>{emptyMessage}</li>\n ) : (\n filtered.map((option, index) => {\n const isSelected = selectedSet.has(option.value);\n return (\n <li\n key={option.value}\n role=\"option\"\n aria-selected={isSelected}\n aria-disabled={option.disabled || (!isSelected && atMax)}\n className={cn(\n styles.option,\n index === activeIndex && styles.active,\n isSelected && styles.selected,\n )}\n onMouseEnter={() => setActiveIndex(index)}\n onMouseDown={(event) => {\n event.preventDefault();\n toggle(option);\n }}\n >\n <span className={styles.check} aria-hidden>\n {isSelected ? \"✓\" : \"\"}\n </span>\n {option.label}\n </li>\n );\n })\n )}\n </ul>\n );\n\n return (\n <div ref={rootRef} className={cn(styles.wrapper, error && styles.error, className)}>\n {label && (\n <label htmlFor={id} className={styles.label}>\n {label}\n </label>\n )}\n <div\n ref={setFieldNode}\n className={cn(styles.field, disabled && styles.disabled)}\n onMouseDown={handleFieldMouseDown}\n >\n {selectedOptions.map((option) => (\n <span key={option.value} className={styles.chip}>\n {option.label}\n <button\n type=\"button\"\n className={styles.chipRemove}\n aria-label={`Remover ${option.label}`}\n disabled={disabled}\n onMouseDown={(event) => {\n event.preventDefault();\n removeAt(option.value);\n }}\n >\n ×\n </button>\n </span>\n ))}\n <input\n id={id}\n ref={inputRef}\n type=\"text\"\n role=\"combobox\"\n aria-expanded={open}\n aria-controls={`${id}-listbox`}\n aria-autocomplete=\"list\"\n className={styles.input}\n placeholder={value.length === 0 ? placeholder : \"\"}\n disabled={disabled}\n value={query}\n onFocus={() => setOpen(true)}\n onChange={(event) => {\n setQuery(event.target.value);\n setOpen(true);\n setActiveIndex(0);\n }}\n onKeyDown={handleKeyDown}\n />\n </div>\n {open && (portal ? <Portal>{list}</Portal> : list)}\n {error ? (\n <span className={styles.errorText}>{error}</span>\n ) : helperText ? (\n <span className={styles.helper}>{helperText}</span>\n ) : null}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
require("../../styles/component/MultiSelect.css");
|
|
2
|
-
var e=`tempest_wrapper_6xZZr`,t=`tempest_label_Jwj7I`,n=`tempest_field_GpA0V`,r=`tempest_disabled_BIKaG`,i=`tempest_chip_Zk0ch`,a=`tempest_chipRemove_R-mNv`,o=`tempest_input_SjOXU`,s=`tempest_menu_3xnmo`,c=`tempest_option_lqEl1`,l=`tempest_check_aUM1w`,u=`tempest_active_S9sQ-`,d=`tempest_selected_Rw8yr`,f=`tempest_empty_-5B16`,p=`tempest_helper_c7aH3`,m=`tempest_errorText_qZR9s`,h=`tempest_error_DIOTW`,g={wrapper:e,label:t,field:n,disabled:r,chip:i,chipRemove:a,input:o,menu:s,option:c,check:l,active:u,selected:d,empty:f,helper:p,errorText:m,error:h};exports.active=u,exports.check=l,exports.chip=i,exports.chipRemove=a,exports.default=
|
|
2
|
+
var e=`tempest_wrapper_6xZZr`,t=`tempest_label_Jwj7I`,n=`tempest_field_GpA0V`,r=`tempest_disabled_BIKaG`,i=`tempest_chip_Zk0ch`,a=`tempest_chipRemove_R-mNv`,o=`tempest_input_SjOXU`,s=`tempest_menu_3xnmo`,c=`tempest_option_lqEl1`,l=`tempest_check_aUM1w`,u=`tempest_active_S9sQ-`,d=`tempest_selected_Rw8yr`,f=`tempest_empty_-5B16`,p=`tempest_helper_c7aH3`,m=`tempest_errorText_qZR9s`,h=`tempest_error_DIOTW`,g=`tempest_portalled_i1wmW`,_={wrapper:e,label:t,field:n,disabled:r,chip:i,chipRemove:a,input:o,menu:s,option:c,check:l,active:u,selected:d,empty:f,helper:p,errorText:m,error:h,portalled:g};exports.active=u,exports.check=l,exports.chip=i,exports.chipRemove=a,exports.default=_,exports.disabled=r,exports.empty=f,exports.error=h,exports.errorText=m,exports.field=n,exports.helper=p,exports.input=o,exports.label=t,exports.menu=s,exports.option=c,exports.portalled=g,exports.selected=d,exports.wrapper=e;
|
|
3
3
|
//# sourceMappingURL=MultiSelect.module.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MultiSelect.module.cjs","names":[],"sources":["../../../src/components/MultiSelect/MultiSelect.module.css"],"sourcesContent":[".wrapper {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-1);\n width: 100%;\n font-family: var(--tempest-font-sans);\n position: relative;\n}\n\n.label {\n font-size: var(--tempest-text-sm);\n font-weight: var(--tempest-weight-semibold);\n color: var(--tempest-text);\n line-height: var(--tempest-leading-snug);\n}\n\n.field {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--tempest-space-1);\n min-height: var(--tempest-control-height-md);\n padding: 4px var(--tempest-space-2);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-control-radius);\n background-color: var(--tempest-bg);\n box-shadow: var(--tempest-shadow-xs);\n cursor: text;\n transition: var(--tempest-transition-color), var(--tempest-transition-shadow);\n}\n\n.field:focus-within {\n border-color: var(--tempest-primary);\n box-shadow: 0 0 0 var(--tempest-focus-ring-width) var(--tempest-focus-ring-color);\n}\n\n.field.disabled {\n background-color: var(--tempest-surface);\n cursor: not-allowed;\n}\n\n.chip {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n padding: 2px 4px 2px 8px;\n border-radius: var(--tempest-radius-full);\n background-color: var(--tempest-primary-soft);\n color: var(--tempest-primary-on-soft);\n font-size: var(--tempest-text-xs);\n font-weight: var(--tempest-weight-medium);\n line-height: 1.4;\n}\n\n.chipRemove {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 16px;\n height: 16px;\n border: none;\n border-radius: 50%;\n background: transparent;\n color: inherit;\n font-size: 14px;\n line-height: 1;\n cursor: pointer;\n}\n\n.chipRemove:hover {\n background-color: var(--tempest-primary);\n color: var(--tempest-primary-foreground);\n}\n\n.input {\n flex: 1;\n min-width: 80px;\n border: none;\n outline: none;\n background: transparent;\n color: var(--tempest-text);\n font-family: inherit;\n font-size: var(--tempest-control-font-md);\n height: 24px;\n}\n\n.input::placeholder {\n color: var(--tempest-text-subtle);\n}\n\n.menu {\n position: absolute;\n top: calc(100% + 4px);\n left: 0;\n right: 0;\n z-index: var(--tempest-z-dropdown);\n background-color: var(--tempest-bg);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-lg);\n box-shadow: var(--tempest-shadow-lg);\n max-height: 280px;\n overflow-y: auto;\n padding: var(--tempest-space-1);\n list-style: none;\n margin: 0;\n}\n\n.option {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-2);\n padding: var(--tempest-space-2) var(--tempest-space-3);\n border-radius: var(--tempest-radius-sm);\n cursor: pointer;\n color: var(--tempest-text);\n font-size: var(--tempest-text-base);\n line-height: var(--tempest-leading-snug);\n}\n\n.check {\n display: inline-flex;\n width: 16px;\n justify-content: center;\n color: var(--tempest-primary);\n font-weight: var(--tempest-weight-bold);\n}\n\n@media (hover: hover) and (pointer: fine) {\n .option:hover {\n background-color: var(--tempest-surface);\n }\n}\n\n.option.active {\n background-color: var(--tempest-primary-soft);\n color: var(--tempest-primary-on-soft);\n}\n\n.option.selected {\n font-weight: var(--tempest-weight-semibold);\n}\n\n.empty {\n padding: var(--tempest-space-3);\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-sm);\n text-align: center;\n}\n\n.helper {\n font-size: var(--tempest-text-xs);\n color: var(--tempest-text-muted);\n}\n\n.errorText {\n font-size: var(--tempest-text-xs);\n color: var(--tempest-danger);\n}\n\n.wrapper.error .field {\n border-color: var(--tempest-danger);\n}\n"],"mappings":";"}
|
|
1
|
+
{"version":3,"file":"MultiSelect.module.cjs","names":[],"sources":["../../../src/components/MultiSelect/MultiSelect.module.css"],"sourcesContent":[".wrapper {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-1);\n width: 100%;\n font-family: var(--tempest-font-sans);\n position: relative;\n}\n\n.label {\n font-size: var(--tempest-text-sm);\n font-weight: var(--tempest-weight-semibold);\n color: var(--tempest-text);\n line-height: var(--tempest-leading-snug);\n}\n\n.field {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--tempest-space-1);\n min-height: var(--tempest-control-height-md);\n padding: 4px var(--tempest-space-2);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-control-radius);\n background-color: var(--tempest-bg);\n box-shadow: var(--tempest-shadow-xs);\n cursor: text;\n transition: var(--tempest-transition-color), var(--tempest-transition-shadow);\n}\n\n.field:focus-within {\n border-color: var(--tempest-primary);\n box-shadow: 0 0 0 var(--tempest-focus-ring-width) var(--tempest-focus-ring-color);\n}\n\n.field.disabled {\n background-color: var(--tempest-surface);\n cursor: not-allowed;\n}\n\n.chip {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n padding: 2px 4px 2px 8px;\n border-radius: var(--tempest-radius-full);\n background-color: var(--tempest-primary-soft);\n color: var(--tempest-primary-on-soft);\n font-size: var(--tempest-text-xs);\n font-weight: var(--tempest-weight-medium);\n line-height: 1.4;\n}\n\n.chipRemove {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 16px;\n height: 16px;\n border: none;\n border-radius: 50%;\n background: transparent;\n color: inherit;\n font-size: 14px;\n line-height: 1;\n cursor: pointer;\n}\n\n.chipRemove:hover {\n background-color: var(--tempest-primary);\n color: var(--tempest-primary-foreground);\n}\n\n.input {\n flex: 1;\n min-width: 80px;\n border: none;\n outline: none;\n background: transparent;\n color: var(--tempest-text);\n font-family: inherit;\n font-size: var(--tempest-control-font-md);\n height: 24px;\n}\n\n.input::placeholder {\n color: var(--tempest-text-subtle);\n}\n\n.menu {\n position: absolute;\n top: calc(100% + 4px);\n left: 0;\n right: 0;\n z-index: var(--tempest-z-dropdown);\n background-color: var(--tempest-bg);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-lg);\n box-shadow: var(--tempest-shadow-lg);\n max-height: 280px;\n overflow-y: auto;\n padding: var(--tempest-space-1);\n list-style: none;\n margin: 0;\n}\n\n.option {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-2);\n padding: var(--tempest-space-2) var(--tempest-space-3);\n border-radius: var(--tempest-radius-sm);\n cursor: pointer;\n color: var(--tempest-text);\n font-size: var(--tempest-text-base);\n line-height: var(--tempest-leading-snug);\n}\n\n.check {\n display: inline-flex;\n width: 16px;\n justify-content: center;\n color: var(--tempest-primary);\n font-weight: var(--tempest-weight-bold);\n}\n\n@media (hover: hover) and (pointer: fine) {\n .option:hover {\n background-color: var(--tempest-surface);\n }\n}\n\n.option.active {\n background-color: var(--tempest-primary-soft);\n color: var(--tempest-primary-on-soft);\n}\n\n.option.selected {\n font-weight: var(--tempest-weight-semibold);\n}\n\n.empty {\n padding: var(--tempest-space-3);\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-sm);\n text-align: center;\n}\n\n.helper {\n font-size: var(--tempest-text-xs);\n color: var(--tempest-text-muted);\n}\n\n.errorText {\n font-size: var(--tempest-text-xs);\n color: var(--tempest-danger);\n}\n\n.wrapper.error .field {\n border-color: var(--tempest-danger);\n}\n\n.portalled {\n font-family: var(--tempest-font-sans);\n right: auto;\n z-index: var(--tempest-z-popover);\n}\n"],"mappings":";"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import "../../styles/component/MultiSelect.css";
|
|
2
2
|
//#region src/components/MultiSelect/MultiSelect.module.css
|
|
3
|
-
var e = "tempest_wrapper_6xZZr", t = "tempest_label_Jwj7I", n = "tempest_field_GpA0V", r = "tempest_disabled_BIKaG", i = "tempest_chip_Zk0ch", a = "tempest_chipRemove_R-mNv", o = "tempest_input_SjOXU", s = "tempest_menu_3xnmo", c = "tempest_option_lqEl1", l = "tempest_check_aUM1w", u = "tempest_active_S9sQ-", d = "tempest_selected_Rw8yr", f = "tempest_empty_-5B16", p = "tempest_helper_c7aH3", m = "tempest_errorText_qZR9s", h = "tempest_error_DIOTW", g = {
|
|
3
|
+
var e = "tempest_wrapper_6xZZr", t = "tempest_label_Jwj7I", n = "tempest_field_GpA0V", r = "tempest_disabled_BIKaG", i = "tempest_chip_Zk0ch", a = "tempest_chipRemove_R-mNv", o = "tempest_input_SjOXU", s = "tempest_menu_3xnmo", c = "tempest_option_lqEl1", l = "tempest_check_aUM1w", u = "tempest_active_S9sQ-", d = "tempest_selected_Rw8yr", f = "tempest_empty_-5B16", p = "tempest_helper_c7aH3", m = "tempest_errorText_qZR9s", h = "tempest_error_DIOTW", g = "tempest_portalled_i1wmW", _ = {
|
|
4
4
|
wrapper: e,
|
|
5
5
|
label: t,
|
|
6
6
|
field: n,
|
|
@@ -16,9 +16,10 @@ var e = "tempest_wrapper_6xZZr", t = "tempest_label_Jwj7I", n = "tempest_field_G
|
|
|
16
16
|
empty: f,
|
|
17
17
|
helper: p,
|
|
18
18
|
errorText: m,
|
|
19
|
-
error: h
|
|
19
|
+
error: h,
|
|
20
|
+
portalled: g
|
|
20
21
|
};
|
|
21
22
|
//#endregion
|
|
22
|
-
export { u as active, l as check, i as chip, a as chipRemove,
|
|
23
|
+
export { u as active, l as check, i as chip, a as chipRemove, _ as default, r as disabled, f as empty, h as error, m as errorText, n as field, p as helper, o as input, t as label, s as menu, c as option, g as portalled, d as selected, e as wrapper };
|
|
23
24
|
|
|
24
25
|
//# sourceMappingURL=MultiSelect.module.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MultiSelect.module.js","names":[],"sources":["../../../src/components/MultiSelect/MultiSelect.module.css"],"sourcesContent":[".wrapper {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-1);\n width: 100%;\n font-family: var(--tempest-font-sans);\n position: relative;\n}\n\n.label {\n font-size: var(--tempest-text-sm);\n font-weight: var(--tempest-weight-semibold);\n color: var(--tempest-text);\n line-height: var(--tempest-leading-snug);\n}\n\n.field {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--tempest-space-1);\n min-height: var(--tempest-control-height-md);\n padding: 4px var(--tempest-space-2);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-control-radius);\n background-color: var(--tempest-bg);\n box-shadow: var(--tempest-shadow-xs);\n cursor: text;\n transition: var(--tempest-transition-color), var(--tempest-transition-shadow);\n}\n\n.field:focus-within {\n border-color: var(--tempest-primary);\n box-shadow: 0 0 0 var(--tempest-focus-ring-width) var(--tempest-focus-ring-color);\n}\n\n.field.disabled {\n background-color: var(--tempest-surface);\n cursor: not-allowed;\n}\n\n.chip {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n padding: 2px 4px 2px 8px;\n border-radius: var(--tempest-radius-full);\n background-color: var(--tempest-primary-soft);\n color: var(--tempest-primary-on-soft);\n font-size: var(--tempest-text-xs);\n font-weight: var(--tempest-weight-medium);\n line-height: 1.4;\n}\n\n.chipRemove {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 16px;\n height: 16px;\n border: none;\n border-radius: 50%;\n background: transparent;\n color: inherit;\n font-size: 14px;\n line-height: 1;\n cursor: pointer;\n}\n\n.chipRemove:hover {\n background-color: var(--tempest-primary);\n color: var(--tempest-primary-foreground);\n}\n\n.input {\n flex: 1;\n min-width: 80px;\n border: none;\n outline: none;\n background: transparent;\n color: var(--tempest-text);\n font-family: inherit;\n font-size: var(--tempest-control-font-md);\n height: 24px;\n}\n\n.input::placeholder {\n color: var(--tempest-text-subtle);\n}\n\n.menu {\n position: absolute;\n top: calc(100% + 4px);\n left: 0;\n right: 0;\n z-index: var(--tempest-z-dropdown);\n background-color: var(--tempest-bg);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-lg);\n box-shadow: var(--tempest-shadow-lg);\n max-height: 280px;\n overflow-y: auto;\n padding: var(--tempest-space-1);\n list-style: none;\n margin: 0;\n}\n\n.option {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-2);\n padding: var(--tempest-space-2) var(--tempest-space-3);\n border-radius: var(--tempest-radius-sm);\n cursor: pointer;\n color: var(--tempest-text);\n font-size: var(--tempest-text-base);\n line-height: var(--tempest-leading-snug);\n}\n\n.check {\n display: inline-flex;\n width: 16px;\n justify-content: center;\n color: var(--tempest-primary);\n font-weight: var(--tempest-weight-bold);\n}\n\n@media (hover: hover) and (pointer: fine) {\n .option:hover {\n background-color: var(--tempest-surface);\n }\n}\n\n.option.active {\n background-color: var(--tempest-primary-soft);\n color: var(--tempest-primary-on-soft);\n}\n\n.option.selected {\n font-weight: var(--tempest-weight-semibold);\n}\n\n.empty {\n padding: var(--tempest-space-3);\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-sm);\n text-align: center;\n}\n\n.helper {\n font-size: var(--tempest-text-xs);\n color: var(--tempest-text-muted);\n}\n\n.errorText {\n font-size: var(--tempest-text-xs);\n color: var(--tempest-danger);\n}\n\n.wrapper.error .field {\n border-color: var(--tempest-danger);\n}\n"],"mappings":";"}
|
|
1
|
+
{"version":3,"file":"MultiSelect.module.js","names":[],"sources":["../../../src/components/MultiSelect/MultiSelect.module.css"],"sourcesContent":[".wrapper {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-1);\n width: 100%;\n font-family: var(--tempest-font-sans);\n position: relative;\n}\n\n.label {\n font-size: var(--tempest-text-sm);\n font-weight: var(--tempest-weight-semibold);\n color: var(--tempest-text);\n line-height: var(--tempest-leading-snug);\n}\n\n.field {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--tempest-space-1);\n min-height: var(--tempest-control-height-md);\n padding: 4px var(--tempest-space-2);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-control-radius);\n background-color: var(--tempest-bg);\n box-shadow: var(--tempest-shadow-xs);\n cursor: text;\n transition: var(--tempest-transition-color), var(--tempest-transition-shadow);\n}\n\n.field:focus-within {\n border-color: var(--tempest-primary);\n box-shadow: 0 0 0 var(--tempest-focus-ring-width) var(--tempest-focus-ring-color);\n}\n\n.field.disabled {\n background-color: var(--tempest-surface);\n cursor: not-allowed;\n}\n\n.chip {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n padding: 2px 4px 2px 8px;\n border-radius: var(--tempest-radius-full);\n background-color: var(--tempest-primary-soft);\n color: var(--tempest-primary-on-soft);\n font-size: var(--tempest-text-xs);\n font-weight: var(--tempest-weight-medium);\n line-height: 1.4;\n}\n\n.chipRemove {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 16px;\n height: 16px;\n border: none;\n border-radius: 50%;\n background: transparent;\n color: inherit;\n font-size: 14px;\n line-height: 1;\n cursor: pointer;\n}\n\n.chipRemove:hover {\n background-color: var(--tempest-primary);\n color: var(--tempest-primary-foreground);\n}\n\n.input {\n flex: 1;\n min-width: 80px;\n border: none;\n outline: none;\n background: transparent;\n color: var(--tempest-text);\n font-family: inherit;\n font-size: var(--tempest-control-font-md);\n height: 24px;\n}\n\n.input::placeholder {\n color: var(--tempest-text-subtle);\n}\n\n.menu {\n position: absolute;\n top: calc(100% + 4px);\n left: 0;\n right: 0;\n z-index: var(--tempest-z-dropdown);\n background-color: var(--tempest-bg);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-lg);\n box-shadow: var(--tempest-shadow-lg);\n max-height: 280px;\n overflow-y: auto;\n padding: var(--tempest-space-1);\n list-style: none;\n margin: 0;\n}\n\n.option {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-2);\n padding: var(--tempest-space-2) var(--tempest-space-3);\n border-radius: var(--tempest-radius-sm);\n cursor: pointer;\n color: var(--tempest-text);\n font-size: var(--tempest-text-base);\n line-height: var(--tempest-leading-snug);\n}\n\n.check {\n display: inline-flex;\n width: 16px;\n justify-content: center;\n color: var(--tempest-primary);\n font-weight: var(--tempest-weight-bold);\n}\n\n@media (hover: hover) and (pointer: fine) {\n .option:hover {\n background-color: var(--tempest-surface);\n }\n}\n\n.option.active {\n background-color: var(--tempest-primary-soft);\n color: var(--tempest-primary-on-soft);\n}\n\n.option.selected {\n font-weight: var(--tempest-weight-semibold);\n}\n\n.empty {\n padding: var(--tempest-space-3);\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-sm);\n text-align: center;\n}\n\n.helper {\n font-size: var(--tempest-text-xs);\n color: var(--tempest-text-muted);\n}\n\n.errorText {\n font-size: var(--tempest-text-xs);\n color: var(--tempest-danger);\n}\n\n.wrapper.error .field {\n border-color: var(--tempest-danger);\n}\n\n.portalled {\n font-family: var(--tempest-font-sans);\n right: auto;\n z-index: var(--tempest-z-popover);\n}\n"],"mappings":";"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("../../utils/cn.cjs"),t=require("./NavigationMenu.module.cjs");let
|
|
1
|
+
const e=require("../../utils/cn.cjs"),t=require("../Portal/escape-layer.cjs"),n=require("../Portal/Portal.cjs"),r=require("../Portal/anchor-position.cjs"),i=require("../Portal/tab-bridge.cjs"),a=require("./NavigationMenu.module.cjs");let o=require("react"),s=require("react/jsx-runtime");var c=4;function l({items:l,portal:u=!0,className:d,...f}){let[p,m]=(0,o.useState)(-1),[h,g]=(0,o.useState)(-1),_=(0,o.useId)(),v=(0,o.useRef)(null),y=(0,o.useCallback)(()=>{m(-1),g(-1)},[]),b=p===-1?h:p,[x,S]=(0,o.useState)(null),[C,w]=(0,o.useState)(null),T=u&&b!==-1,E=r.useAnchorPosition({anchor:x,floating:C,side:`bottom`,align:`start`,offset:c,enabled:T});i.usePortalTabOrder({enabled:T,anchor:x,panel:C}),t.useEscapeLayer(b!==-1,y),(0,o.useEffect)(()=>{if(b===-1)return;let e=e=>{let t=e.target;v.current?.contains(t)||C?.contains(t)||y()};return window.addEventListener(`mousedown`,e),()=>{window.removeEventListener(`mousedown`,e)}},[b,y,C]);let D=(e,t)=>{let n=()=>{e.onSelect?.(),y()};return e.href?(0,s.jsx)(`a`,{href:e.href,role:t===`menuitem`?`menuitem`:void 0,className:a.default.link,onClick:n,children:e.label}):(0,s.jsx)(`button`,{type:`button`,role:t===`menuitem`?`menuitem`:void 0,className:a.default.link,onClick:n,children:e.label})};return(0,s.jsx)(`nav`,{ref:v,className:e.cn(a.default.root,d),...f,children:(0,s.jsx)(`ul`,{className:a.default.list,children:l.map((t,r)=>{if(!(t.children&&t.children.length>0))return(0,s.jsx)(`li`,{className:a.default.item,children:D(t,`none`)},r);let i=b===r,o=`${_}-panel-${r}`,c=(0,s.jsx)(`ul`,{ref:w,id:o,role:`menu`,className:e.cn(a.default.panel,u&&a.default.portalled),style:E,children:t.children.map((e,t)=>(0,s.jsx)(`li`,{role:`none`,className:a.default.subItem,children:D(e,`menuitem`)},t))});return(0,s.jsxs)(`li`,{ref:i?S:void 0,className:a.default.item,onMouseEnter:()=>g(r),onMouseLeave:()=>g(e=>e===r?-1:e),children:[(0,s.jsx)(`button`,{type:`button`,className:a.default.trigger,"aria-haspopup":`menu`,"aria-expanded":i,"aria-controls":o,onClick:()=>m(e=>e===r?-1:r),onFocus:()=>g(r),children:t.label}),i&&(u?(0,s.jsx)(n.Portal,{children:c}):c)]},r)})})})}exports.NavigationMenu=l;
|
|
2
2
|
//# sourceMappingURL=NavigationMenu.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NavigationMenu.cjs","names":[],"sources":["../../../src/components/NavigationMenu/NavigationMenu.tsx"],"sourcesContent":["/**\n * @tempest-limits function-lines — the body resolves the active item from the\n * current path, owns the open submenu and the hover-intent delay, all of which\n * decide the same highlighted row.\n */\nimport { useCallback, useEffect, useId, useRef, useState } from \"react\";\nimport type { HTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./NavigationMenu.module.css\";\n\nexport interface NavigationMenuItem {\n /** Visible label for the entry. */\n label: ReactNode;\n /** When set, the entry renders as an anchor. */\n href?: string;\n /** Invoked when the entry is activated (click / Enter). */\n onSelect?: () => void;\n /** Nested entries — presence turns the item into a submenu trigger. */\n children?: NavigationMenuItem[];\n}\n\nexport interface NavigationMenuProps extends HTMLAttributes<HTMLElement> {\n /** Top-level navigation entries. */\n items: NavigationMenuItem[];\n}\n\n/**\n * Horizontal navigation menu with hover/click/focus dropdown submenus.\n *\n * - Top-level items render in a `<nav><ul>`.\n * - Items with `children` open a submenu panel (`role=\"menu\"`) on hover, focus,\n * or click. Only one panel is open at a time.\n * - Closes on outside click, Escape, or selecting a leaf entry.\n *\n * @example\n * <NavigationMenu\n * items={[\n * { label: \"Home\", href: \"/\" },\n * {\n * label: \"Products\",\n * children: [\n * { label: \"Analytics\", href: \"/analytics\" },\n * { label: \"Billing\", onSelect: () => openBilling() },\n * ],\n * },\n * ]}\n * />\n */\nexport function NavigationMenu({ items, className, ...props }: NavigationMenuProps) {\n const [clickedIndex, setClickedIndex] = useState<number>(-1);\n const [hovered, setHovered] = useState<number>(-1);\n const baseId = useId();\n const rootRef = useRef<HTMLElement>(null);\n\n const close = useCallback((): void => {\n setClickedIndex(-1);\n setHovered(-1);\n }, []);\n\n const openIndex = clickedIndex !== -1 ? clickedIndex : hovered;\n\n useEffect(() => {\n if (openIndex === -1) return;\n const onKey = (event: KeyboardEvent): void => {\n if (event.key === \"Escape\") close();\n };\n const onDown = (event: MouseEvent): void => {\n if (rootRef.current && !rootRef.current.contains(event.target as Node)) close();\n };\n window.addEventListener(\"keydown\", onKey);\n window.addEventListener(\"mousedown\", onDown);\n return () => {\n window.removeEventListener(\"keydown\", onKey);\n window.removeEventListener(\"mousedown\", onDown);\n };\n }, [openIndex, close]);\n\n const renderLeaf = (item: NavigationMenuItem, role: \"none\" | \"menuitem\"): ReactNode => {\n const handleClick = (): void => {\n item.onSelect?.();\n close();\n };\n if (item.href) {\n return (\n <a\n href={item.href}\n role={role === \"menuitem\" ? \"menuitem\" : undefined}\n className={styles.link}\n onClick={handleClick}\n >\n {item.label}\n </a>\n );\n }\n return (\n <button\n type=\"button\"\n role={role === \"menuitem\" ? \"menuitem\" : undefined}\n className={styles.link}\n onClick={handleClick}\n >\n {item.label}\n </button>\n );\n };\n\n return (\n <nav ref={rootRef} className={cn(styles.root, className)} {...props}>\n <ul className={styles.list}>\n {items.map((item, index) => {\n const hasChildren = !!item.children && item.children.length > 0;\n if (!hasChildren) {\n return (\n <li key={index} className={styles.item}>\n {renderLeaf(item, \"none\")}\n </li>\n );\n }\n const open = openIndex === index;\n const panelId = `${baseId}-panel-${index}`;\n return (\n <li\n key={index}\n className={styles.item}\n onMouseEnter={() => setHovered(index)}\n onMouseLeave={() => setHovered((prev) => (prev === index ? -1 : prev))}\n >\n <button\n type=\"button\"\n className={styles.trigger}\n aria-haspopup=\"menu\"\n aria-expanded={open}\n aria-controls={panelId}\n onClick={() =>\n setClickedIndex((prev) => (prev === index ? -1 : index))\n }\n onFocus={() => setHovered(index)}\n >\n {item.label}\n </button>\n {open && (\n <ul id={panelId} role=\"menu\" className={styles.panel}>\n {item.children!.map((child, childIndex) => (\n <li key={childIndex} role=\"none\" className={styles.subItem}>\n {renderLeaf(child, \"menuitem\")}\n </li>\n ))}\n </ul>\n )}\n </li>\n );\n })}\n </ul>\n </nav>\n );\n}\n"],"mappings":"qIAgDA,SAAgB,EAAe,CAAE,QAAO,YAAW,GAAG,GAA8B,CAChF,GAAM,CAAC,EAAc,IAAA,EAAmB,EAAA,SAAA,CAAiB,EAAE,EACrD,CAAC,EAAS,IAAA,EAAc,EAAA,SAAA,CAAiB,EAAE,EAC3C,GAAA,EAAS,EAAA,MAAA,CAAM,EACf,GAAA,EAAU,EAAA,OAAA,CAAoB,IAAI,EAElC,GAAA,EAAQ,EAAA,YAAA,KAAwB,CAClC,EAAgB,EAAE,EAClB,EAAW,EAAE,CACjB,EAAG,CAAC,CAAC,EAEC,EAAY,IAAiB,GAAoB,EAAf,GAExC,EAAA,EAAA,UAAA,KAAgB,CACZ,GAAI,IAAc,GAAI,OACtB,IAAM,EAAS,GAA+B,CACtC,EAAM,MAAQ,UAAU,EAAM,CACtC,EACM,EAAU,GAA4B,CACpC,EAAQ,SAAW,CAAC,EAAQ,QAAQ,SAAS,EAAM,MAAc,GAAG,EAAM,CAClF,EAGA,OAFA,OAAO,iBAAiB,UAAW,CAAK,EACxC,OAAO,iBAAiB,YAAa,CAAM,MAC9B,CACT,OAAO,oBAAoB,UAAW,CAAK,EAC3C,OAAO,oBAAoB,YAAa,CAAM,CAClD,CACJ,EAAG,CAAC,EAAW,CAAK,CAAC,EAErB,IAAM,GAAc,EAA0B,IAAyC,CACnF,IAAM,MAA0B,CAC5B,EAAK,WAAW,EAChB,EAAM,CACV,EAaA,OAZI,EAAK,MAED,EAAA,EAAA,IAAA,CAAC,IAAD,CACI,KAAM,EAAK,KACX,KAAM,IAAS,WAAa,WAAa,IAAA,GACzC,UAAW,EAAA,QAAO,KAClB,QAAS,EAER,SAAA,EAAK,KACP,CAAA,GAIP,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,KAAK,SACL,KAAM,IAAS,WAAa,WAAa,IAAA,GACzC,UAAW,EAAA,QAAO,KAClB,QAAS,EAER,SAAA,EAAK,KACF,CAAA,CAEhB,EAEA,OACI,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,IAAK,EAAS,UAAW,EAAA,GAAG,EAAA,QAAO,KAAM,CAAS,EAAG,GAAI,EAC1D,UAAA,EAAA,EAAA,IAAA,CAAC,KAAD,CAAI,UAAW,EAAA,QAAO,KACjB,SAAA,EAAM,KAAK,EAAM,IAAU,CAExB,GAAI,EADkB,EAAK,UAAY,EAAK,SAAS,OAAS,GAE1D,OACI,EAAA,EAAA,IAAA,CAAC,KAAD,CAAgB,UAAW,EAAA,QAAO,KAC7B,SAAA,EAAW,EAAM,MAAM,CACxB,EAFK,CAEL,EAGZ,IAAM,EAAO,IAAc,EACrB,EAAU,GAAG,EAAO,SAAS,IACnC,OACI,EAAA,EAAA,KAAA,CAAC,KAAD,CAEI,UAAW,EAAA,QAAO,KAClB,iBAAoB,EAAW,CAAK,EACpC,iBAAoB,EAAY,GAAU,IAAS,EAAQ,GAAK,CAAK,EAJzE,SAAA,EAMI,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,KAAK,SACL,UAAW,EAAA,QAAO,QAClB,gBAAc,OACd,gBAAe,EACf,gBAAe,EACf,YACI,EAAiB,GAAU,IAAS,EAAQ,GAAK,CAAM,EAE3D,YAAe,EAAW,CAAK,EAE9B,SAAA,EAAK,KACF,CAAA,EACP,IACG,EAAA,EAAA,IAAA,CAAC,KAAD,CAAI,GAAI,EAAS,KAAK,OAAO,UAAW,EAAA,QAAO,MAC1C,SAAA,EAAK,SAAU,KAAK,EAAO,KACxB,EAAA,EAAA,IAAA,CAAC,KAAD,CAAqB,KAAK,OAAO,UAAW,EAAA,QAAO,QAC9C,SAAA,EAAW,EAAO,UAAU,CAC7B,EAFK,CAEL,CACP,CACD,CAAA,CAER,CA3BK,EAAA,CA2BL,CAEZ,CAAC,CACD,CAAA,CACH,CAAA,CAEb"}
|
|
1
|
+
{"version":3,"file":"NavigationMenu.cjs","names":[],"sources":["../../../src/components/NavigationMenu/NavigationMenu.tsx"],"sourcesContent":["/**\n * @tempest-limits function-lines — the body resolves the active item from the\n * current path, owns the open submenu and the hover-intent delay, all of which\n * decide the same highlighted row.\n */\nimport { useCallback, useEffect, useId, useRef, useState } from \"react\";\nimport type { HTMLAttributes, ReactNode } from \"react\";\nimport { Portal } from \"@/components/Portal\";\nimport { useAnchorPosition } from \"@/components/Portal/anchor-position\";\nimport { useEscapeLayer } from \"@/components/Portal/escape-layer\";\nimport { usePortalTabOrder } from \"@/components/Portal/tab-bridge\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./NavigationMenu.module.css\";\n\nexport interface NavigationMenuItem {\n /** Visible label for the entry. */\n label: ReactNode;\n /** When set, the entry renders as an anchor. */\n href?: string;\n /** Invoked when the entry is activated (click / Enter). */\n onSelect?: () => void;\n /** Nested entries — presence turns the item into a submenu trigger. */\n children?: NavigationMenuItem[];\n}\n\nexport interface NavigationMenuProps extends HTMLAttributes<HTMLElement> {\n /** Top-level navigation entries. */\n items: NavigationMenuItem[];\n /**\n * Render the open panel in a portal, positioned under its trigger. Default\n * `true`.\n *\n * In flow, an ancestor with `overflow` other than `visible` clips the panel,\n * and the SDK's own `Navbar` is one: its `nav` slot scrolls horizontally, and\n * `overflow-x: auto` forces `overflow-y` to `auto` too — a submenu showed\n * 0% of its panel there (measured in Chrome at 1440 px). In a portal the\n * panel escapes the clip, flips at the viewport edge, stays in the tab order\n * right after its trigger, and follows it on scroll and resize.\n */\n portal?: boolean;\n}\n\n/** Gap between trigger and panel, in px — the in-flow CSS uses the same 4 px. */\nconst PANEL_OFFSET = 4;\n\n/**\n * Horizontal navigation menu with hover/click/focus dropdown submenus.\n *\n * - Top-level items render in a `<nav><ul>`.\n * - Items with `children` open a submenu panel (`role=\"menu\"`) on hover, focus,\n * or click. Only one panel is open at a time.\n * - Closes on outside click, Escape, or selecting a leaf entry.\n *\n * @example\n * <NavigationMenu\n * items={[\n * { label: \"Home\", href: \"/\" },\n * {\n * label: \"Products\",\n * children: [\n * { label: \"Analytics\", href: \"/analytics\" },\n * { label: \"Billing\", onSelect: () => openBilling() },\n * ],\n * },\n * ]}\n * />\n */\nexport function NavigationMenu({ items, portal = true, className, ...props }: NavigationMenuProps) {\n const [clickedIndex, setClickedIndex] = useState<number>(-1);\n const [hovered, setHovered] = useState<number>(-1);\n const baseId = useId();\n const rootRef = useRef<HTMLElement>(null);\n\n const close = useCallback((): void => {\n setClickedIndex(-1);\n setHovered(-1);\n }, []);\n\n const openIndex = clickedIndex !== -1 ? clickedIndex : hovered;\n\n const [anchorNode, setAnchorNode] = useState<HTMLLIElement | null>(null);\n const [panelNode, setPanelNode] = useState<HTMLUListElement | null>(null);\n const floating = portal && openIndex !== -1;\n const floatingStyle = useAnchorPosition({\n anchor: anchorNode,\n floating: panelNode,\n side: \"bottom\",\n align: \"start\",\n offset: PANEL_OFFSET,\n enabled: floating,\n });\n usePortalTabOrder({ enabled: floating, anchor: anchorNode, panel: panelNode });\n\n useEscapeLayer(openIndex !== -1, close);\n\n /**\n * Close on a press outside both the menu and the open panel.\n *\n * The panel is checked on its own because in a portal it is not inside the\n * menu's root: a press on one of its entries would otherwise count as\n * outside, close the panel on `mousedown`, and swallow the entry's click.\n */\n useEffect(() => {\n if (openIndex === -1) return;\n const onDown = (event: MouseEvent): void => {\n const target = event.target as Node;\n if (rootRef.current?.contains(target) || panelNode?.contains(target)) return;\n close();\n };\n window.addEventListener(\"mousedown\", onDown);\n return () => {\n window.removeEventListener(\"mousedown\", onDown);\n };\n }, [openIndex, close, panelNode]);\n\n const renderLeaf = (item: NavigationMenuItem, role: \"none\" | \"menuitem\"): ReactNode => {\n const handleClick = (): void => {\n item.onSelect?.();\n close();\n };\n if (item.href) {\n return (\n <a\n href={item.href}\n role={role === \"menuitem\" ? \"menuitem\" : undefined}\n className={styles.link}\n onClick={handleClick}\n >\n {item.label}\n </a>\n );\n }\n return (\n <button\n type=\"button\"\n role={role === \"menuitem\" ? \"menuitem\" : undefined}\n className={styles.link}\n onClick={handleClick}\n >\n {item.label}\n </button>\n );\n };\n\n return (\n <nav ref={rootRef} className={cn(styles.root, className)} {...props}>\n <ul className={styles.list}>\n {items.map((item, index) => {\n const hasChildren = !!item.children && item.children.length > 0;\n if (!hasChildren) {\n return (\n <li key={index} className={styles.item}>\n {renderLeaf(item, \"none\")}\n </li>\n );\n }\n const open = openIndex === index;\n const panelId = `${baseId}-panel-${index}`;\n const panel = (\n <ul\n ref={setPanelNode}\n id={panelId}\n role=\"menu\"\n className={cn(styles.panel, portal && styles.portalled)}\n style={floatingStyle}\n >\n {item.children!.map((child, childIndex) => (\n <li key={childIndex} role=\"none\" className={styles.subItem}>\n {renderLeaf(child, \"menuitem\")}\n </li>\n ))}\n </ul>\n );\n return (\n <li\n key={index}\n ref={open ? setAnchorNode : undefined}\n className={styles.item}\n onMouseEnter={() => setHovered(index)}\n onMouseLeave={() => setHovered((prev) => (prev === index ? -1 : prev))}\n >\n <button\n type=\"button\"\n className={styles.trigger}\n aria-haspopup=\"menu\"\n aria-expanded={open}\n aria-controls={panelId}\n onClick={() =>\n setClickedIndex((prev) => (prev === index ? -1 : index))\n }\n onFocus={() => setHovered(index)}\n >\n {item.label}\n </button>\n {open && (portal ? <Portal>{panel}</Portal> : panel)}\n </li>\n );\n })}\n </ul>\n </nav>\n );\n}\n"],"mappings":"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"}
|