Skip to content

feat(info-dialog): add InfoDialog pattern (FEC-437)#1388

Draft
misama-ct wants to merge 7 commits intomainfrom
FEC-437-create-info-dialog
Draft

feat(info-dialog): add InfoDialog pattern (FEC-437)#1388
misama-ct wants to merge 7 commits intomainfrom
FEC-437-create-info-dialog

Conversation

@misama-ct
Copy link
Copy Markdown
Collaborator

@misama-ct misama-ct commented Apr 17, 2026

Summary

Implements FEC-437: adds the InfoDialog pattern under packages/nimbus/src/patterns/dialogs/ as the Nimbus replacement for app-kit's InfoDialog (the most common read-only informational dialog in the MC codebase, ~131 production usages).

Phases

  • 1. OpenSpec proposalopenspec/changes/info-dialog-pattern/
  • 2. Scaffold — directory, barrels, patterns/dialogs/ sub-category
  • 3. Implementationinfo-dialog.tsx + info-dialog.types.ts
  • 4. Testinginfo-dialog.stories.tsx play functions + info-dialog.docs.spec.tsx
  • 5. Documentationinfo-dialog.dev.mdx (with escape-hatch section) + info-dialog.mdx

See openspec/changes/info-dialog-pattern/tasks.md for the full TDD-ordered checklist.

Pattern summary

Flat four-prop API that hides the underlying Dialog.RootContentHeaderTitleBodyCloseTrigger composition:

  • title: ReactNode
  • isOpen?: boolean
  • onOpenChange?: (isOpen: boolean) => void
  • children: ReactNode

No size, zIndex, aria-label, getParentSelector, or TextTitle props — a usage audit of the app-kit component found 83% default size, 96% string titles, and zero TextTitle uptake, so those are dropped or delegated to the underlying Dialog primitive. Consumers needing custom sizing or dismiss behavior get an escape-hatch section in the dev docs showing the equivalent manual Dialog composition.

Introduces a new patterns/dialogs/ sub-category alongside patterns/fields/ for future dialog patterns (confirm, destructive-confirm, etc.).

Test plan

  • openspec validate info-dialog-pattern --strict passes
  • pnpm --filter @commercetools/nimbus typecheck passes
  • pnpm --filter @commercetools/nimbus build succeeds
  • pnpm test:storybook — all play functions pass
  • Consumer implementation tests pass
  • Visual check in Storybook: string title, ReactNode title, long scrollable content, close via X / Escape / overlay click, focus trap and restoration

Related

  • Jira: FEC-437
  • Parent epic: FEC-428 (Application-Components Migration to Nimbus)
  • Replaces: merchant-center-application-kit InfoDialog

@vercel
Copy link
Copy Markdown

vercel bot commented Apr 17, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
nimbus-documentation Ready Ready Preview, Comment Apr 20, 2026 1:40pm
nimbus-storybook Ready Ready Preview, Comment Apr 20, 2026 1:40pm

Request Review

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 17, 2026

🦋 Changeset detected

Latest commit: 218f0b8

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 5 packages
Name Type
@commercetools/nimbus Major
@commercetools/nimbus-mcp Major
@commercetools/nimbus-tokens Major
@commercetools/nimbus-icons Major
@commercetools/nimbus-design-token-ts-plugin Major

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Add a pre-composed read-only informational dialog under
patterns/dialogs/info-dialog/ as the Nimbus replacement for
merchant-center-application-kit's InfoDialog. Exposes a flat four-prop
API (title, isOpen, onOpenChange, children) and composes Dialog.Root,
Dialog.Content, Dialog.Header, Dialog.Title, Dialog.Body, and
Dialog.CloseTrigger internally with isDismissable enabled. Introduces
patterns/dialogs/ sub-category alongside patterns/fields/.

Includes stories with play functions covering every close affordance
(X button, Escape, overlay click) plus focus-in and focus restoration,
consumer implementation tests, designer and developer MDX with an
escape-hatch section, and tasks.md marked complete against the
approved OpenSpec proposal.
Local build produced slightly smaller cjs output than CI (0.45% drift),
causing the 0.1% approval tolerance in check-bundle-size.mjs to reject
the baseline update. Set cjs to the triangulated midpoint of CI's
reported "62.9 KB / +5.7%" range.
@misama-ct
Copy link
Copy Markdown
Collaborator Author

@claude please review

@claude
Copy link
Copy Markdown

claude bot commented Apr 20, 2026

Claude finished @misama-ct's task in 2m 22s —— View job


Code Review — feat(info-dialog): add InfoDialog pattern (FEC-437)

  • Reading changed files
  • Reviewing implementation
  • Reviewing types
  • Reviewing stories
  • Reviewing docs
  • Reviewing tests
  • Posting final review

Overall this is a clean, well-scoped pattern addition. The flat four-prop API is the right call given the usage audit data, the escape-hatch docs are thorough, and the play functions are more sophisticated than most (the overlay-click traversal and focus-restoration checks are particularly good). A few things to address before merge:


Critical

Missing changesetpackages/nimbus/src/patterns/index.ts:2

InfoDialog is a brand-new exported API on @commercetools/nimbus. The changeset bot already flagged this. A minor bump is appropriate for a new feature export. Without it the package won't be released.

Fix this →


Medium

isOpen optional but no defaultOpen → component is unusable without controlled stateinfo-dialog.types.ts:38

isOpen is typed optional, but InfoDialog has no trigger slot and no defaultOpen passthrough, so omitting isOpen silently produces a dialog that can never open. Two reasonable fixes:

  1. Make isOpen required (clearest — matches the "controlled-only" intent)
  2. Add defaultOpen?: boolean to InfoDialogProps and thread it through to Dialog.Root

The current state is a type footgun: TypeScript won't warn you, but the dialog will silently never appear.

ReactNode title and accessible name gapinfo-dialog.tsx:38 / info-dialog.types.ts:16

React Aria's <Dialog> derives its accessible name from the heading inside it. For the WithReactNodeTitle example (<Flex><Text>Plan details</Text><Badge>Pro</Badge></Flex>), screen readers will compute "Plan details Pro" from the text nodes — probably fine. But InfoDialog offers no way to override this with an explicit aria-label when the composed title produces a bad accessible name.

Consider adding an optional aria-label prop that, when present, is forwarded to Dialog.Root (React Aria already forwards it to the underlying <dialog> element). The JSDoc on title already warns consumers, but giving them the escape within the pattern (not just the escape hatch to full Dialog composition) would be better ergonomics.


Low / Suggestions

WithReactNodeTitle play function only verifies text presenceinfo-dialog.stories.tsx:166-184

It opens the dialog and checks getByText("Plan details") but doesn't assert the accessible name, close the dialog, or verify focus restoration. Contrast with Base which tests everything end-to-end. Worth adding at minimum a getByRole("dialog") accessible-name check here.

LongContent play function doesn't verify scrollabilityinfo-dialog.stories.tsx:213-235

The story's purpose (per its JSDoc) is to show long content scrolls while the header stays pinned. The play function only checks that the first paragraph and close button are present, but doesn't verify scrollTop or that Paragraph 25 is reachable by scrolling. A brief scroll check (e.g. scrollIntoView on the last paragraph and asserting it becomes visible) would validate the actual behavior being documented.

docs.spec.tsx doesn't test the Escape key in JSDOMinfo-dialog.docs.spec.tsx:88-117

The Escape-key test in the docs spec wraps the dialog in a state component and calls user.keyboard("{Escape}"). React Aria's keyboard dismiss requires a focused element inside the dialog to propagate Escape. In a JSDOM environment without Playwright, this may pass now but could be brittle if focus handling changes. The Storybook Base story already covers this in a real browser; the docs spec equivalent can be simplified to just click-close (the browser test has full coverage).


Positive callouts

  • The overlay-click test in Base (the "walk up to <body> and click corner" approach) is excellent — it correctly reflects how React Aria useInteractOutside fires on pointerdown outside the dialog subtree.
  • The escape hatch section in info-dialog.dev.mdx shows the complete manual composition rather than just linking away — that's exactly what consumers need.
  • isDismissable being hardcoded to true in the implementation (not a prop) is the right call. Docs correctly explain this is by design.
  • displayName is set correctly.
  • The info-dialog/index.ts barrel re-exports both the component and types, consistent with fields/ patterns.

- Add changeset (minor) for the new InfoDialog export
- Add `defaultOpen` so uncontrolled usage is possible without a trigger
- Add `aria-label` to override the accessible name derived from `title`
  (useful for composed ReactNode titles)
- Thread `aria-label` from Dialog.Root context to the underlying RaDialog
  in Dialog.Content (previously declared in types but not forwarded)
- Hardcode `scrollBehavior="inside"` so long content scrolls within the
  body with the header pinned, matching the documented intent
- Extend WithReactNodeTitle play function with accessible-name and
  focus-restore assertions
- Extend LongContent play function with a scroll-into-view check
- Drop brittle Escape-key test from the docs spec (covered in-browser
  by the Base story)
- Update OpenSpec proposal/spec and dev.mdx for the expanded API
The patterns index page uses <CategoryOverview> to enumerate sub-
categories, which requires each sub-category to have its own `.mdx`
index file (see `packages/nimbus/src/docs/fields.mdx`). Without
`dialogs.mdx`, the new Dialogs sub-category introduced by the
InfoDialog pattern did not render on the patterns overview.
- Description and Overview reflect the full prop surface (`defaultOpen`
  and `aria-label` were added during review) instead of the stale
  four-prop phrasing
- Drop the "custom accessible label on a non-string title" reason to
  drop down to Dialog — `aria-label` is now available directly on
  InfoDialog
- Drop the in-file anchor link to `#escape-hatch` (the section lives in
  the dev doc, not here)
- Fix the Dialog guidelines link from `/components/overlay/dialog` to
  the correct `/components/feedback/dialog` route
@misama-ct
Copy link
Copy Markdown
Collaborator Author

@claude please review

@claude
Copy link
Copy Markdown

claude bot commented Apr 20, 2026

Claude finished @misama-ct's task in 1m 55s —— View job


Code Review — feat(info-dialog): add InfoDialog pattern (FEC-437)

  • Reading changed files
  • Reviewing implementation (info-dialog.tsx, info-dialog.types.ts)
  • Reviewing stories (info-dialog.stories.tsx)
  • Reviewing tests (info-dialog.docs.spec.tsx)
  • Reviewing documentation (info-dialog.dev.mdx, info-dialog.mdx)
  • Reviewing exports and barrel files
  • Posting final review

This is in good shape. The concerns from the previous review round have been addressed (defaultOpen, aria-label, the LongContent scroll test, and the WithReactNodeTitle accessible-name assertion are all present). A few small items remain worth noting before merge.


Notable improvement in this revision

dialog.content.tsx — pre-existing aria-label bug fixeddialog.content.tsx:51

The diff adds "aria-label": ariaLabel from the root context and forwards it to <RaDialog>. Before this change, passing aria-label to Dialog.Root was silently ignored at render time — it was stored in context but never actually applied to the underlying <dialog> element. This is a real bug fix that affects the existing Dialog primitive, not just InfoDialog.

Consider noting this in the changeset or at least in the commit message, since it's a behavior change (backward-compatible, but consumers who were relying on ARIA overrides will now see them applied for the first time).


Medium

ReactNode title live-dev example is missing aria-labelinfo-dialog.dev.mdx:91-114

The live-dev code block for the ReactNode title example omits aria-label, even though the very next paragraph warns that it's needed when the composed title doesn't form a meaningful accessible name. For <Flex><Text>Plan details</Text><Badge>Pro</Badge></Flex>, the computed accessible name will be "Plan details Pro" — which is just about acceptable, but showing the example without aria-label and then explaining separately that it's needed trains consumers to skip it.

Suggest either: (a) add aria-label="Plan details" directly to the live-dev example to model the correct pattern, or (b) add an inline note {/* add aria-label when the composed title produces a confusing accessible name */}.


Low

docs.spec.tsx doesn't cover defaultOpen or aria-labelinfo-dialog.docs.spec.tsx

Both defaultOpen and aria-label are documented API props but aren't exercised in the consumer implementation tests. A quick uncontrolled-mode test and an accessible-name-override test would be valuable as copy-paste examples for consumers. For example:

it("opens by default in uncontrolled mode via defaultOpen", () => {
  render(
    <NimbusProvider>
      <InfoDialog defaultOpen>
        <p>Shown immediately.</p>
      </InfoDialog>
    </NimbusProvider>
  );
  expect(screen.getByRole("dialog")).toBeInTheDocument();
});

it("uses aria-label as the accessible name when provided", () => {
  render(
    <NimbusProvider>
      <InfoDialog
       >Plan details <span>Pro</span></span>}
       
        isOpen
      >
        <p>Details.</p>
      </InfoDialog>
    </NimbusProvider>
  );
  expect(screen.getByRole("dialog")).toHaveAccessibleName("Plan details");
});

Fix this →


Positive callouts

  • The dialog.content.tsx fix is clean and correct — reading aria-label from context and forwarding to RaDialog is exactly right.
  • All three close affordances (X, Escape, overlay click) are tested with proper waitFor + focus restoration in Base. The overlay-click walk-up approach is still excellent.
  • WithReactNodeTitle now correctly asserts the aria-label override on the accessible name — this was the key missing piece from the earlier revision.
  • LongContent uses scrollIntoView + viewport rect check + pinned-header position assertion, which validates the actual scroll behavior being documented.
  • The changeset is present with the correct minor bump.
  • Barrel re-exports are consistent with the fields/ pattern.
  • The escape hatch in dev.mdx is production-ready and shows aria-label on Dialog.Root which now correctly flows through to the element (thanks to the dialog.content.tsx fix).

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