@motir/design-system 0.8.1 → 0.10.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/theme/AppearancePickers.js +1 -1
- package/dist/components/theme/AppearancePickers.js.map +1 -1
- package/dist/components/ui/Combobox.js +82 -6
- package/dist/components/ui/Combobox.js.map +1 -1
- package/dist/components/ui/Modal.d.ts +8 -1
- package/dist/components/ui/Modal.js +19 -3
- package/dist/components/ui/Modal.js.map +1 -1
- package/dist/components/ui/MultiSelectPicker.js +15 -1
- package/dist/components/ui/MultiSelectPicker.js.map +1 -1
- package/dist/components/ui/Popover.js +16 -2
- package/dist/components/ui/Popover.js.map +1 -1
- package/dist/components/ui/Tooltip.js +16 -1
- package/dist/components/ui/Tooltip.js.map +1 -1
- package/dist/contexts/theme-context.js +1 -1
- package/dist/contexts/theme-context.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/mock/index.js +1 -1
- package/dist/mock/index.js.map +1 -1
- package/dist/mock/renderMock.js +1 -1
- package/dist/mock/renderMock.js.map +1 -1
- package/dist/specimen/TokensSpecimen.js +1 -1
- package/dist/specimen/TokensSpecimen.js.map +1 -1
- package/dist/theme/appearance-resolution.js +1 -1
- package/dist/theme/appearance-resolution.js.map +1 -1
- package/dist/theme/init-script.js +1 -1
- package/dist/theme/init-script.js.map +1 -1
- package/dist/theme/palettes.js +1 -1
- package/dist/theme/palettes.js.map +1 -1
- package/dist/theme/types.js.map +1 -1
- package/dist/utils/fullscreen.d.ts +17 -0
- package/dist/utils/fullscreen.js +20 -0
- package/dist/utils/fullscreen.js.map +1 -0
- package/package.json +1 -1
- package/theme.css +260 -6
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/utils/cn.ts","../../../src/components/ui/Combobox.tsx"],"names":["Fragment"],"mappings":"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","file":"Combobox.js","sourcesContent":["import { clsx, type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\n/**\n * Merge Tailwind class names without conflicts.\n *\n * `clsx` handles conditional inclusion (booleans, undefineds, arrays);\n * `twMerge` resolves conflicting Tailwind classes (later wins, e.g.\n * `px-2 px-4` → `px-4`). Together they are the de facto standard for\n * composing Tailwind classes in React components.\n *\n * @example\n * cn('px-2 py-1', isActive && 'bg-primary', className)\n */\nexport function cn(...inputs: ClassValue[]): string {\n return twMerge(clsx(inputs));\n}\n","'use client';\n\nimport {\n Fragment,\n useCallback,\n useEffect,\n useId,\n useLayoutEffect,\n useMemo,\n useRef,\n useState,\n useSyncExternalStore,\n type CSSProperties,\n type ReactNode,\n} from 'react';\nimport { createPortal } from 'react-dom';\nimport { Check, ChevronsUpDown } from 'lucide-react';\nimport { cn } from '../../utils/cn';\n\n// Run layout effects on the client, fall back to useEffect during SSR (the menu\n// only mounts client-side anyway — see the `mounted` gate below).\nconst useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;\n\n// `false` during SSR / first hydration render, `true` on the client thereafter —\n// gates the createPortal call so document.body is guaranteed present. Hydration-\n// safe (matches the server's `false`) and avoids setState-in-effect.\nconst subscribeNoop = () => () => {};\n\n/**\n * The LEADING-VISUAL slot, on the trigger and on every option row (MOTIR-3080).\n *\n * ⚠️ `inline-flex` is load-bearing, not decoration. The wrapper is a flex item of\n * its row, so IT is blockified — but whatever a caller passes INSIDE it is not,\n * and a plain `<span>` therefore stays `display: inline`, where width and height\n * do not apply at all. `StatusPicker`'s 10px status dot rendered as a 2 × 18\n * sliver for exactly that reason: three cards of palette work (MOTIR-1273 /\n * -2073 / -2075) measured that dot's HUE while its box was two pixels wide.\n *\n * It survived every check because the markup was right — the classes ask for\n * 10 × 10, `getComputedStyle` reports 10px, and only the USED value disagreed.\n * A used value exists only in a browser, so `tests/e2e/issue-detail-flow` MEASURES\n * it rather than asserting the element exists.\n *\n * Making the slot a flex CONTAINER blockifies its child, which is what the slot's\n * contract (\"a leading visual\") always implied. A lucide `<svg>` is replaced and\n * was never affected, so no existing caller's icon changes size.\n */\nconst ICON_SLOT = 'inline-flex shrink-0 items-center';\n\nfunction useMounted() {\n return useSyncExternalStore(\n subscribeNoop,\n () => true,\n () => false,\n );\n}\n\n/**\n * Combobox — an accessible select/combobox primitive (Subtask 2.3.4). A trigger\n * button (`role=\"combobox\"`, `aria-haspopup=\"listbox\"`) opens an anchored panel\n * holding an optional type-ahead filter and a `role=\"listbox\"` of\n * `role=\"option\"` rows. The active option is tracked with `aria-activedescendant`\n * on whichever control holds focus (the filter input when `searchable`, else the\n * listbox) — the CommandPalette pattern that already clears the STRICT axe sweep.\n *\n * Deliberately NOT built on the Radix Popover primitive: that injects\n * `aria-haspopup=\"dialog\"` + dialog focus semantics onto its trigger, which\n * conflicts with the listbox combobox pattern. This is a self-contained anchored\n * dropdown (click-outside + Escape + focus return handled here) so the ARIA is\n * exactly the WAI-ARIA combobox shape.\n *\n * Composed by `components/issues/TypePicker` (searchable=false, 5 options) and\n * `ParentPicker` (searchable, async candidate list).\n */\nexport interface ComboboxOption<T extends string> {\n value: T;\n /** Primary text — also the accessible name of the option. */\n label: string;\n /** Extra text matched by the filter (e.g. a PROD-N identifier). */\n keywords?: string;\n /** Leading visual (a kind icon); decorative — label carries the name. */\n icon?: ReactNode;\n /** Trailing muted text (e.g. the identifier). */\n secondary?: string;\n /**\n * A one-line description drawn UNDER the label in the menu row only — never in\n * the trigger, which stays the label alone (the workspace role picker's\n * \"what this role does\" line, MOTIR-6465). Decorative for the accessible name,\n * which stays `label`.\n */\n description?: string;\n /** Trailing rich content pinned to the option row's far end (e.g. a state\n * Pill or a \"Linked to …\" chip in the PR-link picker, MOTIR-1596).\n * Decorative — the accessible name is `label`; renders after `secondary`. */\n trailing?: ReactNode;\n /**\n * Optional section label. When consecutive options carry different `group`\n * values, a non-interactive header row is rendered at each transition (the\n * advanced filter's \"Fields\" / \"Custom fields\" / \"Other\" field menu). Pass\n * options pre-sorted by group; headers follow list order, so a group whose\n * every option is filtered out disappears with it. Decorative — not part of\n * the listbox's option indices, so keyboard nav is unaffected.\n */\n group?: string;\n /**\n * The option is shown but cannot be picked (MOTIR-5528 — a status move an\n * approval holds). It stays FOCUSABLE and is announced as unavailable\n * (`aria-disabled`), rather than skipped, so a keyboard reader learns the move\n * exists and why it is held; a click or Enter on it does nothing. Its label\n * reads in `--el-text-secondary`, never an opacity dim, which keeps AA.\n */\n disabled?: boolean;\n}\n\nexport interface ComboboxProps<T extends string> {\n options: ComboboxOption<T>[];\n value: T | null;\n onChange: (value: T) => void;\n /** Accessible name for the trigger + listbox. */\n label: string;\n /** Trigger text when nothing is selected. */\n placeholder?: string;\n searchable?: boolean;\n searchPlaceholder?: string;\n /** Empty-listbox content. A string, or rich nodes (e.g. a \"no matches\" line\n * plus a hint sub-line — use block `<span>`s, since this renders in a `<p>`). */\n emptyText?: ReactNode;\n loading?: boolean;\n loadingText?: string;\n disabled?: boolean;\n /** id for the trigger button. */\n id?: string;\n className?: string;\n /**\n * Mount already-open (Subtask 2.5.5) — for an inline-edit cell that renders the\n * picker on a single click, so the menu is open immediately (mirrors\n * `DatePicker`'s `autoOpen`). Focus lands on the search input / listbox.\n */\n autoOpen?: boolean;\n /**\n * Fired whenever the menu transitions open → closed (a pick, Escape,\n * click-outside, or toggling the trigger). The inline-edit cell uses it to\n * leave edit mode and return to the static pill/avatar (Subtask 2.5.5).\n */\n onClose?: () => void;\n /**\n * Server-driven search mode (Subtask 6.9.2 — the link picker). When\n * `onQueryChange` is provided the type-ahead query is CONTROLLED by the parent\n * (`query`) and client-side filtering is SKIPPED — `options` is taken as the\n * server's authoritative result for the current query (the parent debounces\n * its fetch off `onQueryChange`, the same contract as `MultiSelectPicker`).\n * Omit both for the default self-filtering behaviour. The parent owns the\n * query lifecycle, so opening the menu does NOT reset it.\n */\n query?: string;\n onQueryChange?: (query: string) => void;\n /**\n * A non-interactive note pinned BELOW the listbox, inside the open menu\n * (MOTIR-5582 — the Monitoring room's minimum-level control says, at the\n * action that causes it, that lowering also re-checks earlier issues). It is\n * not an option: keyboard navigation and the listbox's option indices are\n * unaffected, and it is announced as ordinary text. Omit it for no note.\n */\n footer?: ReactNode;\n}\n\n// Walk up from a node to the nearest ancestor that clips overflow, classifying\n// it as scrolling (overflow auto/scroll — the menu can be scrolled into view)\n// or a hard `hidden`/`clip` box (no scroll — the menu is trapped). Used by the\n// inline (in-dialog) branch to decide whether to clamp + flip the menu (only a\n// hard non-scrolling clip needs it). Stops at <body>; returns null if nothing\n// up the chain clips.\nfunction nearestClipBox(el: HTMLElement): { box: HTMLElement; scrolls: boolean } | null {\n let node = el.parentElement;\n while (node && node !== document.body) {\n const oy = getComputedStyle(node).overflowY;\n if (oy === 'auto' || oy === 'scroll') return { box: node, scrolls: true };\n if (oy === 'hidden' || oy === 'clip') return { box: node, scrolls: false };\n node = node.parentElement;\n }\n return null;\n}\n\nexport function Combobox<T extends string>({\n options,\n value,\n onChange,\n label,\n placeholder = 'Select…',\n searchable = false,\n searchPlaceholder = 'Search…',\n emptyText = 'No matches',\n loading = false,\n loadingText = 'Loading…',\n disabled = false,\n id,\n className,\n autoOpen = false,\n onClose,\n query: controlledQuery,\n onQueryChange,\n footer,\n}: ComboboxProps<T>) {\n // A DISABLED combobox never opens — not even when it mounts with `autoOpen`\n // (MOTIR-6173). An inline editor that mounts open for an actor who may not\n // write used to hand them a live menu whose pick the server then refused\n // (MOTIR-4822); the trigger was disabled, the menu was not.\n const [open, setOpen] = useState(autoOpen && !disabled);\n // Server-driven mode (6.9.2): when `onQueryChange` is provided the query is\n // controlled by the parent and client filtering is bypassed (the server's\n // `options` are authoritative). Otherwise the query is internal state.\n const serverFiltered = onQueryChange !== undefined;\n const [internalQuery, setInternalQuery] = useState('');\n const query = serverFiltered ? (controlledQuery ?? '') : internalQuery;\n const setQuery = (q: string) => (serverFiltered ? onQueryChange(q) : setInternalQuery(q));\n const [activeIndex, setActiveIndex] = useState(0);\n // The menu is portaled to <body> by default, so it escapes any clipping\n // ancestor — a short table's overflow:hidden\n // (bug-inline-edit-clipped-when-table-short) OR the Advanced-filter popover's\n // overflow-hidden + overflow-y-auto body (MOTIR-1346). We render INLINE only\n // inside a focus-trapping MODAL (the shared Modal primitive — `data-surface=\n // \"modal\"`; `aria-modal` covers any other), where a portaled menu would land\n // outside the dialog's focus scope (focus-trap war → a click on it hangs /\n // dismisses) AND the modal centers with a CSS transform that breaks a fixed-\n // positioned child's coords. A NON-modal Radix Popover (the filter builder,\n // promote-sprint, mark-duplicate — `data-surface=\"popover\"`) carries\n // `role=\"dialog\"` for a11y but does NOT trap focus, so it portals safely —\n // keyed on the modal surface, NOT `role`, so those popovers escape the clip\n // too. Only render the portal once mounted, since createPortal needs document.body.\n const mounted = useMounted();\n // Viewport-anchored position for the portaled menu + the listbox's available\n // height, recomputed from the trigger rect on open / scroll / resize.\n const [menuStyle, setMenuStyle] = useState<CSSProperties | null>(null);\n const [listMaxHeight, setListMaxHeight] = useState(256);\n // Inline (in-dialog) branch only: whether the menu is flipped ABOVE the\n // trigger because there's more room there than below within the dialog's\n // overflow-hidden clip box (bug-combobox-menu-clipped-inside-modal).\n const [inlineAbove, setInlineAbove] = useState(false);\n const baseId = useId();\n const listId = `${baseId}-listbox`;\n const optionId = (i: number) => `${baseId}-opt-${i}`;\n\n const containerRef = useRef<HTMLDivElement>(null);\n const triggerRef = useRef<HTMLButtonElement>(null);\n const inputRef = useRef<HTMLInputElement>(null);\n const listRef = useRef<HTMLDivElement>(null);\n const menuRef = useRef<HTMLDivElement>(null);\n\n const filtered = useMemo(() => {\n // Server-driven mode: `options` is already the query's result — never filter\n // it again client-side (a substring re-filter would hide trigram / token\n // matches the server legitimately returned).\n if (serverFiltered || !searchable || query.trim() === '') return options;\n const needle = query.trim().toLowerCase();\n return options.filter(\n (o) =>\n o.label.toLowerCase().includes(needle) ||\n (o.secondary ? o.secondary.toLowerCase().includes(needle) : false) ||\n (o.keywords ? o.keywords.toLowerCase().includes(needle) : false),\n );\n }, [options, query, searchable, serverFiltered]);\n\n const selected = value != null ? (options.find((o) => o.value === value) ?? null) : null;\n\n // The active row, clamped to the (possibly just-filtered) list — DERIVED, not\n // an effect, so a shrinking filter never needs a setState-in-effect.\n const active = filtered.length > 0 ? Math.min(activeIndex, filtered.length - 1) : 0;\n\n // Anchor the portaled menu to the trigger in VIEWPORT coordinates (position:\n // fixed), flipping above the trigger when there's more room there, and capping\n // the listbox height to the available space so it never runs off-screen.\n const updatePosition = useCallback(() => {\n const trigger = triggerRef.current;\n if (!trigger) return;\n const rect = trigger.getBoundingClientRect();\n const gap = 4; // matches the old mt-1\n const viewportH = window.innerHeight;\n const spaceBelow = viewportH - rect.bottom - gap;\n const spaceAbove = rect.top - gap;\n const placeBelow = spaceBelow >= spaceAbove;\n const avail = Math.max(120, placeBelow ? spaceBelow : spaceAbove);\n // Reserve room for the optional search input + container padding.\n setListMaxHeight(Math.max(80, Math.min(256, avail - (searchable ? 52 : 12))));\n const style: CSSProperties = {\n position: 'fixed',\n left: Math.round(rect.left),\n minWidth: Math.round(rect.width),\n };\n if (placeBelow) style.top = Math.round(rect.bottom + gap);\n else style.bottom = Math.round(viewportH - rect.top + gap);\n setMenuStyle(style);\n }, [searchable]);\n\n // The in-dialog branch renders INLINE (an absolute child of the trigger),\n // so — unlike the body-portaled branch — it cannot escape the modal's\n // `overflow-hidden` panel: an absolute child contributes nothing to the\n // dialog body's scroll height, so a menu taller than the room below the\n // trigger is simply CLIPPED, not scrolled-to (bug-combobox-menu-clipped-\n // inside-modal). Mirror the portaled branch's logic against the DIALOG's\n // clip box instead of the viewport: measure the space above/below the\n // trigger inside the dialog, flip to whichever side is taller, and clamp the\n // listbox height to it so the menu scrolls INTERNALLY and always fits.\n const updateInlinePosition = useCallback(() => {\n const trigger = triggerRef.current;\n if (!trigger) return;\n // Find the nearest ancestor that CLIPS the inline menu. If that ancestor\n // SCROLLS (overflow auto/scroll — e.g. the Advanced-filter popover's\n // `overflow-y-auto` body, or a `Modal.Body`), the menu can be scrolled into\n // view, so leave it exactly as before (top-full, max-h-64) — flipping/\n // clamping against a scroll box would wrongly shove the menu up under the\n // panel's header (the regression the first cut caused). Only a NON-scrolling\n // `overflow: hidden` clip — the centered Modal panel's `overflow-hidden\n // max-h-[90vh]` box — actually traps the menu (bug-combobox-menu-clipped-\n // inside-modal); clamp + flip against THAT box.\n const clip = nearestClipBox(trigger);\n if (!clip || clip.scrolls) {\n setInlineAbove(false);\n setListMaxHeight(256); // == the max-h-64 fallback (original behaviour)\n return;\n }\n const rect = trigger.getBoundingClientRect();\n const clipRect = clip.box.getBoundingClientRect();\n const gap = 4; // matches mt-1 / mb-1\n const inset = 8; // stay a hair inside the panel's rounded clip edge\n const spaceBelow = clipRect.bottom - rect.bottom - gap - inset;\n const spaceAbove = rect.top - clipRect.top - gap - inset;\n const placeAbove = spaceAbove > spaceBelow;\n setInlineAbove(placeAbove);\n const avail = Math.max(80, placeAbove ? spaceAbove : spaceBelow);\n // Reserve room for the optional search input + container padding (same\n // budget as the portaled branch).\n setListMaxHeight(Math.max(80, Math.min(256, avail - (searchable ? 52 : 12))));\n }, [searchable]);\n\n // On open: focus the right control (the only side effect — query/active reset\n // happens in openMenu so this effect never calls setState).\n useEffect(() => {\n if (!(open && mounted)) return;\n const t = setTimeout(() => {\n if (searchable) inputRef.current?.focus();\n else listRef.current?.focus();\n }, 0);\n return () => clearTimeout(t);\n }, [open, mounted, searchable]);\n\n // Position the menu before paint, and keep it glued to the trigger while open\n // (ancestor scroll uses capture so a scrolling table re-anchors the menu).\n // Skip entirely for the inline (in-dialog) branch — it positions via CSS.\n useIsomorphicLayoutEffect(() => {\n if (!(open && mounted)) return;\n if (triggerRef.current?.closest('[data-surface=\"modal\"],[aria-modal=\"true\"]')) return;\n updatePosition();\n const onReflow = () => updatePosition();\n window.addEventListener('scroll', onReflow, true);\n window.addEventListener('resize', onReflow);\n return () => {\n window.removeEventListener('scroll', onReflow, true);\n window.removeEventListener('resize', onReflow);\n };\n }, [open, mounted, updatePosition, filtered.length]);\n\n // Inline (in-dialog) counterpart: measure the menu against the dialog's clip\n // box before paint and keep it clamped while open (re-measure on resize, and\n // on capture-phase scroll so a scrolling dialog body re-anchors it).\n useIsomorphicLayoutEffect(() => {\n if (!(open && mounted)) return;\n if (!triggerRef.current?.closest('[data-surface=\"modal\"],[aria-modal=\"true\"]')) return;\n updateInlinePosition();\n const onReflow = () => updateInlinePosition();\n window.addEventListener('scroll', onReflow, true);\n window.addEventListener('resize', onReflow);\n return () => {\n window.removeEventListener('scroll', onReflow, true);\n window.removeEventListener('resize', onReflow);\n };\n }, [open, mounted, updateInlinePosition, filtered.length]);\n\n // Click-outside closes, restoring focus to the trigger. The menu is portaled\n // out of containerRef, so a click on it must also count as \"inside\".\n useEffect(() => {\n if (!open) return;\n function onDocMouseDown(e: MouseEvent) {\n const target = e.target as Node;\n if (containerRef.current?.contains(target) || menuRef.current?.contains(target)) return;\n closeMenu();\n }\n document.addEventListener('mousedown', onDocMouseDown);\n return () => document.removeEventListener('mousedown', onDocMouseDown);\n }, [open]); // eslint-disable-line react-hooks/exhaustive-deps\n\n // Keep the active row in view.\n useEffect(() => {\n if (open) document.getElementById(optionId(active))?.scrollIntoView({ block: 'nearest' });\n }, [active, open]); // eslint-disable-line react-hooks/exhaustive-deps\n\n function openMenu() {\n if (disabled) return;\n // Uncontrolled mode resets its own type-ahead on open; server-driven mode\n // leaves the query to the parent (it owns the lifecycle — resets on form\n // open / relationship change), so reopening keeps the current results.\n if (!serverFiltered) setInternalQuery('');\n const sel = value != null ? options.findIndex((o) => o.value === value) : -1;\n setActiveIndex(sel >= 0 ? sel : 0);\n setOpen(true);\n }\n\n // Close the menu, notifying the consumer (Subtask 2.5.5's inline-edit cell\n // leaves edit mode on close). Every close path funnels through here.\n function closeMenu() {\n setOpen(false);\n onClose?.();\n }\n\n function closeAndRefocus() {\n closeMenu();\n triggerRef.current?.focus();\n }\n\n function commit(i: number) {\n if (disabled) return;\n const opt = filtered[i];\n if (!opt || opt.disabled) return;\n onChange(opt.value);\n closeAndRefocus();\n }\n\n function onListKeyDown(e: React.KeyboardEvent) {\n if (e.key === 'ArrowDown') {\n e.preventDefault();\n setActiveIndex(Math.min(active + 1, filtered.length - 1));\n } else if (e.key === 'ArrowUp') {\n e.preventDefault();\n setActiveIndex(Math.max(active - 1, 0));\n } else if (e.key === 'Home') {\n e.preventDefault();\n setActiveIndex(0);\n } else if (e.key === 'End') {\n e.preventDefault();\n setActiveIndex(filtered.length - 1);\n } else if (e.key === 'Enter') {\n e.preventDefault();\n commit(active);\n } else if (e.key === 'Escape') {\n e.preventDefault();\n closeAndRefocus();\n }\n }\n\n function onTriggerKeyDown(e: React.KeyboardEvent) {\n if (!open && (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ')) {\n e.preventDefault();\n openMenu();\n } else if (open && e.key === 'Escape') {\n // The open menu takes focus from a `setTimeout(0)` (the on-open effect),\n // so an Escape pressed before that fires lands HERE, not on the list.\n // Ignoring it left the menu open, and the next click on the trigger then\n // toggled it SHUT — the picker closed for good (MOTIR-7345). Focus is\n // already on the trigger, so close without moving it.\n e.preventDefault();\n closeMenu();\n }\n }\n\n const activeId = filtered.length > 0 ? optionId(active) : undefined;\n // A `role=\"listbox\"` MUST own `role=\"option\"` children, so the listbox role\n // only applies when options are actually rendered. While loading or with no\n // matches the panel holds a status message instead — present it as a\n // `role=\"status\"` live region (announced to AT, and not an empty/childless\n // listbox, which is a critical `aria-required-children` axe violation). The\n // empty state is the link picker's DEFAULT on-open state since the 6.9.2\n // query-driven retrofit, so this is now reachable on every open.\n const hasOptions = !loading && filtered.length > 0;\n\n // Portal out to escape a short table's overflow:hidden — UNLESS we're inside a\n // focus-trapping dialog, where an inline menu is required (see the `mounted`\n // comment above).\n //\n // ⚠️ This is SETTLED IN A LAYOUT EFFECT, not read during render. It used to be\n // read during render on the stated invariant that \"the only ref-null case is\n // autoOpen, used solely by inline-edit cells (always in a table, never a\n // dialog)\". That invariant no longer holds: the quick-view peek's editable\n // rail (MOTIR-2563) is an `autoOpen` caller INSIDE the modal. The render-time\n // read fails permanently for it — `useMounted` returns `true` on the very\n // first client render, `triggerRef.current` is still null at that point, and\n // `subscribeNoop` means nothing ever re-renders to correct it. The menu then\n // portals to <body>, lands outside the dialog's focus scope, and every one of\n // its options is `aria-hidden` to assistive tech and to the accessibility\n // tree. A layout effect runs after the ref attaches and before paint, so the\n // branch is right for click-opened AND mount-opened menus alike.\n const [inDialog, setInDialog] = useState(false);\n useIsomorphicLayoutEffect(() => {\n setInDialog(!!triggerRef.current?.closest('[data-surface=\"modal\"],[aria-modal=\"true\"]'));\n }, [open]);\n\n const menuInner = (\n <>\n {searchable ? (\n <input\n ref={inputRef}\n type=\"text\"\n role=\"combobox\"\n aria-expanded\n aria-controls={listId}\n aria-activedescendant={activeId}\n aria-label={searchPlaceholder}\n value={query}\n onChange={(e) => {\n setQuery(e.target.value);\n setActiveIndex(0);\n }}\n onKeyDown={onListKeyDown}\n placeholder={searchPlaceholder}\n className=\"border-(--el-border) bg-(--el-page-bg) mb-1 w-full rounded-(--radius-input) border px-(--spacing-control-x) py-(--spacing-control-y) text-sm focus-visible:outline-none\"\n />\n ) : null}\n <div\n ref={listRef}\n id={listId}\n role={hasOptions ? 'listbox' : 'status'}\n aria-label={label}\n tabIndex={hasOptions && !searchable ? 0 : -1}\n aria-activedescendant={hasOptions && !searchable ? activeId : undefined}\n onKeyDown={hasOptions && !searchable ? onListKeyDown : undefined}\n // Both branches cap height to their measured available space (inline\n // style wins over the max-h-64 fallback): the portaled menu against the\n // viewport, the inline (in-dialog) menu against the dialog's clip box —\n // so an over-tall list scrolls internally instead of being clipped.\n style={{ maxHeight: listMaxHeight }}\n className=\"max-h-64 overflow-y-auto focus:outline-none\"\n >\n {loading ? (\n <p className=\"text-(--el-text-muted) px-2.5 py-2 text-sm\">{loadingText}</p>\n ) : filtered.length === 0 ? (\n <p className=\"text-(--el-text-muted) px-2.5 py-2 text-sm\">{emptyText}</p>\n ) : (\n filtered.map((opt, i) => {\n const isSelected = opt.value === value;\n const isActive = i === active;\n // A non-interactive group header at each group transition (in list\n // order, so a fully-filtered group's header drops with it). Not an\n // option row — keyboard nav over `filtered` is unaffected.\n const header =\n opt.group !== undefined && opt.group !== filtered[i - 1]?.group ? (\n <div\n key={`group-${opt.group}`}\n role=\"presentation\"\n className=\"px-(--spacing-control-x) pt-2 pb-1 font-mono text-[11px] font-semibold tracking-wider text-(--el-text-eyebrow) uppercase\"\n >\n {opt.group}\n </div>\n ) : null;\n return (\n <Fragment key={opt.value}>\n {header}\n <div\n id={optionId(i)}\n role=\"option\"\n aria-selected={isSelected}\n aria-disabled={opt.disabled ? true : undefined}\n onMouseEnter={() => setActiveIndex(i)}\n onClick={() => commit(i)}\n className={cn(\n 'flex items-center gap-2 rounded-(--radius-control) px-(--spacing-control-x) py-(--spacing-control-y) text-sm',\n opt.disabled ? 'cursor-default' : 'cursor-pointer',\n isActive ? 'bg-(--el-option-active-bg)' : '',\n opt.disabled ? 'text-(--el-text-secondary)' : 'text-(--el-text)',\n )}\n >\n {opt.icon ? (\n <span aria-hidden className={ICON_SLOT}>\n {opt.icon}\n </span>\n ) : null}\n {opt.description ? (\n <span className=\"flex min-w-0 flex-col\">\n <span className=\"truncate\">{opt.label}</span>\n <span className=\"text-(--el-text-secondary) max-w-[18rem] text-xs leading-snug whitespace-normal\">\n {opt.description}\n </span>\n </span>\n ) : (\n <span className=\"truncate\">{opt.label}</span>\n )}\n {opt.secondary ? (\n // --el-text-identifier (= --color-slate, the -secondary\n // weight), NOT -muted: muted (#787671) fails AA (4.16:1) on\n // the panel surface (#f6f5f4) at this 12px size (the\n // sidebar-caption contrast lesson) — identifier/slate clears it.\n <span className=\"text-(--el-text-identifier) ml-auto truncate text-xs\">\n {opt.secondary}\n </span>\n ) : null}\n {opt.trailing ? (\n // Rich trailing content (a Pill / chip). Pinned right; takes\n // the `ml-auto` itself when there is no `secondary` before it.\n <span\n className={cn(\n 'flex shrink-0 items-center gap-1.5',\n opt.secondary ? 'ml-1.5' : 'ml-auto',\n )}\n >\n {opt.trailing}\n </span>\n ) : null}\n {isSelected ? (\n // The selected-row check uses the accent (matching\n // MultiSelectPicker) — unifies the check-icon colour across\n // the two pickers (was bare ink here, accent there).\n <Check\n className=\"ml-1 h-4 w-4 shrink-0 text-(--el-accent-on-surface)\"\n aria-hidden\n />\n ) : null}\n </div>\n </Fragment>\n );\n })\n )}\n </div>\n {footer ? (\n <p className=\"border-(--el-border-soft) mt-1 max-w-[18rem] border-t px-(--spacing-control-x) pt-2 pb-1 text-xs leading-relaxed text-(--el-text-secondary)\">\n {footer}\n </p>\n ) : null}\n </>\n );\n\n // The shared menu panel. In a dialog: an inline absolute panel (the original,\n // proven layout). Otherwise: a body-portaled panel with viewport-anchored\n // fixed positioning that escapes every overflow ancestor.\n const menu = inDialog ? (\n <div\n ref={menuRef}\n data-menu-surface=\"\"\n className={cn(\n 'absolute left-0 z-50 w-max min-w-full max-w-[18rem] rounded-(--radius-card) bg-(--el-page-bg) p-1',\n 'shadow-(--shadow-elevated) border border-(--el-border)',\n // Flip above the trigger when the dialog has more room there, so a tall\n // list near the modal's bottom edge isn't clipped (clamped + scrolled).\n inlineAbove ? 'bottom-full mb-1' : 'top-full mt-1',\n )}\n >\n {menuInner}\n </div>\n ) : (\n <div\n ref={menuRef}\n data-menu-surface=\"\"\n // Width sizes to the widest option but never narrower than the trigger\n // (minWidth set in updatePosition) and is capped so a long label can't run\n // off-screen. Hidden until positioned to avoid a first-paint flash at 0,0.\n style={menuStyle ?? { position: 'fixed', visibility: 'hidden' }}\n className={cn(\n 'z-50 w-max max-w-[18rem] rounded-(--radius-card) bg-(--el-page-bg) p-1',\n 'shadow-(--shadow-elevated) border border-(--el-border)',\n )}\n >\n {menuInner}\n </div>\n );\n\n return (\n // `data-inner-dismiss` while the menu is open — see MultiSelectPicker:\n // an enclosing Radix layer's onEscapeKeyDown checks it so Esc closes the\n // MENU (our own handler) instead of dismissing the whole layer.\n <div ref={containerRef} data-inner-dismiss={open ? true : undefined} className=\"relative\">\n <button\n ref={triggerRef}\n type=\"button\"\n id={id}\n role=\"combobox\"\n aria-haspopup=\"listbox\"\n aria-expanded={open}\n aria-controls={open ? listId : undefined}\n aria-label={label}\n disabled={disabled}\n onClick={() => (open ? closeMenu() : openMenu())}\n onKeyDown={onTriggerKeyDown}\n className={cn(\n 'border-(--el-border) bg-(--el-page-bg) flex h-(--height-control) w-full items-center gap-2 rounded-(--radius-input) border px-(--spacing-control-x) text-sm',\n 'focus-visible:ring-(--focus-ring-color) focus-visible:outline-none focus-visible:ring-2',\n 'disabled:opacity-50',\n className,\n )}\n >\n {selected ? (\n <>\n {selected.icon ? (\n <span aria-hidden className={ICON_SLOT}>\n {selected.icon}\n </span>\n ) : null}\n <span className=\"text-(--el-text) truncate\">{selected.label}</span>\n {selected.secondary ? (\n // identifier (= slate, the -secondary weight): AA on the trigger surface at 12px (as above).\n <span className=\"text-(--el-text-identifier) ml-auto truncate text-xs\">\n {selected.secondary}\n </span>\n ) : null}\n </>\n ) : (\n <span className=\"text-(--el-text-muted) truncate\">{placeholder}</span>\n )}\n <ChevronsUpDown className=\"text-(--el-icon-muted) ml-auto h-4 w-4 shrink-0\" aria-hidden />\n </button>\n\n {open ? (inDialog ? menu : mounted ? createPortal(menu, document.body) : null) : null}\n </div>\n );\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../../src/utils/cn.ts","../../../src/utils/fullscreen.ts","../../../src/components/ui/Combobox.tsx"],"names":["useSyncExternalStore","rect","gap","inset","spaceBelow","spaceAbove","placeAbove","avail","menu","Fragment"],"mappings":"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","file":"Combobox.js","sourcesContent":["import { clsx, type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\n/**\n * Merge Tailwind class names without conflicts.\n *\n * `clsx` handles conditional inclusion (booleans, undefineds, arrays);\n * `twMerge` resolves conflicting Tailwind classes (later wins, e.g.\n * `px-2 px-4` → `px-4`). Together they are the de facto standard for\n * composing Tailwind classes in React components.\n *\n * @example\n * cn('px-2 py-1', isActive && 'bg-primary', className)\n */\nexport function cn(...inputs: ClassValue[]): string {\n return twMerge(clsx(inputs));\n}\n","'use client';\n\nimport { useSyncExternalStore } from 'react';\n\n/**\n * The element currently in NATIVE full screen (`document.fullscreenElement`), or\n * `null` — re-read on every `fullscreenchange`.\n *\n * While an element is in native full screen the browser paints ONLY that\n * element's subtree, in the top layer. Anything portalled to `document.body` —\n * a dialog, a popover, a picker's menu — is not shown at all, whatever its\n * z-index (MOTIR-7658: the roadmap canvas's quick view opened invisibly and\n * held focus and scroll lock until Esc). So every portalling primitive in this\n * package mounts into this element while there is one, and into\n * `document.body` otherwise — exactly as before.\n *\n * `null` on the server and in a document with no Fullscreen API.\n */\nexport function useFullscreenElement(): Element | null {\n return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);\n}\n\nfunction subscribe(onChange: () => void): () => void {\n document.addEventListener('fullscreenchange', onChange);\n return () => document.removeEventListener('fullscreenchange', onChange);\n}\n\nfunction getSnapshot(): Element | null {\n return document.fullscreenElement ?? null;\n}\n\nfunction getServerSnapshot(): null {\n return null;\n}\n","'use client';\n\nimport {\n Fragment,\n useCallback,\n useEffect,\n useId,\n useLayoutEffect,\n useMemo,\n useRef,\n useState,\n useSyncExternalStore,\n type CSSProperties,\n type ReactNode,\n} from 'react';\nimport { createPortal } from 'react-dom';\nimport { Check, ChevronsUpDown } from 'lucide-react';\nimport { cn } from '../../utils/cn';\nimport { useFullscreenElement } from '../../utils/fullscreen';\n\n// Run layout effects on the client, fall back to useEffect during SSR (the menu\n// only mounts client-side anyway — see the `mounted` gate below).\nconst useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;\n\n// `false` during SSR / first hydration render, `true` on the client thereafter —\n// gates the createPortal call so document.body is guaranteed present. Hydration-\n// safe (matches the server's `false`) and avoids setState-in-effect.\nconst subscribeNoop = () => () => {};\n\n/**\n * The LEADING-VISUAL slot, on the trigger and on every option row (MOTIR-3080).\n *\n * ⚠️ `inline-flex` is load-bearing, not decoration. The wrapper is a flex item of\n * its row, so IT is blockified — but whatever a caller passes INSIDE it is not,\n * and a plain `<span>` therefore stays `display: inline`, where width and height\n * do not apply at all. `StatusPicker`'s 10px status dot rendered as a 2 × 18\n * sliver for exactly that reason: three cards of palette work (MOTIR-1273 /\n * -2073 / -2075) measured that dot's HUE while its box was two pixels wide.\n *\n * It survived every check because the markup was right — the classes ask for\n * 10 × 10, `getComputedStyle` reports 10px, and only the USED value disagreed.\n * A used value exists only in a browser, so `tests/e2e/issue-detail-flow` MEASURES\n * it rather than asserting the element exists.\n *\n * Making the slot a flex CONTAINER blockifies its child, which is what the slot's\n * contract (\"a leading visual\") always implied. A lucide `<svg>` is replaced and\n * was never affected, so no existing caller's icon changes size.\n */\nconst ICON_SLOT = 'inline-flex shrink-0 items-center';\n\nfunction useMounted() {\n return useSyncExternalStore(\n subscribeNoop,\n () => true,\n () => false,\n );\n}\n\n/**\n * Combobox — an accessible select/combobox primitive (Subtask 2.3.4). A trigger\n * button (`role=\"combobox\"`, `aria-haspopup=\"listbox\"`) opens an anchored panel\n * holding an optional type-ahead filter and a `role=\"listbox\"` of\n * `role=\"option\"` rows. The active option is tracked with `aria-activedescendant`\n * on whichever control holds focus (the filter input when `searchable`, else the\n * listbox) — the CommandPalette pattern that already clears the STRICT axe sweep.\n *\n * Deliberately NOT built on the Radix Popover primitive: that injects\n * `aria-haspopup=\"dialog\"` + dialog focus semantics onto its trigger, which\n * conflicts with the listbox combobox pattern. This is a self-contained anchored\n * dropdown (click-outside + Escape + focus return handled here) so the ARIA is\n * exactly the WAI-ARIA combobox shape.\n *\n * Composed by `components/issues/TypePicker` (searchable=false, 5 options) and\n * `ParentPicker` (searchable, async candidate list).\n */\nexport interface ComboboxOption<T extends string> {\n value: T;\n /** Primary text — also the accessible name of the option. */\n label: string;\n /** Extra text matched by the filter (e.g. a PROD-N identifier). */\n keywords?: string;\n /** Leading visual (a kind icon); decorative — label carries the name. */\n icon?: ReactNode;\n /** Trailing muted text (e.g. the identifier). */\n secondary?: string;\n /**\n * A one-line description drawn UNDER the label in the menu row only — never in\n * the trigger, which stays the label alone (the workspace role picker's\n * \"what this role does\" line, MOTIR-6465). Decorative for the accessible name,\n * which stays `label`.\n */\n description?: string;\n /** Trailing rich content pinned to the option row's far end (e.g. a state\n * Pill or a \"Linked to …\" chip in the PR-link picker, MOTIR-1596).\n * Decorative — the accessible name is `label`; renders after `secondary`. */\n trailing?: ReactNode;\n /**\n * Optional section label. When consecutive options carry different `group`\n * values, a non-interactive header row is rendered at each transition (the\n * advanced filter's \"Fields\" / \"Custom fields\" / \"Other\" field menu). Pass\n * options pre-sorted by group; headers follow list order, so a group whose\n * every option is filtered out disappears with it. Decorative — not part of\n * the listbox's option indices, so keyboard nav is unaffected.\n */\n group?: string;\n /**\n * The option is shown but cannot be picked (MOTIR-5528 — a status move an\n * approval holds). It stays FOCUSABLE and is announced as unavailable\n * (`aria-disabled`), rather than skipped, so a keyboard reader learns the move\n * exists and why it is held; a click or Enter on it does nothing. Its label\n * reads in `--el-text-secondary`, never an opacity dim, which keeps AA.\n */\n disabled?: boolean;\n}\n\nexport interface ComboboxProps<T extends string> {\n options: ComboboxOption<T>[];\n value: T | null;\n onChange: (value: T) => void;\n /** Accessible name for the trigger + listbox. */\n label: string;\n /** Trigger text when nothing is selected. */\n placeholder?: string;\n searchable?: boolean;\n searchPlaceholder?: string;\n /** Empty-listbox content. A string, or rich nodes (e.g. a \"no matches\" line\n * plus a hint sub-line — use block `<span>`s, since this renders in a `<p>`). */\n emptyText?: ReactNode;\n loading?: boolean;\n loadingText?: string;\n disabled?: boolean;\n /** id for the trigger button. */\n id?: string;\n className?: string;\n /**\n * Mount already-open (Subtask 2.5.5) — for an inline-edit cell that renders the\n * picker on a single click, so the menu is open immediately (mirrors\n * `DatePicker`'s `autoOpen`). Focus lands on the search input / listbox.\n */\n autoOpen?: boolean;\n /**\n * Fired whenever the menu transitions open → closed (a pick, Escape,\n * click-outside, or toggling the trigger). The inline-edit cell uses it to\n * leave edit mode and return to the static pill/avatar (Subtask 2.5.5).\n */\n onClose?: () => void;\n /**\n * Server-driven search mode (Subtask 6.9.2 — the link picker). When\n * `onQueryChange` is provided the type-ahead query is CONTROLLED by the parent\n * (`query`) and client-side filtering is SKIPPED — `options` is taken as the\n * server's authoritative result for the current query (the parent debounces\n * its fetch off `onQueryChange`, the same contract as `MultiSelectPicker`).\n * Omit both for the default self-filtering behaviour. The parent owns the\n * query lifecycle, so opening the menu does NOT reset it.\n */\n query?: string;\n onQueryChange?: (query: string) => void;\n /**\n * A non-interactive note pinned BELOW the listbox, inside the open menu\n * (MOTIR-5582 — the Monitoring room's minimum-level control says, at the\n * action that causes it, that lowering also re-checks earlier issues). It is\n * not an option: keyboard navigation and the listbox's option indices are\n * unaffected, and it is announced as ordinary text. Omit it for no note.\n */\n footer?: ReactNode;\n}\n\n// Walk up from a node to the nearest ancestor that clips overflow, classifying\n// it as scrolling (overflow auto/scroll — the menu can be scrolled into view)\n// or a hard `hidden`/`clip` box (no scroll — the menu is trapped). Used by the\n// inline (in-dialog) branch to decide whether to clamp + flip the menu (only a\n// hard non-scrolling clip needs it). Stops at <body>; returns null if nothing\n// up the chain clips.\nfunction nearestClipBox(el: HTMLElement): { box: HTMLElement; scrolls: boolean } | null {\n let node = el.parentElement;\n while (node && node !== document.body) {\n const oy = getComputedStyle(node).overflowY;\n if (oy === 'auto' || oy === 'scroll') return { box: node, scrolls: true };\n if (oy === 'hidden' || oy === 'clip') return { box: node, scrolls: false };\n node = node.parentElement;\n }\n return null;\n}\n\n// The element a `position: fixed` descendant of `el` is laid out against, or\n// null when that is the viewport. A transform (the centered Modal panel's\n// `-translate-x/y-1/2`, which Tailwind v4 emits as the `translate` property), a\n// filter, a backdrop-filter (the glassmorphism material), `contain` or a\n// matching `will-change` all make an ancestor the containing block for fixed\n// descendants — so a fixed child of the Modal panel is positioned relative to\n// the PANEL, not the viewport.\nfunction fixedContainingBlock(el: HTMLElement): HTMLElement | null {\n let node = el.parentElement;\n while (node && node !== document.documentElement) {\n const s = getComputedStyle(node);\n const set = (v: string | undefined) => !!v && v !== 'none';\n if (\n set(s.transform) ||\n set(s.translate) ||\n set(s.scale) ||\n set(s.rotate) ||\n set(s.perspective) ||\n set(s.filter) ||\n set(s.backdropFilter) ||\n /\\b(paint|layout|strict|content)\\b/.test(s.contain || '') ||\n /\\b(transform|translate|scale|rotate|perspective|filter)\\b/.test(s.willChange || '')\n ) {\n return node;\n }\n node = node.parentElement;\n }\n return null;\n}\n\n// An element's padding box in viewport coordinates — the edge its overflow\n// clips at (and, for a containing block, the box fixed offsets are measured\n// from).\nfunction paddingBox(el: HTMLElement): { top: number; left: number; bottom: number } {\n const r = el.getBoundingClientRect();\n const top = r.top + el.clientTop;\n // happy-dom reports clientHeight 0; fall back to the border box there.\n const height = el.clientHeight || r.height;\n return { top, left: r.left + el.clientLeft, bottom: top + height };\n}\n\nexport function Combobox<T extends string>({\n options,\n value,\n onChange,\n label,\n placeholder = 'Select…',\n searchable = false,\n searchPlaceholder = 'Search…',\n emptyText = 'No matches',\n loading = false,\n loadingText = 'Loading…',\n disabled = false,\n id,\n className,\n autoOpen = false,\n onClose,\n query: controlledQuery,\n onQueryChange,\n footer,\n}: ComboboxProps<T>) {\n // A DISABLED combobox never opens — not even when it mounts with `autoOpen`\n // (MOTIR-6173). An inline editor that mounts open for an actor who may not\n // write used to hand them a live menu whose pick the server then refused\n // (MOTIR-4822); the trigger was disabled, the menu was not.\n const [open, setOpen] = useState(autoOpen && !disabled);\n // Server-driven mode (6.9.2): when `onQueryChange` is provided the query is\n // controlled by the parent and client filtering is bypassed (the server's\n // `options` are authoritative). Otherwise the query is internal state.\n const serverFiltered = onQueryChange !== undefined;\n const [internalQuery, setInternalQuery] = useState('');\n const query = serverFiltered ? (controlledQuery ?? '') : internalQuery;\n const setQuery = (q: string) => (serverFiltered ? onQueryChange(q) : setInternalQuery(q));\n const [activeIndex, setActiveIndex] = useState(0);\n // The menu is portaled to <body> by default, so it escapes any clipping\n // ancestor — a short table's overflow:hidden\n // (bug-inline-edit-clipped-when-table-short) OR the Advanced-filter popover's\n // overflow-hidden + overflow-y-auto body (MOTIR-1346). We render INLINE only\n // inside a focus-trapping MODAL (the shared Modal primitive — `data-surface=\n // \"modal\"`; `aria-modal` covers any other), where a portaled menu would land\n // outside the dialog's focus scope (focus-trap war → a click on it hangs /\n // dismisses) AND the modal centers with a CSS transform that breaks a fixed-\n // positioned child's coords. A NON-modal Radix Popover (the filter builder,\n // promote-sprint, mark-duplicate — `data-surface=\"popover\"`) carries\n // `role=\"dialog\"` for a11y but does NOT trap focus, so it portals safely —\n // keyed on the modal surface, NOT `role`, so those popovers escape the clip\n // too. Only render the portal once mounted, since createPortal needs document.body.\n const mounted = useMounted();\n // The portal target: the element in native full screen while there is one,\n // else <body> — the browser paints nothing outside a full-screen element\n // (MOTIR-7658).\n const fullscreenElement = useFullscreenElement();\n // Viewport-anchored position for the portaled menu + the listbox's available\n // height, recomputed from the trigger rect on open / scroll / resize.\n const [menuStyle, setMenuStyle] = useState<CSSProperties | null>(null);\n const [listMaxHeight, setListMaxHeight] = useState(256);\n // Inline (in-dialog) branch only: whether the menu is flipped ABOVE the\n // trigger because there's more room there than below within the dialog's\n // overflow-hidden clip box (bug-combobox-menu-clipped-inside-modal).\n const [inlineAbove, setInlineAbove] = useState(false);\n // Inline (in-dialog) branch, SCROLLING clip only (a `Modal.Body`): the fixed\n // offsets that lift the menu out of the scroll box while keeping it in the\n // DOM — and so inside the dialog's focus scope (MOTIR-7655). Null otherwise.\n const [inlineFixed, setInlineFixed] = useState<CSSProperties | null>(null);\n const baseId = useId();\n const listId = `${baseId}-listbox`;\n const optionId = (i: number) => `${baseId}-opt-${i}`;\n\n const containerRef = useRef<HTMLDivElement>(null);\n const triggerRef = useRef<HTMLButtonElement>(null);\n const inputRef = useRef<HTMLInputElement>(null);\n const listRef = useRef<HTMLDivElement>(null);\n const menuRef = useRef<HTMLDivElement>(null);\n\n const filtered = useMemo(() => {\n // Server-driven mode: `options` is already the query's result — never filter\n // it again client-side (a substring re-filter would hide trigram / token\n // matches the server legitimately returned).\n if (serverFiltered || !searchable || query.trim() === '') return options;\n const needle = query.trim().toLowerCase();\n return options.filter(\n (o) =>\n o.label.toLowerCase().includes(needle) ||\n (o.secondary ? o.secondary.toLowerCase().includes(needle) : false) ||\n (o.keywords ? o.keywords.toLowerCase().includes(needle) : false),\n );\n }, [options, query, searchable, serverFiltered]);\n\n const selected = value != null ? (options.find((o) => o.value === value) ?? null) : null;\n\n // The active row, clamped to the (possibly just-filtered) list — DERIVED, not\n // an effect, so a shrinking filter never needs a setState-in-effect.\n const active = filtered.length > 0 ? Math.min(activeIndex, filtered.length - 1) : 0;\n\n // Anchor the portaled menu to the trigger in VIEWPORT coordinates (position:\n // fixed), flipping above the trigger when there's more room there, and capping\n // the listbox height to the available space so it never runs off-screen.\n const updatePosition = useCallback(() => {\n const trigger = triggerRef.current;\n if (!trigger) return;\n const rect = trigger.getBoundingClientRect();\n const gap = 4; // matches the old mt-1\n const viewportH = window.innerHeight;\n const spaceBelow = viewportH - rect.bottom - gap;\n const spaceAbove = rect.top - gap;\n const placeBelow = spaceBelow >= spaceAbove;\n const avail = Math.max(120, placeBelow ? spaceBelow : spaceAbove);\n // Reserve room for the optional search input + container padding.\n setListMaxHeight(Math.max(80, Math.min(256, avail - (searchable ? 52 : 12))));\n const style: CSSProperties = {\n position: 'fixed',\n left: Math.round(rect.left),\n minWidth: Math.round(rect.width),\n };\n if (placeBelow) style.top = Math.round(rect.bottom + gap);\n else style.bottom = Math.round(viewportH - rect.top + gap);\n setMenuStyle(style);\n }, [searchable]);\n\n // The in-dialog branch renders INLINE (an absolute child of the trigger),\n // so — unlike the body-portaled branch — it cannot escape the modal's\n // `overflow-hidden` panel: an absolute child contributes nothing to the\n // dialog body's scroll height, so a menu taller than the room below the\n // trigger is simply CLIPPED, not scrolled-to (bug-combobox-menu-clipped-\n // inside-modal). Mirror the portaled branch's logic against the DIALOG's\n // clip box instead of the viewport: measure the space above/below the\n // trigger inside the dialog, flip to whichever side is taller, and clamp the\n // listbox height to it so the menu scrolls INTERNALLY and always fits.\n const updateInlinePosition = useCallback(() => {\n const trigger = triggerRef.current;\n if (!trigger) return;\n // Find the nearest ancestor that CLIPS the inline menu. A NON-scrolling\n // `overflow: hidden` clip — the centered Modal panel's `overflow-hidden\n // max-h-[90vh]` box — traps the menu, so clamp + flip against THAT box\n // (bug-combobox-menu-clipped-inside-modal). A SCROLLING clip lifts the menu\n // out with fixed positioning instead (MOTIR-7655, below), bounded above by\n // the scroll box's visible top so it never lands under the panel's header\n // (the regression the first clamp-against-the-scroll-box cut caused).\n const clip = nearestClipBox(trigger);\n if (!clip) {\n setInlineFixed(null);\n setInlineAbove(false);\n setListMaxHeight(256); // == the max-h-64 fallback (original behaviour)\n return;\n }\n if (clip.scrolls) {\n // A SCROLLING clip (`Modal.Body`, a popover's scroll body) does trap an\n // absolute menu after all: the menu extends the box's scroll area, the\n // part past its visible edge is cut, and the box grows a second\n // scrollbar — on a short dialog body that leaves one option visible\n // (MOTIR-7655). Lift the menu out instead: `position: fixed`, still in\n // the DOM. A fixed box is clipped only by the ancestors on its\n // containing-block chain, so the scroll box stops clipping it while the\n // panel (the containing block, through its transform) still bounds it.\n const rect = trigger.getBoundingClientRect();\n const gap = 4; // matches mt-1 / mb-1\n const inset = 8; // stay a hair inside the panel's rounded clip edge\n const cb = fixedContainingBlock(trigger);\n // The region the menu may paint in: never above the scroll box's visible\n // top (that is the dialog's header), and never past the viewport or any\n // clip at or above the containing block (the panel) — which lets the\n // menu overlay the dialog's footer, as a dropdown should.\n let top = Math.max(0, paddingBox(clip.box).top);\n let bottom = window.innerHeight;\n let node: HTMLElement | null = cb;\n while (node && node !== document.body && node !== document.documentElement) {\n const oy = getComputedStyle(node).overflowY;\n if (oy === 'hidden' || oy === 'clip' || oy === 'auto' || oy === 'scroll') {\n const b = paddingBox(node);\n top = Math.max(top, b.top);\n bottom = Math.min(bottom, b.bottom);\n }\n node = node.parentElement;\n }\n const spaceBelow = bottom - rect.bottom - gap - inset;\n const spaceAbove = rect.top - top - gap - inset;\n const placeAbove = spaceAbove > spaceBelow;\n const avail = Math.max(80, placeAbove ? spaceAbove : spaceBelow);\n // The menu's own chrome (search input, padding, footer note), measured\n // when it is laid out; the portaled branch's budget before that.\n const menu = menuRef.current;\n const list = listRef.current;\n const measured = menu && list ? menu.offsetHeight - list.offsetHeight : 0;\n const chrome = measured > 0 ? measured : searchable ? 52 : 12;\n setListMaxHeight(Math.max(80, Math.min(256, avail - chrome)));\n // Fixed offsets are measured from the containing block's padding box\n // (the viewport when there is none).\n const origin = cb ? paddingBox(cb) : { top: 0, left: 0, bottom: window.innerHeight };\n const style: CSSProperties = {\n position: 'fixed',\n left: Math.round(rect.left - origin.left),\n minWidth: Math.round(rect.width),\n };\n if (placeAbove) style.bottom = Math.round(origin.bottom - (rect.top - gap));\n else style.top = Math.round(rect.bottom + gap - origin.top);\n setInlineAbove(placeAbove);\n setInlineFixed(style);\n return;\n }\n setInlineFixed(null);\n const rect = trigger.getBoundingClientRect();\n const clipRect = clip.box.getBoundingClientRect();\n const gap = 4; // matches mt-1 / mb-1\n const inset = 8; // stay a hair inside the panel's rounded clip edge\n const spaceBelow = clipRect.bottom - rect.bottom - gap - inset;\n const spaceAbove = rect.top - clipRect.top - gap - inset;\n const placeAbove = spaceAbove > spaceBelow;\n setInlineAbove(placeAbove);\n const avail = Math.max(80, placeAbove ? spaceAbove : spaceBelow);\n // Reserve room for the optional search input + container padding (same\n // budget as the portaled branch).\n setListMaxHeight(Math.max(80, Math.min(256, avail - (searchable ? 52 : 12))));\n }, [searchable]);\n\n // On open: focus the right control (the only side effect — query/active reset\n // happens in openMenu so this effect never calls setState).\n useEffect(() => {\n if (!(open && mounted)) return;\n const t = setTimeout(() => {\n if (searchable) inputRef.current?.focus();\n else listRef.current?.focus();\n }, 0);\n return () => clearTimeout(t);\n }, [open, mounted, searchable]);\n\n // Position the menu before paint, and keep it glued to the trigger while open\n // (ancestor scroll uses capture so a scrolling table re-anchors the menu).\n // Skip entirely for the inline (in-dialog) branch — it positions via CSS.\n useIsomorphicLayoutEffect(() => {\n if (!(open && mounted)) return;\n if (triggerRef.current?.closest('[data-surface=\"modal\"],[aria-modal=\"true\"]')) return;\n updatePosition();\n const onReflow = () => updatePosition();\n window.addEventListener('scroll', onReflow, true);\n window.addEventListener('resize', onReflow);\n return () => {\n window.removeEventListener('scroll', onReflow, true);\n window.removeEventListener('resize', onReflow);\n };\n }, [open, mounted, updatePosition, filtered.length]);\n\n // Inline (in-dialog) counterpart: measure the menu against the dialog's clip\n // box before paint and keep it clamped while open (re-measure on resize, and\n // on capture-phase scroll so a scrolling dialog body re-anchors it).\n useIsomorphicLayoutEffect(() => {\n if (!(open && mounted)) return;\n if (!triggerRef.current?.closest('[data-surface=\"modal\"],[aria-modal=\"true\"]')) return;\n updateInlinePosition();\n const onReflow = () => updateInlinePosition();\n window.addEventListener('scroll', onReflow, true);\n window.addEventListener('resize', onReflow);\n return () => {\n window.removeEventListener('scroll', onReflow, true);\n window.removeEventListener('resize', onReflow);\n };\n }, [open, mounted, updateInlinePosition, filtered.length]);\n\n // Click-outside closes, restoring focus to the trigger. The menu is portaled\n // out of containerRef, so a click on it must also count as \"inside\".\n useEffect(() => {\n if (!open) return;\n function onDocMouseDown(e: MouseEvent) {\n const target = e.target as Node;\n if (containerRef.current?.contains(target) || menuRef.current?.contains(target)) return;\n closeMenu();\n }\n document.addEventListener('mousedown', onDocMouseDown);\n return () => document.removeEventListener('mousedown', onDocMouseDown);\n }, [open]); // eslint-disable-line react-hooks/exhaustive-deps\n\n // Keep the active row in view.\n useEffect(() => {\n if (open) document.getElementById(optionId(active))?.scrollIntoView({ block: 'nearest' });\n }, [active, open]); // eslint-disable-line react-hooks/exhaustive-deps\n\n function openMenu() {\n if (disabled) return;\n // Uncontrolled mode resets its own type-ahead on open; server-driven mode\n // leaves the query to the parent (it owns the lifecycle — resets on form\n // open / relationship change), so reopening keeps the current results.\n if (!serverFiltered) setInternalQuery('');\n const sel = value != null ? options.findIndex((o) => o.value === value) : -1;\n setActiveIndex(sel >= 0 ? sel : 0);\n setOpen(true);\n }\n\n // Close the menu, notifying the consumer (Subtask 2.5.5's inline-edit cell\n // leaves edit mode on close). Every close path funnels through here.\n function closeMenu() {\n setOpen(false);\n onClose?.();\n }\n\n function closeAndRefocus() {\n closeMenu();\n triggerRef.current?.focus();\n }\n\n function commit(i: number) {\n if (disabled) return;\n const opt = filtered[i];\n if (!opt || opt.disabled) return;\n onChange(opt.value);\n closeAndRefocus();\n }\n\n function onListKeyDown(e: React.KeyboardEvent) {\n if (e.key === 'ArrowDown') {\n e.preventDefault();\n setActiveIndex(Math.min(active + 1, filtered.length - 1));\n } else if (e.key === 'ArrowUp') {\n e.preventDefault();\n setActiveIndex(Math.max(active - 1, 0));\n } else if (e.key === 'Home') {\n e.preventDefault();\n setActiveIndex(0);\n } else if (e.key === 'End') {\n e.preventDefault();\n setActiveIndex(filtered.length - 1);\n } else if (e.key === 'Enter') {\n e.preventDefault();\n commit(active);\n } else if (e.key === 'Escape') {\n e.preventDefault();\n closeAndRefocus();\n }\n }\n\n function onTriggerKeyDown(e: React.KeyboardEvent) {\n if (!open && (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ')) {\n e.preventDefault();\n openMenu();\n } else if (open && e.key === 'Escape') {\n // The open menu takes focus from a `setTimeout(0)` (the on-open effect),\n // so an Escape pressed before that fires lands HERE, not on the list.\n // Ignoring it left the menu open, and the next click on the trigger then\n // toggled it SHUT — the picker closed for good (MOTIR-7345). Focus is\n // already on the trigger, so close without moving it.\n e.preventDefault();\n closeMenu();\n }\n }\n\n const activeId = filtered.length > 0 ? optionId(active) : undefined;\n // A `role=\"listbox\"` MUST own `role=\"option\"` children, so the listbox role\n // only applies when options are actually rendered. While loading or with no\n // matches the panel holds a status message instead — present it as a\n // `role=\"status\"` live region (announced to AT, and not an empty/childless\n // listbox, which is a critical `aria-required-children` axe violation). The\n // empty state is the link picker's DEFAULT on-open state since the 6.9.2\n // query-driven retrofit, so this is now reachable on every open.\n const hasOptions = !loading && filtered.length > 0;\n\n // Portal out to escape a short table's overflow:hidden — UNLESS we're inside a\n // focus-trapping dialog, where an inline menu is required (see the `mounted`\n // comment above).\n //\n // ⚠️ This is SETTLED IN A LAYOUT EFFECT, not read during render. It used to be\n // read during render on the stated invariant that \"the only ref-null case is\n // autoOpen, used solely by inline-edit cells (always in a table, never a\n // dialog)\". That invariant no longer holds: the quick-view peek's editable\n // rail (MOTIR-2563) is an `autoOpen` caller INSIDE the modal. The render-time\n // read fails permanently for it — `useMounted` returns `true` on the very\n // first client render, `triggerRef.current` is still null at that point, and\n // `subscribeNoop` means nothing ever re-renders to correct it. The menu then\n // portals to <body>, lands outside the dialog's focus scope, and every one of\n // its options is `aria-hidden` to assistive tech and to the accessibility\n // tree. A layout effect runs after the ref attaches and before paint, so the\n // branch is right for click-opened AND mount-opened menus alike.\n const [inDialog, setInDialog] = useState(false);\n useIsomorphicLayoutEffect(() => {\n setInDialog(!!triggerRef.current?.closest('[data-surface=\"modal\"],[aria-modal=\"true\"]'));\n }, [open]);\n\n const menuInner = (\n <>\n {searchable ? (\n <input\n ref={inputRef}\n type=\"text\"\n role=\"combobox\"\n aria-expanded\n aria-controls={listId}\n aria-activedescendant={activeId}\n aria-label={searchPlaceholder}\n value={query}\n onChange={(e) => {\n setQuery(e.target.value);\n setActiveIndex(0);\n }}\n onKeyDown={onListKeyDown}\n placeholder={searchPlaceholder}\n className=\"border-(--el-border) bg-(--el-page-bg) mb-1 w-full rounded-(--radius-input) border px-(--spacing-control-x) py-(--spacing-control-y) text-sm focus-visible:outline-none\"\n />\n ) : null}\n <div\n ref={listRef}\n id={listId}\n role={hasOptions ? 'listbox' : 'status'}\n aria-label={label}\n tabIndex={hasOptions && !searchable ? 0 : -1}\n aria-activedescendant={hasOptions && !searchable ? activeId : undefined}\n onKeyDown={hasOptions && !searchable ? onListKeyDown : undefined}\n // Both branches cap height to their measured available space (inline\n // style wins over the max-h-64 fallback): the portaled menu against the\n // viewport, the inline (in-dialog) menu against the dialog's clip box —\n // so an over-tall list scrolls internally instead of being clipped.\n style={{ maxHeight: listMaxHeight }}\n className=\"max-h-64 overflow-y-auto focus:outline-none\"\n >\n {loading ? (\n <p className=\"text-(--el-text-muted) px-2.5 py-2 text-sm\">{loadingText}</p>\n ) : filtered.length === 0 ? (\n <p className=\"text-(--el-text-muted) px-2.5 py-2 text-sm\">{emptyText}</p>\n ) : (\n filtered.map((opt, i) => {\n const isSelected = opt.value === value;\n const isActive = i === active;\n // A non-interactive group header at each group transition (in list\n // order, so a fully-filtered group's header drops with it). Not an\n // option row — keyboard nav over `filtered` is unaffected.\n const header =\n opt.group !== undefined && opt.group !== filtered[i - 1]?.group ? (\n <div\n key={`group-${opt.group}`}\n role=\"presentation\"\n className=\"px-(--spacing-control-x) pt-2 pb-1 font-mono text-[11px] font-semibold tracking-wider text-(--el-text-eyebrow) uppercase\"\n >\n {opt.group}\n </div>\n ) : null;\n return (\n <Fragment key={opt.value}>\n {header}\n <div\n id={optionId(i)}\n role=\"option\"\n aria-selected={isSelected}\n aria-disabled={opt.disabled ? true : undefined}\n onMouseEnter={() => setActiveIndex(i)}\n onClick={() => commit(i)}\n className={cn(\n 'flex items-center gap-2 rounded-(--radius-control) px-(--spacing-control-x) py-(--spacing-control-y) text-sm',\n opt.disabled ? 'cursor-default' : 'cursor-pointer',\n isActive ? 'bg-(--el-option-active-bg)' : '',\n opt.disabled ? 'text-(--el-text-secondary)' : 'text-(--el-text)',\n )}\n >\n {opt.icon ? (\n <span aria-hidden className={ICON_SLOT}>\n {opt.icon}\n </span>\n ) : null}\n {opt.description ? (\n <span className=\"flex min-w-0 flex-col\">\n <span className=\"truncate\">{opt.label}</span>\n <span className=\"text-(--el-text-secondary) max-w-[18rem] text-xs leading-snug whitespace-normal\">\n {opt.description}\n </span>\n </span>\n ) : (\n <span className=\"truncate\">{opt.label}</span>\n )}\n {opt.secondary ? (\n // --el-text-identifier (= --color-slate, the -secondary\n // weight), NOT -muted: muted (#787671) fails AA (4.16:1) on\n // the panel surface (#f6f5f4) at this 12px size (the\n // sidebar-caption contrast lesson) — identifier/slate clears it.\n <span className=\"text-(--el-text-identifier) ml-auto truncate text-xs\">\n {opt.secondary}\n </span>\n ) : null}\n {opt.trailing ? (\n // Rich trailing content (a Pill / chip). Pinned right; takes\n // the `ml-auto` itself when there is no `secondary` before it.\n <span\n className={cn(\n 'flex shrink-0 items-center gap-1.5',\n opt.secondary ? 'ml-1.5' : 'ml-auto',\n )}\n >\n {opt.trailing}\n </span>\n ) : null}\n {isSelected ? (\n // The selected-row check uses the accent (matching\n // MultiSelectPicker) — unifies the check-icon colour across\n // the two pickers (was bare ink here, accent there).\n <Check\n className=\"ml-1 h-4 w-4 shrink-0 text-(--el-accent-on-surface)\"\n aria-hidden\n />\n ) : null}\n </div>\n </Fragment>\n );\n })\n )}\n </div>\n {footer ? (\n <p className=\"border-(--el-border-soft) mt-1 max-w-[18rem] border-t px-(--spacing-control-x) pt-2 pb-1 text-xs leading-relaxed text-(--el-text-secondary)\">\n {footer}\n </p>\n ) : null}\n </>\n );\n\n // The shared menu panel. In a dialog: an inline absolute panel (the original,\n // proven layout). Otherwise: a body-portaled panel with viewport-anchored\n // fixed positioning that escapes every overflow ancestor.\n const menu = inDialog ? (\n <div\n ref={menuRef}\n data-menu-surface=\"\"\n style={inlineFixed ?? undefined}\n className={cn(\n 'z-50 w-max max-w-[18rem] rounded-(--radius-card) bg-(--el-page-bg) p-1',\n 'shadow-(--shadow-elevated) border border-(--el-border)',\n // Lifted out of a scrolling clip (MOTIR-7655): positioned by\n // `inlineFixed`. Otherwise an absolute child of the trigger that flips\n // above it when the dialog has more room there, so a tall list near the\n // modal's bottom edge isn't clipped (clamped + scrolled).\n inlineFixed\n ? null\n : cn('absolute left-0 min-w-full', inlineAbove ? 'bottom-full mb-1' : 'top-full mt-1'),\n )}\n >\n {menuInner}\n </div>\n ) : (\n <div\n ref={menuRef}\n data-menu-surface=\"\"\n // Width sizes to the widest option but never narrower than the trigger\n // (minWidth set in updatePosition) and is capped so a long label can't run\n // off-screen. Hidden until positioned to avoid a first-paint flash at 0,0.\n style={menuStyle ?? { position: 'fixed', visibility: 'hidden' }}\n className={cn(\n 'z-50 w-max max-w-[18rem] rounded-(--radius-card) bg-(--el-page-bg) p-1',\n 'shadow-(--shadow-elevated) border border-(--el-border)',\n )}\n >\n {menuInner}\n </div>\n );\n\n return (\n // `data-inner-dismiss` while the menu is open — see MultiSelectPicker:\n // an enclosing Radix layer's onEscapeKeyDown checks it so Esc closes the\n // MENU (our own handler) instead of dismissing the whole layer.\n <div ref={containerRef} data-inner-dismiss={open ? true : undefined} className=\"relative\">\n <button\n ref={triggerRef}\n type=\"button\"\n id={id}\n role=\"combobox\"\n aria-haspopup=\"listbox\"\n aria-expanded={open}\n aria-controls={open ? listId : undefined}\n aria-label={label}\n disabled={disabled}\n onClick={() => (open ? closeMenu() : openMenu())}\n onKeyDown={onTriggerKeyDown}\n className={cn(\n 'border-(--el-border) bg-(--el-page-bg) flex h-(--height-control) w-full items-center gap-2 rounded-(--radius-input) border px-(--spacing-control-x) text-sm',\n 'focus-visible:ring-(--focus-ring-color) focus-visible:outline-none focus-visible:ring-2',\n 'disabled:opacity-50',\n className,\n )}\n >\n {selected ? (\n <>\n {selected.icon ? (\n <span aria-hidden className={ICON_SLOT}>\n {selected.icon}\n </span>\n ) : null}\n <span className=\"text-(--el-text) truncate\">{selected.label}</span>\n {selected.secondary ? (\n // identifier (= slate, the -secondary weight): AA on the trigger surface at 12px (as above).\n <span className=\"text-(--el-text-identifier) ml-auto truncate text-xs\">\n {selected.secondary}\n </span>\n ) : null}\n </>\n ) : (\n <span className=\"text-(--el-text-muted) truncate\">{placeholder}</span>\n )}\n <ChevronsUpDown className=\"text-(--el-icon-muted) ml-auto h-4 w-4 shrink-0\" aria-hidden />\n </button>\n\n {open\n ? inDialog\n ? menu\n : mounted\n ? createPortal(menu, fullscreenElement ?? document.body)\n : null\n : null}\n </div>\n );\n}\n"]}
|
|
@@ -73,8 +73,15 @@ interface ModalProps extends VariantProps<typeof contentVariants> {
|
|
|
73
73
|
* Defaults to `'Dialog'`. See `closeLabel` for the i18n rationale.
|
|
74
74
|
*/
|
|
75
75
|
dialogLabel?: string;
|
|
76
|
+
/**
|
|
77
|
+
* The element the dialog portals into. Defaults to the element in NATIVE full
|
|
78
|
+
* screen while there is one (`document.fullscreenElement` — the browser paints
|
|
79
|
+
* nothing outside it, so a body-portalled dialog would open invisibly,
|
|
80
|
+
* MOTIR-7658), else `document.body`. Pass `null` to force `document.body`.
|
|
81
|
+
*/
|
|
82
|
+
container?: Element | null;
|
|
76
83
|
}
|
|
77
|
-
declare function ModalRoot({ open, onOpenChange, title, description, size, hideClose, srTitle, className, overlayClassName, role, children, closeLabel, dialogLabel, }: ModalProps): react_jsx_runtime.JSX.Element;
|
|
84
|
+
declare function ModalRoot({ open, onOpenChange, title, description, size, hideClose, srTitle, className, overlayClassName, role, children, closeLabel, dialogLabel, container, }: ModalProps): react_jsx_runtime.JSX.Element;
|
|
78
85
|
declare const Modal: typeof ModalRoot & {
|
|
79
86
|
Body: react.ForwardRefExoticComponent<react.HTMLAttributes<HTMLDivElement> & react.RefAttributes<HTMLDivElement>>;
|
|
80
87
|
Footer: react.ForwardRefExoticComponent<react.HTMLAttributes<HTMLDivElement> & react.RefAttributes<HTMLDivElement>>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { forwardRef } from 'react';
|
|
2
|
+
import { forwardRef, useSyncExternalStore } from 'react';
|
|
3
3
|
import * as Dialog from '@radix-ui/react-dialog';
|
|
4
4
|
import { X } from 'lucide-react';
|
|
5
5
|
import { cva } from 'class-variance-authority';
|
|
@@ -10,6 +10,19 @@ import { jsx, jsxs } from 'react/jsx-runtime';
|
|
|
10
10
|
function cn(...inputs) {
|
|
11
11
|
return twMerge(clsx(inputs));
|
|
12
12
|
}
|
|
13
|
+
function useFullscreenElement() {
|
|
14
|
+
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
15
|
+
}
|
|
16
|
+
function subscribe(onChange) {
|
|
17
|
+
document.addEventListener("fullscreenchange", onChange);
|
|
18
|
+
return () => document.removeEventListener("fullscreenchange", onChange);
|
|
19
|
+
}
|
|
20
|
+
function getSnapshot() {
|
|
21
|
+
return document.fullscreenElement ?? null;
|
|
22
|
+
}
|
|
23
|
+
function getServerSnapshot() {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
13
26
|
var contentVariants = cva(
|
|
14
27
|
cn(
|
|
15
28
|
"fixed left-1/2 top-1/2 z-50 -translate-x-1/2 -translate-y-1/2",
|
|
@@ -65,9 +78,12 @@ function ModalRoot({
|
|
|
65
78
|
role,
|
|
66
79
|
children,
|
|
67
80
|
closeLabel = "Close",
|
|
68
|
-
dialogLabel = "Dialog"
|
|
81
|
+
dialogLabel = "Dialog",
|
|
82
|
+
container
|
|
69
83
|
}) {
|
|
70
|
-
|
|
84
|
+
const fullscreenElement = useFullscreenElement();
|
|
85
|
+
const portalContainer = container === void 0 ? fullscreenElement ?? void 0 : container;
|
|
86
|
+
return /* @__PURE__ */ jsx(Dialog.Root, { open, onOpenChange, children: /* @__PURE__ */ jsxs(Dialog.Portal, { container: portalContainer, children: [
|
|
71
87
|
/* @__PURE__ */ jsx(
|
|
72
88
|
Dialog.Overlay,
|
|
73
89
|
{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/utils/cn.ts","../../../src/components/ui/Modal.tsx"],"names":["ModalFooter","ModalBody"],"mappings":"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","file":"Modal.js","sourcesContent":["import { clsx, type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\n/**\n * Merge Tailwind class names without conflicts.\n *\n * `clsx` handles conditional inclusion (booleans, undefineds, arrays);\n * `twMerge` resolves conflicting Tailwind classes (later wins, e.g.\n * `px-2 px-4` → `px-4`). Together they are the de facto standard for\n * composing Tailwind classes in React components.\n *\n * @example\n * cn('px-2 py-1', isActive && 'bg-primary', className)\n */\nexport function cn(...inputs: ClassValue[]): string {\n return twMerge(clsx(inputs));\n}\n","'use client';\n\nimport { forwardRef, type ReactNode } from 'react';\nimport * as Dialog from '@radix-ui/react-dialog';\nimport { X } from 'lucide-react';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { cn } from '../../utils/cn';\n\n/**\n * Modal — accessible dialog wrapping @radix-ui/react-dialog.\n *\n * Radix handles focus trap, ESC-to-close, click-outside-to-close, and\n * focus-return-on-close out of the box. We style on top.\n *\n * Open/close is controlled by the consumer via `open` + `onOpenChange`.\n *\n * For a scrollable body with a pinned footer, wrap the fields in `Modal.Body`\n * (it owns the `flex-1 overflow-y-auto` scroll recipe and keeps focus rings\n * from clipping against the scroll edge — see `ModalBody`).\n *\n * @example\n * const [open, setOpen] = useState(false);\n * <Modal open={open} onOpenChange={setOpen} title=\"Confirm\" size=\"md\">\n * <Modal.Body className=\"gap-4\">\n * <Input label=\"Name\" autoFocus />\n * </Modal.Body>\n * <Modal.Footer>\n * <Button variant=\"ghost\" onClick={() => setOpen(false)}>Cancel</Button>\n * <Button variant=\"primary\" onClick={confirm}>Confirm</Button>\n * </Modal.Footer>\n * </Modal>\n */\nconst contentVariants = cva(\n cn(\n 'fixed left-1/2 top-1/2 z-50 -translate-x-1/2 -translate-y-1/2',\n 'w-[90vw] rounded-(--radius-modal) bg-(--el-page-bg)',\n 'shadow-(--shadow-modal) border border-(--el-border)',\n 'p-(--spacing-card-padding)',\n 'focus:outline-none',\n // Never exceed the viewport: cap height and lay out as a column so a\n // consumer can wrap its fields in `Modal.Body` (which owns the\n // `flex-1 overflow-y-auto` scroll recipe) and pin a `Modal.Footer`,\n // instead of the dialog growing off-screen (e.g. the create modal's\n // expandable Explanation pushing the Create button out of view).\n 'flex max-h-[90vh] flex-col overflow-hidden',\n ),\n {\n variants: {\n // Literal widths, NOT the max-w-sm/md/lg utilities: the design\n // system's @theme defines --spacing-sm/md/lg (12/16/20px), and\n // Tailwind v4 resolves `max-w-{key}` against the --spacing-* scale\n // when that key exists — so `max-w-md` would collapse the modal to\n // 16px wide. Pinning the rem values (Tailwind's stock sm/md/lg) keeps\n // the design-system token set untouched and the dialog readable.\n size: {\n sm: 'max-w-[24rem]',\n md: 'max-w-[28rem]',\n lg: 'max-w-[32rem]',\n // The large peek surface (Subtask 2.5.19's quick view) — a generous\n // dialog that takes a big part of the screen; pairs with a height +\n // p-0 className on the consumer side. Stock Tailwind 4xl rem value, so\n // the design-system --spacing-* scale stays untouched (see the comment\n // above on why we pin rems instead of max-w-{key}).\n xl: 'max-w-[58rem]',\n // The whole viewport (Subtask 5.2.6's attachment preview lightbox —\n // the 2.5.19 growth pattern one step further). Like `xl`, the variant\n // only lifts the size caps; the consumer pairs it with bg/border/\n // radius/padding overrides to drop the panel chrome it doesn't want.\n full: 'h-dvh max-h-dvh w-screen max-w-none',\n },\n },\n defaultVariants: { size: 'md' },\n },\n);\n\nexport interface ModalProps extends VariantProps<typeof contentVariants> {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n title?: string;\n description?: string;\n children?: ReactNode;\n /** Hide the default close (×) button in the corner. */\n hideClose?: boolean;\n /**\n * Accessible name for a dialog that renders its OWN visible heading (so no\n * `title` is passed). Radix requires a Dialog.Title; without `title`/`srTitle`\n * we fall back to the generic \"Dialog\". Supply this to label the dialog with\n * something meaningful (e.g. the previewed issue's key) while keeping the\n * heading inside `children` (Subtask 2.5.19's quick-view peek).\n */\n srTitle?: string;\n className?: string;\n /**\n * Extra classes merged onto the backdrop — e.g. the attachment preview\n * lightbox (Subtask 5.2.6) deepens the default `--el-overlay-scrim` to\n * `bg-black/80` per `design/work-items/attachments.mock.html` panel 6.\n */\n overlayClassName?: string;\n /**\n * The dialog's ARIA role. Defaults to Radix's `dialog`; a DESTRUCTIVE confirm\n * passes `alertdialog` (Subtask 2.8.4's delete confirm) so assistive tech\n * treats it as an alert that interrupts and announces its description.\n */\n role?: 'dialog' | 'alertdialog';\n /**\n * Accessible label for the corner close (×) button. Defaults to `'Close'`.\n * i18n is the consuming app's concern — the design-system package is\n * framework-agnostic (no `next-intl` dependency), so a localized app passes\n * the translated string here (motir-core wires it from its `common.close`).\n */\n closeLabel?: string;\n /**\n * Fallback accessible name when neither `title` nor `srTitle` is given.\n * Defaults to `'Dialog'`. See `closeLabel` for the i18n rationale.\n */\n dialogLabel?: string;\n}\n\nfunction ModalRoot({\n open,\n onOpenChange,\n title,\n description,\n size,\n hideClose,\n srTitle,\n className,\n overlayClassName,\n role,\n children,\n closeLabel = 'Close',\n dialogLabel = 'Dialog',\n}: ModalProps) {\n return (\n <Dialog.Root open={open} onOpenChange={onOpenChange}>\n <Dialog.Portal>\n {/* `data-surface=\"overlay\"` lets the glassmorphism style turn this dark\n scrim into a LIGHT, page-blurring backdrop, so the frosted modal\n refracts the (blurred) page behind it instead of darkness — the\n point of a glass modal. Inert for every other style. */}\n <Dialog.Overlay\n data-surface=\"overlay\"\n className={cn('fixed inset-0 z-40 bg-(--el-overlay-scrim)', overlayClassName)}\n />\n {/*\n PRODECT_FINDINGS #8: Radix checks for a describing element\n (Dialog.Description → aria-describedby) independently of the Title\n check. When no `description` prop is passed we render no\n Dialog.Description, so Radix logs a \"Missing Description or\n aria-describedby={undefined}\" warning. We pick the explicit opt-out\n (aria-describedby={undefined}) rather than emitting an empty sr-only\n Description — an empty description announces nothing useful and is\n worse for screen-reader users than declaring the dialog has none.\n */}\n <Dialog.Content\n className={cn(contentVariants({ size }), className)}\n // `data-surface` lets a surface-material style (glassmorphism) frost\n // the dialog panel — see globals.css's material layer.\n data-surface=\"modal\"\n // A destructive confirm opts into `alertdialog` (2.8.4); otherwise\n // Radix's default `dialog` role applies.\n {...(role ? { role } : {})}\n // Only opt out when there's no description. When `description` is\n // set we omit the prop entirely so Radix keeps auto-wiring\n // aria-describedby to the rendered Dialog.Description's id.\n {...(description ? {} : { 'aria-describedby': undefined })}\n >\n {title || description ? (\n <div className=\"mb-(--spacing-md) shrink-0\">\n {title ? (\n <Dialog.Title className=\"font-serif text-xl font-semibold text-(--el-text)\">\n {title}\n </Dialog.Title>\n ) : null}\n {description ? (\n <Dialog.Description className=\"text-(--el-text-subtitle) mt-1 font-sans text-sm\">\n {description}\n </Dialog.Description>\n ) : null}\n </div>\n ) : (\n // Radix requires Title for a11y; provide a visually-hidden one if\n // missing. `srTitle` lets a consumer with its own visible heading\n // still give the dialog a meaningful accessible name.\n <Dialog.Title className=\"sr-only\">{srTitle ?? dialogLabel}</Dialog.Title>\n )}\n {children}\n {!hideClose ? (\n <Dialog.Close\n aria-label={closeLabel}\n className=\"text-(--el-icon-muted) hover:text-(--el-text) absolute right-3 top-3 rounded-(--radius-control) p-(--spacing-icon-btn) transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--focus-ring-color)\"\n >\n <X className=\"h-4 w-4\" />\n </Dialog.Close>\n ) : null}\n </Dialog.Content>\n </Dialog.Portal>\n </Dialog.Root>\n );\n}\n\nconst ModalFooter = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n function ModalFooter({ className, children, ...rest }, ref) {\n return (\n <div\n ref={ref}\n className={cn(\n 'border-(--el-border) mt-(--spacing-md) flex items-center justify-end gap-2 border-t pt-(--spacing-md)',\n className,\n )}\n {...rest}\n >\n {children}\n </div>\n );\n },\n);\n\n/**\n * ModalBody — the scrollable field area of a modal. Owns the scroll recipe\n * once so consumers don't re-derive it (and re-introduce the focus-ring clip):\n * it fills the remaining column height (the panel caps at `max-h-[90vh]`) and\n * scrolls, so a sibling `Modal.Footer` stays pinned.\n *\n * Ring-safe by construction. `Input` draws its focus ring as\n * `focus-within:ring-2 … ring-offset-2` — a box-shadow extending ~4px OUTSIDE\n * the field's border box. Per the CSS spec, `overflow-y: auto` forces the\n * computed `overflow-x` from `visible` to `auto`, so this body is a clip box on\n * BOTH axes; a full-width field's ring overhang would be painted outside it and\n * silently clipped (rings are box-shadows — they never grow the layout box, so\n * nothing scrolls, the paint is just cut). We pad the scroll container by more\n * than the overhang (`p-1.5` = 6px > 4px) and pull an equal negative margin\n * (`-m-1.5`), so the ring gets room while the body's visual gutter — and the\n * fields' alignment with the modal title/footer — is unchanged.\n *\n * Pass `gap-*` (and any extra layout) via `className`.\n */\nconst ModalBody = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n function ModalBody({ className, children, ...rest }, ref) {\n return (\n <div\n ref={ref}\n className={cn(\n 'flex min-h-0 flex-1 flex-col overflow-y-auto',\n // Ring-safe inset (see the component doc above): padding ≥ the focus\n // ring's ~4px overhang + an equal negative margin to keep the gutter.\n '-m-1.5 p-1.5',\n className,\n )}\n {...rest}\n >\n {children}\n </div>\n );\n },\n);\n\n/** Convenience trigger — wires children to Radix's DialogTrigger. */\nconst ModalTrigger = Dialog.Trigger;\n\nexport const Modal = Object.assign(ModalRoot, {\n Body: ModalBody,\n Footer: ModalFooter,\n Trigger: ModalTrigger,\n});\n"]}
|
|
1
|
+
{"version":3,"sources":["../../../src/utils/cn.ts","../../../src/utils/fullscreen.ts","../../../src/components/ui/Modal.tsx"],"names":["ModalFooter","ModalBody"],"mappings":"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","file":"Modal.js","sourcesContent":["import { clsx, type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\n/**\n * Merge Tailwind class names without conflicts.\n *\n * `clsx` handles conditional inclusion (booleans, undefineds, arrays);\n * `twMerge` resolves conflicting Tailwind classes (later wins, e.g.\n * `px-2 px-4` → `px-4`). Together they are the de facto standard for\n * composing Tailwind classes in React components.\n *\n * @example\n * cn('px-2 py-1', isActive && 'bg-primary', className)\n */\nexport function cn(...inputs: ClassValue[]): string {\n return twMerge(clsx(inputs));\n}\n","'use client';\n\nimport { useSyncExternalStore } from 'react';\n\n/**\n * The element currently in NATIVE full screen (`document.fullscreenElement`), or\n * `null` — re-read on every `fullscreenchange`.\n *\n * While an element is in native full screen the browser paints ONLY that\n * element's subtree, in the top layer. Anything portalled to `document.body` —\n * a dialog, a popover, a picker's menu — is not shown at all, whatever its\n * z-index (MOTIR-7658: the roadmap canvas's quick view opened invisibly and\n * held focus and scroll lock until Esc). So every portalling primitive in this\n * package mounts into this element while there is one, and into\n * `document.body` otherwise — exactly as before.\n *\n * `null` on the server and in a document with no Fullscreen API.\n */\nexport function useFullscreenElement(): Element | null {\n return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);\n}\n\nfunction subscribe(onChange: () => void): () => void {\n document.addEventListener('fullscreenchange', onChange);\n return () => document.removeEventListener('fullscreenchange', onChange);\n}\n\nfunction getSnapshot(): Element | null {\n return document.fullscreenElement ?? null;\n}\n\nfunction getServerSnapshot(): null {\n return null;\n}\n","'use client';\n\nimport { forwardRef, type ReactNode } from 'react';\nimport * as Dialog from '@radix-ui/react-dialog';\nimport { X } from 'lucide-react';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { cn } from '../../utils/cn';\nimport { useFullscreenElement } from '../../utils/fullscreen';\n\n/**\n * Modal — accessible dialog wrapping @radix-ui/react-dialog.\n *\n * Radix handles focus trap, ESC-to-close, click-outside-to-close, and\n * focus-return-on-close out of the box. We style on top.\n *\n * Open/close is controlled by the consumer via `open` + `onOpenChange`.\n *\n * For a scrollable body with a pinned footer, wrap the fields in `Modal.Body`\n * (it owns the `flex-1 overflow-y-auto` scroll recipe and keeps focus rings\n * from clipping against the scroll edge — see `ModalBody`).\n *\n * @example\n * const [open, setOpen] = useState(false);\n * <Modal open={open} onOpenChange={setOpen} title=\"Confirm\" size=\"md\">\n * <Modal.Body className=\"gap-4\">\n * <Input label=\"Name\" autoFocus />\n * </Modal.Body>\n * <Modal.Footer>\n * <Button variant=\"ghost\" onClick={() => setOpen(false)}>Cancel</Button>\n * <Button variant=\"primary\" onClick={confirm}>Confirm</Button>\n * </Modal.Footer>\n * </Modal>\n */\nconst contentVariants = cva(\n cn(\n 'fixed left-1/2 top-1/2 z-50 -translate-x-1/2 -translate-y-1/2',\n 'w-[90vw] rounded-(--radius-modal) bg-(--el-page-bg)',\n 'shadow-(--shadow-modal) border border-(--el-border)',\n 'p-(--spacing-card-padding)',\n 'focus:outline-none',\n // Never exceed the viewport: cap height and lay out as a column so a\n // consumer can wrap its fields in `Modal.Body` (which owns the\n // `flex-1 overflow-y-auto` scroll recipe) and pin a `Modal.Footer`,\n // instead of the dialog growing off-screen (e.g. the create modal's\n // expandable Explanation pushing the Create button out of view).\n 'flex max-h-[90vh] flex-col overflow-hidden',\n ),\n {\n variants: {\n // Literal widths, NOT the max-w-sm/md/lg utilities: the design\n // system's @theme defines --spacing-sm/md/lg (12/16/20px), and\n // Tailwind v4 resolves `max-w-{key}` against the --spacing-* scale\n // when that key exists — so `max-w-md` would collapse the modal to\n // 16px wide. Pinning the rem values (Tailwind's stock sm/md/lg) keeps\n // the design-system token set untouched and the dialog readable.\n size: {\n sm: 'max-w-[24rem]',\n md: 'max-w-[28rem]',\n lg: 'max-w-[32rem]',\n // The large peek surface (Subtask 2.5.19's quick view) — a generous\n // dialog that takes a big part of the screen; pairs with a height +\n // p-0 className on the consumer side. Stock Tailwind 4xl rem value, so\n // the design-system --spacing-* scale stays untouched (see the comment\n // above on why we pin rems instead of max-w-{key}).\n xl: 'max-w-[58rem]',\n // The whole viewport (Subtask 5.2.6's attachment preview lightbox —\n // the 2.5.19 growth pattern one step further). Like `xl`, the variant\n // only lifts the size caps; the consumer pairs it with bg/border/\n // radius/padding overrides to drop the panel chrome it doesn't want.\n full: 'h-dvh max-h-dvh w-screen max-w-none',\n },\n },\n defaultVariants: { size: 'md' },\n },\n);\n\nexport interface ModalProps extends VariantProps<typeof contentVariants> {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n title?: string;\n description?: string;\n children?: ReactNode;\n /** Hide the default close (×) button in the corner. */\n hideClose?: boolean;\n /**\n * Accessible name for a dialog that renders its OWN visible heading (so no\n * `title` is passed). Radix requires a Dialog.Title; without `title`/`srTitle`\n * we fall back to the generic \"Dialog\". Supply this to label the dialog with\n * something meaningful (e.g. the previewed issue's key) while keeping the\n * heading inside `children` (Subtask 2.5.19's quick-view peek).\n */\n srTitle?: string;\n className?: string;\n /**\n * Extra classes merged onto the backdrop — e.g. the attachment preview\n * lightbox (Subtask 5.2.6) deepens the default `--el-overlay-scrim` to\n * `bg-black/80` per `design/work-items/attachments.mock.html` panel 6.\n */\n overlayClassName?: string;\n /**\n * The dialog's ARIA role. Defaults to Radix's `dialog`; a DESTRUCTIVE confirm\n * passes `alertdialog` (Subtask 2.8.4's delete confirm) so assistive tech\n * treats it as an alert that interrupts and announces its description.\n */\n role?: 'dialog' | 'alertdialog';\n /**\n * Accessible label for the corner close (×) button. Defaults to `'Close'`.\n * i18n is the consuming app's concern — the design-system package is\n * framework-agnostic (no `next-intl` dependency), so a localized app passes\n * the translated string here (motir-core wires it from its `common.close`).\n */\n closeLabel?: string;\n /**\n * Fallback accessible name when neither `title` nor `srTitle` is given.\n * Defaults to `'Dialog'`. See `closeLabel` for the i18n rationale.\n */\n dialogLabel?: string;\n /**\n * The element the dialog portals into. Defaults to the element in NATIVE full\n * screen while there is one (`document.fullscreenElement` — the browser paints\n * nothing outside it, so a body-portalled dialog would open invisibly,\n * MOTIR-7658), else `document.body`. Pass `null` to force `document.body`.\n */\n container?: Element | null;\n}\n\nfunction ModalRoot({\n open,\n onOpenChange,\n title,\n description,\n size,\n hideClose,\n srTitle,\n className,\n overlayClassName,\n role,\n children,\n closeLabel = 'Close',\n dialogLabel = 'Dialog',\n container,\n}: ModalProps) {\n const fullscreenElement = useFullscreenElement();\n // `undefined` lets Radix fall back to `document.body`.\n const portalContainer = container === undefined ? (fullscreenElement ?? undefined) : container;\n return (\n <Dialog.Root open={open} onOpenChange={onOpenChange}>\n <Dialog.Portal container={portalContainer}>\n {/* `data-surface=\"overlay\"` lets the glassmorphism style turn this dark\n scrim into a LIGHT, page-blurring backdrop, so the frosted modal\n refracts the (blurred) page behind it instead of darkness — the\n point of a glass modal. Inert for every other style. */}\n <Dialog.Overlay\n data-surface=\"overlay\"\n className={cn('fixed inset-0 z-40 bg-(--el-overlay-scrim)', overlayClassName)}\n />\n {/*\n PRODECT_FINDINGS #8: Radix checks for a describing element\n (Dialog.Description → aria-describedby) independently of the Title\n check. When no `description` prop is passed we render no\n Dialog.Description, so Radix logs a \"Missing Description or\n aria-describedby={undefined}\" warning. We pick the explicit opt-out\n (aria-describedby={undefined}) rather than emitting an empty sr-only\n Description — an empty description announces nothing useful and is\n worse for screen-reader users than declaring the dialog has none.\n */}\n <Dialog.Content\n className={cn(contentVariants({ size }), className)}\n // `data-surface` lets a surface-material style (glassmorphism) frost\n // the dialog panel — see globals.css's material layer.\n data-surface=\"modal\"\n // A destructive confirm opts into `alertdialog` (2.8.4); otherwise\n // Radix's default `dialog` role applies.\n {...(role ? { role } : {})}\n // Only opt out when there's no description. When `description` is\n // set we omit the prop entirely so Radix keeps auto-wiring\n // aria-describedby to the rendered Dialog.Description's id.\n {...(description ? {} : { 'aria-describedby': undefined })}\n >\n {title || description ? (\n <div className=\"mb-(--spacing-md) shrink-0\">\n {title ? (\n <Dialog.Title className=\"font-serif text-xl font-semibold text-(--el-text)\">\n {title}\n </Dialog.Title>\n ) : null}\n {description ? (\n <Dialog.Description className=\"text-(--el-text-subtitle) mt-1 font-sans text-sm\">\n {description}\n </Dialog.Description>\n ) : null}\n </div>\n ) : (\n // Radix requires Title for a11y; provide a visually-hidden one if\n // missing. `srTitle` lets a consumer with its own visible heading\n // still give the dialog a meaningful accessible name.\n <Dialog.Title className=\"sr-only\">{srTitle ?? dialogLabel}</Dialog.Title>\n )}\n {children}\n {!hideClose ? (\n <Dialog.Close\n aria-label={closeLabel}\n className=\"text-(--el-icon-muted) hover:text-(--el-text) absolute right-3 top-3 rounded-(--radius-control) p-(--spacing-icon-btn) transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--focus-ring-color)\"\n >\n <X className=\"h-4 w-4\" />\n </Dialog.Close>\n ) : null}\n </Dialog.Content>\n </Dialog.Portal>\n </Dialog.Root>\n );\n}\n\nconst ModalFooter = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n function ModalFooter({ className, children, ...rest }, ref) {\n return (\n <div\n ref={ref}\n className={cn(\n 'border-(--el-border) mt-(--spacing-md) flex items-center justify-end gap-2 border-t pt-(--spacing-md)',\n className,\n )}\n {...rest}\n >\n {children}\n </div>\n );\n },\n);\n\n/**\n * ModalBody — the scrollable field area of a modal. Owns the scroll recipe\n * once so consumers don't re-derive it (and re-introduce the focus-ring clip):\n * it fills the remaining column height (the panel caps at `max-h-[90vh]`) and\n * scrolls, so a sibling `Modal.Footer` stays pinned.\n *\n * Ring-safe by construction. `Input` draws its focus ring as\n * `focus-within:ring-2 … ring-offset-2` — a box-shadow extending ~4px OUTSIDE\n * the field's border box. Per the CSS spec, `overflow-y: auto` forces the\n * computed `overflow-x` from `visible` to `auto`, so this body is a clip box on\n * BOTH axes; a full-width field's ring overhang would be painted outside it and\n * silently clipped (rings are box-shadows — they never grow the layout box, so\n * nothing scrolls, the paint is just cut). We pad the scroll container by more\n * than the overhang (`p-1.5` = 6px > 4px) and pull an equal negative margin\n * (`-m-1.5`), so the ring gets room while the body's visual gutter — and the\n * fields' alignment with the modal title/footer — is unchanged.\n *\n * Pass `gap-*` (and any extra layout) via `className`.\n */\nconst ModalBody = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n function ModalBody({ className, children, ...rest }, ref) {\n return (\n <div\n ref={ref}\n className={cn(\n 'flex min-h-0 flex-1 flex-col overflow-y-auto',\n // Ring-safe inset (see the component doc above): padding ≥ the focus\n // ring's ~4px overhang + an equal negative margin to keep the gutter.\n '-m-1.5 p-1.5',\n className,\n )}\n {...rest}\n >\n {children}\n </div>\n );\n },\n);\n\n/** Convenience trigger — wires children to Radix's DialogTrigger. */\nconst ModalTrigger = Dialog.Trigger;\n\nexport const Modal = Object.assign(ModalRoot, {\n Body: ModalBody,\n Footer: ModalFooter,\n Trigger: ModalTrigger,\n});\n"]}
|
|
@@ -9,6 +9,19 @@ import { jsxs, jsx } from 'react/jsx-runtime';
|
|
|
9
9
|
function cn(...inputs) {
|
|
10
10
|
return twMerge(clsx(inputs));
|
|
11
11
|
}
|
|
12
|
+
function useFullscreenElement() {
|
|
13
|
+
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
14
|
+
}
|
|
15
|
+
function subscribe(onChange) {
|
|
16
|
+
document.addEventListener("fullscreenchange", onChange);
|
|
17
|
+
return () => document.removeEventListener("fullscreenchange", onChange);
|
|
18
|
+
}
|
|
19
|
+
function getSnapshot() {
|
|
20
|
+
return document.fullscreenElement ?? null;
|
|
21
|
+
}
|
|
22
|
+
function getServerSnapshot() {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
12
25
|
var useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect;
|
|
13
26
|
var subscribeNoop = () => () => {
|
|
14
27
|
};
|
|
@@ -99,6 +112,7 @@ function MultiSelectPicker({
|
|
|
99
112
|
const [open, setOpen] = useState(false);
|
|
100
113
|
const [activeIndex, setActiveIndex] = useState(0);
|
|
101
114
|
const mounted = useMounted();
|
|
115
|
+
const fullscreenElement = useFullscreenElement();
|
|
102
116
|
const [inModal, setInModal] = useState(false);
|
|
103
117
|
const [menuStyle, setMenuStyle] = useState(null);
|
|
104
118
|
const [listMaxHeight, setListMaxHeight] = useState(256);
|
|
@@ -191,7 +205,7 @@ function MultiSelectPicker({
|
|
|
191
205
|
if (open && rowCount > 0) commitRow(clampedActive);
|
|
192
206
|
}
|
|
193
207
|
}
|
|
194
|
-
const wrapMenu = (el) => inModal ? el : createPortal(el, document.body);
|
|
208
|
+
const wrapMenu = (el) => inModal ? el : createPortal(el, fullscreenElement ?? document.body);
|
|
195
209
|
return (
|
|
196
210
|
// `data-inner-dismiss` (set while the listbox is open) lets an enclosing
|
|
197
211
|
// Radix layer's `onEscapeKeyDown` defer to us: Radix listens for Escape
|