Skip to content

Max/dialog composition apis - #9420

Closed
maxyinger wants to merge 6 commits into
mainfrom
max/dialog-composition-apis
Closed

Max/dialog composition apis#9420
maxyinger wants to merge 6 commits into
mainfrom
max/dialog-composition-apis

Conversation

@maxyinger

Copy link
Copy Markdown
Collaborator

Description

Adds Base UI-style composition APIs to the headless Dialog. Split out of #9388 so the runtime changes are reviewed on their own.

  • Detached triggersDialog.createHandle() returns a handle; pass it to a Dialog.Trigger and a Dialog.Root and the trigger drives the dialog from anywhere in the tree, with no JSX nesting.
  • Imperative control — the same handle carries open() / close() / isOpen for opens with no trigger element. Calls made while no root is mounted are ignored.
  • Multiple triggers — each trigger takes an id and a payload; the root's children can be a function of the active trigger's payload, so one dialog renders per-trigger content.
  • Trigger attributiononOpenChange gains a details argument naming the trigger behind each change, and triggerId on the root drives attribution in controlled mode. Focus returns to whichever trigger was actually used.
  • Custom focusinitialFocus and finalFocus on Dialog.Popup take true, false, a ref, or a function of the interaction type behind the open/close.
  • A root with no handle creates a private one, so nested and detached triggers share a single registration and open/close path.

Internals worth a look on review:

  • Trigger activation moves off floating-ui's useClick — a detached trigger can't collect reference props from context.
  • Every open and close (trigger, dismissal, programmatic) funnels through one path, so the onOpenChange details and the finalFocus resolution each happen exactly once, synchronously, before focus restoration runs.
  • The trigger registry is subscribed to imperatively rather than via useSyncExternalStore, so a trigger with an inline-object payload can't loop with the root re-registering it.

Checklist

  • pnpm test runs as expected.
  • pnpm build runs as expected.
  • (If applicable) JSDoc comments have been added or updated for any package exports
  • (If applicable) Documentation has been updated

Type of change

  • 🐛 Bug fix
  • 🌟 New feature
  • 🔨 Breaking change
  • 📖 Refactoring / dependency upgrade / documentation
  • other:

maxyingerand others added 6 commits August 7, 2026 15:16
Adds `closedBy: 'any' | 'closerequest' | 'none'` to the headless Dialog root,
driving `escapeKey` and `outsidePress` on `useDismiss`. Defaults to `any`, so
existing callers are unaffected.
The five Mosaic dialogs now use `closerequest`, which stops a stray backdrop
click from discarding the type-to-confirm input in `Destructive` or closing a
dialog mid-request.
`trigger` on the Mosaic `Dialog` becomes optional, so the machine-driven
dialogs no longer render a button they don't use.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Moves the dialog off the Emotion slot-recipe engine onto StyleX, leaving
`tabs` as the last component on the old path, and reworks its sizing,
motion and mobile behaviour on top of that.
`size` becomes three named surfaces — `prompt`, `card`, `panel` — and
moves to `Dialog.Root`, since the backdrop reads it too. The gap to the
screen edge is a fixed inset at three breakpoints rather than a
percentage, which is what makes the surround an even frame. A `panel`
clips and carries no padding, so its scroll region is composed inside it
from the ScrollArea atoms; that keeps the close button anchored and makes
a sidebar a plain flex row.
Below 48rem a `prompt` becomes a bottom sheet, and `Dialog.Viewport`
measures the on-screen keyboard so the sheet rises above it while a card
re-centres and a panel shrinks. The chrome of a mobile browser is tinted
to match the scrim, derived from the backdrop rather than shipped as a
colour, refcounted across stacked dialogs and reverting exactly.
Adds `Dialog.CloseButton`, `data-nested` for stacked scrims, and
`--cl-dialog-origin` so a dialog scales out of whatever opened it.
Also fixes a transition that never ran: it was keyed to a
`data-cl-starting-style` attribute the headless layer does not emit, so
dialogs appeared with no animation at all.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Drop the trigger-origin open animation in favour of a plain centre scale,
deleting the headless `useDialogOrigin` hook and `--cl-dialog-origin` with it.
Motion. Add a `--cl-ease-enter` token — a decelerate curve that lands on
target rather than carrying ~2% past it like `--cl-ease-default` — and take it
for every dialog entrance. The backdrop fades a step faster than the popup, so
the scrim answers the gesture and the surface arrives into an already-dimmed
page. The mobile sheet now fades over the full length of its slide instead of
holding opaque. Enter scale 0.98 -> 0.94.
Fix a `prompt` that never scaled at all. Its transform was a single
media-scoped rule with no resting declaration, leaving the transition with
`transform: none` as its other endpoint; it now mirrors `card` exactly.
Surface. Match the popup shadow to Menu's, which restores a visible hairline
on dark surfaces. A `card` no longer paints itself: it takes its surface from a
`Card` rendered AS the popup, so one element both paints and animates and the
radius counter-scale keeps landing on the corners you can see.
Scrolling. A dialog taller than the screen now scrolls, and how follows from
its size rather than a prop. `panel` keeps a pinned viewport and scrolls
inside; `prompt` and `card` grow with their content, so the dialog moves within
the viewport and keeps its inset at both ends. `Dialog.Viewport` gains
`data-size`.
Spacing. `prompt` padding to 1rem; phone-band inset to 1.25rem block, 1rem
inline.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Draw a real border on the popup under `forced-colors: active`. The mode
discards `box-shadow` and the scrim alike, so the dialog otherwise floats
edgeless over the page with nothing to separate it. A border rather than an
outline, since the popup clears its outline deliberately — `FloatingFocusManager`
focuses it when it holds no tabbable content — and the two would collide.
Wrap long unbroken strings on the popup, matching `Popover`. A dialog holds
prose it did not author, and an email address or an API key would otherwise
push past the size's width clamp.
Warn in development when a dialog has no accessible name, extracting
`Popover`'s check into a shared `useAccessibleNameWarning` and pointing both
at it. The check now RESOLVES `aria-labelledby` rather than testing for its
presence: `Dialog` emits the attribute unconditionally, so with no
`Dialog.Title` it references an id that is not in the document, which a
presence check waves through while naming the dialog no better than having no
attribute at all.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`Dialog.createHandle()` returns a handle passed to both a `Dialog.Trigger`
and a `Dialog.Root`, so a trigger drives a dialog it is not nested under.
The handle also exposes imperative `open()` / `close()` / `isOpen`.
Several triggers can share one dialog, each carrying an `id` and a
`payload`, with the root's children as a function of `{ payload }` so one
dialog renders per-trigger content. Everything keyed to "the trigger" now
follows the one actually used — the dialog scales out of it and returns
focus to it — and `triggerId` names the active trigger in controlled mode,
which also gives controlled, trigger-less dialogs the origin-aware open.
`initialFocus` and `finalFocus` on `Dialog.Popup` take `true`, `false`, a
ref, or a function of the interaction type behind the change. Defaults are
unchanged: first tabbable on open, the trigger on close, except after a
pointer-driven dismissal.
Also retunes the dialog for dark mode. The scrim veils rather than darkens
there — light grey at low alpha over a dark page, against black over a
light one — so the two schemes are unrelated colours rather than one
colour at two opacities. The nested scrim stays solved rather than picked,
landing two levels on the same proportional deepening as light. The popup
shadow becomes one three-layer shadow shared by both schemes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@changeset-bot

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f563af4

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

This PR includes changesets to release 4 packages
NameType
@clerk/uiPatch
@clerk/headlessPatch
@clerk/chrome-extensionPatch
@clerk/swingsetPatch

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

@coderabbitai

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 36688779-e45d-44ad-9ad5-2320a2b713b5

📥 Commits

Reviewing files that changed from the base of the PR and between f9c97ef and f563af4.

📒 Files selected for processing (38)
  • .changeset/lucky-donuts-invite.md
  • .changeset/olive-doors-tell.md
  • .changeset/spicy-clocks-argue.md
  • packages/headless/src/primitives/dialog/README.md
  • packages/headless/src/primitives/dialog/dialog-backdrop.tsx
  • packages/headless/src/primitives/dialog/dialog-context.ts
  • packages/headless/src/primitives/dialog/dialog-handle.ts
  • packages/headless/src/primitives/dialog/dialog-popup.tsx
  • packages/headless/src/primitives/dialog/dialog-root.tsx
  • packages/headless/src/primitives/dialog/dialog-trigger.tsx
  • packages/headless/src/primitives/dialog/dialog-viewport.tsx
  • packages/headless/src/primitives/dialog/dialog.test.tsx
  • packages/headless/src/primitives/dialog/index.ts
  • packages/headless/src/primitives/dialog/parts.ts
  • packages/headless/src/primitives/drawer/drawer-context.ts
  • packages/headless/src/utils/interaction-modality.ts
  • packages/swingset/src/stories/dialog.component.mdx
  • packages/swingset/src/stories/dialog.component.stories.tsx
  • packages/swingset/src/stories/dialog.mdx
  • packages/swingset/src/stories/dialog.stories.tsx
  • packages/ui/src/mosaic/block/destructive.tsx
  • packages/ui/src/mosaic/components/button/button.tsx
  • packages/ui/src/mosaic/components/dialog.tsx
  • packages/ui/src/mosaic/components/dialog/browser-chrome.ts
  • packages/ui/src/mosaic/components/dialog/dialog.styles.ts
  • packages/ui/src/mosaic/components/dialog/dialog.test.tsx
  • packages/ui/src/mosaic/components/dialog/dialog.tsx
  • packages/ui/src/mosaic/components/dialog/index.ts
  • packages/ui/src/mosaic/components/dialog/keyboard-inset.ts
  • packages/ui/src/mosaic/components/popover/popover.tsx
  • packages/ui/src/mosaic/hooks/useAccessibleNameWarning.ts
  • packages/ui/src/mosaic/organization/organization-profile-domains-section-add-verify.view.tsx
  • packages/ui/src/mosaic/organization/organization-profile-domains-section-enrollment.view.tsx
  • packages/ui/src/mosaic/organization/organization-profile-domains-section-remove.view.tsx
  • packages/ui/src/mosaic/organization/organization-profile-profile-section.view.tsx
  • packages/ui/src/mosaic/primitives/dialog.tsx
  • packages/ui/src/mosaic/styles/index.ts
  • packages/ui/src/mosaic/tokens.stylex.ts

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.


📝 Walkthrough

Walkthrough

The pull request rebuilds Mosaic Dialog on top of new headless Dialog primitives. It adds detached and multiple triggers, typed payloads, dismissal policies, detailed callbacks, and configurable focus targets. The Mosaic implementation adds StyleX surfaces, nested-dialog behavior, responsive sizing, motion, keyboard insets, browser-chrome synchronization, close buttons, and accessibility diagnostics. Tests, stories, documentation, consumer configurations, exports, tokens, and changesets are updated.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested reviewers:alexcarpenter

✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

API Changes Report

Generated by Break Check on 2026-08-12T19:16:25.571Z

Summary

MetricCount
Packages analyzed19
Packages with changes1
🔴 Breaking changes3
🟡 Non-breaking changes0
🟢 Additions0

Warning
3 breaking change(s) detected - Major version bump required

🤖 This report was reviewed by claude-sonnet-4-6.

🔴 Breaking changes index (3)

Every breaking change, up front. Full diffs are in the package sections below.

PackageSubpathChange
@clerk/nextjs.Protect
@clerk/nextjs.SignedIn
@clerk/nextjs.SignedOut

@clerk/nextjs

Current version: 7.7.4
Recommended bump: MAJOR → 8.0.0

🔴 Breaking Changes (3)

Changed: Protect

- export declare function Protect(_props: RemovedControlComponentProps): never;

Static analyzer: Removed function Protect

🤖 AI review (confirmed) (95%): The Protect function was previously exported and consumers may import and use it; removing it breaks any such import.

Migration: Remove any usage of Protect from @clerk/nextjs or migrate to the equivalent component from the appropriate Clerk package.

Changed: SignedIn

- export declare function SignedIn(_props: RemovedControlComponentProps): never;

Static analyzer: Removed function SignedIn

🤖 AI review (confirmed) (95%): The SignedIn function was previously exported and consumers may import and use it; removing it breaks any such import.

Migration: Remove any usage of SignedIn from @clerk/nextjs or migrate to the equivalent component from the appropriate Clerk package.

Changed: SignedOut

- export declare function SignedOut(_props: RemovedControlComponentProps): never;

Static analyzer: Removed function SignedOut

🤖 AI review (confirmed) (95%): The SignedOut function was previously exported and consumers may import and use it; removing it breaks any such import.

Migration: Remove any usage of SignedOut from @clerk/nextjs or migrate to the equivalent component from the appropriate Clerk package.


Report generated by Break Check

Last ran on f563af4.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@maxyinger
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Max/dialog composition apis by maxyinger · Pull Request #9420 · clerk/javascript · GitHub
Skip to content

Max/dialog composition apis - #9420

Closed
maxyinger wants to merge 6 commits into
mainfrom
max/dialog-composition-apis
Closed

Max/dialog composition apis#9420
maxyinger wants to merge 6 commits into
mainfrom
max/dialog-composition-apis

Conversation

@maxyinger

Copy link
Copy Markdown
Collaborator

Description

Adds Base UI-style composition APIs to the headless Dialog. Split out of #9388 so the runtime changes are reviewed on their own.

  • Detached triggersDialog.createHandle() returns a handle; pass it to a Dialog.Trigger and a Dialog.Root and the trigger drives the dialog from anywhere in the tree, with no JSX nesting.
  • Imperative control — the same handle carries open() / close() / isOpen for opens with no trigger element. Calls made while no root is mounted are ignored.
  • Multiple triggers — each trigger takes an id and a payload; the root's children can be a function of the active trigger's payload, so one dialog renders per-trigger content.
  • Trigger attributiononOpenChange gains a details argument naming the trigger behind each change, and triggerId on the root drives attribution in controlled mode. Focus returns to whichever trigger was actually used.
  • Custom focusinitialFocus and finalFocus on Dialog.Popup take true, false, a ref, or a function of the interaction type behind the open/close.
  • A root with no handle creates a private one, so nested and detached triggers share a single registration and open/close path.

Internals worth a look on review:

  • Trigger activation moves off floating-ui's useClick — a detached trigger can't collect reference props from context.
  • Every open and close (trigger, dismissal, programmatic) funnels through one path, so the onOpenChange details and the finalFocus resolution each happen exactly once, synchronously, before focus restoration runs.
  • The trigger registry is subscribed to imperatively rather than via useSyncExternalStore, so a trigger with an inline-object payload can't loop with the root re-registering it.

Checklist

  • pnpm test runs as expected.
  • pnpm build runs as expected.
  • (If applicable) JSDoc comments have been added or updated for any package exports
  • (If applicable) Documentation has been updated

Type of change

  • 🐛 Bug fix
  • 🌟 New feature
  • 🔨 Breaking change
  • 📖 Refactoring / dependency upgrade / documentation
  • other:

maxyingerand others added 6 commits August 7, 2026 15:16
Adds `closedBy: 'any' | 'closerequest' | 'none'` to the headless Dialog root,
driving `escapeKey` and `outsidePress` on `useDismiss`. Defaults to `any`, so
existing callers are unaffected.
The five Mosaic dialogs now use `closerequest`, which stops a stray backdrop
click from discarding the type-to-confirm input in `Destructive` or closing a
dialog mid-request.
`trigger` on the Mosaic `Dialog` becomes optional, so the machine-driven
dialogs no longer render a button they don't use.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Moves the dialog off the Emotion slot-recipe engine onto StyleX, leaving
`tabs` as the last component on the old path, and reworks its sizing,
motion and mobile behaviour on top of that.
`size` becomes three named surfaces — `prompt`, `card`, `panel` — and
moves to `Dialog.Root`, since the backdrop reads it too. The gap to the
screen edge is a fixed inset at three breakpoints rather than a
percentage, which is what makes the surround an even frame. A `panel`
clips and carries no padding, so its scroll region is composed inside it
from the ScrollArea atoms; that keeps the close button anchored and makes
a sidebar a plain flex row.
Below 48rem a `prompt` becomes a bottom sheet, and `Dialog.Viewport`
measures the on-screen keyboard so the sheet rises above it while a card
re-centres and a panel shrinks. The chrome of a mobile browser is tinted
to match the scrim, derived from the backdrop rather than shipped as a
colour, refcounted across stacked dialogs and reverting exactly.
Adds `Dialog.CloseButton`, `data-nested` for stacked scrims, and
`--cl-dialog-origin` so a dialog scales out of whatever opened it.
Also fixes a transition that never ran: it was keyed to a
`data-cl-starting-style` attribute the headless layer does not emit, so
dialogs appeared with no animation at all.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Drop the trigger-origin open animation in favour of a plain centre scale,
deleting the headless `useDialogOrigin` hook and `--cl-dialog-origin` with it.
Motion. Add a `--cl-ease-enter` token — a decelerate curve that lands on
target rather than carrying ~2% past it like `--cl-ease-default` — and take it
for every dialog entrance. The backdrop fades a step faster than the popup, so
the scrim answers the gesture and the surface arrives into an already-dimmed
page. The mobile sheet now fades over the full length of its slide instead of
holding opaque. Enter scale 0.98 -> 0.94.
Fix a `prompt` that never scaled at all. Its transform was a single
media-scoped rule with no resting declaration, leaving the transition with
`transform: none` as its other endpoint; it now mirrors `card` exactly.
Surface. Match the popup shadow to Menu's, which restores a visible hairline
on dark surfaces. A `card` no longer paints itself: it takes its surface from a
`Card` rendered AS the popup, so one element both paints and animates and the
radius counter-scale keeps landing on the corners you can see.
Scrolling. A dialog taller than the screen now scrolls, and how follows from
its size rather than a prop. `panel` keeps a pinned viewport and scrolls
inside; `prompt` and `card` grow with their content, so the dialog moves within
the viewport and keeps its inset at both ends. `Dialog.Viewport` gains
`data-size`.
Spacing. `prompt` padding to 1rem; phone-band inset to 1.25rem block, 1rem
inline.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Draw a real border on the popup under `forced-colors: active`. The mode
discards `box-shadow` and the scrim alike, so the dialog otherwise floats
edgeless over the page with nothing to separate it. A border rather than an
outline, since the popup clears its outline deliberately — `FloatingFocusManager`
focuses it when it holds no tabbable content — and the two would collide.
Wrap long unbroken strings on the popup, matching `Popover`. A dialog holds
prose it did not author, and an email address or an API key would otherwise
push past the size's width clamp.
Warn in development when a dialog has no accessible name, extracting
`Popover`'s check into a shared `useAccessibleNameWarning` and pointing both
at it. The check now RESOLVES `aria-labelledby` rather than testing for its
presence: `Dialog` emits the attribute unconditionally, so with no
`Dialog.Title` it references an id that is not in the document, which a
presence check waves through while naming the dialog no better than having no
attribute at all.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`Dialog.createHandle()` returns a handle passed to both a `Dialog.Trigger`
and a `Dialog.Root`, so a trigger drives a dialog it is not nested under.
The handle also exposes imperative `open()` / `close()` / `isOpen`.
Several triggers can share one dialog, each carrying an `id` and a
`payload`, with the root's children as a function of `{ payload }` so one
dialog renders per-trigger content. Everything keyed to "the trigger" now
follows the one actually used — the dialog scales out of it and returns
focus to it — and `triggerId` names the active trigger in controlled mode,
which also gives controlled, trigger-less dialogs the origin-aware open.
`initialFocus` and `finalFocus` on `Dialog.Popup` take `true`, `false`, a
ref, or a function of the interaction type behind the change. Defaults are
unchanged: first tabbable on open, the trigger on close, except after a
pointer-driven dismissal.
Also retunes the dialog for dark mode. The scrim veils rather than darkens
there — light grey at low alpha over a dark page, against black over a
light one — so the two schemes are unrelated colours rather than one
colour at two opacities. The nested scrim stays solved rather than picked,
landing two levels on the same proportional deepening as light. The popup
shadow becomes one three-layer shadow shared by both schemes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@changeset-bot

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f563af4

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

This PR includes changesets to release 4 packages
NameType
@clerk/uiPatch
@clerk/headlessPatch
@clerk/chrome-extensionPatch
@clerk/swingsetPatch

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

@coderabbitai

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 36688779-e45d-44ad-9ad5-2320a2b713b5

📥 Commits

Reviewing files that changed from the base of the PR and between f9c97ef and f563af4.

📒 Files selected for processing (38)
  • .changeset/lucky-donuts-invite.md
  • .changeset/olive-doors-tell.md
  • .changeset/spicy-clocks-argue.md
  • packages/headless/src/primitives/dialog/README.md
  • packages/headless/src/primitives/dialog/dialog-backdrop.tsx
  • packages/headless/src/primitives/dialog/dialog-context.ts
  • packages/headless/src/primitives/dialog/dialog-handle.ts
  • packages/headless/src/primitives/dialog/dialog-popup.tsx
  • packages/headless/src/primitives/dialog/dialog-root.tsx
  • packages/headless/src/primitives/dialog/dialog-trigger.tsx
  • packages/headless/src/primitives/dialog/dialog-viewport.tsx
  • packages/headless/src/primitives/dialog/dialog.test.tsx
  • packages/headless/src/primitives/dialog/index.ts
  • packages/headless/src/primitives/dialog/parts.ts
  • packages/headless/src/primitives/drawer/drawer-context.ts
  • packages/headless/src/utils/interaction-modality.ts
  • packages/swingset/src/stories/dialog.component.mdx
  • packages/swingset/src/stories/dialog.component.stories.tsx
  • packages/swingset/src/stories/dialog.mdx
  • packages/swingset/src/stories/dialog.stories.tsx
  • packages/ui/src/mosaic/block/destructive.tsx
  • packages/ui/src/mosaic/components/button/button.tsx
  • packages/ui/src/mosaic/components/dialog.tsx
  • packages/ui/src/mosaic/components/dialog/browser-chrome.ts
  • packages/ui/src/mosaic/components/dialog/dialog.styles.ts
  • packages/ui/src/mosaic/components/dialog/dialog.test.tsx
  • packages/ui/src/mosaic/components/dialog/dialog.tsx
  • packages/ui/src/mosaic/components/dialog/index.ts
  • packages/ui/src/mosaic/components/dialog/keyboard-inset.ts
  • packages/ui/src/mosaic/components/popover/popover.tsx
  • packages/ui/src/mosaic/hooks/useAccessibleNameWarning.ts
  • packages/ui/src/mosaic/organization/organization-profile-domains-section-add-verify.view.tsx
  • packages/ui/src/mosaic/organization/organization-profile-domains-section-enrollment.view.tsx
  • packages/ui/src/mosaic/organization/organization-profile-domains-section-remove.view.tsx
  • packages/ui/src/mosaic/organization/organization-profile-profile-section.view.tsx
  • packages/ui/src/mosaic/primitives/dialog.tsx
  • packages/ui/src/mosaic/styles/index.ts
  • packages/ui/src/mosaic/tokens.stylex.ts

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.


📝 Walkthrough

Walkthrough

The pull request rebuilds Mosaic Dialog on top of new headless Dialog primitives. It adds detached and multiple triggers, typed payloads, dismissal policies, detailed callbacks, and configurable focus targets. The Mosaic implementation adds StyleX surfaces, nested-dialog behavior, responsive sizing, motion, keyboard insets, browser-chrome synchronization, close buttons, and accessibility diagnostics. Tests, stories, documentation, consumer configurations, exports, tokens, and changesets are updated.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested reviewers:alexcarpenter

✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

API Changes Report

Generated by Break Check on 2026-08-12T19:16:25.571Z

Summary

MetricCount
Packages analyzed19
Packages with changes1
🔴 Breaking changes3
🟡 Non-breaking changes0
🟢 Additions0

Warning
3 breaking change(s) detected - Major version bump required

🤖 This report was reviewed by claude-sonnet-4-6.

🔴 Breaking changes index (3)

Every breaking change, up front. Full diffs are in the package sections below.

PackageSubpathChange
@clerk/nextjs.Protect
@clerk/nextjs.SignedIn
@clerk/nextjs.SignedOut

@clerk/nextjs

Current version: 7.7.4
Recommended bump: MAJOR → 8.0.0

🔴 Breaking Changes (3)

Changed: Protect

- export declare function Protect(_props: RemovedControlComponentProps): never;

Static analyzer: Removed function Protect

🤖 AI review (confirmed) (95%): The Protect function was previously exported and consumers may import and use it; removing it breaks any such import.

Migration: Remove any usage of Protect from @clerk/nextjs or migrate to the equivalent component from the appropriate Clerk package.

Changed: SignedIn

- export declare function SignedIn(_props: RemovedControlComponentProps): never;

Static analyzer: Removed function SignedIn

🤖 AI review (confirmed) (95%): The SignedIn function was previously exported and consumers may import and use it; removing it breaks any such import.

Migration: Remove any usage of SignedIn from @clerk/nextjs or migrate to the equivalent component from the appropriate Clerk package.

Changed: SignedOut

- export declare function SignedOut(_props: RemovedControlComponentProps): never;

Static analyzer: Removed function SignedOut

🤖 AI review (confirmed) (95%): The SignedOut function was previously exported and consumers may import and use it; removing it breaks any such import.

Migration: Remove any usage of SignedOut from @clerk/nextjs or migrate to the equivalent component from the appropriate Clerk package.


Report generated by Break Check

Last ran on f563af4.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@maxyinger
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Max/dialog composition apis by maxyinger · Pull Request #9420 · clerk/javascript · GitHub
Skip to content

Max/dialog composition apis - #9420

Closed
maxyinger wants to merge 6 commits into
mainfrom
max/dialog-composition-apis
Closed

Max/dialog composition apis#9420
maxyinger wants to merge 6 commits into
mainfrom
max/dialog-composition-apis

Conversation

@maxyinger

Copy link
Copy Markdown
Collaborator

Description

Adds Base UI-style composition APIs to the headless Dialog. Split out of #9388 so the runtime changes are reviewed on their own.

  • Detached triggersDialog.createHandle() returns a handle; pass it to a Dialog.Trigger and a Dialog.Root and the trigger drives the dialog from anywhere in the tree, with no JSX nesting.
  • Imperative control — the same handle carries open() / close() / isOpen for opens with no trigger element. Calls made while no root is mounted are ignored.
  • Multiple triggers — each trigger takes an id and a payload; the root's children can be a function of the active trigger's payload, so one dialog renders per-trigger content.
  • Trigger attributiononOpenChange gains a details argument naming the trigger behind each change, and triggerId on the root drives attribution in controlled mode. Focus returns to whichever trigger was actually used.
  • Custom focusinitialFocus and finalFocus on Dialog.Popup take true, false, a ref, or a function of the interaction type behind the open/close.
  • A root with no handle creates a private one, so nested and detached triggers share a single registration and open/close path.

Internals worth a look on review:

  • Trigger activation moves off floating-ui's useClick — a detached trigger can't collect reference props from context.
  • Every open and close (trigger, dismissal, programmatic) funnels through one path, so the onOpenChange details and the finalFocus resolution each happen exactly once, synchronously, before focus restoration runs.
  • The trigger registry is subscribed to imperatively rather than via useSyncExternalStore, so a trigger with an inline-object payload can't loop with the root re-registering it.

Checklist

  • pnpm test runs as expected.
  • pnpm build runs as expected.
  • (If applicable) JSDoc comments have been added or updated for any package exports
  • (If applicable) Documentation has been updated

Type of change

  • 🐛 Bug fix
  • 🌟 New feature
  • 🔨 Breaking change
  • 📖 Refactoring / dependency upgrade / documentation
  • other:

maxyingerand others added 6 commits August 7, 2026 15:16
Adds `closedBy: 'any' | 'closerequest' | 'none'` to the headless Dialog root,
driving `escapeKey` and `outsidePress` on `useDismiss`. Defaults to `any`, so
existing callers are unaffected.
The five Mosaic dialogs now use `closerequest`, which stops a stray backdrop
click from discarding the type-to-confirm input in `Destructive` or closing a
dialog mid-request.
`trigger` on the Mosaic `Dialog` becomes optional, so the machine-driven
dialogs no longer render a button they don't use.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Moves the dialog off the Emotion slot-recipe engine onto StyleX, leaving
`tabs` as the last component on the old path, and reworks its sizing,
motion and mobile behaviour on top of that.
`size` becomes three named surfaces — `prompt`, `card`, `panel` — and
moves to `Dialog.Root`, since the backdrop reads it too. The gap to the
screen edge is a fixed inset at three breakpoints rather than a
percentage, which is what makes the surround an even frame. A `panel`
clips and carries no padding, so its scroll region is composed inside it
from the ScrollArea atoms; that keeps the close button anchored and makes
a sidebar a plain flex row.
Below 48rem a `prompt` becomes a bottom sheet, and `Dialog.Viewport`
measures the on-screen keyboard so the sheet rises above it while a card
re-centres and a panel shrinks. The chrome of a mobile browser is tinted
to match the scrim, derived from the backdrop rather than shipped as a
colour, refcounted across stacked dialogs and reverting exactly.
Adds `Dialog.CloseButton`, `data-nested` for stacked scrims, and
`--cl-dialog-origin` so a dialog scales out of whatever opened it.
Also fixes a transition that never ran: it was keyed to a
`data-cl-starting-style` attribute the headless layer does not emit, so
dialogs appeared with no animation at all.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Drop the trigger-origin open animation in favour of a plain centre scale,
deleting the headless `useDialogOrigin` hook and `--cl-dialog-origin` with it.
Motion. Add a `--cl-ease-enter` token — a decelerate curve that lands on
target rather than carrying ~2% past it like `--cl-ease-default` — and take it
for every dialog entrance. The backdrop fades a step faster than the popup, so
the scrim answers the gesture and the surface arrives into an already-dimmed
page. The mobile sheet now fades over the full length of its slide instead of
holding opaque. Enter scale 0.98 -> 0.94.
Fix a `prompt` that never scaled at all. Its transform was a single
media-scoped rule with no resting declaration, leaving the transition with
`transform: none` as its other endpoint; it now mirrors `card` exactly.
Surface. Match the popup shadow to Menu's, which restores a visible hairline
on dark surfaces. A `card` no longer paints itself: it takes its surface from a
`Card` rendered AS the popup, so one element both paints and animates and the
radius counter-scale keeps landing on the corners you can see.
Scrolling. A dialog taller than the screen now scrolls, and how follows from
its size rather than a prop. `panel` keeps a pinned viewport and scrolls
inside; `prompt` and `card` grow with their content, so the dialog moves within
the viewport and keeps its inset at both ends. `Dialog.Viewport` gains
`data-size`.
Spacing. `prompt` padding to 1rem; phone-band inset to 1.25rem block, 1rem
inline.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Draw a real border on the popup under `forced-colors: active`. The mode
discards `box-shadow` and the scrim alike, so the dialog otherwise floats
edgeless over the page with nothing to separate it. A border rather than an
outline, since the popup clears its outline deliberately — `FloatingFocusManager`
focuses it when it holds no tabbable content — and the two would collide.
Wrap long unbroken strings on the popup, matching `Popover`. A dialog holds
prose it did not author, and an email address or an API key would otherwise
push past the size's width clamp.
Warn in development when a dialog has no accessible name, extracting
`Popover`'s check into a shared `useAccessibleNameWarning` and pointing both
at it. The check now RESOLVES `aria-labelledby` rather than testing for its
presence: `Dialog` emits the attribute unconditionally, so with no
`Dialog.Title` it references an id that is not in the document, which a
presence check waves through while naming the dialog no better than having no
attribute at all.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`Dialog.createHandle()` returns a handle passed to both a `Dialog.Trigger`
and a `Dialog.Root`, so a trigger drives a dialog it is not nested under.
The handle also exposes imperative `open()` / `close()` / `isOpen`.
Several triggers can share one dialog, each carrying an `id` and a
`payload`, with the root's children as a function of `{ payload }` so one
dialog renders per-trigger content. Everything keyed to "the trigger" now
follows the one actually used — the dialog scales out of it and returns
focus to it — and `triggerId` names the active trigger in controlled mode,
which also gives controlled, trigger-less dialogs the origin-aware open.
`initialFocus` and `finalFocus` on `Dialog.Popup` take `true`, `false`, a
ref, or a function of the interaction type behind the change. Defaults are
unchanged: first tabbable on open, the trigger on close, except after a
pointer-driven dismissal.
Also retunes the dialog for dark mode. The scrim veils rather than darkens
there — light grey at low alpha over a dark page, against black over a
light one — so the two schemes are unrelated colours rather than one
colour at two opacities. The nested scrim stays solved rather than picked,
landing two levels on the same proportional deepening as light. The popup
shadow becomes one three-layer shadow shared by both schemes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@changeset-bot

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f563af4

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

This PR includes changesets to release 4 packages
NameType
@clerk/uiPatch
@clerk/headlessPatch
@clerk/chrome-extensionPatch
@clerk/swingsetPatch

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

@coderabbitai

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 36688779-e45d-44ad-9ad5-2320a2b713b5

📥 Commits

Reviewing files that changed from the base of the PR and between f9c97ef and f563af4.

📒 Files selected for processing (38)
  • .changeset/lucky-donuts-invite.md
  • .changeset/olive-doors-tell.md
  • .changeset/spicy-clocks-argue.md
  • packages/headless/src/primitives/dialog/README.md
  • packages/headless/src/primitives/dialog/dialog-backdrop.tsx
  • packages/headless/src/primitives/dialog/dialog-context.ts
  • packages/headless/src/primitives/dialog/dialog-handle.ts
  • packages/headless/src/primitives/dialog/dialog-popup.tsx
  • packages/headless/src/primitives/dialog/dialog-root.tsx
  • packages/headless/src/primitives/dialog/dialog-trigger.tsx
  • packages/headless/src/primitives/dialog/dialog-viewport.tsx
  • packages/headless/src/primitives/dialog/dialog.test.tsx
  • packages/headless/src/primitives/dialog/index.ts
  • packages/headless/src/primitives/dialog/parts.ts
  • packages/headless/src/primitives/drawer/drawer-context.ts
  • packages/headless/src/utils/interaction-modality.ts
  • packages/swingset/src/stories/dialog.component.mdx
  • packages/swingset/src/stories/dialog.component.stories.tsx
  • packages/swingset/src/stories/dialog.mdx
  • packages/swingset/src/stories/dialog.stories.tsx
  • packages/ui/src/mosaic/block/destructive.tsx
  • packages/ui/src/mosaic/components/button/button.tsx
  • packages/ui/src/mosaic/components/dialog.tsx
  • packages/ui/src/mosaic/components/dialog/browser-chrome.ts
  • packages/ui/src/mosaic/components/dialog/dialog.styles.ts
  • packages/ui/src/mosaic/components/dialog/dialog.test.tsx
  • packages/ui/src/mosaic/components/dialog/dialog.tsx
  • packages/ui/src/mosaic/components/dialog/index.ts
  • packages/ui/src/mosaic/components/dialog/keyboard-inset.ts
  • packages/ui/src/mosaic/components/popover/popover.tsx
  • packages/ui/src/mosaic/hooks/useAccessibleNameWarning.ts
  • packages/ui/src/mosaic/organization/organization-profile-domains-section-add-verify.view.tsx
  • packages/ui/src/mosaic/organization/organization-profile-domains-section-enrollment.view.tsx
  • packages/ui/src/mosaic/organization/organization-profile-domains-section-remove.view.tsx
  • packages/ui/src/mosaic/organization/organization-profile-profile-section.view.tsx
  • packages/ui/src/mosaic/primitives/dialog.tsx
  • packages/ui/src/mosaic/styles/index.ts
  • packages/ui/src/mosaic/tokens.stylex.ts

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.


📝 Walkthrough

Walkthrough

The pull request rebuilds Mosaic Dialog on top of new headless Dialog primitives. It adds detached and multiple triggers, typed payloads, dismissal policies, detailed callbacks, and configurable focus targets. The Mosaic implementation adds StyleX surfaces, nested-dialog behavior, responsive sizing, motion, keyboard insets, browser-chrome synchronization, close buttons, and accessibility diagnostics. Tests, stories, documentation, consumer configurations, exports, tokens, and changesets are updated.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested reviewers:alexcarpenter

✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

API Changes Report

Generated by Break Check on 2026-08-12T19:16:25.571Z

Summary

MetricCount
Packages analyzed19
Packages with changes1
🔴 Breaking changes3
🟡 Non-breaking changes0
🟢 Additions0

Warning
3 breaking change(s) detected - Major version bump required

🤖 This report was reviewed by claude-sonnet-4-6.

🔴 Breaking changes index (3)

Every breaking change, up front. Full diffs are in the package sections below.

PackageSubpathChange
@clerk/nextjs.Protect
@clerk/nextjs.SignedIn
@clerk/nextjs.SignedOut

@clerk/nextjs

Current version: 7.7.4
Recommended bump: MAJOR → 8.0.0

🔴 Breaking Changes (3)

Changed: Protect

- export declare function Protect(_props: RemovedControlComponentProps): never;

Static analyzer: Removed function Protect

🤖 AI review (confirmed) (95%): The Protect function was previously exported and consumers may import and use it; removing it breaks any such import.

Migration: Remove any usage of Protect from @clerk/nextjs or migrate to the equivalent component from the appropriate Clerk package.

Changed: SignedIn

- export declare function SignedIn(_props: RemovedControlComponentProps): never;

Static analyzer: Removed function SignedIn

🤖 AI review (confirmed) (95%): The SignedIn function was previously exported and consumers may import and use it; removing it breaks any such import.

Migration: Remove any usage of SignedIn from @clerk/nextjs or migrate to the equivalent component from the appropriate Clerk package.

Changed: SignedOut

- export declare function SignedOut(_props: RemovedControlComponentProps): never;

Static analyzer: Removed function SignedOut

🤖 AI review (confirmed) (95%): The SignedOut function was previously exported and consumers may import and use it; removing it breaks any such import.

Migration: Remove any usage of SignedOut from @clerk/nextjs or migrate to the equivalent component from the appropriate Clerk package.


Report generated by Break Check

Last ran on f563af4.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@maxyinger
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Max/dialog composition apis by maxyinger · Pull Request #9420 · clerk/javascript · GitHub
Skip to content

Max/dialog composition apis - #9420

Closed
maxyinger wants to merge 6 commits into
mainfrom
max/dialog-composition-apis
Closed

Max/dialog composition apis#9420
maxyinger wants to merge 6 commits into
mainfrom
max/dialog-composition-apis

Conversation

@maxyinger

Copy link
Copy Markdown
Collaborator

Description

Adds Base UI-style composition APIs to the headless Dialog. Split out of #9388 so the runtime changes are reviewed on their own.

  • Detached triggersDialog.createHandle() returns a handle; pass it to a Dialog.Trigger and a Dialog.Root and the trigger drives the dialog from anywhere in the tree, with no JSX nesting.
  • Imperative control — the same handle carries open() / close() / isOpen for opens with no trigger element. Calls made while no root is mounted are ignored.
  • Multiple triggers — each trigger takes an id and a payload; the root's children can be a function of the active trigger's payload, so one dialog renders per-trigger content.
  • Trigger attributiononOpenChange gains a details argument naming the trigger behind each change, and triggerId on the root drives attribution in controlled mode. Focus returns to whichever trigger was actually used.
  • Custom focusinitialFocus and finalFocus on Dialog.Popup take true, false, a ref, or a function of the interaction type behind the open/close.
  • A root with no handle creates a private one, so nested and detached triggers share a single registration and open/close path.

Internals worth a look on review:

  • Trigger activation moves off floating-ui's useClick — a detached trigger can't collect reference props from context.
  • Every open and close (trigger, dismissal, programmatic) funnels through one path, so the onOpenChange details and the finalFocus resolution each happen exactly once, synchronously, before focus restoration runs.
  • The trigger registry is subscribed to imperatively rather than via useSyncExternalStore, so a trigger with an inline-object payload can't loop with the root re-registering it.

Checklist

  • pnpm test runs as expected.
  • pnpm build runs as expected.
  • (If applicable) JSDoc comments have been added or updated for any package exports
  • (If applicable) Documentation has been updated

Type of change

  • 🐛 Bug fix
  • 🌟 New feature
  • 🔨 Breaking change
  • 📖 Refactoring / dependency upgrade / documentation
  • other:

maxyingerand others added 6 commits August 7, 2026 15:16
Adds `closedBy: 'any' | 'closerequest' | 'none'` to the headless Dialog root,
driving `escapeKey` and `outsidePress` on `useDismiss`. Defaults to `any`, so
existing callers are unaffected.
The five Mosaic dialogs now use `closerequest`, which stops a stray backdrop
click from discarding the type-to-confirm input in `Destructive` or closing a
dialog mid-request.
`trigger` on the Mosaic `Dialog` becomes optional, so the machine-driven
dialogs no longer render a button they don't use.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Moves the dialog off the Emotion slot-recipe engine onto StyleX, leaving
`tabs` as the last component on the old path, and reworks its sizing,
motion and mobile behaviour on top of that.
`size` becomes three named surfaces — `prompt`, `card`, `panel` — and
moves to `Dialog.Root`, since the backdrop reads it too. The gap to the
screen edge is a fixed inset at three breakpoints rather than a
percentage, which is what makes the surround an even frame. A `panel`
clips and carries no padding, so its scroll region is composed inside it
from the ScrollArea atoms; that keeps the close button anchored and makes
a sidebar a plain flex row.
Below 48rem a `prompt` becomes a bottom sheet, and `Dialog.Viewport`
measures the on-screen keyboard so the sheet rises above it while a card
re-centres and a panel shrinks. The chrome of a mobile browser is tinted
to match the scrim, derived from the backdrop rather than shipped as a
colour, refcounted across stacked dialogs and reverting exactly.
Adds `Dialog.CloseButton`, `data-nested` for stacked scrims, and
`--cl-dialog-origin` so a dialog scales out of whatever opened it.
Also fixes a transition that never ran: it was keyed to a
`data-cl-starting-style` attribute the headless layer does not emit, so
dialogs appeared with no animation at all.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Drop the trigger-origin open animation in favour of a plain centre scale,
deleting the headless `useDialogOrigin` hook and `--cl-dialog-origin` with it.
Motion. Add a `--cl-ease-enter` token — a decelerate curve that lands on
target rather than carrying ~2% past it like `--cl-ease-default` — and take it
for every dialog entrance. The backdrop fades a step faster than the popup, so
the scrim answers the gesture and the surface arrives into an already-dimmed
page. The mobile sheet now fades over the full length of its slide instead of
holding opaque. Enter scale 0.98 -> 0.94.
Fix a `prompt` that never scaled at all. Its transform was a single
media-scoped rule with no resting declaration, leaving the transition with
`transform: none` as its other endpoint; it now mirrors `card` exactly.
Surface. Match the popup shadow to Menu's, which restores a visible hairline
on dark surfaces. A `card` no longer paints itself: it takes its surface from a
`Card` rendered AS the popup, so one element both paints and animates and the
radius counter-scale keeps landing on the corners you can see.
Scrolling. A dialog taller than the screen now scrolls, and how follows from
its size rather than a prop. `panel` keeps a pinned viewport and scrolls
inside; `prompt` and `card` grow with their content, so the dialog moves within
the viewport and keeps its inset at both ends. `Dialog.Viewport` gains
`data-size`.
Spacing. `prompt` padding to 1rem; phone-band inset to 1.25rem block, 1rem
inline.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Draw a real border on the popup under `forced-colors: active`. The mode
discards `box-shadow` and the scrim alike, so the dialog otherwise floats
edgeless over the page with nothing to separate it. A border rather than an
outline, since the popup clears its outline deliberately — `FloatingFocusManager`
focuses it when it holds no tabbable content — and the two would collide.
Wrap long unbroken strings on the popup, matching `Popover`. A dialog holds
prose it did not author, and an email address or an API key would otherwise
push past the size's width clamp.
Warn in development when a dialog has no accessible name, extracting
`Popover`'s check into a shared `useAccessibleNameWarning` and pointing both
at it. The check now RESOLVES `aria-labelledby` rather than testing for its
presence: `Dialog` emits the attribute unconditionally, so with no
`Dialog.Title` it references an id that is not in the document, which a
presence check waves through while naming the dialog no better than having no
attribute at all.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`Dialog.createHandle()` returns a handle passed to both a `Dialog.Trigger`
and a `Dialog.Root`, so a trigger drives a dialog it is not nested under.
The handle also exposes imperative `open()` / `close()` / `isOpen`.
Several triggers can share one dialog, each carrying an `id` and a
`payload`, with the root's children as a function of `{ payload }` so one
dialog renders per-trigger content. Everything keyed to "the trigger" now
follows the one actually used — the dialog scales out of it and returns
focus to it — and `triggerId` names the active trigger in controlled mode,
which also gives controlled, trigger-less dialogs the origin-aware open.
`initialFocus` and `finalFocus` on `Dialog.Popup` take `true`, `false`, a
ref, or a function of the interaction type behind the change. Defaults are
unchanged: first tabbable on open, the trigger on close, except after a
pointer-driven dismissal.
Also retunes the dialog for dark mode. The scrim veils rather than darkens
there — light grey at low alpha over a dark page, against black over a
light one — so the two schemes are unrelated colours rather than one
colour at two opacities. The nested scrim stays solved rather than picked,
landing two levels on the same proportional deepening as light. The popup
shadow becomes one three-layer shadow shared by both schemes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@changeset-bot

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f563af4

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

This PR includes changesets to release 4 packages
NameType
@clerk/uiPatch
@clerk/headlessPatch
@clerk/chrome-extensionPatch
@clerk/swingsetPatch

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

@coderabbitai

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 36688779-e45d-44ad-9ad5-2320a2b713b5

📥 Commits

Reviewing files that changed from the base of the PR and between f9c97ef and f563af4.

📒 Files selected for processing (38)
  • .changeset/lucky-donuts-invite.md
  • .changeset/olive-doors-tell.md
  • .changeset/spicy-clocks-argue.md
  • packages/headless/src/primitives/dialog/README.md
  • packages/headless/src/primitives/dialog/dialog-backdrop.tsx
  • packages/headless/src/primitives/dialog/dialog-context.ts
  • packages/headless/src/primitives/dialog/dialog-handle.ts
  • packages/headless/src/primitives/dialog/dialog-popup.tsx
  • packages/headless/src/primitives/dialog/dialog-root.tsx
  • packages/headless/src/primitives/dialog/dialog-trigger.tsx
  • packages/headless/src/primitives/dialog/dialog-viewport.tsx
  • packages/headless/src/primitives/dialog/dialog.test.tsx
  • packages/headless/src/primitives/dialog/index.ts
  • packages/headless/src/primitives/dialog/parts.ts
  • packages/headless/src/primitives/drawer/drawer-context.ts
  • packages/headless/src/utils/interaction-modality.ts
  • packages/swingset/src/stories/dialog.component.mdx
  • packages/swingset/src/stories/dialog.component.stories.tsx
  • packages/swingset/src/stories/dialog.mdx
  • packages/swingset/src/stories/dialog.stories.tsx
  • packages/ui/src/mosaic/block/destructive.tsx
  • packages/ui/src/mosaic/components/button/button.tsx
  • packages/ui/src/mosaic/components/dialog.tsx
  • packages/ui/src/mosaic/components/dialog/browser-chrome.ts
  • packages/ui/src/mosaic/components/dialog/dialog.styles.ts
  • packages/ui/src/mosaic/components/dialog/dialog.test.tsx
  • packages/ui/src/mosaic/components/dialog/dialog.tsx
  • packages/ui/src/mosaic/components/dialog/index.ts
  • packages/ui/src/mosaic/components/dialog/keyboard-inset.ts
  • packages/ui/src/mosaic/components/popover/popover.tsx
  • packages/ui/src/mosaic/hooks/useAccessibleNameWarning.ts
  • packages/ui/src/mosaic/organization/organization-profile-domains-section-add-verify.view.tsx
  • packages/ui/src/mosaic/organization/organization-profile-domains-section-enrollment.view.tsx
  • packages/ui/src/mosaic/organization/organization-profile-domains-section-remove.view.tsx
  • packages/ui/src/mosaic/organization/organization-profile-profile-section.view.tsx
  • packages/ui/src/mosaic/primitives/dialog.tsx
  • packages/ui/src/mosaic/styles/index.ts
  • packages/ui/src/mosaic/tokens.stylex.ts

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.


📝 Walkthrough

Walkthrough

The pull request rebuilds Mosaic Dialog on top of new headless Dialog primitives. It adds detached and multiple triggers, typed payloads, dismissal policies, detailed callbacks, and configurable focus targets. The Mosaic implementation adds StyleX surfaces, nested-dialog behavior, responsive sizing, motion, keyboard insets, browser-chrome synchronization, close buttons, and accessibility diagnostics. Tests, stories, documentation, consumer configurations, exports, tokens, and changesets are updated.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested reviewers:alexcarpenter

✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

API Changes Report

Generated by Break Check on 2026-08-12T19:16:25.571Z

Summary

MetricCount
Packages analyzed19
Packages with changes1
🔴 Breaking changes3
🟡 Non-breaking changes0
🟢 Additions0

Warning
3 breaking change(s) detected - Major version bump required

🤖 This report was reviewed by claude-sonnet-4-6.

🔴 Breaking changes index (3)

Every breaking change, up front. Full diffs are in the package sections below.

PackageSubpathChange
@clerk/nextjs.Protect
@clerk/nextjs.SignedIn
@clerk/nextjs.SignedOut

@clerk/nextjs

Current version: 7.7.4
Recommended bump: MAJOR → 8.0.0

🔴 Breaking Changes (3)

Changed: Protect

- export declare function Protect(_props: RemovedControlComponentProps): never;

Static analyzer: Removed function Protect

🤖 AI review (confirmed) (95%): The Protect function was previously exported and consumers may import and use it; removing it breaks any such import.

Migration: Remove any usage of Protect from @clerk/nextjs or migrate to the equivalent component from the appropriate Clerk package.

Changed: SignedIn

- export declare function SignedIn(_props: RemovedControlComponentProps): never;

Static analyzer: Removed function SignedIn

🤖 AI review (confirmed) (95%): The SignedIn function was previously exported and consumers may import and use it; removing it breaks any such import.

Migration: Remove any usage of SignedIn from @clerk/nextjs or migrate to the equivalent component from the appropriate Clerk package.

Changed: SignedOut

- export declare function SignedOut(_props: RemovedControlComponentProps): never;

Static analyzer: Removed function SignedOut

🤖 AI review (confirmed) (95%): The SignedOut function was previously exported and consumers may import and use it; removing it breaks any such import.

Migration: Remove any usage of SignedOut from @clerk/nextjs or migrate to the equivalent component from the appropriate Clerk package.


Report generated by Break Check

Last ran on f563af4.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@maxyinger
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Max/dialog composition apis by maxyinger · Pull Request #9420 · clerk/javascript · GitHub
Skip to content

Max/dialog composition apis - #9420

Closed
maxyinger wants to merge 6 commits into
mainfrom
max/dialog-composition-apis
Closed

Max/dialog composition apis#9420
maxyinger wants to merge 6 commits into
mainfrom
max/dialog-composition-apis

Conversation

@maxyinger

Copy link
Copy Markdown
Collaborator

Description

Adds Base UI-style composition APIs to the headless Dialog. Split out of #9388 so the runtime changes are reviewed on their own.

  • Detached triggersDialog.createHandle() returns a handle; pass it to a Dialog.Trigger and a Dialog.Root and the trigger drives the dialog from anywhere in the tree, with no JSX nesting.
  • Imperative control — the same handle carries open() / close() / isOpen for opens with no trigger element. Calls made while no root is mounted are ignored.
  • Multiple triggers — each trigger takes an id and a payload; the root's children can be a function of the active trigger's payload, so one dialog renders per-trigger content.
  • Trigger attributiononOpenChange gains a details argument naming the trigger behind each change, and triggerId on the root drives attribution in controlled mode. Focus returns to whichever trigger was actually used.
  • Custom focusinitialFocus and finalFocus on Dialog.Popup take true, false, a ref, or a function of the interaction type behind the open/close.
  • A root with no handle creates a private one, so nested and detached triggers share a single registration and open/close path.

Internals worth a look on review:

  • Trigger activation moves off floating-ui's useClick — a detached trigger can't collect reference props from context.
  • Every open and close (trigger, dismissal, programmatic) funnels through one path, so the onOpenChange details and the finalFocus resolution each happen exactly once, synchronously, before focus restoration runs.
  • The trigger registry is subscribed to imperatively rather than via useSyncExternalStore, so a trigger with an inline-object payload can't loop with the root re-registering it.

Checklist

  • pnpm test runs as expected.
  • pnpm build runs as expected.
  • (If applicable) JSDoc comments have been added or updated for any package exports
  • (If applicable) Documentation has been updated

Type of change

  • 🐛 Bug fix
  • 🌟 New feature
  • 🔨 Breaking change
  • 📖 Refactoring / dependency upgrade / documentation
  • other:

maxyingerand others added 6 commits August 7, 2026 15:16
Adds `closedBy: 'any' | 'closerequest' | 'none'` to the headless Dialog root,
driving `escapeKey` and `outsidePress` on `useDismiss`. Defaults to `any`, so
existing callers are unaffected.
The five Mosaic dialogs now use `closerequest`, which stops a stray backdrop
click from discarding the type-to-confirm input in `Destructive` or closing a
dialog mid-request.
`trigger` on the Mosaic `Dialog` becomes optional, so the machine-driven
dialogs no longer render a button they don't use.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Moves the dialog off the Emotion slot-recipe engine onto StyleX, leaving
`tabs` as the last component on the old path, and reworks its sizing,
motion and mobile behaviour on top of that.
`size` becomes three named surfaces — `prompt`, `card`, `panel` — and
moves to `Dialog.Root`, since the backdrop reads it too. The gap to the
screen edge is a fixed inset at three breakpoints rather than a
percentage, which is what makes the surround an even frame. A `panel`
clips and carries no padding, so its scroll region is composed inside it
from the ScrollArea atoms; that keeps the close button anchored and makes
a sidebar a plain flex row.
Below 48rem a `prompt` becomes a bottom sheet, and `Dialog.Viewport`
measures the on-screen keyboard so the sheet rises above it while a card
re-centres and a panel shrinks. The chrome of a mobile browser is tinted
to match the scrim, derived from the backdrop rather than shipped as a
colour, refcounted across stacked dialogs and reverting exactly.
Adds `Dialog.CloseButton`, `data-nested` for stacked scrims, and
`--cl-dialog-origin` so a dialog scales out of whatever opened it.
Also fixes a transition that never ran: it was keyed to a
`data-cl-starting-style` attribute the headless layer does not emit, so
dialogs appeared with no animation at all.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Drop the trigger-origin open animation in favour of a plain centre scale,
deleting the headless `useDialogOrigin` hook and `--cl-dialog-origin` with it.
Motion. Add a `--cl-ease-enter` token — a decelerate curve that lands on
target rather than carrying ~2% past it like `--cl-ease-default` — and take it
for every dialog entrance. The backdrop fades a step faster than the popup, so
the scrim answers the gesture and the surface arrives into an already-dimmed
page. The mobile sheet now fades over the full length of its slide instead of
holding opaque. Enter scale 0.98 -> 0.94.
Fix a `prompt` that never scaled at all. Its transform was a single
media-scoped rule with no resting declaration, leaving the transition with
`transform: none` as its other endpoint; it now mirrors `card` exactly.
Surface. Match the popup shadow to Menu's, which restores a visible hairline
on dark surfaces. A `card` no longer paints itself: it takes its surface from a
`Card` rendered AS the popup, so one element both paints and animates and the
radius counter-scale keeps landing on the corners you can see.
Scrolling. A dialog taller than the screen now scrolls, and how follows from
its size rather than a prop. `panel` keeps a pinned viewport and scrolls
inside; `prompt` and `card` grow with their content, so the dialog moves within
the viewport and keeps its inset at both ends. `Dialog.Viewport` gains
`data-size`.
Spacing. `prompt` padding to 1rem; phone-band inset to 1.25rem block, 1rem
inline.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Draw a real border on the popup under `forced-colors: active`. The mode
discards `box-shadow` and the scrim alike, so the dialog otherwise floats
edgeless over the page with nothing to separate it. A border rather than an
outline, since the popup clears its outline deliberately — `FloatingFocusManager`
focuses it when it holds no tabbable content — and the two would collide.
Wrap long unbroken strings on the popup, matching `Popover`. A dialog holds
prose it did not author, and an email address or an API key would otherwise
push past the size's width clamp.
Warn in development when a dialog has no accessible name, extracting
`Popover`'s check into a shared `useAccessibleNameWarning` and pointing both
at it. The check now RESOLVES `aria-labelledby` rather than testing for its
presence: `Dialog` emits the attribute unconditionally, so with no
`Dialog.Title` it references an id that is not in the document, which a
presence check waves through while naming the dialog no better than having no
attribute at all.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`Dialog.createHandle()` returns a handle passed to both a `Dialog.Trigger`
and a `Dialog.Root`, so a trigger drives a dialog it is not nested under.
The handle also exposes imperative `open()` / `close()` / `isOpen`.
Several triggers can share one dialog, each carrying an `id` and a
`payload`, with the root's children as a function of `{ payload }` so one
dialog renders per-trigger content. Everything keyed to "the trigger" now
follows the one actually used — the dialog scales out of it and returns
focus to it — and `triggerId` names the active trigger in controlled mode,
which also gives controlled, trigger-less dialogs the origin-aware open.
`initialFocus` and `finalFocus` on `Dialog.Popup` take `true`, `false`, a
ref, or a function of the interaction type behind the change. Defaults are
unchanged: first tabbable on open, the trigger on close, except after a
pointer-driven dismissal.
Also retunes the dialog for dark mode. The scrim veils rather than darkens
there — light grey at low alpha over a dark page, against black over a
light one — so the two schemes are unrelated colours rather than one
colour at two opacities. The nested scrim stays solved rather than picked,
landing two levels on the same proportional deepening as light. The popup
shadow becomes one three-layer shadow shared by both schemes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@changeset-bot

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f563af4

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

This PR includes changesets to release 4 packages
NameType
@clerk/uiPatch
@clerk/headlessPatch
@clerk/chrome-extensionPatch
@clerk/swingsetPatch

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

@coderabbitai

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 36688779-e45d-44ad-9ad5-2320a2b713b5

📥 Commits

Reviewing files that changed from the base of the PR and between f9c97ef and f563af4.

📒 Files selected for processing (38)
  • .changeset/lucky-donuts-invite.md
  • .changeset/olive-doors-tell.md
  • .changeset/spicy-clocks-argue.md
  • packages/headless/src/primitives/dialog/README.md
  • packages/headless/src/primitives/dialog/dialog-backdrop.tsx
  • packages/headless/src/primitives/dialog/dialog-context.ts
  • packages/headless/src/primitives/dialog/dialog-handle.ts
  • packages/headless/src/primitives/dialog/dialog-popup.tsx
  • packages/headless/src/primitives/dialog/dialog-root.tsx
  • packages/headless/src/primitives/dialog/dialog-trigger.tsx
  • packages/headless/src/primitives/dialog/dialog-viewport.tsx
  • packages/headless/src/primitives/dialog/dialog.test.tsx
  • packages/headless/src/primitives/dialog/index.ts
  • packages/headless/src/primitives/dialog/parts.ts
  • packages/headless/src/primitives/drawer/drawer-context.ts
  • packages/headless/src/utils/interaction-modality.ts
  • packages/swingset/src/stories/dialog.component.mdx
  • packages/swingset/src/stories/dialog.component.stories.tsx
  • packages/swingset/src/stories/dialog.mdx
  • packages/swingset/src/stories/dialog.stories.tsx
  • packages/ui/src/mosaic/block/destructive.tsx
  • packages/ui/src/mosaic/components/button/button.tsx
  • packages/ui/src/mosaic/components/dialog.tsx
  • packages/ui/src/mosaic/components/dialog/browser-chrome.ts
  • packages/ui/src/mosaic/components/dialog/dialog.styles.ts
  • packages/ui/src/mosaic/components/dialog/dialog.test.tsx
  • packages/ui/src/mosaic/components/dialog/dialog.tsx
  • packages/ui/src/mosaic/components/dialog/index.ts
  • packages/ui/src/mosaic/components/dialog/keyboard-inset.ts
  • packages/ui/src/mosaic/components/popover/popover.tsx
  • packages/ui/src/mosaic/hooks/useAccessibleNameWarning.ts
  • packages/ui/src/mosaic/organization/organization-profile-domains-section-add-verify.view.tsx
  • packages/ui/src/mosaic/organization/organization-profile-domains-section-enrollment.view.tsx
  • packages/ui/src/mosaic/organization/organization-profile-domains-section-remove.view.tsx
  • packages/ui/src/mosaic/organization/organization-profile-profile-section.view.tsx
  • packages/ui/src/mosaic/primitives/dialog.tsx
  • packages/ui/src/mosaic/styles/index.ts
  • packages/ui/src/mosaic/tokens.stylex.ts

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.


📝 Walkthrough

Walkthrough

The pull request rebuilds Mosaic Dialog on top of new headless Dialog primitives. It adds detached and multiple triggers, typed payloads, dismissal policies, detailed callbacks, and configurable focus targets. The Mosaic implementation adds StyleX surfaces, nested-dialog behavior, responsive sizing, motion, keyboard insets, browser-chrome synchronization, close buttons, and accessibility diagnostics. Tests, stories, documentation, consumer configurations, exports, tokens, and changesets are updated.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested reviewers:alexcarpenter

✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

API Changes Report

Generated by Break Check on 2026-08-12T19:16:25.571Z

Summary

MetricCount
Packages analyzed19
Packages with changes1
🔴 Breaking changes3
🟡 Non-breaking changes0
🟢 Additions0

Warning
3 breaking change(s) detected - Major version bump required

🤖 This report was reviewed by claude-sonnet-4-6.

🔴 Breaking changes index (3)

Every breaking change, up front. Full diffs are in the package sections below.

PackageSubpathChange
@clerk/nextjs.Protect
@clerk/nextjs.SignedIn
@clerk/nextjs.SignedOut

@clerk/nextjs

Current version: 7.7.4
Recommended bump: MAJOR → 8.0.0

🔴 Breaking Changes (3)

Changed: Protect

- export declare function Protect(_props: RemovedControlComponentProps): never;

Static analyzer: Removed function Protect

🤖 AI review (confirmed) (95%): The Protect function was previously exported and consumers may import and use it; removing it breaks any such import.

Migration: Remove any usage of Protect from @clerk/nextjs or migrate to the equivalent component from the appropriate Clerk package.

Changed: SignedIn

- export declare function SignedIn(_props: RemovedControlComponentProps): never;

Static analyzer: Removed function SignedIn

🤖 AI review (confirmed) (95%): The SignedIn function was previously exported and consumers may import and use it; removing it breaks any such import.

Migration: Remove any usage of SignedIn from @clerk/nextjs or migrate to the equivalent component from the appropriate Clerk package.

Changed: SignedOut

- export declare function SignedOut(_props: RemovedControlComponentProps): never;

Static analyzer: Removed function SignedOut

🤖 AI review (confirmed) (95%): The SignedOut function was previously exported and consumers may import and use it; removing it breaks any such import.

Migration: Remove any usage of SignedOut from @clerk/nextjs or migrate to the equivalent component from the appropriate Clerk package.


Report generated by Break Check

Last ran on f563af4.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@maxyinger
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Max/dialog composition apis by maxyinger · Pull Request #9420 · clerk/javascript · GitHub
Skip to content

Max/dialog composition apis - #9420

Closed
maxyinger wants to merge 6 commits into
mainfrom
max/dialog-composition-apis
Closed

Max/dialog composition apis#9420
maxyinger wants to merge 6 commits into
mainfrom
max/dialog-composition-apis

Conversation

@maxyinger

Copy link
Copy Markdown
Collaborator

Description

Adds Base UI-style composition APIs to the headless Dialog. Split out of #9388 so the runtime changes are reviewed on their own.

  • Detached triggersDialog.createHandle() returns a handle; pass it to a Dialog.Trigger and a Dialog.Root and the trigger drives the dialog from anywhere in the tree, with no JSX nesting.
  • Imperative control — the same handle carries open() / close() / isOpen for opens with no trigger element. Calls made while no root is mounted are ignored.
  • Multiple triggers — each trigger takes an id and a payload; the root's children can be a function of the active trigger's payload, so one dialog renders per-trigger content.
  • Trigger attributiononOpenChange gains a details argument naming the trigger behind each change, and triggerId on the root drives attribution in controlled mode. Focus returns to whichever trigger was actually used.
  • Custom focusinitialFocus and finalFocus on Dialog.Popup take true, false, a ref, or a function of the interaction type behind the open/close.
  • A root with no handle creates a private one, so nested and detached triggers share a single registration and open/close path.

Internals worth a look on review:

  • Trigger activation moves off floating-ui's useClick — a detached trigger can't collect reference props from context.
  • Every open and close (trigger, dismissal, programmatic) funnels through one path, so the onOpenChange details and the finalFocus resolution each happen exactly once, synchronously, before focus restoration runs.
  • The trigger registry is subscribed to imperatively rather than via useSyncExternalStore, so a trigger with an inline-object payload can't loop with the root re-registering it.

Checklist

  • pnpm test runs as expected.
  • pnpm build runs as expected.
  • (If applicable) JSDoc comments have been added or updated for any package exports
  • (If applicable) Documentation has been updated

Type of change

  • 🐛 Bug fix
  • 🌟 New feature
  • 🔨 Breaking change
  • 📖 Refactoring / dependency upgrade / documentation
  • other:

maxyingerand others added 6 commits August 7, 2026 15:16
Adds `closedBy: 'any' | 'closerequest' | 'none'` to the headless Dialog root,
driving `escapeKey` and `outsidePress` on `useDismiss`. Defaults to `any`, so
existing callers are unaffected.
The five Mosaic dialogs now use `closerequest`, which stops a stray backdrop
click from discarding the type-to-confirm input in `Destructive` or closing a
dialog mid-request.
`trigger` on the Mosaic `Dialog` becomes optional, so the machine-driven
dialogs no longer render a button they don't use.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Moves the dialog off the Emotion slot-recipe engine onto StyleX, leaving
`tabs` as the last component on the old path, and reworks its sizing,
motion and mobile behaviour on top of that.
`size` becomes three named surfaces — `prompt`, `card`, `panel` — and
moves to `Dialog.Root`, since the backdrop reads it too. The gap to the
screen edge is a fixed inset at three breakpoints rather than a
percentage, which is what makes the surround an even frame. A `panel`
clips and carries no padding, so its scroll region is composed inside it
from the ScrollArea atoms; that keeps the close button anchored and makes
a sidebar a plain flex row.
Below 48rem a `prompt` becomes a bottom sheet, and `Dialog.Viewport`
measures the on-screen keyboard so the sheet rises above it while a card
re-centres and a panel shrinks. The chrome of a mobile browser is tinted
to match the scrim, derived from the backdrop rather than shipped as a
colour, refcounted across stacked dialogs and reverting exactly.
Adds `Dialog.CloseButton`, `data-nested` for stacked scrims, and
`--cl-dialog-origin` so a dialog scales out of whatever opened it.
Also fixes a transition that never ran: it was keyed to a
`data-cl-starting-style` attribute the headless layer does not emit, so
dialogs appeared with no animation at all.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Drop the trigger-origin open animation in favour of a plain centre scale,
deleting the headless `useDialogOrigin` hook and `--cl-dialog-origin` with it.
Motion. Add a `--cl-ease-enter` token — a decelerate curve that lands on
target rather than carrying ~2% past it like `--cl-ease-default` — and take it
for every dialog entrance. The backdrop fades a step faster than the popup, so
the scrim answers the gesture and the surface arrives into an already-dimmed
page. The mobile sheet now fades over the full length of its slide instead of
holding opaque. Enter scale 0.98 -> 0.94.
Fix a `prompt` that never scaled at all. Its transform was a single
media-scoped rule with no resting declaration, leaving the transition with
`transform: none` as its other endpoint; it now mirrors `card` exactly.
Surface. Match the popup shadow to Menu's, which restores a visible hairline
on dark surfaces. A `card` no longer paints itself: it takes its surface from a
`Card` rendered AS the popup, so one element both paints and animates and the
radius counter-scale keeps landing on the corners you can see.
Scrolling. A dialog taller than the screen now scrolls, and how follows from
its size rather than a prop. `panel` keeps a pinned viewport and scrolls
inside; `prompt` and `card` grow with their content, so the dialog moves within
the viewport and keeps its inset at both ends. `Dialog.Viewport` gains
`data-size`.
Spacing. `prompt` padding to 1rem; phone-band inset to 1.25rem block, 1rem
inline.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Draw a real border on the popup under `forced-colors: active`. The mode
discards `box-shadow` and the scrim alike, so the dialog otherwise floats
edgeless over the page with nothing to separate it. A border rather than an
outline, since the popup clears its outline deliberately — `FloatingFocusManager`
focuses it when it holds no tabbable content — and the two would collide.
Wrap long unbroken strings on the popup, matching `Popover`. A dialog holds
prose it did not author, and an email address or an API key would otherwise
push past the size's width clamp.
Warn in development when a dialog has no accessible name, extracting
`Popover`'s check into a shared `useAccessibleNameWarning` and pointing both
at it. The check now RESOLVES `aria-labelledby` rather than testing for its
presence: `Dialog` emits the attribute unconditionally, so with no
`Dialog.Title` it references an id that is not in the document, which a
presence check waves through while naming the dialog no better than having no
attribute at all.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`Dialog.createHandle()` returns a handle passed to both a `Dialog.Trigger`
and a `Dialog.Root`, so a trigger drives a dialog it is not nested under.
The handle also exposes imperative `open()` / `close()` / `isOpen`.
Several triggers can share one dialog, each carrying an `id` and a
`payload`, with the root's children as a function of `{ payload }` so one
dialog renders per-trigger content. Everything keyed to "the trigger" now
follows the one actually used — the dialog scales out of it and returns
focus to it — and `triggerId` names the active trigger in controlled mode,
which also gives controlled, trigger-less dialogs the origin-aware open.
`initialFocus` and `finalFocus` on `Dialog.Popup` take `true`, `false`, a
ref, or a function of the interaction type behind the change. Defaults are
unchanged: first tabbable on open, the trigger on close, except after a
pointer-driven dismissal.
Also retunes the dialog for dark mode. The scrim veils rather than darkens
there — light grey at low alpha over a dark page, against black over a
light one — so the two schemes are unrelated colours rather than one
colour at two opacities. The nested scrim stays solved rather than picked,
landing two levels on the same proportional deepening as light. The popup
shadow becomes one three-layer shadow shared by both schemes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@changeset-bot

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f563af4

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

This PR includes changesets to release 4 packages
NameType
@clerk/uiPatch
@clerk/headlessPatch
@clerk/chrome-extensionPatch
@clerk/swingsetPatch

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

@coderabbitai

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 36688779-e45d-44ad-9ad5-2320a2b713b5

📥 Commits

Reviewing files that changed from the base of the PR and between f9c97ef and f563af4.

📒 Files selected for processing (38)
  • .changeset/lucky-donuts-invite.md
  • .changeset/olive-doors-tell.md
  • .changeset/spicy-clocks-argue.md
  • packages/headless/src/primitives/dialog/README.md
  • packages/headless/src/primitives/dialog/dialog-backdrop.tsx
  • packages/headless/src/primitives/dialog/dialog-context.ts
  • packages/headless/src/primitives/dialog/dialog-handle.ts
  • packages/headless/src/primitives/dialog/dialog-popup.tsx
  • packages/headless/src/primitives/dialog/dialog-root.tsx
  • packages/headless/src/primitives/dialog/dialog-trigger.tsx
  • packages/headless/src/primitives/dialog/dialog-viewport.tsx
  • packages/headless/src/primitives/dialog/dialog.test.tsx
  • packages/headless/src/primitives/dialog/index.ts
  • packages/headless/src/primitives/dialog/parts.ts
  • packages/headless/src/primitives/drawer/drawer-context.ts
  • packages/headless/src/utils/interaction-modality.ts
  • packages/swingset/src/stories/dialog.component.mdx
  • packages/swingset/src/stories/dialog.component.stories.tsx
  • packages/swingset/src/stories/dialog.mdx
  • packages/swingset/src/stories/dialog.stories.tsx
  • packages/ui/src/mosaic/block/destructive.tsx
  • packages/ui/src/mosaic/components/button/button.tsx
  • packages/ui/src/mosaic/components/dialog.tsx
  • packages/ui/src/mosaic/components/dialog/browser-chrome.ts
  • packages/ui/src/mosaic/components/dialog/dialog.styles.ts
  • packages/ui/src/mosaic/components/dialog/dialog.test.tsx
  • packages/ui/src/mosaic/components/dialog/dialog.tsx
  • packages/ui/src/mosaic/components/dialog/index.ts
  • packages/ui/src/mosaic/components/dialog/keyboard-inset.ts
  • packages/ui/src/mosaic/components/popover/popover.tsx
  • packages/ui/src/mosaic/hooks/useAccessibleNameWarning.ts
  • packages/ui/src/mosaic/organization/organization-profile-domains-section-add-verify.view.tsx
  • packages/ui/src/mosaic/organization/organization-profile-domains-section-enrollment.view.tsx
  • packages/ui/src/mosaic/organization/organization-profile-domains-section-remove.view.tsx
  • packages/ui/src/mosaic/organization/organization-profile-profile-section.view.tsx
  • packages/ui/src/mosaic/primitives/dialog.tsx
  • packages/ui/src/mosaic/styles/index.ts
  • packages/ui/src/mosaic/tokens.stylex.ts

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.


📝 Walkthrough

Walkthrough

The pull request rebuilds Mosaic Dialog on top of new headless Dialog primitives. It adds detached and multiple triggers, typed payloads, dismissal policies, detailed callbacks, and configurable focus targets. The Mosaic implementation adds StyleX surfaces, nested-dialog behavior, responsive sizing, motion, keyboard insets, browser-chrome synchronization, close buttons, and accessibility diagnostics. Tests, stories, documentation, consumer configurations, exports, tokens, and changesets are updated.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested reviewers:alexcarpenter

✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

API Changes Report

Generated by Break Check on 2026-08-12T19:16:25.571Z

Summary

MetricCount
Packages analyzed19
Packages with changes1
🔴 Breaking changes3
🟡 Non-breaking changes0
🟢 Additions0

Warning
3 breaking change(s) detected - Major version bump required

🤖 This report was reviewed by claude-sonnet-4-6.

🔴 Breaking changes index (3)

Every breaking change, up front. Full diffs are in the package sections below.

PackageSubpathChange
@clerk/nextjs.Protect
@clerk/nextjs.SignedIn
@clerk/nextjs.SignedOut

@clerk/nextjs

Current version: 7.7.4
Recommended bump: MAJOR → 8.0.0

🔴 Breaking Changes (3)

Changed: Protect

- export declare function Protect(_props: RemovedControlComponentProps): never;

Static analyzer: Removed function Protect

🤖 AI review (confirmed) (95%): The Protect function was previously exported and consumers may import and use it; removing it breaks any such import.

Migration: Remove any usage of Protect from @clerk/nextjs or migrate to the equivalent component from the appropriate Clerk package.

Changed: SignedIn

- export declare function SignedIn(_props: RemovedControlComponentProps): never;

Static analyzer: Removed function SignedIn

🤖 AI review (confirmed) (95%): The SignedIn function was previously exported and consumers may import and use it; removing it breaks any such import.

Migration: Remove any usage of SignedIn from @clerk/nextjs or migrate to the equivalent component from the appropriate Clerk package.

Changed: SignedOut

- export declare function SignedOut(_props: RemovedControlComponentProps): never;

Static analyzer: Removed function SignedOut

🤖 AI review (confirmed) (95%): The SignedOut function was previously exported and consumers may import and use it; removing it breaks any such import.

Migration: Remove any usage of SignedOut from @clerk/nextjs or migrate to the equivalent component from the appropriate Clerk package.


Report generated by Break Check

Last ran on f563af4.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@maxyinger
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Max/dialog composition apis by maxyinger · Pull Request #9420 · clerk/javascript · GitHub
Skip to content

Max/dialog composition apis - #9420

Closed
maxyinger wants to merge 6 commits into
mainfrom
max/dialog-composition-apis
Closed

Max/dialog composition apis#9420
maxyinger wants to merge 6 commits into
mainfrom
max/dialog-composition-apis

Conversation

@maxyinger

Copy link
Copy Markdown
Collaborator

Description

Adds Base UI-style composition APIs to the headless Dialog. Split out of #9388 so the runtime changes are reviewed on their own.

  • Detached triggersDialog.createHandle() returns a handle; pass it to a Dialog.Trigger and a Dialog.Root and the trigger drives the dialog from anywhere in the tree, with no JSX nesting.
  • Imperative control — the same handle carries open() / close() / isOpen for opens with no trigger element. Calls made while no root is mounted are ignored.
  • Multiple triggers — each trigger takes an id and a payload; the root's children can be a function of the active trigger's payload, so one dialog renders per-trigger content.
  • Trigger attributiononOpenChange gains a details argument naming the trigger behind each change, and triggerId on the root drives attribution in controlled mode. Focus returns to whichever trigger was actually used.
  • Custom focusinitialFocus and finalFocus on Dialog.Popup take true, false, a ref, or a function of the interaction type behind the open/close.
  • A root with no handle creates a private one, so nested and detached triggers share a single registration and open/close path.

Internals worth a look on review:

  • Trigger activation moves off floating-ui's useClick — a detached trigger can't collect reference props from context.
  • Every open and close (trigger, dismissal, programmatic) funnels through one path, so the onOpenChange details and the finalFocus resolution each happen exactly once, synchronously, before focus restoration runs.
  • The trigger registry is subscribed to imperatively rather than via useSyncExternalStore, so a trigger with an inline-object payload can't loop with the root re-registering it.

Checklist

  • pnpm test runs as expected.
  • pnpm build runs as expected.
  • (If applicable) JSDoc comments have been added or updated for any package exports
  • (If applicable) Documentation has been updated

Type of change

  • 🐛 Bug fix
  • 🌟 New feature
  • 🔨 Breaking change
  • 📖 Refactoring / dependency upgrade / documentation
  • other:

maxyingerand others added 6 commits August 7, 2026 15:16
Adds `closedBy: 'any' | 'closerequest' | 'none'` to the headless Dialog root,
driving `escapeKey` and `outsidePress` on `useDismiss`. Defaults to `any`, so
existing callers are unaffected.
The five Mosaic dialogs now use `closerequest`, which stops a stray backdrop
click from discarding the type-to-confirm input in `Destructive` or closing a
dialog mid-request.
`trigger` on the Mosaic `Dialog` becomes optional, so the machine-driven
dialogs no longer render a button they don't use.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Moves the dialog off the Emotion slot-recipe engine onto StyleX, leaving
`tabs` as the last component on the old path, and reworks its sizing,
motion and mobile behaviour on top of that.
`size` becomes three named surfaces — `prompt`, `card`, `panel` — and
moves to `Dialog.Root`, since the backdrop reads it too. The gap to the
screen edge is a fixed inset at three breakpoints rather than a
percentage, which is what makes the surround an even frame. A `panel`
clips and carries no padding, so its scroll region is composed inside it
from the ScrollArea atoms; that keeps the close button anchored and makes
a sidebar a plain flex row.
Below 48rem a `prompt` becomes a bottom sheet, and `Dialog.Viewport`
measures the on-screen keyboard so the sheet rises above it while a card
re-centres and a panel shrinks. The chrome of a mobile browser is tinted
to match the scrim, derived from the backdrop rather than shipped as a
colour, refcounted across stacked dialogs and reverting exactly.
Adds `Dialog.CloseButton`, `data-nested` for stacked scrims, and
`--cl-dialog-origin` so a dialog scales out of whatever opened it.
Also fixes a transition that never ran: it was keyed to a
`data-cl-starting-style` attribute the headless layer does not emit, so
dialogs appeared with no animation at all.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Drop the trigger-origin open animation in favour of a plain centre scale,
deleting the headless `useDialogOrigin` hook and `--cl-dialog-origin` with it.
Motion. Add a `--cl-ease-enter` token — a decelerate curve that lands on
target rather than carrying ~2% past it like `--cl-ease-default` — and take it
for every dialog entrance. The backdrop fades a step faster than the popup, so
the scrim answers the gesture and the surface arrives into an already-dimmed
page. The mobile sheet now fades over the full length of its slide instead of
holding opaque. Enter scale 0.98 -> 0.94.
Fix a `prompt` that never scaled at all. Its transform was a single
media-scoped rule with no resting declaration, leaving the transition with
`transform: none` as its other endpoint; it now mirrors `card` exactly.
Surface. Match the popup shadow to Menu's, which restores a visible hairline
on dark surfaces. A `card` no longer paints itself: it takes its surface from a
`Card` rendered AS the popup, so one element both paints and animates and the
radius counter-scale keeps landing on the corners you can see.
Scrolling. A dialog taller than the screen now scrolls, and how follows from
its size rather than a prop. `panel` keeps a pinned viewport and scrolls
inside; `prompt` and `card` grow with their content, so the dialog moves within
the viewport and keeps its inset at both ends. `Dialog.Viewport` gains
`data-size`.
Spacing. `prompt` padding to 1rem; phone-band inset to 1.25rem block, 1rem
inline.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Draw a real border on the popup under `forced-colors: active`. The mode
discards `box-shadow` and the scrim alike, so the dialog otherwise floats
edgeless over the page with nothing to separate it. A border rather than an
outline, since the popup clears its outline deliberately — `FloatingFocusManager`
focuses it when it holds no tabbable content — and the two would collide.
Wrap long unbroken strings on the popup, matching `Popover`. A dialog holds
prose it did not author, and an email address or an API key would otherwise
push past the size's width clamp.
Warn in development when a dialog has no accessible name, extracting
`Popover`'s check into a shared `useAccessibleNameWarning` and pointing both
at it. The check now RESOLVES `aria-labelledby` rather than testing for its
presence: `Dialog` emits the attribute unconditionally, so with no
`Dialog.Title` it references an id that is not in the document, which a
presence check waves through while naming the dialog no better than having no
attribute at all.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`Dialog.createHandle()` returns a handle passed to both a `Dialog.Trigger`
and a `Dialog.Root`, so a trigger drives a dialog it is not nested under.
The handle also exposes imperative `open()` / `close()` / `isOpen`.
Several triggers can share one dialog, each carrying an `id` and a
`payload`, with the root's children as a function of `{ payload }` so one
dialog renders per-trigger content. Everything keyed to "the trigger" now
follows the one actually used — the dialog scales out of it and returns
focus to it — and `triggerId` names the active trigger in controlled mode,
which also gives controlled, trigger-less dialogs the origin-aware open.
`initialFocus` and `finalFocus` on `Dialog.Popup` take `true`, `false`, a
ref, or a function of the interaction type behind the change. Defaults are
unchanged: first tabbable on open, the trigger on close, except after a
pointer-driven dismissal.
Also retunes the dialog for dark mode. The scrim veils rather than darkens
there — light grey at low alpha over a dark page, against black over a
light one — so the two schemes are unrelated colours rather than one
colour at two opacities. The nested scrim stays solved rather than picked,
landing two levels on the same proportional deepening as light. The popup
shadow becomes one three-layer shadow shared by both schemes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@changeset-bot

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f563af4

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

This PR includes changesets to release 4 packages
NameType
@clerk/uiPatch
@clerk/headlessPatch
@clerk/chrome-extensionPatch
@clerk/swingsetPatch

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

@coderabbitai

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 36688779-e45d-44ad-9ad5-2320a2b713b5

📥 Commits

Reviewing files that changed from the base of the PR and between f9c97ef and f563af4.

📒 Files selected for processing (38)
  • .changeset/lucky-donuts-invite.md
  • .changeset/olive-doors-tell.md
  • .changeset/spicy-clocks-argue.md
  • packages/headless/src/primitives/dialog/README.md
  • packages/headless/src/primitives/dialog/dialog-backdrop.tsx
  • packages/headless/src/primitives/dialog/dialog-context.ts
  • packages/headless/src/primitives/dialog/dialog-handle.ts
  • packages/headless/src/primitives/dialog/dialog-popup.tsx
  • packages/headless/src/primitives/dialog/dialog-root.tsx
  • packages/headless/src/primitives/dialog/dialog-trigger.tsx
  • packages/headless/src/primitives/dialog/dialog-viewport.tsx
  • packages/headless/src/primitives/dialog/dialog.test.tsx
  • packages/headless/src/primitives/dialog/index.ts
  • packages/headless/src/primitives/dialog/parts.ts
  • packages/headless/src/primitives/drawer/drawer-context.ts
  • packages/headless/src/utils/interaction-modality.ts
  • packages/swingset/src/stories/dialog.component.mdx
  • packages/swingset/src/stories/dialog.component.stories.tsx
  • packages/swingset/src/stories/dialog.mdx
  • packages/swingset/src/stories/dialog.stories.tsx
  • packages/ui/src/mosaic/block/destructive.tsx
  • packages/ui/src/mosaic/components/button/button.tsx
  • packages/ui/src/mosaic/components/dialog.tsx
  • packages/ui/src/mosaic/components/dialog/browser-chrome.ts
  • packages/ui/src/mosaic/components/dialog/dialog.styles.ts
  • packages/ui/src/mosaic/components/dialog/dialog.test.tsx
  • packages/ui/src/mosaic/components/dialog/dialog.tsx
  • packages/ui/src/mosaic/components/dialog/index.ts
  • packages/ui/src/mosaic/components/dialog/keyboard-inset.ts
  • packages/ui/src/mosaic/components/popover/popover.tsx
  • packages/ui/src/mosaic/hooks/useAccessibleNameWarning.ts
  • packages/ui/src/mosaic/organization/organization-profile-domains-section-add-verify.view.tsx
  • packages/ui/src/mosaic/organization/organization-profile-domains-section-enrollment.view.tsx
  • packages/ui/src/mosaic/organization/organization-profile-domains-section-remove.view.tsx
  • packages/ui/src/mosaic/organization/organization-profile-profile-section.view.tsx
  • packages/ui/src/mosaic/primitives/dialog.tsx
  • packages/ui/src/mosaic/styles/index.ts
  • packages/ui/src/mosaic/tokens.stylex.ts

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.


📝 Walkthrough

Walkthrough

The pull request rebuilds Mosaic Dialog on top of new headless Dialog primitives. It adds detached and multiple triggers, typed payloads, dismissal policies, detailed callbacks, and configurable focus targets. The Mosaic implementation adds StyleX surfaces, nested-dialog behavior, responsive sizing, motion, keyboard insets, browser-chrome synchronization, close buttons, and accessibility diagnostics. Tests, stories, documentation, consumer configurations, exports, tokens, and changesets are updated.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested reviewers:alexcarpenter

✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

API Changes Report

Generated by Break Check on 2026-08-12T19:16:25.571Z

Summary

MetricCount
Packages analyzed19
Packages with changes1
🔴 Breaking changes3
🟡 Non-breaking changes0
🟢 Additions0

Warning
3 breaking change(s) detected - Major version bump required

🤖 This report was reviewed by claude-sonnet-4-6.

🔴 Breaking changes index (3)

Every breaking change, up front. Full diffs are in the package sections below.

PackageSubpathChange
@clerk/nextjs.Protect
@clerk/nextjs.SignedIn
@clerk/nextjs.SignedOut

@clerk/nextjs

Current version: 7.7.4
Recommended bump: MAJOR → 8.0.0

🔴 Breaking Changes (3)

Changed: Protect

- export declare function Protect(_props: RemovedControlComponentProps): never;

Static analyzer: Removed function Protect

🤖 AI review (confirmed) (95%): The Protect function was previously exported and consumers may import and use it; removing it breaks any such import.

Migration: Remove any usage of Protect from @clerk/nextjs or migrate to the equivalent component from the appropriate Clerk package.

Changed: SignedIn

- export declare function SignedIn(_props: RemovedControlComponentProps): never;

Static analyzer: Removed function SignedIn

🤖 AI review (confirmed) (95%): The SignedIn function was previously exported and consumers may import and use it; removing it breaks any such import.

Migration: Remove any usage of SignedIn from @clerk/nextjs or migrate to the equivalent component from the appropriate Clerk package.

Changed: SignedOut

- export declare function SignedOut(_props: RemovedControlComponentProps): never;

Static analyzer: Removed function SignedOut

🤖 AI review (confirmed) (95%): The SignedOut function was previously exported and consumers may import and use it; removing it breaks any such import.

Migration: Remove any usage of SignedOut from @clerk/nextjs or migrate to the equivalent component from the appropriate Clerk package.


Report generated by Break Check

Last ran on f563af4.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@maxyinger
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); Max/dialog composition apis by maxyinger · Pull Request #9420 · clerk/javascript · GitHub
Skip to content

Max/dialog composition apis - #9420

Closed
maxyinger wants to merge 6 commits into
mainfrom
max/dialog-composition-apis
Closed

Max/dialog composition apis#9420
maxyinger wants to merge 6 commits into
mainfrom
max/dialog-composition-apis

Conversation

@maxyinger

Copy link
Copy Markdown
Collaborator

Description

Adds Base UI-style composition APIs to the headless Dialog. Split out of #9388 so the runtime changes are reviewed on their own.

  • Detached triggersDialog.createHandle() returns a handle; pass it to a Dialog.Trigger and a Dialog.Root and the trigger drives the dialog from anywhere in the tree, with no JSX nesting.
  • Imperative control — the same handle carries open() / close() / isOpen for opens with no trigger element. Calls made while no root is mounted are ignored.
  • Multiple triggers — each trigger takes an id and a payload; the root's children can be a function of the active trigger's payload, so one dialog renders per-trigger content.
  • Trigger attributiononOpenChange gains a details argument naming the trigger behind each change, and triggerId on the root drives attribution in controlled mode. Focus returns to whichever trigger was actually used.
  • Custom focusinitialFocus and finalFocus on Dialog.Popup take true, false, a ref, or a function of the interaction type behind the open/close.
  • A root with no handle creates a private one, so nested and detached triggers share a single registration and open/close path.

Internals worth a look on review:

  • Trigger activation moves off floating-ui's useClick — a detached trigger can't collect reference props from context.
  • Every open and close (trigger, dismissal, programmatic) funnels through one path, so the onOpenChange details and the finalFocus resolution each happen exactly once, synchronously, before focus restoration runs.
  • The trigger registry is subscribed to imperatively rather than via useSyncExternalStore, so a trigger with an inline-object payload can't loop with the root re-registering it.

Checklist

  • pnpm test runs as expected.
  • pnpm build runs as expected.
  • (If applicable) JSDoc comments have been added or updated for any package exports
  • (If applicable) Documentation has been updated

Type of change

  • 🐛 Bug fix
  • 🌟 New feature
  • 🔨 Breaking change
  • 📖 Refactoring / dependency upgrade / documentation
  • other:

maxyingerand others added 6 commits August 7, 2026 15:16
Adds `closedBy: 'any' | 'closerequest' | 'none'` to the headless Dialog root,
driving `escapeKey` and `outsidePress` on `useDismiss`. Defaults to `any`, so
existing callers are unaffected.
The five Mosaic dialogs now use `closerequest`, which stops a stray backdrop
click from discarding the type-to-confirm input in `Destructive` or closing a
dialog mid-request.
`trigger` on the Mosaic `Dialog` becomes optional, so the machine-driven
dialogs no longer render a button they don't use.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Moves the dialog off the Emotion slot-recipe engine onto StyleX, leaving
`tabs` as the last component on the old path, and reworks its sizing,
motion and mobile behaviour on top of that.
`size` becomes three named surfaces — `prompt`, `card`, `panel` — and
moves to `Dialog.Root`, since the backdrop reads it too. The gap to the
screen edge is a fixed inset at three breakpoints rather than a
percentage, which is what makes the surround an even frame. A `panel`
clips and carries no padding, so its scroll region is composed inside it
from the ScrollArea atoms; that keeps the close button anchored and makes
a sidebar a plain flex row.
Below 48rem a `prompt` becomes a bottom sheet, and `Dialog.Viewport`
measures the on-screen keyboard so the sheet rises above it while a card
re-centres and a panel shrinks. The chrome of a mobile browser is tinted
to match the scrim, derived from the backdrop rather than shipped as a
colour, refcounted across stacked dialogs and reverting exactly.
Adds `Dialog.CloseButton`, `data-nested` for stacked scrims, and
`--cl-dialog-origin` so a dialog scales out of whatever opened it.
Also fixes a transition that never ran: it was keyed to a
`data-cl-starting-style` attribute the headless layer does not emit, so
dialogs appeared with no animation at all.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Drop the trigger-origin open animation in favour of a plain centre scale,
deleting the headless `useDialogOrigin` hook and `--cl-dialog-origin` with it.
Motion. Add a `--cl-ease-enter` token — a decelerate curve that lands on
target rather than carrying ~2% past it like `--cl-ease-default` — and take it
for every dialog entrance. The backdrop fades a step faster than the popup, so
the scrim answers the gesture and the surface arrives into an already-dimmed
page. The mobile sheet now fades over the full length of its slide instead of
holding opaque. Enter scale 0.98 -> 0.94.
Fix a `prompt` that never scaled at all. Its transform was a single
media-scoped rule with no resting declaration, leaving the transition with
`transform: none` as its other endpoint; it now mirrors `card` exactly.
Surface. Match the popup shadow to Menu's, which restores a visible hairline
on dark surfaces. A `card` no longer paints itself: it takes its surface from a
`Card` rendered AS the popup, so one element both paints and animates and the
radius counter-scale keeps landing on the corners you can see.
Scrolling. A dialog taller than the screen now scrolls, and how follows from
its size rather than a prop. `panel` keeps a pinned viewport and scrolls
inside; `prompt` and `card` grow with their content, so the dialog moves within
the viewport and keeps its inset at both ends. `Dialog.Viewport` gains
`data-size`.
Spacing. `prompt` padding to 1rem; phone-band inset to 1.25rem block, 1rem
inline.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Draw a real border on the popup under `forced-colors: active`. The mode
discards `box-shadow` and the scrim alike, so the dialog otherwise floats
edgeless over the page with nothing to separate it. A border rather than an
outline, since the popup clears its outline deliberately — `FloatingFocusManager`
focuses it when it holds no tabbable content — and the two would collide.
Wrap long unbroken strings on the popup, matching `Popover`. A dialog holds
prose it did not author, and an email address or an API key would otherwise
push past the size's width clamp.
Warn in development when a dialog has no accessible name, extracting
`Popover`'s check into a shared `useAccessibleNameWarning` and pointing both
at it. The check now RESOLVES `aria-labelledby` rather than testing for its
presence: `Dialog` emits the attribute unconditionally, so with no
`Dialog.Title` it references an id that is not in the document, which a
presence check waves through while naming the dialog no better than having no
attribute at all.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`Dialog.createHandle()` returns a handle passed to both a `Dialog.Trigger`
and a `Dialog.Root`, so a trigger drives a dialog it is not nested under.
The handle also exposes imperative `open()` / `close()` / `isOpen`.
Several triggers can share one dialog, each carrying an `id` and a
`payload`, with the root's children as a function of `{ payload }` so one
dialog renders per-trigger content. Everything keyed to "the trigger" now
follows the one actually used — the dialog scales out of it and returns
focus to it — and `triggerId` names the active trigger in controlled mode,
which also gives controlled, trigger-less dialogs the origin-aware open.
`initialFocus` and `finalFocus` on `Dialog.Popup` take `true`, `false`, a
ref, or a function of the interaction type behind the change. Defaults are
unchanged: first tabbable on open, the trigger on close, except after a
pointer-driven dismissal.
Also retunes the dialog for dark mode. The scrim veils rather than darkens
there — light grey at low alpha over a dark page, against black over a
light one — so the two schemes are unrelated colours rather than one
colour at two opacities. The nested scrim stays solved rather than picked,
landing two levels on the same proportional deepening as light. The popup
shadow becomes one three-layer shadow shared by both schemes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@changeset-bot

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f563af4

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

This PR includes changesets to release 4 packages
NameType
@clerk/uiPatch
@clerk/headlessPatch
@clerk/chrome-extensionPatch
@clerk/swingsetPatch

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

@coderabbitai

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 36688779-e45d-44ad-9ad5-2320a2b713b5

📥 Commits

Reviewing files that changed from the base of the PR and between f9c97ef and f563af4.

📒 Files selected for processing (38)
  • .changeset/lucky-donuts-invite.md
  • .changeset/olive-doors-tell.md
  • .changeset/spicy-clocks-argue.md
  • packages/headless/src/primitives/dialog/README.md
  • packages/headless/src/primitives/dialog/dialog-backdrop.tsx
  • packages/headless/src/primitives/dialog/dialog-context.ts
  • packages/headless/src/primitives/dialog/dialog-handle.ts
  • packages/headless/src/primitives/dialog/dialog-popup.tsx
  • packages/headless/src/primitives/dialog/dialog-root.tsx
  • packages/headless/src/primitives/dialog/dialog-trigger.tsx
  • packages/headless/src/primitives/dialog/dialog-viewport.tsx
  • packages/headless/src/primitives/dialog/dialog.test.tsx
  • packages/headless/src/primitives/dialog/index.ts
  • packages/headless/src/primitives/dialog/parts.ts
  • packages/headless/src/primitives/drawer/drawer-context.ts
  • packages/headless/src/utils/interaction-modality.ts
  • packages/swingset/src/stories/dialog.component.mdx
  • packages/swingset/src/stories/dialog.component.stories.tsx
  • packages/swingset/src/stories/dialog.mdx
  • packages/swingset/src/stories/dialog.stories.tsx
  • packages/ui/src/mosaic/block/destructive.tsx
  • packages/ui/src/mosaic/components/button/button.tsx
  • packages/ui/src/mosaic/components/dialog.tsx
  • packages/ui/src/mosaic/components/dialog/browser-chrome.ts
  • packages/ui/src/mosaic/components/dialog/dialog.styles.ts
  • packages/ui/src/mosaic/components/dialog/dialog.test.tsx
  • packages/ui/src/mosaic/components/dialog/dialog.tsx
  • packages/ui/src/mosaic/components/dialog/index.ts
  • packages/ui/src/mosaic/components/dialog/keyboard-inset.ts
  • packages/ui/src/mosaic/components/popover/popover.tsx
  • packages/ui/src/mosaic/hooks/useAccessibleNameWarning.ts
  • packages/ui/src/mosaic/organization/organization-profile-domains-section-add-verify.view.tsx
  • packages/ui/src/mosaic/organization/organization-profile-domains-section-enrollment.view.tsx
  • packages/ui/src/mosaic/organization/organization-profile-domains-section-remove.view.tsx
  • packages/ui/src/mosaic/organization/organization-profile-profile-section.view.tsx
  • packages/ui/src/mosaic/primitives/dialog.tsx
  • packages/ui/src/mosaic/styles/index.ts
  • packages/ui/src/mosaic/tokens.stylex.ts

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.


📝 Walkthrough

Walkthrough

The pull request rebuilds Mosaic Dialog on top of new headless Dialog primitives. It adds detached and multiple triggers, typed payloads, dismissal policies, detailed callbacks, and configurable focus targets. The Mosaic implementation adds StyleX surfaces, nested-dialog behavior, responsive sizing, motion, keyboard insets, browser-chrome synchronization, close buttons, and accessibility diagnostics. Tests, stories, documentation, consumer configurations, exports, tokens, and changesets are updated.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested reviewers:alexcarpenter

✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

API Changes Report

Generated by Break Check on 2026-08-12T19:16:25.571Z

Summary

MetricCount
Packages analyzed19
Packages with changes1
🔴 Breaking changes3
🟡 Non-breaking changes0
🟢 Additions0

Warning
3 breaking change(s) detected - Major version bump required

🤖 This report was reviewed by claude-sonnet-4-6.

🔴 Breaking changes index (3)

Every breaking change, up front. Full diffs are in the package sections below.

PackageSubpathChange
@clerk/nextjs.Protect
@clerk/nextjs.SignedIn
@clerk/nextjs.SignedOut

@clerk/nextjs

Current version: 7.7.4
Recommended bump: MAJOR → 8.0.0

🔴 Breaking Changes (3)

Changed: Protect

- export declare function Protect(_props: RemovedControlComponentProps): never;

Static analyzer: Removed function Protect

🤖 AI review (confirmed) (95%): The Protect function was previously exported and consumers may import and use it; removing it breaks any such import.

Migration: Remove any usage of Protect from @clerk/nextjs or migrate to the equivalent component from the appropriate Clerk package.

Changed: SignedIn

- export declare function SignedIn(_props: RemovedControlComponentProps): never;

Static analyzer: Removed function SignedIn

🤖 AI review (confirmed) (95%): The SignedIn function was previously exported and consumers may import and use it; removing it breaks any such import.

Migration: Remove any usage of SignedIn from @clerk/nextjs or migrate to the equivalent component from the appropriate Clerk package.

Changed: SignedOut

- export declare function SignedOut(_props: RemovedControlComponentProps): never;

Static analyzer: Removed function SignedOut

🤖 AI review (confirmed) (95%): The SignedOut function was previously exported and consumers may import and use it; removing it breaks any such import.

Migration: Remove any usage of SignedOut from @clerk/nextjs or migrate to the equivalent component from the appropriate Clerk package.


Report generated by Break Check

Last ran on f563af4.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@maxyinger