3.8 KiB
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 statusreports nothing) and no in-flight worktrees with uncommitted work (git worktree listthengit statusin 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.mdmatchesfind packages -maxdepth 3 -name package.jsoncount. Drift here means a prior change archived without regenerating the manifest.
1. Scaffold
- Create directory tree per proposal layout.
- Write
package.jsonat0.1.0with"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
ModalErrorextendingSuaveplanError; 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)usingquerySelectorAllfor focusable elements;activate()/deactivate(); wraps focus at boundaries. Write test, md.
4. Components
Modal
- Portal via
createPortaltodocument.body(or customportalTarget). - Activates
focus-trapon open, restores previous focus on close. - Calls
disableBodyScrollon open,enableBodyScrollon close. - ESC keydown closes if
closeOnEscprop (defaulttrue). - Backdrop click closes if
closeOnBackdropprop (defaulttrue). - Emits OTel
modal.openspan on open,modal.closeon close withmodal.idattribute. - 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
Modalwithtitle,description,actionsslots; renders<dialog>semantics (role="dialog",aria-labelledby,aria-describedby). - Write test, stories, md.
ConfirmDialog
- Composed on
Dialogwith confirm / cancel buttons; usesuseTranslationfor button labels. - Write test, stories, md.
ModalContext + ModalProvider
ModalProvidermaintains aMap<string, ReactElement>of open modals; renders each viaModal.useModalContextthrowsModalError(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, verifyoverflow: hiddenonbody. - Open two stacked modals via
ModalProvider, close inner, verify outer still rendered.
- Open
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.