diff --git a/.changeset/five-carpets-begin.md b/.changeset/five-carpets-begin.md
new file mode 100644
index 000000000..07d896f5f
--- /dev/null
+++ b/.changeset/five-carpets-begin.md
@@ -0,0 +1,6 @@
+---
+"@radix-ui/react-scroll-area": patch
+"radix-ui": patch
+---
+
+Stabilize viewport style tag unless nonce changes.
diff --git a/.changeset/tiny-groups-behave.md b/.changeset/tiny-groups-behave.md
new file mode 100644
index 000000000..631e413de
--- /dev/null
+++ b/.changeset/tiny-groups-behave.md
@@ -0,0 +1,6 @@
+---
+"@radix-ui/react-password-toggle-field": patch
+"radix-ui": patch
+---
+
+Fixed prop type definitions to include `asChild` for all component parts
diff --git a/apps/storybook/stories/dialog.stories.tsx b/apps/storybook/stories/dialog.stories.tsx
index 6b03ddef9..2e9aeac8e 100644
--- a/apps/storybook/stories/dialog.stories.tsx
+++ b/apps/storybook/stories/dialog.stories.tsx
@@ -176,6 +176,55 @@ export const NoPointerDownOutsideDismiss = () => (
);
+export const EventPropagation = () => {
+ const count = React.useRef(0);
+ const [stopPropagation, setStopPropagation] = React.useState(true);
+ return (
+ <>
+
{
+ count.current++;
+ console.log(`clicked the card ${count.current} time${count.current === 1 ? '' : 's'}!`);
+ }}
+ >
+
+ event.stopPropagation()}>open
+
+ {
+ if (stopPropagation) {
+ event.stopPropagation();
+ }
+ console.log('clicked the dialog overlay!');
+ }}
+ />
+ {
+ if (stopPropagation) {
+ event.stopPropagation();
+ }
+ console.log('clicked the dialog content!');
+ }}
+ >
+
+ close
+
+ Title
+ You can close me now!
+
+
+
+
+
+ >
+ );
+};
+
export const WithPortalContainer = () => {
const [portalContainer, setPortalContainer] = React.useState(null);
return (
diff --git a/cypress/e2e/Select.cy.ts b/cypress/e2e/Select.cy.ts
index f902b252d..2cd68670a 100644
--- a/cypress/e2e/Select.cy.ts
+++ b/cypress/e2e/Select.cy.ts
@@ -94,6 +94,9 @@ describe('Select (shadow DOM)', () => {
.findByLabelText(/pick a food/i)
.realTouch();
+ // wait for the content to be open and settled before interacting with it
+ cy.get('.shadow-host').shadow().findByRole('listbox').should('be.visible');
+
// trigger a touch scroll, triggering the pointer move event and ensuring
// we do not preventDefault on the upcoming pointer up event
cy.get('.shadow-host').shadow().find('[data-radix-select-viewport]').realSwipe('toTop', {
@@ -103,14 +106,23 @@ describe('Select (shadow DOM)', () => {
// assert the select content is still open after swiping
cy.get('.shadow-host').shadow().findByRole('listbox').should('exist');
- // select an item after scrolling
+ // select an item after scrolling, ensuring it is scrolled into view and
+ // visible so the touch reliably lands within the constrained viewport
cy.get('.shadow-host')
.shadow()
.findByRole('option', { name: /Grapes/i })
+ .scrollIntoView()
+ .should('be.visible')
.realTouch();
- // assert the select value has been updated
- cy.get('.shadow-host').shadow().findByText(/food:/).should('include.text', 'grapes');
+ // selecting an item should close the content, which confirms the
+ // selection registered before we assert on the bound value
+ cy.get('.shadow-host').shadow().findByRole('listbox').should('not.exist');
+
+ // assert the select value has been updated. We query the element directly
+ // rather than running `findByText` against the shadow root, which throws a
+ // confusing "got ShadowRoot" error while retrying when the node is absent.
+ cy.get('.shadow-host').shadow().find('p').should('include.text', 'food: grapes');
});
});
});
diff --git a/packages/react/dialog/src/dialog.tsx b/packages/react/dialog/src/dialog.tsx
index f993e2e1e..8aa78f712 100644
--- a/packages/react/dialog/src/dialog.tsx
+++ b/packages/react/dialog/src/dialog.tsx
@@ -4,7 +4,7 @@ import { useComposedRefs } from '@radix-ui/react-compose-refs';
import { createContextScope } from '@radix-ui/react-context';
import { useId } from '@radix-ui/react-id';
import { useControllableState } from '@radix-ui/react-use-controllable-state';
-import { DismissableLayer } from '@radix-ui/react-dismissable-layer';
+import { DismissableLayer, useDismissableLayerSurface } from '@radix-ui/react-dismissable-layer';
import { FocusScope } from '@radix-ui/react-focus-scope';
import { Portal as PortalPrimitive } from '@radix-ui/react-portal';
import { Presence } from '@radix-ui/react-presence';
@@ -200,6 +200,13 @@ const DialogOverlayImpl = React.forwardRef, forwardedRef) => {
const { __scopeDialog, ...overlayProps } = props;
const context = useDialogContext(OVERLAY_NAME, __scopeDialog);
+
+ // Register the overlay as a dismiss surface so a consumer calling
+ // `stopPropagation` on it (eg. to avoid triggering parent handlers) does not
+ // prevent the dialog from closing. See: https://github.com/radix-ui/primitives/issues/3346
+ const registerDismissableSurface = useDismissableLayerSurface();
+ const composedRefs = useComposedRefs(forwardedRef, registerDismissableSurface);
+
return (
// Make sure `Content` is scrollable even when it doesn't live inside `RemoveScroll`
// ie. when `Overlay` and `Content` are siblings
@@ -207,7 +214,7 @@ const DialogOverlayImpl = React.forwardRef
diff --git a/packages/react/dismissable-layer/src/dismissable-layer.test.tsx b/packages/react/dismissable-layer/src/dismissable-layer.test.tsx
index 83d6e6787..4ad5769f4 100644
--- a/packages/react/dismissable-layer/src/dismissable-layer.test.tsx
+++ b/packages/react/dismissable-layer/src/dismissable-layer.test.tsx
@@ -281,6 +281,100 @@ describe('DismissableLayer', () => {
expect(onDismiss).not.toHaveBeenCalled();
});
+ it('dismisses when a registered dismiss surface stops propagation', async () => {
+ const onDismiss = vi.fn();
+
+ function Surface() {
+ const registerSurface = DismissableLayer.useDismissableLayerSurface();
+ return (
+