@stackone/malachite 2.68.1 → 2.69.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.
@@ -1 +1 @@
1
- {"version":3,"file":"overlayBehaviour.js","sources":["../../src/components/overlayBehaviour.ts"],"sourcesContent":["import { useCallback, useEffect, useLayoutEffect, useRef, useSyncExternalStore } from 'react';\nimport { containsComposed, getDeepActiveElement } from './activeElement';\nimport { pushEscapeLayer } from './escapeLayers';\nimport { FOCUS_RING } from './focusRing';\nimport { nextLayerTicket, popupLayerHolding } from './overlayLayers';\nimport { useOwnedByPopover } from './popoverTree';\nimport { OVERLAY_Z_BASE, ensureElevatedOverlayStyles } from './portalOverlay';\nimport { lockBodyScroll } from './scrollLock';\nimport { leaveTopLayer, raiseToTopLayer } from './topLayer';\n\n/** Back/close button chrome, shared so the two overlays cannot drift apart. */\nexport const OVERLAY_ICON_BUTTON_CLASSES = `flex shrink-0 cursor-pointer items-center justify-center rounded-md border-0 bg-transparent p-[4px] text-[var(--malachite-text-secondary)] hover:text-[var(--malachite-text-primary)] transition-colors ${FOCUS_RING}`;\n\n// `summary`: a disclosure's first one is a native tab stop and its only keyboard control. A loose\n// one reports `tabIndex === -1`, which `isTabStop` drops.\nexport const FOCUSABLE_SELECTOR =\n 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), summary, [tabindex]:not([tabindex=\"-1\"])';\n\n// The flat-tree parent: a slotted element renders under its slot, a shadow root under its host.\nconst flatParent = (element: Element): Element | null =>\n element.assignedSlot ??\n element.parentElement ??\n (element.parentNode instanceof ShadowRoot ? element.parentNode.host : null);\n\n// `children`, not `:scope > summary`: inside a shadow root jsdom's selector engine matches\n// nothing for `:scope`, which silently hid the summary along with the rest.\nconst firstSummaryOf = (details: HTMLDetailsElement): Element | undefined =>\n Array.from(details.children).find((child) => child.tagName === 'SUMMARY');\n\ntype RenderState = { hidden: boolean; invisible: boolean; hidesContent: boolean };\n\n// Reads computed style: call at interaction time only, never from a mount path. Under jsdom,\n// `offsetParent`, `getClientRects()` and `checkVisibility()` report every element invisible.\n// One per Tab press, so an ancestor shared by many controls has its style read once.\nconst createRenderStates = (): ((element: Element) => RenderState) => {\n const states = new Map<Element, RenderState>();\n const stateOf = (element: Element): RenderState => {\n const cached = states.get(element);\n if (cached) {\n return cached;\n }\n const { display, visibility, contentVisibility } = getComputedStyle(element);\n const parent = flatParent(element);\n const parentState = parent ? stateOf(parent) : null;\n // A closed `<details>` hides all but its summary in the UA shadow slot, so the hidden\n // descendants keep `tabIndex === 0` and a normal computed display.\n const closedDisclosure =\n parent instanceof HTMLDetailsElement &&\n !parent.open &&\n firstSummaryOf(parent) !== element;\n const state = {\n hidden:\n element.hasAttribute('inert') ||\n display === 'none' ||\n closedDisclosure ||\n Boolean(parentState && (parentState.hidden || parentState.hidesContent)),\n invisible: visibility === 'hidden' || visibility === 'collapse',\n hidesContent: contentVisibility === 'hidden',\n };\n states.set(element, state);\n return state;\n };\n return stateOf;\n};\n\nconst isTabStop = (element: HTMLElement, stateOf: (element: Element) => RenderState): boolean => {\n if (element.tabIndex < 0 || element.matches(':disabled')) {\n return false;\n }\n const { hidden, invisible } = stateOf(element);\n\n return !hidden && !invisible;\n};\n\nconst isGroupedRadio = (element: Element): element is HTMLInputElement =>\n element instanceof HTMLInputElement && element.type === 'radio' && element.name !== '';\n\n// Only a group's checked radio, or its first with none checked, is a tab stop, yet every member\n// reports `tabIndex === 0`.\nconst skipsWithinRadioGroup = (element: HTMLElement, reachable: HTMLElement[]): boolean => {\n if (!isGroupedRadio(element)) {\n return false;\n }\n const group = reachable.filter(\n (candidate): candidate is HTMLInputElement =>\n isGroupedRadio(candidate) &&\n candidate.name === element.name &&\n candidate.form === element.form,\n );\n\n return (group.find((radio) => radio.checked) ?? group[0]) !== element;\n};\n\nconst inTabOrder = (entries: Element[], tabIndexOf: (element: Element) => number): Element[] =>\n entries\n .map((element, index) => ({ element, index, tabIndex: tabIndexOf(element) }))\n .sort((left, right) => {\n if (left.tabIndex !== right.tabIndex) {\n if (left.tabIndex === 0) {\n return 1;\n }\n if (right.tabIndex === 0) {\n return -1;\n }\n\n return left.tabIndex - right.tabIndex;\n }\n\n return left.index - right.index;\n })\n .map(({ element }) => element);\n\n// Only a slot inside a shadow root projects assigned content; elsewhere it renders its children.\nconst isShadowSlot = (element: Element): element is HTMLSlotElement =>\n element instanceof HTMLSlotElement && element.getRootNode() instanceof ShadowRoot;\n\n// One focus navigation scope in tree order. A host's light children render only through its\n// slots, and hosts and slots each own a scope, so the walk descends into neither.\nconst scopeMembers = (elements: Iterable<Element>, into: Element[] = []): Element[] => {\n for (const element of elements) {\n into.push(element);\n if (!element.shadowRoot && !isShadowSlot(element)) {\n scopeMembers(element.children, into);\n }\n }\n return into;\n};\n\n// Ordered per scope, as browsers do: positive tabindex orders within its own scope, and a nested\n// scope takes its owner's place, an owner that is no stop itself ordering as tabindex 0.\nconst stopsInScope = (\n members: Element[],\n stateOf: (element: Element) => RenderState,\n): HTMLElement[] => {\n const reachable = members.filter(\n (element): element is HTMLElement =>\n element instanceof HTMLElement &&\n element.matches(FOCUSABLE_SELECTOR) &&\n isTabStop(element, stateOf),\n );\n const stops = new Set<Element>(\n reachable.filter((element) => !skipsWithinRadioGroup(element, reachable)),\n );\n const entries = members.filter(\n (element) => stops.has(element) || element.shadowRoot || isShadowSlot(element),\n );\n\n return inTabOrder(entries, (element) =>\n stops.has(element) ? (element as HTMLElement).tabIndex : 0,\n ).flatMap((element) => {\n if (isShadowSlot(element)) {\n return stopsInScope(scopeMembers(element.assignedElements({ flatten: true })), stateOf);\n }\n const root = element.shadowRoot;\n // A host given a negative tabindex keeps its whole shadow tree out of the order; an\n // unparsable value counts as absent, as it does for any element.\n if (root && Number.parseInt(element.getAttribute('tabindex') ?? '', 10) < 0) {\n return [];\n }\n const own = stops.has(element) && !root?.delegatesFocus ? [element as HTMLElement] : [];\n\n return root ? [...own, ...stopsInScope(scopeMembers(root.children), stateOf)] : own;\n });\n};\n\nconst tabStopsIn = (root: ParentNode): HTMLElement[] =>\n stopsInScope(scopeMembers(root.children), createRenderStates());\n\n// `compareDocumentPosition` calls nodes in different shadow trees disconnected, so lift both\n// into their deepest shared tree first; a host precedes its own shadow content.\nconst composedPosition = (from: Node, to: Node): number => {\n const ancestry = (node: Node): Node[] => {\n const chain = [node];\n for (\n let root = node.getRootNode();\n root instanceof ShadowRoot;\n root = root.host.getRootNode()\n ) {\n chain.push(root.host);\n }\n return chain;\n };\n const fromChain = ancestry(from);\n const toChain = ancestry(to);\n for (const fromStep of fromChain) {\n const toStep = toChain.find((step) => step.getRootNode() === fromStep.getRootNode());\n if (!toStep) {\n continue;\n }\n if (fromStep !== toStep) {\n return fromStep.compareDocumentPosition(toStep);\n }\n\n return fromStep === from\n ? Node.DOCUMENT_POSITION_FOLLOWING\n : Node.DOCUMENT_POSITION_PRECEDING;\n }\n\n return Node.DOCUMENT_POSITION_DISCONNECTED;\n};\n\n/** Where Tab would land first, for initial focus: the raw selector also matches hidden controls. */\nexport const firstTabStopIn = (root: ParentNode): HTMLElement | null => tabStopsIn(root)[0] ?? null;\n\ntype PanelSource = () => HTMLElement | null;\n\n// LIFO like escapeLayers: with a listener per overlay the first-registered one wins, which\n// sent Tab from a dialog into the panel behind it.\nconst trapLayers: PanelSource[] = [];\n// Elevation belongs to the stack: a Modal left in the base band opens behind the panel that\n// elevated it.\nconst elevatedLayers = new Set<PanelSource>();\n// The shadow host per layer, so promotion can replay in stack order.\nconst hostSources = new Map<PanelSource, () => HTMLElement | null>();\nconst trapTickets = new Map<PanelSource, number>();\nconst stackListeners = new Set<() => void>();\nconst NO_SUBSCRIPTION = () => {\n // Nothing to tear down: a closed overlay never subscribed.\n};\n\n// Top-layer paint order is entry order, so replay the whole stack bottom-up rather than letting\n// each overlay promote itself: those effects run in React tree order, which need not match.\nlet promoted: HTMLElement[] = [];\n\nconst syncTopLayer = (): void => {\n const wanted = elevatedLayers.size\n ? trapLayers\n .map((layer) => hostSources.get(layer)?.())\n .filter((host): host is HTMLElement => Boolean(host))\n : [];\n // Compared, not reapplied: this runs on every commit, because a host can appear a commit\n // after the layer that owns it, and re-entering the top layer unprompted costs a recalc.\n if (\n wanted.length === promoted.length &&\n wanted.every((host, index) => host === promoted[index])\n ) {\n return;\n }\n const stillWanted = new Set(wanted);\n for (const host of promoted) {\n if (!stillWanted.has(host)) {\n leaveTopLayer(host);\n }\n }\n for (const host of wanted) {\n raiseToTopLayer(host);\n }\n promoted = wanted;\n};\n\nconst notifyStackChanged = (): void => {\n for (const listener of stackListeners) {\n listener();\n }\n};\n\nconst subscribeToStack = (listener: () => void): (() => void) => {\n stackListeners.add(listener);\n return () => {\n stackListeners.delete(listener);\n };\n};\n\nconst wrapTab = (\n event: KeyboardEvent,\n active: HTMLElement | null,\n within: Node,\n stops: HTMLElement[],\n): void => {\n const first = stops[0];\n const last = stops[stops.length - 1];\n const inside = containsComposed(within, active);\n // A mouse-only control, such as a search Dropdown's caret, is no edge: Tab resumes from its\n // place in the DOM and would leave the overlay where no real stop follows it.\n if (active && inside && active !== within && !stops.includes(active)) {\n const wanted = event.shiftKey\n ? Node.DOCUMENT_POSITION_PRECEDING\n : Node.DOCUMENT_POSITION_FOLLOWING;\n if (!stops.some((stop) => composedPosition(active, stop) & wanted)) {\n event.preventDefault();\n (event.shiftKey ? last : first).focus();\n }\n\n return;\n }\n if (event.shiftKey) {\n if (!inside || active === first || active === within) {\n event.preventDefault();\n last.focus();\n }\n } else if (!inside || active === last || active === within) {\n event.preventDefault();\n first.focus();\n }\n};\n\nconst handleTabKeyDown = (event: KeyboardEvent): void => {\n if (event.key !== 'Tab' || event.defaultPrevented) {\n return;\n }\n // Walk down rather than bail: React detaches refs before flushing the cleanup that pops\n // the layer, so the topmost can be registered-but-null and Tab would escape entirely.\n let panel: HTMLElement | null = null;\n let trapTicket = 0;\n for (let index = trapLayers.length - 1; index >= 0 && !panel; index -= 1) {\n panel = trapLayers[index]();\n if (panel) {\n trapTicket = trapTickets.get(trapLayers[index]) ?? 0;\n }\n }\n if (!panel) {\n return;\n }\n const active = getDeepActiveElement();\n const layer = active ? popupLayerHolding(active) : null;\n if (active && layer && layer.ticket > trapTicket) {\n const root = layer.popup.shadowRoot ?? layer.popup;\n const popupStops = tabStopsIn(root);\n if (popupStops.length === 0) {\n event.preventDefault();\n return;\n }\n wrapTab(event, active, root, popupStops);\n return;\n }\n const focusables = tabStopsIn(panel);\n if (focusables.length === 0) {\n event.preventDefault();\n panel.focus();\n return;\n }\n wrapTab(event, active, panel, focusables);\n};\n\n// On `document` so it also catches focus that escaped behind the overlay.\nconst pushFocusTrapLayer = (getPanel: PanelSource, getHost: () => HTMLElement | null) => {\n if (trapLayers.length === 0) {\n document.addEventListener('keydown', handleTabKeyDown);\n }\n trapLayers.push(getPanel);\n hostSources.set(getPanel, getHost);\n trapTickets.set(getPanel, nextLayerTicket());\n notifyStackChanged();\n return () => {\n const index = trapLayers.lastIndexOf(getPanel);\n if (index !== -1) {\n trapLayers.splice(index, 1);\n }\n elevatedLayers.delete(getPanel);\n hostSources.delete(getPanel);\n trapTickets.delete(getPanel);\n const host = getHost();\n if (host) {\n leaveTopLayer(host);\n }\n if (trapLayers.length === 0) {\n document.removeEventListener('keydown', handleTabKeyDown);\n }\n notifyStackChanged();\n };\n};\n\n/**\n * Whether a SidePanel, Modal or CommandPalette is open anywhere, for host chrome that has to move out of the way.\n * Overlays portal, so the only other signal is this module's markers, which a host cannot read\n * without coupling to an internal. Dropdowns and tooltips do not count: they trap no focus.\n */\nexport const useHasOpenOverlay = (): boolean =>\n useSyncExternalStore(\n subscribeToStack,\n () => trapLayers.length > 0,\n () => false,\n );\n\nconst takeInitialFocus = (panel: HTMLElement, initialFocus: 'panel' | 'content'): void => {\n if (initialFocus === 'content') {\n const active = getDeepActiveElement();\n if (containsComposed(panel, active)) {\n return;\n }\n (firstTabStopIn(panel) ?? panel).focus();\n return;\n }\n panel.focus();\n};\n\n/**\n * The modal-overlay behaviour shared by SidePanel, Modal and CommandPalette: ESC and the Tab focus trap\n * (topmost overlay only, so these may be stacked), background scroll lock, and focus moved\n * into the overlay on open then restored to the trigger on close.\n *\n * Returns the ref callback the dialog element must take, whether this overlay is the topmost\n * one — only that one may claim `aria-modal`, or a screen reader can still browse the overlay\n * underneath — the z-index its host must carry, which keeps paint order with the focus stack,\n * and whether it is elevated. Presentation otherwise stays with the calling component.\n */\nexport const useOverlayBehaviour = ({\n open,\n onClose,\n elevated = false,\n hostRef,\n initialFocus = 'panel',\n}: {\n open: boolean;\n onClose: () => void;\n elevated?: boolean;\n /** The shadow host, which the stack promotes into the top layer while elevated. */\n hostRef?: { current: HTMLElement | null };\n /** Where focus lands on open: the panel itself, or the content's own first control. */\n initialFocus?: 'panel' | 'content';\n}): {\n ref: (node: HTMLElement | null) => void;\n isTopmost: boolean;\n zIndex: number | undefined;\n elevated: boolean;\n} => {\n const panelRef = useRef<HTMLElement | null>(null);\n // One identity for the life of the instance: elevation registers it in the layout phase,\n // before the effect below pushes it onto the stack.\n const source = useRef<PanelSource>(() => panelRef.current).current;\n useOwnedByPopover(\n useCallback(() => hostRef?.current ?? panelRef.current, [hostRef]),\n open,\n );\n const previousFocusRef = useRef<HTMLElement | null>(null);\n const layerRef = useRef<PanelSource | null>(null);\n const initialFocusRef = useRef(initialFocus);\n\n // Ref so the ESC layer registers once per open, not on every inline-onClose change.\n const onCloseRef = useRef(onClose);\n useEffect(() => {\n onCloseRef.current = onClose;\n }, [onClose]);\n\n // Before the trap effect, so the trigger is recorded before focus moves in — and relies on\n // ShadowRootWrapper portaling a commit late, or this would capture the panel itself.\n useEffect(() => {\n if (!open) {\n return;\n }\n previousFocusRef.current = getDeepActiveElement();\n return () => {\n const previous = previousFocusRef.current;\n previousFocusRef.current = null;\n if (previous?.isConnected) {\n previous.focus();\n }\n };\n }, [open]);\n\n useEffect(() => {\n if (!open) {\n return;\n }\n layerRef.current = source;\n const popEscapeLayer = pushEscapeLayer(() => onCloseRef.current());\n const releaseScrollLock = lockBodyScroll();\n const popFocusTrapLayer = pushFocusTrapLayer(source, () => hostRef?.current ?? null);\n // Reopening inside the exit transition reuses the same node, so the ref callback\n // never fires again and nothing would take focus.\n if (panelRef.current) {\n takeInitialFocus(panelRef.current, initialFocusRef.current);\n }\n return () => {\n layerRef.current = null;\n popFocusTrapLayer();\n popEscapeLayer();\n releaseScrollLock();\n };\n }, [open, hostRef, source]);\n\n // Its own effect: re-pushing the trap layer to change elevation would move this overlay to\n // the top of the stack, handing it the Escape and Tab that belong to a dialog above it.\n // Layout, and declared above the sync below, so that sync sees it in the same commit.\n useLayoutEffect(() => {\n if (!open || !elevated) {\n return;\n }\n elevatedLayers.add(source);\n notifyStackChanged();\n return () => {\n elevatedLayers.delete(source);\n notifyStackChanged();\n };\n }, [open, elevated, source]);\n\n // Only an open overlay subscribes: a mounted-but-idle one must carry no at-rest cost,\n // and its snapshot is a constant `false` regardless of what the stack does.\n const subscribe = useCallback(\n (onStackChange: () => void) => (open ? subscribeToStack(onStackChange) : NO_SUBSCRIPTION),\n [open],\n );\n\n const isTopmost = useSyncExternalStore(\n subscribe,\n // Gated on `open` so an overlay fading out stops claiming modal. Before it registers\n // it is the one about to go on top, or aria-modal would drop for a frame on open.\n () =>\n open &&\n (layerRef.current === null || trapLayers[trapLayers.length - 1] === layerRef.current),\n () => true,\n );\n\n // Its own request counts before the stack has it: the layer is not pushed until effects run.\n const stackElevated = useSyncExternalStore(\n subscribe,\n () => open && (elevated || elevatedLayers.size > 0),\n () => false,\n );\n\n // Overlay hosts share one z-index band, so paint order would otherwise fall out of body\n // order: an overlay reopened over a newer one takes the keyboard while painting under it.\n const zIndex = useSyncExternalStore(\n subscribe,\n () => {\n const layer = layerRef.current;\n return OVERLAY_Z_BASE + (open && layer ? Math.max(0, trapLayers.indexOf(layer)) : 0);\n },\n () => OVERLAY_Z_BASE,\n );\n\n // Layout, not passive: the rule has to exist before the first paint the attribute applies to.\n useLayoutEffect(() => {\n if (stackElevated) {\n ensureElevatedOverlayStyles();\n }\n }, [stackElevated]);\n\n // No deps: the host mounts a commit after the layer that owns it, so there is no stack\n // change left to key this on. `syncTopLayer` compares before it touches anything.\n useLayoutEffect(() => {\n syncTopLayer();\n });\n\n const ref = useCallback((node: HTMLElement | null) => {\n panelRef.current = node;\n // The panel mounts fresh on every open, so first attach = time to take focus.\n if (node) {\n takeInitialFocus(node, initialFocusRef.current);\n }\n }, []);\n\n return { ref, isTopmost, zIndex: stackElevated ? undefined : zIndex, elevated: stackElevated };\n};\n\nexport default useOverlayBehaviour;\n"],"names":["OVERLAY_ICON_BUTTON_CLASSES","FOCUS_RING","FOCUSABLE_SELECTOR","flatParent","element","firstSummaryOf","details","child","createRenderStates","states","stateOf","cached","display","visibility","contentVisibility","parent","parentState","closedDisclosure","state","isTabStop","hidden","invisible","isGroupedRadio","skipsWithinRadioGroup","reachable","group","candidate","radio","inTabOrder","entries","tabIndexOf","index","left","right","isShadowSlot","scopeMembers","elements","into","stopsInScope","members","stops","root","own","tabStopsIn","composedPosition","from","to","ancestry","node","chain","fromChain","toChain","fromStep","toStep","step","firstTabStopIn","trapLayers","elevatedLayers","hostSources","trapTickets","stackListeners","NO_SUBSCRIPTION","promoted","syncTopLayer","wanted","layer","_a","host","stillWanted","leaveTopLayer","raiseToTopLayer","notifyStackChanged","listener","subscribeToStack","wrapTab","event","active","within","first","last","inside","containsComposed","stop","handleTabKeyDown","panel","trapTicket","getDeepActiveElement","popupLayerHolding","popupStops","focusables","pushFocusTrapLayer","getPanel","getHost","nextLayerTicket","useHasOpenOverlay","useSyncExternalStore","takeInitialFocus","initialFocus","useOverlayBehaviour","open","onClose","elevated","hostRef","panelRef","useRef","source","useOwnedByPopover","useCallback","previousFocusRef","layerRef","initialFocusRef","onCloseRef","useEffect","previous","popEscapeLayer","pushEscapeLayer","releaseScrollLock","lockBodyScroll","popFocusTrapLayer","useLayoutEffect","subscribe","onStackChange","isTopmost","stackElevated","zIndex","OVERLAY_Z_BASE","ensureElevatedOverlayStyles"],"mappings":"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"}
1
+ {"version":3,"file":"overlayBehaviour.js","sources":["../../src/components/overlayBehaviour.ts"],"sourcesContent":["import { useCallback, useEffect, useLayoutEffect, useRef, useSyncExternalStore } from 'react';\nimport { containsComposed, getDeepActiveElement } from './activeElement';\nimport { pushEscapeLayer } from './escapeLayers';\nimport { FOCUS_RING } from './focusRing';\nimport { nextLayerTicket, popupLayerHolding } from './overlayLayers';\nimport { useOwnedByPopover } from './popoverTree';\nimport { OVERLAY_Z_BASE, ensureElevatedOverlayStyles } from './portalOverlay';\nimport { lockBodyScroll } from './scrollLock';\nimport { leaveTopLayer, raiseToTopLayer } from './topLayer';\n\n/** Back/close button chrome, shared so the two overlays cannot drift apart. */\nexport const OVERLAY_ICON_BUTTON_CLASSES = `flex shrink-0 cursor-pointer items-center justify-center rounded-md border-0 bg-transparent p-[4px] text-[var(--malachite-text-secondary)] hover:text-[var(--malachite-text-primary)] transition-colors ${FOCUS_RING}`;\n\n// `summary`: a disclosure's first one is a native tab stop and its only keyboard control. A loose\n// one reports `tabIndex === -1`, which `isTabStop` drops.\nexport const FOCUSABLE_SELECTOR =\n 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), summary, [tabindex]:not([tabindex=\"-1\"])';\n\n// The flat-tree parent: a slotted element renders under its slot, a shadow root under its host.\nconst flatParent = (element: Element): Element | null =>\n element.assignedSlot ??\n element.parentElement ??\n (element.parentNode instanceof ShadowRoot ? element.parentNode.host : null);\n\n// `children`, not `:scope > summary`: inside a shadow root jsdom's selector engine matches\n// nothing for `:scope`, which silently hid the summary along with the rest.\nconst firstSummaryOf = (details: HTMLDetailsElement): Element | undefined =>\n Array.from(details.children).find((child) => child.tagName === 'SUMMARY');\n\ntype RenderState = { hidden: boolean; invisible: boolean; hidesContent: boolean };\n\n// Reads computed style: call at interaction time only, never from a mount path. Under jsdom,\n// `offsetParent`, `getClientRects()` and `checkVisibility()` report every element invisible.\n// One per Tab press, so an ancestor shared by many controls has its style read once.\nconst createRenderStates = (): ((element: Element) => RenderState) => {\n const states = new Map<Element, RenderState>();\n const stateOf = (element: Element): RenderState => {\n const cached = states.get(element);\n if (cached) {\n return cached;\n }\n const { display, visibility, contentVisibility } = getComputedStyle(element);\n const parent = flatParent(element);\n const parentState = parent ? stateOf(parent) : null;\n // A closed `<details>` hides all but its summary in the UA shadow slot, so the hidden\n // descendants keep `tabIndex === 0` and a normal computed display.\n const closedDisclosure =\n parent instanceof HTMLDetailsElement &&\n !parent.open &&\n firstSummaryOf(parent) !== element;\n const state = {\n hidden:\n element.hasAttribute('inert') ||\n display === 'none' ||\n closedDisclosure ||\n Boolean(parentState && (parentState.hidden || parentState.hidesContent)),\n invisible: visibility === 'hidden' || visibility === 'collapse',\n hidesContent: contentVisibility === 'hidden',\n };\n states.set(element, state);\n return state;\n };\n return stateOf;\n};\n\nconst isTabStop = (element: HTMLElement, stateOf: (element: Element) => RenderState): boolean => {\n if (element.tabIndex < 0 || element.matches(':disabled')) {\n return false;\n }\n const { hidden, invisible } = stateOf(element);\n\n return !hidden && !invisible;\n};\n\nconst isGroupedRadio = (element: Element): element is HTMLInputElement =>\n element instanceof HTMLInputElement && element.type === 'radio' && element.name !== '';\n\n// Only a group's checked radio, or its first with none checked, is a tab stop, yet every member\n// reports `tabIndex === 0`.\nconst skipsWithinRadioGroup = (element: HTMLElement, reachable: HTMLElement[]): boolean => {\n if (!isGroupedRadio(element)) {\n return false;\n }\n const group = reachable.filter(\n (candidate): candidate is HTMLInputElement =>\n isGroupedRadio(candidate) &&\n candidate.name === element.name &&\n candidate.form === element.form,\n );\n\n return (group.find((radio) => radio.checked) ?? group[0]) !== element;\n};\n\nconst inTabOrder = (entries: Element[], tabIndexOf: (element: Element) => number): Element[] =>\n entries\n .map((element, index) => ({ element, index, tabIndex: tabIndexOf(element) }))\n .sort((left, right) => {\n if (left.tabIndex !== right.tabIndex) {\n if (left.tabIndex === 0) {\n return 1;\n }\n if (right.tabIndex === 0) {\n return -1;\n }\n\n return left.tabIndex - right.tabIndex;\n }\n\n return left.index - right.index;\n })\n .map(({ element }) => element);\n\n// Only a slot inside a shadow root projects assigned content; elsewhere it renders its children.\nconst isShadowSlot = (element: Element): element is HTMLSlotElement =>\n element instanceof HTMLSlotElement && element.getRootNode() instanceof ShadowRoot;\n\n// One focus navigation scope in tree order. A host's light children render only through its\n// slots, and hosts and slots each own a scope, so the walk descends into neither.\nconst scopeMembers = (elements: Iterable<Element>, into: Element[] = []): Element[] => {\n for (const element of elements) {\n into.push(element);\n if (!element.shadowRoot && !isShadowSlot(element)) {\n scopeMembers(element.children, into);\n }\n }\n return into;\n};\n\n// Ordered per scope, as browsers do: positive tabindex orders within its own scope, and a nested\n// scope takes its owner's place, an owner that is no stop itself ordering as tabindex 0.\nconst stopsInScope = (\n members: Element[],\n stateOf: (element: Element) => RenderState,\n): HTMLElement[] => {\n const reachable = members.filter(\n (element): element is HTMLElement =>\n element instanceof HTMLElement &&\n element.matches(FOCUSABLE_SELECTOR) &&\n isTabStop(element, stateOf),\n );\n const stops = new Set<Element>(\n reachable.filter((element) => !skipsWithinRadioGroup(element, reachable)),\n );\n const entries = members.filter(\n (element) => stops.has(element) || element.shadowRoot || isShadowSlot(element),\n );\n\n return inTabOrder(entries, (element) =>\n stops.has(element) ? (element as HTMLElement).tabIndex : 0,\n ).flatMap((element) => {\n if (isShadowSlot(element)) {\n return stopsInScope(scopeMembers(element.assignedElements({ flatten: true })), stateOf);\n }\n const root = element.shadowRoot;\n // A host given a negative tabindex keeps its whole shadow tree out of the order; an\n // unparsable value counts as absent, as it does for any element.\n if (root && Number.parseInt(element.getAttribute('tabindex') ?? '', 10) < 0) {\n return [];\n }\n const own = stops.has(element) && !root?.delegatesFocus ? [element as HTMLElement] : [];\n\n return root ? [...own, ...stopsInScope(scopeMembers(root.children), stateOf)] : own;\n });\n};\n\nconst tabStopsIn = (root: ParentNode): HTMLElement[] =>\n stopsInScope(scopeMembers(root.children), createRenderStates());\n\n// `compareDocumentPosition` calls nodes in different shadow trees disconnected, so lift both\n// into their deepest shared tree first; a host precedes its own shadow content.\nconst composedPosition = (from: Node, to: Node): number => {\n const ancestry = (node: Node): Node[] => {\n const chain = [node];\n for (\n let root = node.getRootNode();\n root instanceof ShadowRoot;\n root = root.host.getRootNode()\n ) {\n chain.push(root.host);\n }\n return chain;\n };\n const fromChain = ancestry(from);\n const toChain = ancestry(to);\n for (const fromStep of fromChain) {\n const toStep = toChain.find((step) => step.getRootNode() === fromStep.getRootNode());\n if (!toStep) {\n continue;\n }\n if (fromStep !== toStep) {\n return fromStep.compareDocumentPosition(toStep);\n }\n\n return fromStep === from\n ? Node.DOCUMENT_POSITION_FOLLOWING\n : Node.DOCUMENT_POSITION_PRECEDING;\n }\n\n return Node.DOCUMENT_POSITION_DISCONNECTED;\n};\n\n/** Where Tab would land first, for initial focus: the raw selector also matches hidden controls. */\nexport const firstTabStopIn = (root: ParentNode): HTMLElement | null => tabStopsIn(root)[0] ?? null;\n\ntype PanelSource = () => HTMLElement | null;\n\n// LIFO like escapeLayers: with a listener per overlay the first-registered one wins, which\n// sent Tab from a dialog into the panel behind it.\nconst trapLayers: PanelSource[] = [];\n// Elevation belongs to the stack: a Modal left in the base band opens behind the panel that\n// elevated it.\nconst elevatedLayers = new Set<PanelSource>();\n// The shadow host per layer, so promotion can replay in stack order.\nconst hostSources = new Map<PanelSource, () => HTMLElement | null>();\nconst trapTickets = new Map<PanelSource, number>();\nconst stackListeners = new Set<() => void>();\nconst NO_SUBSCRIPTION = () => {\n // Nothing to tear down: a closed overlay never subscribed.\n};\n\n// Top-layer paint order is entry order, so replay the whole stack bottom-up rather than letting\n// each overlay promote itself: those effects run in React tree order, which need not match.\nlet promoted: HTMLElement[] = [];\nconst aboveStack = new Set<HTMLElement>();\n\nconst syncTopLayer = (): void => {\n const wanted = elevatedLayers.size\n ? trapLayers\n .map((layer) => hostSources.get(layer)?.())\n .filter((host): host is HTMLElement => Boolean(host))\n : [];\n // Compared, not reapplied: this runs on every commit, because a host can appear a commit\n // after the layer that owns it, and re-entering the top layer unprompted costs a recalc.\n if (\n wanted.length === promoted.length &&\n wanted.every((host, index) => host === promoted[index])\n ) {\n return;\n }\n const stillWanted = new Set(wanted);\n for (const host of promoted) {\n if (!stillWanted.has(host)) {\n leaveTopLayer(host);\n }\n }\n for (const host of wanted) {\n raiseToTopLayer(host);\n }\n promoted = wanted;\n for (const host of aboveStack) {\n raiseToTopLayer(host);\n }\n};\n\n/** Keeps a promoted host above the elevated stack each time the stack is replayed. */\nexport const keepAboveOverlayStack = (host: HTMLElement): (() => void) => {\n aboveStack.add(host);\n return () => {\n aboveStack.delete(host);\n };\n};\n\nconst notifyStackChanged = (): void => {\n for (const listener of stackListeners) {\n listener();\n }\n};\n\nconst subscribeToStack = (listener: () => void): (() => void) => {\n stackListeners.add(listener);\n return () => {\n stackListeners.delete(listener);\n };\n};\n\nconst wrapTab = (\n event: KeyboardEvent,\n active: HTMLElement | null,\n within: Node,\n stops: HTMLElement[],\n): void => {\n const first = stops[0];\n const last = stops[stops.length - 1];\n const inside = containsComposed(within, active);\n // A mouse-only control, such as a search Dropdown's caret, is no edge: Tab resumes from its\n // place in the DOM and would leave the overlay where no real stop follows it.\n if (active && inside && active !== within && !stops.includes(active)) {\n const wanted = event.shiftKey\n ? Node.DOCUMENT_POSITION_PRECEDING\n : Node.DOCUMENT_POSITION_FOLLOWING;\n if (!stops.some((stop) => composedPosition(active, stop) & wanted)) {\n event.preventDefault();\n (event.shiftKey ? last : first).focus();\n }\n\n return;\n }\n if (event.shiftKey) {\n if (!inside || active === first || active === within) {\n event.preventDefault();\n last.focus();\n }\n } else if (!inside || active === last || active === within) {\n event.preventDefault();\n first.focus();\n }\n};\n\nconst handleTabKeyDown = (event: KeyboardEvent): void => {\n if (event.key !== 'Tab' || event.defaultPrevented) {\n return;\n }\n // Walk down rather than bail: React detaches refs before flushing the cleanup that pops\n // the layer, so the topmost can be registered-but-null and Tab would escape entirely.\n let panel: HTMLElement | null = null;\n let trapTicket = 0;\n for (let index = trapLayers.length - 1; index >= 0 && !panel; index -= 1) {\n panel = trapLayers[index]();\n if (panel) {\n trapTicket = trapTickets.get(trapLayers[index]) ?? 0;\n }\n }\n if (!panel) {\n return;\n }\n const active = getDeepActiveElement();\n const layer = active ? popupLayerHolding(active) : null;\n if (active && layer && layer.ticket > trapTicket) {\n const root = layer.popup.shadowRoot ?? layer.popup;\n const popupStops = tabStopsIn(root);\n if (popupStops.length === 0) {\n event.preventDefault();\n return;\n }\n wrapTab(event, active, root, popupStops);\n return;\n }\n const focusables = tabStopsIn(panel);\n if (focusables.length === 0) {\n event.preventDefault();\n panel.focus();\n return;\n }\n wrapTab(event, active, panel, focusables);\n};\n\n// On `document` so it also catches focus that escaped behind the overlay.\nconst pushFocusTrapLayer = (getPanel: PanelSource, getHost: () => HTMLElement | null) => {\n if (trapLayers.length === 0) {\n document.addEventListener('keydown', handleTabKeyDown);\n }\n trapLayers.push(getPanel);\n hostSources.set(getPanel, getHost);\n trapTickets.set(getPanel, nextLayerTicket());\n notifyStackChanged();\n return () => {\n const index = trapLayers.lastIndexOf(getPanel);\n if (index !== -1) {\n trapLayers.splice(index, 1);\n }\n elevatedLayers.delete(getPanel);\n hostSources.delete(getPanel);\n trapTickets.delete(getPanel);\n const host = getHost();\n if (host) {\n leaveTopLayer(host);\n }\n if (trapLayers.length === 0) {\n document.removeEventListener('keydown', handleTabKeyDown);\n }\n notifyStackChanged();\n };\n};\n\n/**\n * Whether a SidePanel, Modal or CommandPalette is open anywhere, for host chrome that has to move out of the way.\n * Overlays portal, so the only other signal is this module's markers, which a host cannot read\n * without coupling to an internal. Dropdowns and tooltips do not count: they trap no focus.\n */\nexport const useHasOpenOverlay = (): boolean =>\n useSyncExternalStore(\n subscribeToStack,\n () => trapLayers.length > 0,\n () => false,\n );\n\nconst takeInitialFocus = (panel: HTMLElement, initialFocus: 'panel' | 'content'): void => {\n if (initialFocus === 'content') {\n const active = getDeepActiveElement();\n if (containsComposed(panel, active)) {\n return;\n }\n (firstTabStopIn(panel) ?? panel).focus();\n return;\n }\n panel.focus();\n};\n\n/**\n * The modal-overlay behaviour shared by SidePanel, Modal and CommandPalette: ESC and the Tab focus trap\n * (topmost overlay only, so these may be stacked), background scroll lock, and focus moved\n * into the overlay on open then restored to the trigger on close.\n *\n * Returns the ref callback the dialog element must take, whether this overlay is the topmost\n * one — only that one may claim `aria-modal`, or a screen reader can still browse the overlay\n * underneath — the z-index its host must carry, which keeps paint order with the focus stack,\n * and whether it is elevated. Presentation otherwise stays with the calling component.\n */\nexport const useOverlayBehaviour = ({\n open,\n onClose,\n elevated = false,\n hostRef,\n initialFocus = 'panel',\n}: {\n open: boolean;\n onClose: () => void;\n elevated?: boolean;\n /** The shadow host, which the stack promotes into the top layer while elevated. */\n hostRef?: { current: HTMLElement | null };\n /** Where focus lands on open: the panel itself, or the content's own first control. */\n initialFocus?: 'panel' | 'content';\n}): {\n ref: (node: HTMLElement | null) => void;\n isTopmost: boolean;\n zIndex: number | undefined;\n elevated: boolean;\n} => {\n const panelRef = useRef<HTMLElement | null>(null);\n // One identity for the life of the instance: elevation registers it in the layout phase,\n // before the effect below pushes it onto the stack.\n const source = useRef<PanelSource>(() => panelRef.current).current;\n useOwnedByPopover(\n useCallback(() => hostRef?.current ?? panelRef.current, [hostRef]),\n open,\n );\n const previousFocusRef = useRef<HTMLElement | null>(null);\n const layerRef = useRef<PanelSource | null>(null);\n const initialFocusRef = useRef(initialFocus);\n\n // Ref so the ESC layer registers once per open, not on every inline-onClose change.\n const onCloseRef = useRef(onClose);\n useEffect(() => {\n onCloseRef.current = onClose;\n }, [onClose]);\n\n // Before the trap effect, so the trigger is recorded before focus moves in — and relies on\n // ShadowRootWrapper portaling a commit late, or this would capture the panel itself.\n useEffect(() => {\n if (!open) {\n return;\n }\n previousFocusRef.current = getDeepActiveElement();\n return () => {\n const previous = previousFocusRef.current;\n previousFocusRef.current = null;\n if (previous?.isConnected) {\n previous.focus();\n }\n };\n }, [open]);\n\n useEffect(() => {\n if (!open) {\n return;\n }\n layerRef.current = source;\n const popEscapeLayer = pushEscapeLayer(() => onCloseRef.current());\n const releaseScrollLock = lockBodyScroll();\n const popFocusTrapLayer = pushFocusTrapLayer(source, () => hostRef?.current ?? null);\n // Reopening inside the exit transition reuses the same node, so the ref callback\n // never fires again and nothing would take focus.\n if (panelRef.current) {\n takeInitialFocus(panelRef.current, initialFocusRef.current);\n }\n return () => {\n layerRef.current = null;\n popFocusTrapLayer();\n popEscapeLayer();\n releaseScrollLock();\n };\n }, [open, hostRef, source]);\n\n // Its own effect: re-pushing the trap layer to change elevation would move this overlay to\n // the top of the stack, handing it the Escape and Tab that belong to a dialog above it.\n // Layout, and declared above the sync below, so that sync sees it in the same commit.\n useLayoutEffect(() => {\n if (!open || !elevated) {\n return;\n }\n elevatedLayers.add(source);\n notifyStackChanged();\n return () => {\n elevatedLayers.delete(source);\n notifyStackChanged();\n };\n }, [open, elevated, source]);\n\n // Only an open overlay subscribes: a mounted-but-idle one must carry no at-rest cost,\n // and its snapshot is a constant `false` regardless of what the stack does.\n const subscribe = useCallback(\n (onStackChange: () => void) => (open ? subscribeToStack(onStackChange) : NO_SUBSCRIPTION),\n [open],\n );\n\n const isTopmost = useSyncExternalStore(\n subscribe,\n // Gated on `open` so an overlay fading out stops claiming modal. Before it registers\n // it is the one about to go on top, or aria-modal would drop for a frame on open.\n () =>\n open &&\n (layerRef.current === null || trapLayers[trapLayers.length - 1] === layerRef.current),\n () => true,\n );\n\n // Its own request counts before the stack has it: the layer is not pushed until effects run.\n const stackElevated = useSyncExternalStore(\n subscribe,\n () => open && (elevated || elevatedLayers.size > 0),\n () => false,\n );\n\n // Overlay hosts share one z-index band, so paint order would otherwise fall out of body\n // order: an overlay reopened over a newer one takes the keyboard while painting under it.\n const zIndex = useSyncExternalStore(\n subscribe,\n () => {\n const layer = layerRef.current;\n return OVERLAY_Z_BASE + (open && layer ? Math.max(0, trapLayers.indexOf(layer)) : 0);\n },\n () => OVERLAY_Z_BASE,\n );\n\n // Layout, not passive: the rule has to exist before the first paint the attribute applies to.\n useLayoutEffect(() => {\n if (stackElevated) {\n ensureElevatedOverlayStyles();\n }\n }, [stackElevated]);\n\n // No deps: the host mounts a commit after the layer that owns it, so there is no stack\n // change left to key this on. `syncTopLayer` compares before it touches anything.\n useLayoutEffect(() => {\n syncTopLayer();\n });\n\n const ref = useCallback((node: HTMLElement | null) => {\n panelRef.current = node;\n // The panel mounts fresh on every open, so first attach = time to take focus.\n if (node) {\n takeInitialFocus(node, initialFocusRef.current);\n }\n }, []);\n\n return { ref, isTopmost, zIndex: stackElevated ? undefined : zIndex, elevated: stackElevated };\n};\n\nexport default useOverlayBehaviour;\n"],"names":["OVERLAY_ICON_BUTTON_CLASSES","FOCUS_RING","FOCUSABLE_SELECTOR","flatParent","element","firstSummaryOf","details","child","createRenderStates","states","stateOf","cached","display","visibility","contentVisibility","parent","parentState","closedDisclosure","state","isTabStop","hidden","invisible","isGroupedRadio","skipsWithinRadioGroup","reachable","group","candidate","radio","inTabOrder","entries","tabIndexOf","index","left","right","isShadowSlot","scopeMembers","elements","into","stopsInScope","members","stops","root","own","tabStopsIn","composedPosition","from","to","ancestry","node","chain","fromChain","toChain","fromStep","toStep","step","firstTabStopIn","trapLayers","elevatedLayers","hostSources","trapTickets","stackListeners","NO_SUBSCRIPTION","promoted","aboveStack","syncTopLayer","wanted","layer","_a","host","stillWanted","leaveTopLayer","raiseToTopLayer","keepAboveOverlayStack","notifyStackChanged","listener","subscribeToStack","wrapTab","event","active","within","first","last","inside","containsComposed","stop","handleTabKeyDown","panel","trapTicket","getDeepActiveElement","popupLayerHolding","popupStops","focusables","pushFocusTrapLayer","getPanel","getHost","nextLayerTicket","useHasOpenOverlay","useSyncExternalStore","takeInitialFocus","initialFocus","useOverlayBehaviour","open","onClose","elevated","hostRef","panelRef","useRef","source","useOwnedByPopover","useCallback","previousFocusRef","layerRef","initialFocusRef","onCloseRef","useEffect","previous","popEscapeLayer","pushEscapeLayer","releaseScrollLock","lockBodyScroll","popFocusTrapLayer","useLayoutEffect","subscribe","onStackChange","isTopmost","stackElevated","zIndex","OVERLAY_Z_BASE","ensureElevatedOverlayStyles"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import { injectStyleSheet as t } from "./injectStyleSheet.js";
2
- const s = "data-malachite-overlay", e = {
2
+ const r = "data-malachite-overlay", e = {
3
3
  dropdown: "malachite-dropdown-surface",
4
4
  multiselect: "malachite-multiselect-surface",
5
5
  actionmenu: "malachite-actionmenu-surface",
@@ -7,38 +7,44 @@ const s = "data-malachite-overlay", e = {
7
7
  sidepanel: "malachite-drawer-surface",
8
8
  modal: "malachite-modal-surface",
9
9
  commandpalette: "malachite-cmdpalette-surface",
10
- popover: "malachite-popover-surface"
11
- }, R = (o) => ({
10
+ popover: "malachite-popover-surface",
11
+ toast: "malachite-toast-surface"
12
+ }, T = (o) => ({
12
13
  className: e[o],
13
- [s]: o
14
- }), i = "malachite-overlay-styles", S = 1e3, c = 2147483646, a = "data-malachite-elevated", n = (o) => Object.values(e).map((r) => `.${r}:popover-open${o}`).join(", "), l = "inset: auto; margin: 0; border: 0; padding: 0; width: auto; height: auto; overflow: visible; background: none; color: inherit;", p = [
14
+ [r]: o
15
+ }), i = "malachite-overlay-styles", O = 1e3, s = 2147483646, n = "data-malachite-elevated", c = (o) => Object.values(e).map((l) => `.${l}:popover-open${o}`).join(", "), a = "inset: auto; margin: 0; border: 0; padding: 0; width: auto; height: auto; overflow: visible; background: none; color: inherit;", p = [
15
16
  `.${e.dropdown}, .${e.multiselect}, .${e.actionmenu} { z-index: 2147483646; }`,
16
17
  `.${e.tooltip} { z-index: 2147483647; }`
17
- ], E = "malachite-overlay-menu-box", d = [
18
+ ], d = "malachite-overlay-menu-box", E = [
18
19
  `.${e.dropdown}, .${e.dropdown} *, .${e.multiselect}, .${e.multiselect} *, .${e.actionmenu}, .${e.actionmenu} * { box-sizing: border-box; }`
19
20
  ], m = "malachite-overlay-popover", u = [
20
- `.${e.popover} { z-index: ${c}; }`,
21
- `.${e.popover}:popover-open { ${l} }`,
21
+ `.${e.popover} { z-index: ${s}; }`,
22
+ `.${e.popover}:popover-open { ${a} }`,
22
23
  `.${e.popover}:popover-open::backdrop { display: none; }`
23
- ], y = () => t(m, u), O = () => (t(E, d), t(i, p)), _ = "malachite-overlay-elevated", v = [
24
- `.${e.sidepanel}[${a}], .${e.modal}[${a}] { z-index: ${c}; }`
25
- ], A = () => t(_, v), L = "malachite-overlay-top-layer", $ = [
26
- `${n("")} { ${l} }`,
27
- `${n("::backdrop")} { display: none; }`
28
- ], T = () => {
29
- const o = t(L, $);
24
+ ], R = () => t(m, u), A = () => (t(d, E), t(i, p)), _ = "malachite-overlay-elevated", $ = [
25
+ `.${e.sidepanel}[${n}], .${e.modal}[${n}] { z-index: ${s}; }`
26
+ ], Y = () => t(_, $), v = "malachite-overlay-toast", L = [
27
+ `.${e.toast} { z-index: 2147483647; }`,
28
+ `.${e.toast}:popover-open { ${a} }`,
29
+ `.${e.toast}:popover-open::backdrop { display: none; }`
30
+ ], V = () => t(v, L), S = "malachite-overlay-top-layer", h = [
31
+ `${c("")} { ${a} }`,
32
+ `${c("::backdrop")} { display: none; }`
33
+ ], b = () => {
34
+ const o = t(S, h);
30
35
  return o && o.cssRules.length > 0 ? o : null;
31
36
  };
32
37
  export {
33
- s as OVERLAY_ATTR,
38
+ r as OVERLAY_ATTR,
34
39
  e as OVERLAY_CLASS,
35
- a as OVERLAY_ELEVATED_ATTR,
36
- S as OVERLAY_Z_BASE,
37
- c as OVERLAY_Z_ELEVATED,
38
- A as ensureElevatedOverlayStyles,
39
- O as ensureOverlayStyles,
40
- y as ensurePopoverStyles,
41
- T as ensureTopLayerStyles,
42
- R as overlayIdentity
40
+ n as OVERLAY_ELEVATED_ATTR,
41
+ O as OVERLAY_Z_BASE,
42
+ s as OVERLAY_Z_ELEVATED,
43
+ Y as ensureElevatedOverlayStyles,
44
+ A as ensureOverlayStyles,
45
+ R as ensurePopoverStyles,
46
+ V as ensureToastStyles,
47
+ b as ensureTopLayerStyles,
48
+ T as overlayIdentity
43
49
  };
44
50
  //# sourceMappingURL=portalOverlay.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"portalOverlay.js","sources":["../../src/components/portalOverlay.ts"],"sourcesContent":["// Host \"kill the ad overlay\" CSS erases anything portaled into document.body: `body >\n// div:not([class])` matches an unclassed body child, `[style*=\"2147483\"]` a max-int z-index.\n// The second matches the style ATTRIBUTE, so declaring z-index here keeps max-int stacking.\n\nimport { injectStyleSheet } from './injectStyleSheet';\n\n/** One selector for every malachite overlay, so a host allow-lists them in one rule. */\nexport const OVERLAY_ATTR = 'data-malachite-overlay';\n\n// Values avoid `panel`/`popup`/`overlay`/`tooltip`: a class defeats `:not([class])`, but one\n// matching `[class*=\"panel\"]` trades that hole for another these elements did not have.\nexport const OVERLAY_CLASS = {\n dropdown: 'malachite-dropdown-surface',\n multiselect: 'malachite-multiselect-surface',\n actionmenu: 'malachite-actionmenu-surface',\n tooltip: 'malachite-tip-surface',\n sidepanel: 'malachite-drawer-surface',\n modal: 'malachite-modal-surface',\n commandpalette: 'malachite-cmdpalette-surface',\n popover: 'malachite-popover-surface',\n} as const;\n\nexport type OverlayName = keyof typeof OVERLAY_CLASS;\n\n// Exhaustive, so a new overlay fails to typecheck until classified, and a popup is then refused\n// by `overlayIdentity`: its only identity comes from `usePopupLayer`, which registers it.\nconst OVERLAY_ROLE = {\n dropdown: 'popup',\n multiselect: 'popup',\n actionmenu: 'popup',\n popover: 'popup',\n sidepanel: 'trap',\n modal: 'trap',\n commandpalette: 'trap',\n tooltip: 'unfocusable',\n} as const satisfies Record<OverlayName, 'popup' | 'trap' | 'unfocusable'>;\n\nexport type PopupOverlayName = {\n [Name in OverlayName]: (typeof OVERLAY_ROLE)[Name] extends 'popup' ? Name : never;\n}[OverlayName];\n\nexport const overlayIdentity = (name: Exclude<OverlayName, PopupOverlayName>) => ({\n className: OVERLAY_CLASS[name],\n [OVERLAY_ATTR]: name,\n});\n\nconst OVERLAY_STYLE_ID = 'malachite-overlay-styles';\n\n/**\n * Where SidePanel, Modal and CommandPalette start. `useOverlayBehaviour` steps one per stacked overlay, so\n * paint order follows the focus stack rather than body order — a handful above this at most.\n */\nexport const OVERLAY_Z_BASE = 1000;\n\n/**\n * The dropdown band, which clears app chrome pinned just under max-int. One integer wide: a host\n * pins its chrome at max-int-1 and the tooltip band sits at max-int, so the stack cannot step\n * per layer the way the base band does — the top layer orders it, and where popover is\n * unsupported, document order does.\n */\nexport const OVERLAY_Z_ELEVATED = 2147483646;\n\n/** Marks a host in that band — an attribute, not an inline z-index; see above. */\nexport const OVERLAY_ELEVATED_ATTR = 'data-malachite-elevated';\n\n// Every overlay class, not the four promoted by default: one the reset misses wears the UA box.\nconst promotedSelector = (suffix: string) =>\n Object.values(OVERLAY_CLASS)\n .map((name) => `.${name}:popover-open${suffix}`)\n .join(', ');\n\n// Promotion drags in the UA `[popover]` box: `inset: 0` (which survives as right/bottom and\n// stretches a panel to the viewport edge), auto margins, a border, padding and fit-content\n// sizing. Reset to what these computed before promotion; their own inline styles still win.\nconst POPOVER_RESET =\n 'inset: auto; margin: 0; border: 0; padding: 0; width: auto; height: auto; ' +\n 'overflow: visible; background: none; color: inherit;';\n\nconst OVERLAY_RULES = [\n `.${OVERLAY_CLASS.dropdown}, .${OVERLAY_CLASS.multiselect}, .${OVERLAY_CLASS.actionmenu} { z-index: 2147483646; }`,\n `.${OVERLAY_CLASS.tooltip} { z-index: 2147483647; }`,\n];\n\n// Its own id, never folded into OVERLAY_STYLE_ID: injectStyleSheet keeps one sheet per id across\n// malachite copies sharing a realm, so a copy predating this rule would win that id and leave a\n// portaled menu on the host page's box model — a 280px Options menu painting 298. No copy has\n// ever registered this one.\nconst MENU_BOX_STYLE_ID = 'malachite-overlay-menu-box';\n\n// The scoped stylesheet makes everything border-box; a portal escapes it onto a host page that\n// may have no reset at all, while the geometry is computed as a CSS width.\nconst MENU_BOX_RULES = [\n `.${OVERLAY_CLASS.dropdown}, .${OVERLAY_CLASS.dropdown} *,` +\n ` .${OVERLAY_CLASS.multiselect}, .${OVERLAY_CLASS.multiselect} *,` +\n ` .${OVERLAY_CLASS.actionmenu}, .${OVERLAY_CLASS.actionmenu} *` +\n ' { box-sizing: border-box; }',\n];\n\nconst POPOVER_STYLE_ID = 'malachite-overlay-popover';\n\nconst POPOVER_RULES = [\n `.${OVERLAY_CLASS.popover} { z-index: ${OVERLAY_Z_ELEVATED}; }`,\n `.${OVERLAY_CLASS.popover}:popover-open { ${POPOVER_RESET} }`,\n `.${OVERLAY_CLASS.popover}:popover-open::backdrop { display: none; }`,\n];\n\nexport const ensurePopoverStyles = (): CSSStyleSheet | null =>\n injectStyleSheet(POPOVER_STYLE_ID, POPOVER_RULES);\n\nexport const ensureOverlayStyles = (): CSSStyleSheet | null => {\n injectStyleSheet(MENU_BOX_STYLE_ID, MENU_BOX_RULES);\n return injectStyleSheet(OVERLAY_STYLE_ID, OVERLAY_RULES);\n};\n\n// Its own id like TOP_LAYER_STYLE_ID: a copy predating elevation would own OVERLAY_STYLE_ID.\nconst ELEVATED_STYLE_ID = 'malachite-overlay-elevated';\n\nconst ELEVATED_RULES = [\n `.${OVERLAY_CLASS.sidepanel}[${OVERLAY_ELEVATED_ATTR}], .${OVERLAY_CLASS.modal}[${OVERLAY_ELEVATED_ATTR}] { z-index: ${OVERLAY_Z_ELEVATED}; }`,\n];\n\nexport const ensureElevatedOverlayStyles = (): CSSStyleSheet | null =>\n injectStyleSheet(ELEVATED_STYLE_ID, ELEVATED_RULES);\n\n// Its own id, never folded into OVERLAY_STYLE_ID: injectStyleSheet keeps one sheet per id across\n// malachite copies sharing a realm, so a copy predating the reset would win that id and leave a\n// promoted overlay wearing the raw UA box. No copy has ever registered this one.\nconst TOP_LAYER_STYLE_ID = 'malachite-overlay-top-layer';\n\nconst TOP_LAYER_RULES = [\n `${promotedSelector('')} { ${POPOVER_RESET} }`,\n `${promotedSelector('::backdrop')} { display: none; }`,\n];\n\nexport const ensureTopLayerStyles = (): CSSStyleSheet | null => {\n // A sheet that installed does not mean its rules parsed: replaceSync (and the <style> path)\n // drop an unparseable rule silently rather than throwing, so an engine that chokes on\n // `:popover-open` or `::backdrop` hands back a valid, empty sheet.\n const sheet = injectStyleSheet(TOP_LAYER_STYLE_ID, TOP_LAYER_RULES);\n return sheet && sheet.cssRules.length > 0 ? sheet : null;\n};\n"],"names":["OVERLAY_ATTR","OVERLAY_CLASS","overlayIdentity","name","OVERLAY_STYLE_ID","OVERLAY_Z_BASE","OVERLAY_Z_ELEVATED","OVERLAY_ELEVATED_ATTR","promotedSelector","suffix","POPOVER_RESET","OVERLAY_RULES","MENU_BOX_STYLE_ID","MENU_BOX_RULES","POPOVER_STYLE_ID","POPOVER_RULES","ensurePopoverStyles","injectStyleSheet","ensureOverlayStyles","ELEVATED_STYLE_ID","ELEVATED_RULES","ensureElevatedOverlayStyles","TOP_LAYER_STYLE_ID","TOP_LAYER_RULES","ensureTopLayerStyles","sheet"],"mappings":";AAOO,MAAMA,IAAe,0BAIfC,IAAgB;AAAA,EACzB,UAAU;AAAA,EACV,aAAa;AAAA,EACb,YAAY;AAAA,EACZ,SAAS;AAAA,EACT,WAAW;AAAA,EACX,OAAO;AAAA,EACP,gBAAgB;AAAA,EAChB,SAAS;AACb,GAqBaC,IAAkB,CAACC,OAAkD;AAAA,EAC9E,WAAWF,EAAcE,CAAI;AAAA,EAC7B,CAACH,CAAY,GAAGG;AACpB,IAEMC,IAAmB,4BAMZC,IAAiB,KAQjBC,IAAqB,YAGrBC,IAAwB,2BAG/BC,IAAmB,CAACC,MACtB,OAAO,OAAOR,CAAa,EACtB,IAAI,CAACE,MAAS,IAAIA,CAAI,gBAAgBM,CAAM,EAAE,EAC9C,KAAK,IAAI,GAKZC,IACF,kIAGEC,IAAgB;AAAA,EAClB,IAAIV,EAAc,QAAQ,MAAMA,EAAc,WAAW,MAAMA,EAAc,UAAU;AAAA,EACvF,IAAIA,EAAc,OAAO;AAC7B,GAMMW,IAAoB,8BAIpBC,IAAiB;AAAA,EACnB,IAAIZ,EAAc,QAAQ,MAAMA,EAAc,QAAQ,QAC7CA,EAAc,WAAW,MAAMA,EAAc,WAAW,QACxDA,EAAc,UAAU,MAAMA,EAAc,UAAU;AAEnE,GAEMa,IAAmB,6BAEnBC,IAAgB;AAAA,EAClB,IAAId,EAAc,OAAO,eAAeK,CAAkB;AAAA,EAC1D,IAAIL,EAAc,OAAO,mBAAmBS,CAAa;AAAA,EACzD,IAAIT,EAAc,OAAO;AAC7B,GAEae,IAAsB,MAC/BC,EAAiBH,GAAkBC,CAAa,GAEvCG,IAAsB,OAC/BD,EAAiBL,GAAmBC,CAAc,GAC3CI,EAAiBb,GAAkBO,CAAa,IAIrDQ,IAAoB,8BAEpBC,IAAiB;AAAA,EACnB,IAAInB,EAAc,SAAS,IAAIM,CAAqB,OAAON,EAAc,KAAK,IAAIM,CAAqB,gBAAgBD,CAAkB;AAC7I,GAEae,IAA8B,MACvCJ,EAAiBE,GAAmBC,CAAc,GAKhDE,IAAqB,+BAErBC,IAAkB;AAAA,EACpB,GAAGf,EAAiB,EAAE,CAAC,MAAME,CAAa;AAAA,EAC1C,GAAGF,EAAiB,YAAY,CAAC;AACrC,GAEagB,IAAuB,MAA4B;AAI5D,QAAMC,IAAQR,EAAiBK,GAAoBC,CAAe;AAClE,SAAOE,KAASA,EAAM,SAAS,SAAS,IAAIA,IAAQ;AACxD;"}
1
+ {"version":3,"file":"portalOverlay.js","sources":["../../src/components/portalOverlay.ts"],"sourcesContent":["// Host \"kill the ad overlay\" CSS erases anything portaled into document.body: `body >\n// div:not([class])` matches an unclassed body child, `[style*=\"2147483\"]` a max-int z-index.\n// The second matches the style ATTRIBUTE, so declaring z-index here keeps max-int stacking.\n\nimport { injectStyleSheet } from './injectStyleSheet';\n\n/** One selector for every malachite overlay, so a host allow-lists them in one rule. */\nexport const OVERLAY_ATTR = 'data-malachite-overlay';\n\n// Values avoid `panel`/`popup`/`overlay`/`tooltip`: a class defeats `:not([class])`, but one\n// matching `[class*=\"panel\"]` trades that hole for another these elements did not have.\nexport const OVERLAY_CLASS = {\n dropdown: 'malachite-dropdown-surface',\n multiselect: 'malachite-multiselect-surface',\n actionmenu: 'malachite-actionmenu-surface',\n tooltip: 'malachite-tip-surface',\n sidepanel: 'malachite-drawer-surface',\n modal: 'malachite-modal-surface',\n commandpalette: 'malachite-cmdpalette-surface',\n popover: 'malachite-popover-surface',\n toast: 'malachite-toast-surface',\n} as const;\n\nexport type OverlayName = keyof typeof OVERLAY_CLASS;\n\n// Exhaustive, so a new overlay fails to typecheck until classified, and a popup is then refused\n// by `overlayIdentity`: its only identity comes from `usePopupLayer`, which registers it.\nconst OVERLAY_ROLE = {\n dropdown: 'popup',\n multiselect: 'popup',\n actionmenu: 'popup',\n popover: 'popup',\n sidepanel: 'trap',\n modal: 'trap',\n commandpalette: 'trap',\n tooltip: 'unfocusable',\n toast: 'unfocusable',\n} as const satisfies Record<OverlayName, 'popup' | 'trap' | 'unfocusable'>;\n\nexport type PopupOverlayName = {\n [Name in OverlayName]: (typeof OVERLAY_ROLE)[Name] extends 'popup' ? Name : never;\n}[OverlayName];\n\nexport const overlayIdentity = (name: Exclude<OverlayName, PopupOverlayName>) => ({\n className: OVERLAY_CLASS[name],\n [OVERLAY_ATTR]: name,\n});\n\nconst OVERLAY_STYLE_ID = 'malachite-overlay-styles';\n\n/**\n * Where SidePanel, Modal and CommandPalette start. `useOverlayBehaviour` steps one per stacked overlay, so\n * paint order follows the focus stack rather than body order — a handful above this at most.\n */\nexport const OVERLAY_Z_BASE = 1000;\n\n/**\n * The dropdown band, which clears app chrome pinned just under max-int. One integer wide: a host\n * pins its chrome at max-int-1 and the tooltip band sits at max-int, so the stack cannot step\n * per layer the way the base band does — the top layer orders it, and where popover is\n * unsupported, document order does.\n */\nexport const OVERLAY_Z_ELEVATED = 2147483646;\n\n/** Marks a host in that band — an attribute, not an inline z-index; see above. */\nexport const OVERLAY_ELEVATED_ATTR = 'data-malachite-elevated';\n\n// Every overlay class, not the four promoted by default: one the reset misses wears the UA box.\nconst promotedSelector = (suffix: string) =>\n Object.values(OVERLAY_CLASS)\n .map((name) => `.${name}:popover-open${suffix}`)\n .join(', ');\n\n// Promotion drags in the UA `[popover]` box: `inset: 0` (which survives as right/bottom and\n// stretches a panel to the viewport edge), auto margins, a border, padding and fit-content\n// sizing. Reset to what these computed before promotion; their own inline styles still win.\nconst POPOVER_RESET =\n 'inset: auto; margin: 0; border: 0; padding: 0; width: auto; height: auto; ' +\n 'overflow: visible; background: none; color: inherit;';\n\nconst OVERLAY_RULES = [\n `.${OVERLAY_CLASS.dropdown}, .${OVERLAY_CLASS.multiselect}, .${OVERLAY_CLASS.actionmenu} { z-index: 2147483646; }`,\n `.${OVERLAY_CLASS.tooltip} { z-index: 2147483647; }`,\n];\n\n// Its own id, never folded into OVERLAY_STYLE_ID: injectStyleSheet keeps one sheet per id across\n// malachite copies sharing a realm, so a copy predating this rule would win that id and leave a\n// portaled menu on the host page's box model — a 280px Options menu painting 298. No copy has\n// ever registered this one.\nconst MENU_BOX_STYLE_ID = 'malachite-overlay-menu-box';\n\n// The scoped stylesheet makes everything border-box; a portal escapes it onto a host page that\n// may have no reset at all, while the geometry is computed as a CSS width.\nconst MENU_BOX_RULES = [\n `.${OVERLAY_CLASS.dropdown}, .${OVERLAY_CLASS.dropdown} *,` +\n ` .${OVERLAY_CLASS.multiselect}, .${OVERLAY_CLASS.multiselect} *,` +\n ` .${OVERLAY_CLASS.actionmenu}, .${OVERLAY_CLASS.actionmenu} *` +\n ' { box-sizing: border-box; }',\n];\n\nconst POPOVER_STYLE_ID = 'malachite-overlay-popover';\n\nconst POPOVER_RULES = [\n `.${OVERLAY_CLASS.popover} { z-index: ${OVERLAY_Z_ELEVATED}; }`,\n `.${OVERLAY_CLASS.popover}:popover-open { ${POPOVER_RESET} }`,\n `.${OVERLAY_CLASS.popover}:popover-open::backdrop { display: none; }`,\n];\n\nexport const ensurePopoverStyles = (): CSSStyleSheet | null =>\n injectStyleSheet(POPOVER_STYLE_ID, POPOVER_RULES);\n\nexport const ensureOverlayStyles = (): CSSStyleSheet | null => {\n injectStyleSheet(MENU_BOX_STYLE_ID, MENU_BOX_RULES);\n return injectStyleSheet(OVERLAY_STYLE_ID, OVERLAY_RULES);\n};\n\n// Its own id like TOP_LAYER_STYLE_ID: a copy predating elevation would own OVERLAY_STYLE_ID.\nconst ELEVATED_STYLE_ID = 'malachite-overlay-elevated';\n\nconst ELEVATED_RULES = [\n `.${OVERLAY_CLASS.sidepanel}[${OVERLAY_ELEVATED_ATTR}], .${OVERLAY_CLASS.modal}[${OVERLAY_ELEVATED_ATTR}] { z-index: ${OVERLAY_Z_ELEVATED}; }`,\n];\n\nexport const ensureElevatedOverlayStyles = (): CSSStyleSheet | null =>\n injectStyleSheet(ELEVATED_STYLE_ID, ELEVATED_RULES);\n\n// Its own id, reset included: an older copy may own the shared sheets without the toast class.\nconst TOAST_STYLE_ID = 'malachite-overlay-toast';\n\nconst TOAST_RULES = [\n `.${OVERLAY_CLASS.toast} { z-index: 2147483647; }`,\n `.${OVERLAY_CLASS.toast}:popover-open { ${POPOVER_RESET} }`,\n `.${OVERLAY_CLASS.toast}:popover-open::backdrop { display: none; }`,\n];\n\nexport const ensureToastStyles = (): CSSStyleSheet | null =>\n injectStyleSheet(TOAST_STYLE_ID, TOAST_RULES);\n\n// Its own id, never folded into OVERLAY_STYLE_ID: injectStyleSheet keeps one sheet per id across\n// malachite copies sharing a realm, so a copy predating the reset would win that id and leave a\n// promoted overlay wearing the raw UA box. No copy has ever registered this one.\nconst TOP_LAYER_STYLE_ID = 'malachite-overlay-top-layer';\n\nconst TOP_LAYER_RULES = [\n `${promotedSelector('')} { ${POPOVER_RESET} }`,\n `${promotedSelector('::backdrop')} { display: none; }`,\n];\n\nexport const ensureTopLayerStyles = (): CSSStyleSheet | null => {\n // A sheet that installed does not mean its rules parsed: replaceSync (and the <style> path)\n // drop an unparseable rule silently rather than throwing, so an engine that chokes on\n // `:popover-open` or `::backdrop` hands back a valid, empty sheet.\n const sheet = injectStyleSheet(TOP_LAYER_STYLE_ID, TOP_LAYER_RULES);\n return sheet && sheet.cssRules.length > 0 ? sheet : null;\n};\n"],"names":["OVERLAY_ATTR","OVERLAY_CLASS","overlayIdentity","name","OVERLAY_STYLE_ID","OVERLAY_Z_BASE","OVERLAY_Z_ELEVATED","OVERLAY_ELEVATED_ATTR","promotedSelector","suffix","POPOVER_RESET","OVERLAY_RULES","MENU_BOX_STYLE_ID","MENU_BOX_RULES","POPOVER_STYLE_ID","POPOVER_RULES","ensurePopoverStyles","injectStyleSheet","ensureOverlayStyles","ELEVATED_STYLE_ID","ELEVATED_RULES","ensureElevatedOverlayStyles","TOAST_STYLE_ID","TOAST_RULES","ensureToastStyles","TOP_LAYER_STYLE_ID","TOP_LAYER_RULES","ensureTopLayerStyles","sheet"],"mappings":";AAOO,MAAMA,IAAe,0BAIfC,IAAgB;AAAA,EACzB,UAAU;AAAA,EACV,aAAa;AAAA,EACb,YAAY;AAAA,EACZ,SAAS;AAAA,EACT,WAAW;AAAA,EACX,OAAO;AAAA,EACP,gBAAgB;AAAA,EAChB,SAAS;AAAA,EACT,OAAO;AACX,GAsBaC,IAAkB,CAACC,OAAkD;AAAA,EAC9E,WAAWF,EAAcE,CAAI;AAAA,EAC7B,CAACH,CAAY,GAAGG;AACpB,IAEMC,IAAmB,4BAMZC,IAAiB,KAQjBC,IAAqB,YAGrBC,IAAwB,2BAG/BC,IAAmB,CAACC,MACtB,OAAO,OAAOR,CAAa,EACtB,IAAI,CAACE,MAAS,IAAIA,CAAI,gBAAgBM,CAAM,EAAE,EAC9C,KAAK,IAAI,GAKZC,IACF,kIAGEC,IAAgB;AAAA,EAClB,IAAIV,EAAc,QAAQ,MAAMA,EAAc,WAAW,MAAMA,EAAc,UAAU;AAAA,EACvF,IAAIA,EAAc,OAAO;AAC7B,GAMMW,IAAoB,8BAIpBC,IAAiB;AAAA,EACnB,IAAIZ,EAAc,QAAQ,MAAMA,EAAc,QAAQ,QAC7CA,EAAc,WAAW,MAAMA,EAAc,WAAW,QACxDA,EAAc,UAAU,MAAMA,EAAc,UAAU;AAEnE,GAEMa,IAAmB,6BAEnBC,IAAgB;AAAA,EAClB,IAAId,EAAc,OAAO,eAAeK,CAAkB;AAAA,EAC1D,IAAIL,EAAc,OAAO,mBAAmBS,CAAa;AAAA,EACzD,IAAIT,EAAc,OAAO;AAC7B,GAEae,IAAsB,MAC/BC,EAAiBH,GAAkBC,CAAa,GAEvCG,IAAsB,OAC/BD,EAAiBL,GAAmBC,CAAc,GAC3CI,EAAiBb,GAAkBO,CAAa,IAIrDQ,IAAoB,8BAEpBC,IAAiB;AAAA,EACnB,IAAInB,EAAc,SAAS,IAAIM,CAAqB,OAAON,EAAc,KAAK,IAAIM,CAAqB,gBAAgBD,CAAkB;AAC7I,GAEae,IAA8B,MACvCJ,EAAiBE,GAAmBC,CAAc,GAGhDE,IAAiB,2BAEjBC,IAAc;AAAA,EAChB,IAAItB,EAAc,KAAK;AAAA,EACvB,IAAIA,EAAc,KAAK,mBAAmBS,CAAa;AAAA,EACvD,IAAIT,EAAc,KAAK;AAC3B,GAEauB,IAAoB,MAC7BP,EAAiBK,GAAgBC,CAAW,GAK1CE,IAAqB,+BAErBC,IAAkB;AAAA,EACpB,GAAGlB,EAAiB,EAAE,CAAC,MAAME,CAAa;AAAA,EAC1C,GAAGF,EAAiB,YAAY,CAAC;AACrC,GAEamB,IAAuB,MAA4B;AAI5D,QAAMC,IAAQX,EAAiBQ,GAAoBC,CAAe;AAClE,SAAOE,KAASA,EAAM,SAAS,SAAS,IAAIA,IAAQ;AACxD;"}
@@ -0,0 +1,12 @@
1
+ import { useContext as o, createContext as e } from "react";
2
+ const r = e(null), n = () => {
3
+ const t = o(r);
4
+ if (!t)
5
+ throw new Error("useToast must be used within a ToastProvider");
6
+ return t;
7
+ };
8
+ export {
9
+ r as ToastContext,
10
+ n as useToast
11
+ };
12
+ //# sourceMappingURL=toastContext.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"toastContext.js","sources":["../../src/components/toastContext.ts"],"sourcesContent":["import { createContext, useContext } from 'react';\nimport type { ToastApi } from './toastTypes';\n\nexport const ToastContext = createContext<ToastApi | null>(null);\n\n/** Shows a toast from anywhere under a `ToastProvider`. Throws outside one. */\nexport const useToast = (): ToastApi => {\n const api = useContext(ToastContext);\n if (!api) {\n throw new Error('useToast must be used within a ToastProvider');\n }\n return api;\n};\n"],"names":["ToastContext","createContext","useToast","api","useContext"],"mappings":";AAGO,MAAMA,IAAeC,EAA+B,IAAI,GAGlDC,IAAW,MAAgB;AACpC,QAAMC,IAAMC,EAAWJ,CAAY;AACnC,MAAI,CAACG;AACD,UAAM,IAAI,MAAM,8CAA8C;AAElE,SAAOA;AACX;"}
@@ -0,0 +1,6 @@
1
+ const T = 3e3, _ = 5e3;
2
+ export {
3
+ _ as TOAST_ANNOUNCEMENT_LIFETIME_MS,
4
+ T as TOAST_DEFAULT_DURATION_MS
5
+ };
6
+ //# sourceMappingURL=toastTypes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"toastTypes.js","sources":["../../src/components/toastTypes.ts"],"sourcesContent":["export type ToastVariant = 'success' | 'error';\n\nexport interface ToastOptions {\n message: string;\n /** Defaults to `'success'`. */\n variant?: ToastVariant;\n}\n\nexport interface ToastApi {\n show: (options: ToastOptions) => void;\n}\n\nexport interface QueuedToast {\n id: number;\n message: string;\n variant: ToastVariant;\n}\n\nexport const TOAST_DEFAULT_DURATION_MS = 3000;\nexport const TOAST_MAX_VISIBLE = 3;\nexport const TOAST_ANNOUNCEMENT_LIFETIME_MS = 5000;\n"],"names":["TOAST_DEFAULT_DURATION_MS","TOAST_ANNOUNCEMENT_LIFETIME_MS"],"mappings":"AAkBO,MAAMA,IAA4B,KAE5BC,IAAiC;"}
package/dist/index.d.ts CHANGED
@@ -5326,6 +5326,26 @@ export declare const ThumbsDownIcon: React.FC<IconProps_107>;
5326
5326
 
5327
5327
  export declare const ThumbsUpIcon: React.FC<IconProps_108>;
5328
5328
 
5329
+ export declare interface ToastApi {
5330
+ show: (options: ToastOptions) => void;
5331
+ }
5332
+
5333
+ export declare interface ToastOptions {
5334
+ message: string;
5335
+ /** Defaults to `'success'`. */
5336
+ variant?: ToastVariant;
5337
+ }
5338
+
5339
+ export declare const ToastProvider: React.FC<ToastProviderProps>;
5340
+
5341
+ export declare interface ToastProviderProps {
5342
+ children?: React.ReactNode;
5343
+ /** How long each toast stays, in ms. Defaults to 3000. */
5344
+ duration?: number;
5345
+ }
5346
+
5347
+ export declare type ToastVariant = 'success' | 'error';
5348
+
5329
5349
  export declare const Toggle: default_2.ForwardRefExoticComponent<ToggleProps & default_2.RefAttributes<HTMLElement>>;
5330
5350
 
5331
5351
  declare interface ToggleProps {
@@ -5516,6 +5536,9 @@ export declare const useHasOpenOverlay: () => boolean;
5516
5536
 
5517
5537
  export declare const useSidebarCollapsed: () => boolean;
5518
5538
 
5539
+ /** Shows a toast from anywhere under a `ToastProvider`. Throws outside one. */
5540
+ export declare const useToast: () => ToastApi;
5541
+
5519
5542
  export declare const WarningCircleFilled: React.FC<IconProps_114>;
5520
5543
 
5521
5544
  export declare const WarningTriangleIcon: React.FC<IconProps_115>;