Skip to content

refactor(dialog): extract shared overlay utilities#16

Closed
ivanbanov wants to merge 2 commits into
mainfrom
feat/floating
Closed

refactor(dialog): extract shared overlay utilities#16
ivanbanov wants to merge 2 commits into
mainfrom
feat/floating

Conversation

@ivanbanov

@ivanbanov ivanbanov commented Jul 18, 2026

Copy link
Copy Markdown
Member

Summary

The overlay family — dialog, drawer, alert-dialog, popover, menu, combobox — shares the same coordination problems: which layer owns Escape and outside presses when overlays stack, how nesting is tracked across portals, how the rest of the page is hidden from assistive tech, and how a non-modal overlay detects an outside interaction with no backdrop to catch it. This PR extracts that behavior from the dialog's internals into shared packages so every upcoming overlay primitive builds on one implementation instead of growing its own. The overlay-component PRs are stacked on this branch.

New packages:

  • @dunky.dev/dom-layer-stack — one shared registry of open overlay layers. registerLayer({ id, path, element, modal }) returns an unregister disposer; the path (the enclosing layers' ids, outermost first) carries nesting depth in its length and ancestry in its contents. isTopmostLayer answers which layer owns Escape, the focus trap, and outside presses (longest path wins, open order breaks ties); layerContainsTarget lets outside-press detection treat descendant layers as inside while siblings and unrelated stacks stay outside. Registering also keeps assistive-tech containment in sync: everything outside the topmost modal layer gets aria-hidden + inert — layers stacked above it stay reachable — with an exact undo.
  • @dunky.dev/react-use-layer-stack — the React half: LayerPathContext + useLayerPath() carry the nesting chain across portals, where DOM order would invert logical nesting; every overlay root appends its id and provides the extended path.
  • @dunky.dev/dom-interact-outside — document-level outside-interaction detection for non-modal overlays: trackInteractOutside(container, { onInteractOutside, ignore }) fires on capture-phase pointerdown / focusin outside the container, reporting a press that also moves focus once, not twice. A touch press reports only once its click confirms a tap, so a scroll or pan that starts outside never dismisses.
  • @dunky.dev/react-use-interact-outside — its React lifecycle: useInteractOutside(ref, options) binds while mounted and reads the handler and ignore predicate through refs, so inline closures never re-bind the document listeners.

Changes

  • @dunky.dev/react-dialog drops its private src/utils/stack.ts and src/utils/hide-outside.ts and consumes the shared layer stack instead — same behavior, one refinement: a non-modal layer opening above a modal dialog no longer drops the page's assistive-tech containment; the containment anchors on the topmost modal layer and keeps the layers above it reachable, which is what lets a popover or menu live inside a dialog.
  • Core dialog SPEC.md updated to reference the shared utilities.
  • Workspace wiring (tsconfig.json, tsdown.config.ts, lockfile) for the four new packages, plus a changeset.

Test plan

  • pnpm test:ci — 104 tests, 11 files, all passing
  • pnpm typecheck
  • pnpm lint
  • pnpm format:check
  • pnpm build — publint clean for all packages

🤖 Generated with Claude Code

ivanbanov and others added 2 commits July 18, 2026 11:58
New packages, extracted from the dialog's internals so the whole
overlay family (drawer, alert-dialog, popover, menu, combobox) can
build on them:

- @dunky.dev/dom-layer-stack — shared registry of open overlay layers
  (topmost resolution, nesting containment, aria-hidden/inert sync)
- @dunky.dev/react-use-layer-stack — LayerDepthContext + useLayerDepth
- @dunky.dev/dom-interact-outside — document-level outside-interaction
  tracking for non-modal overlays
- @dunky.dev/react-use-interact-outside — useInteractOutside lifecycle

@dunky.dev/react-dialog now consumes the shared layer stack instead of
its private one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Refine the extracted overlay-utility packages:

- @dunky.dev/dom-layer-stack — shared registry of open overlay layers
  (topmost resolution, nesting containment, aria-hidden/inert sync)
- @dunky.dev/react-use-layer-stack — LayerPathContext + useLayerPath
  (renamed from the depth-based API)
- @dunky.dev/dom-interact-outside — document-level outside-interaction
  tracking for non-modal overlays
- @dunky.dev/react-use-interact-outside — useInteractOutside lifecycle

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ivanbanov

Copy link
Copy Markdown
Member Author

Superseded by #29.

This branch extracted overlay utilities along different lines (separate dom-layer-stack + dom-interact-outside packages, path-based nesting) against a version of the dialog that has since been substantially reworked on main — the diff went stale (conflicting; it deletes src/utils/stack.ts/hide-outside.ts, which no longer exist).

#29 carries the same intent — one shared overlay-coordination layer for the whole family — rebuilt on today's main: an agnostic @dunky.dev/overlay kernel (native-reusable) plus a @dunky.dev/dom-overlay realization, replacing @dunky.dev/dom-dialog. The path-based nesting and interact-outside ideas from here are noted for when the first non-modal overlay lands.

@ivanbanov ivanbanov closed this Jul 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant