Files
2026-05-27 23:29:53 +01:00

3.8 KiB

Tasks: enterprise-react-modal

Change ID: enterprise-react-modal Package: @suaveplan/react-modal Target path: react/ui/react-modal Prerequisite packages on Verdaccio: @suaveplan/error, @suaveplan/react-translate


0. Pre-flight (non-negotiable)

  • 0.1 Confirm clean working tree (git status reports nothing) and no in-flight worktrees with uncommitted work (git worktree list then git status in each)
  • 0.2 If any worktrees exist under .claude/worktrees/, account for each one: still-active agent, work-pending-merge, or stale-and-prune. Stale worktrees MUST be removed (git worktree remove) before opening a new one — code that has been lost twice on main is the failure mode this guards against.
  • 0.3 Confirm the package set in MANIFEST.md matches find packages -maxdepth 3 -name package.json count. Drift here means a prior change archived without regenerating the manifest.

1. Scaffold

  • Create directory tree per proposal layout.
  • Write package.json at 0.1.0 with "sideEffects": false, exports map.
  • Dependencies: @suaveplan/error workspace:^, @suaveplan/react-translate workspace:^, zod catalog:forms, @opentelemetry/api catalog:observability, tslib catalog:build.
  • Write tsconfig.json, vitest.config.ts, scripts.

2. Error codes

  • Define ModalError extending SuaveplanError; code: MODAL_CONTEXT_MISSING.

3. Utilities

  • src/utils/scroll-lock/ — disableBodyScroll() / enableBodyScroll() with ref counting (nested modals). Write test (jsdom), md.
  • src/utils/focus-trap/ — createFocusTrap(container) using querySelectorAll for focusable elements; activate() / deactivate(); wraps focus at boundaries. Write test, md.

4. Components

Modal

  • Portal via createPortal to document.body (or custom portalTarget).
  • Activates focus-trap on open, restores previous focus on close.
  • Calls disableBodyScroll on open, enableBodyScroll on close.
  • ESC keydown closes if closeOnEsc prop (default true).
  • Backdrop click closes if closeOnBackdrop prop (default true).
  • Emits OTel modal.open span on open, modal.close on close with modal.id attribute.
  • Write modal.test.tsx (renders, ESC close, backdrop close, focus trap activated, scroll-lock toggled). { timeout: 5000 }.
  • Write modal.stories.tsx: default, no-backdrop-close, full-screen, playground.
  • Write modal.md (≥200 words).

Dialog

  • Composed on Modal with title, description, actions slots; renders <dialog> semantics (role="dialog", aria-labelledby, aria-describedby).
  • Write test, stories, md.

ConfirmDialog

  • Composed on Dialog with confirm / cancel buttons; uses useTranslation for button labels.
  • Write test, stories, md.

ModalContext + ModalProvider

  • ModalProvider maintains a Map<string, ReactElement> of open modals; renders each via Modal.
  • useModalContext throws ModalError(MODAL_CONTEXT_MISSING) outside provider.
  • open(id, element), close(id), closeAll(), isOpen(id).
  • Write test (open/close/closeAll/isOpen/stacking), stories (stacked-modals, playground), md.

5. E2E

  • e2e/modal.spec.ts — Playwright:
    • Open Modal, verify focus inside, Tab cycles within modal, Shift+Tab cycles backward, ESC closes.
    • Open Modal, verify overflow: hidden on body.
    • Open two stacked modals via ModalProvider, close inner, verify outer still rendered.

6. Verify and publish

  • bunx turbo lint typecheck test --filter=@suaveplan/react-modal — green.
  • bunx turbo test:coverage --filter=@suaveplan/react-modal — 100%.
  • bun scripts/publish.ts --package @suaveplan/react-modal.
  • Archive change folder.