From 724ee378fc27245519ce59dacd4f2a02c8120e06 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 8 Dec 2022 16:37:01 -0500 Subject: [PATCH] Allow clicks inside dialog panel when target is inside shadow root (#2079) * Allow clicks inside dialog panel when target is inside shadow root * fixup * Update changelog --- packages/@headlessui-react/CHANGELOG.md | 1 + .../src/components/dialog/dialog.test.tsx | 92 +++++++++++++++- .../src/hooks/use-outside-click.ts | 6 ++ packages/@headlessui-vue/CHANGELOG.md | 1 + .../src/components/dialog/dialog.test.ts | 101 ++++++++++++++++++ .../src/hooks/use-outside-click.ts | 6 ++ 6 files changed, 206 insertions(+), 1 deletion(-) diff --git a/packages/@headlessui-react/CHANGELOG.md b/packages/@headlessui-react/CHANGELOG.md index 331e8d38f..33be6647e 100644 --- a/packages/@headlessui-react/CHANGELOG.md +++ b/packages/@headlessui-react/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Allow passing in your own `id` prop ([#2060](https://github.com/tailwindlabs/headlessui/pull/2060)) - Fix `Dialog` unmounting problem due to incorrect `transitioncancel` event in the `Transition` component on Android ([#2071](https://github.com/tailwindlabs/headlessui/pull/2071)) - Ignore pointer events in Listbox, Menu, and Combobox when cursor hasn't moved ([#2069](https://github.com/tailwindlabs/headlessui/pull/2069)) +- Allow clicks inside dialog panel when target is inside shadow root ([#2079](https://github.com/tailwindlabs/headlessui/pull/2079)) ## [1.7.4] - 2022-11-03 diff --git a/packages/@headlessui-react/src/components/dialog/dialog.test.tsx b/packages/@headlessui-react/src/components/dialog/dialog.test.tsx index 3cedae9c3..cc6d15874 100644 --- a/packages/@headlessui-react/src/components/dialog/dialog.test.tsx +++ b/packages/@headlessui-react/src/components/dialog/dialog.test.tsx @@ -1,4 +1,4 @@ -import React, { createElement, useRef, useState, Fragment } from 'react' +import React, { createElement, useRef, useState, Fragment, useEffect } from 'react' import { render } from '@testing-library/react' import { Dialog } from './dialog' @@ -1144,6 +1144,96 @@ describe('Mouse interactions', () => { }) ) + it( + 'should be possible to click elements inside the dialog when they reside inside a shadow boundary', + suppressConsoleLogs(async () => { + let fn = jest.fn() + function ShadowChildren({ id, buttonId }: { id: string; buttonId: string }) { + let container = useRef(null) + + useEffect(() => { + if (!container.current || container.current.shadowRoot) { + return + } + + let shadowRoot = container.current.attachShadow({ mode: 'open' }) + let button = document.createElement('button') + button.id = buttonId + button.textContent = 'Inside shadow root' + button.addEventListener('click', fn) + shadowRoot.appendChild(button) + }, []) + + return
+ } + + function Example() { + let [isOpen, setIsOpen] = useState(true) + + return ( +
+ + setIsOpen(false)}> +
+ + +
+ + + + +
+
+ ) + } + + render() + + await nextFrame() + + // Verify it is open + assertDialog({ state: DialogState.Visible }) + + // Click the button inside the dialog (light DOM) + await click(document.querySelector('#btn_inside_light')) + + // Verify the button was clicked + expect(fn).toHaveBeenCalledTimes(1) + + // Verify the dialog is still open + assertDialog({ state: DialogState.Visible }) + + // Click the button inside the dialog (shadow DOM) + await click( + document.querySelector('#inside_shadow')?.shadowRoot?.querySelector('#btn_inside_shadow') ?? + null + ) + + // Verify the button was clicked + expect(fn).toHaveBeenCalledTimes(2) + + // Verify the dialog is still open + assertDialog({ state: DialogState.Visible }) + + // Click the button outside the dialog (shadow DOM) + await click( + document + .querySelector('#outside_shadow') + ?.shadowRoot?.querySelector('#btn_outside_shadow') ?? null + ) + + // Verify the button was clicked + expect(fn).toHaveBeenCalledTimes(3) + + // Verify the dialog is closed + assertDialog({ state: DialogState.InvisibleUnmounted }) + }) + ) + it( 'should close the Dialog if we click outside the Dialog.Panel', suppressConsoleLogs(async () => { diff --git a/packages/@headlessui-react/src/hooks/use-outside-click.ts b/packages/@headlessui-react/src/hooks/use-outside-click.ts index 1569d9ced..a6e240637 100644 --- a/packages/@headlessui-react/src/hooks/use-outside-click.ts +++ b/packages/@headlessui-react/src/hooks/use-outside-click.ts @@ -69,6 +69,12 @@ export function useOutsideClick( if (domNode?.contains(target)) { return } + + // If the click crossed a shadow boundary, we need to check if the container + // is inside the tree by using `composedPath` to "pierce" the shadow boundary + if (event.composed && event.composedPath().includes(domNode as EventTarget)) { + return + } } // This allows us to check whether the event was defaultPrevented when you are nesting this diff --git a/packages/@headlessui-vue/CHANGELOG.md b/packages/@headlessui-vue/CHANGELOG.md index a44981e4c..f6397b116 100644 --- a/packages/@headlessui-vue/CHANGELOG.md +++ b/packages/@headlessui-vue/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add `null` as a valid type for Listbox and Combobox in Vue ([#2064](https://github.com/tailwindlabs/headlessui/pull/2064), [#2067](https://github.com/tailwindlabs/headlessui/pull/2067)) - Improve SSR for Tabs in Vue ([#2068](https://github.com/tailwindlabs/headlessui/pull/2068)) - Ignore pointer events in Listbox, Menu, and Combobox when cursor hasn't moved ([#2069](https://github.com/tailwindlabs/headlessui/pull/2069)) +- Allow clicks inside dialog panel when target is inside shadow root ([#2079](https://github.com/tailwindlabs/headlessui/pull/2079)) ## [1.7.4] - 2022-11-03 diff --git a/packages/@headlessui-vue/src/components/dialog/dialog.test.ts b/packages/@headlessui-vue/src/components/dialog/dialog.test.ts index dba16d14c..22358c55b 100644 --- a/packages/@headlessui-vue/src/components/dialog/dialog.test.ts +++ b/packages/@headlessui-vue/src/components/dialog/dialog.test.ts @@ -1481,6 +1481,107 @@ describe('Mouse interactions', () => { }) ) + it( + 'should be possible to click elements inside the dialog when they reside inside a shadow boundary', + suppressConsoleLogs(async () => { + let fn = jest.fn() + + let ShadowChildren = defineComponent({ + props: ['id', 'buttonId'], + setup(props) { + let container = ref(null) + + onMounted(() => { + if (!container.value || container.value.shadowRoot) { + return + } + + let shadowRoot = container.value.attachShadow({ mode: 'open' }) + let button = document.createElement('button') + button.id = props.buttonId + button.textContent = 'Inside shadow root' + button.addEventListener('click', fn) + shadowRoot.appendChild(button) + }) + + return () => h('div', { id: props.id, ref: container }) + }, + }) + + renderTemplate({ + components: { ShadowChildren }, + template: ` +
+ + +
+ + +
+ + + + +
+
+ `, + setup() { + let isOpen = ref(true) + return { + fn, + isOpen, + setIsOpen(value: boolean) { + isOpen.value = value + }, + } + }, + }) + + await nextFrame() + + // Verify it is open + assertDialog({ state: DialogState.Visible }) + + // Click the button inside the dialog (light DOM) + await click(document.querySelector('#btn_inside_light')) + + // Verify the button was clicked + expect(fn).toHaveBeenCalledTimes(1) + + // Verify the dialog is still open + assertDialog({ state: DialogState.Visible }) + + // Click the button inside the dialog (shadow DOM) + await click( + document.querySelector('#inside_shadow')?.shadowRoot?.querySelector('#btn_inside_shadow') ?? + null + ) + + // Verify the button was clicked + expect(fn).toHaveBeenCalledTimes(2) + + // Verify the dialog is still open + assertDialog({ state: DialogState.Visible }) + + // Click the button outside the dialog (shadow DOM) + await click( + document + .querySelector('#outside_shadow') + ?.shadowRoot?.querySelector('#btn_outside_shadow') ?? null + ) + + // Verify the button was clicked + expect(fn).toHaveBeenCalledTimes(3) + + // Verify the dialog is closed + assertDialog({ state: DialogState.InvisibleUnmounted }) + }) + ) + it( 'should close the Dialog if we click outside the DialogPanel', suppressConsoleLogs(async () => { diff --git a/packages/@headlessui-vue/src/hooks/use-outside-click.ts b/packages/@headlessui-vue/src/hooks/use-outside-click.ts index 3e25ca49d..e4d6b5188 100644 --- a/packages/@headlessui-vue/src/hooks/use-outside-click.ts +++ b/packages/@headlessui-vue/src/hooks/use-outside-click.ts @@ -55,6 +55,12 @@ export function useOutsideClick( if (domNode?.contains(target)) { return } + + // If the click crossed a shadow boundary, we need to check if the container + // is inside the tree by using `composedPath` to "pierce" the shadow boundary + if (event.composed && event.composedPath().includes(domNode as EventTarget)) { + return + } } // This allows us to check whether the event was defaultPrevented when you are nesting this