Skip to content

feat(react): add the Drawer primitive#19

Open
ivanbanov wants to merge 2 commits into
feat/floatingfrom
feat/drawer
Open

feat(react): add the Drawer primitive#19
ivanbanov wants to merge 2 commits into
feat/floatingfrom
feat/drawer

Conversation

@ivanbanov

Copy link
Copy Markdown
Member

Summary

Adds the Drawer primitive across both scopes:

  • @dunky.dev/drawer (packages/core/drawer) — the framework-free state machine carrying all behavior.
  • @dunky.dev/react-drawer (packages/react/drawer) — the thin React binding, stories, and tests.

A drawer is a modal panel that slides in from a screen edge — semantically a dialog with a placement (left | right | top | bottom, default right). Placement is pure configuration: it never gates behavior and rides along as a data-placement styling hook on the visual layers next to data-state, so slide animations key off attributes. Always modal in v0 — no modal/role switch; snap points and swipe gestures are out of scope by decision.

Stacked PR: this PR is stacked on the feat/floating extraction PR and targets that branch; it will retarget to main once feat/floating merges.

Anatomy

  • Drawer — root; owns the machine and the open/close contract
  • Drawer.Trigger — button that toggles the drawer
  • Drawer.Portal — reparents the visual layers to the document body
  • Drawer.Backdrop — dimming layer behind the panel
  • Drawer.Viewport — positioning surface for the panel
  • Drawer.Content — the panel itself (role="dialog")
  • Drawer.Title / Drawer.Description — accessible name and description
  • Drawer.Close — button that closes the drawer

Behavior

  • The full dialog contract: focus trap inside the panel, scroll lock while open, and dismissal via Escape and outside press — each gateable and vetoable.
  • Topmost-layer-first stacking through the shared layer stack, so nested layered primitives dismiss in order.
  • Controlled open contract: a controlled drawer reports every open/close intent through onOpenChange but only moves when the prop changes — ignoring a report is a working veto.
  • ARIA: role="dialog", aria-modal, aria-labelledby/aria-describedby wired to Title/Description; trigger carries aria-haspopup, aria-expanded, and aria-controls.
  • Styling hooks: data-state="open | closed" everywhere, plus data-placement on Backdrop, Viewport, and Content.

Test plan

  • pnpm test:ci — 13 files, 162 tests passing
  • pnpm typecheck
  • pnpm lint
  • pnpm format:check
  • pnpm build

🤖 Generated with Claude Code

ivanbanov and others added 2 commits July 18, 2026 23:31
Adds @dunky.dev/drawer (framework-free machine) and
@dunky.dev/react-drawer (React binding). Anatomy: Drawer root with
Trigger, Portal, Backdrop, Viewport, Content, Title, Description, and
Close. A modal panel that slides in from a screen edge — the dialog
contract (focus trap, scroll lock, Escape/outside-press dismissal,
layer stacking, controlled open) plus a placement option surfaced as a
data-placement styling hook.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ivanbanov ivanbanov changed the title feat: add the Drawer primitive feat(react): add the Drawer primitive Jul 18, 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