Skip to content

Commit 724ee37

Browse files
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
1 parent 2e941f8 commit 724ee37

File tree

6 files changed

+206
-1
lines changed

6 files changed

+206
-1
lines changed

packages/@headlessui-react/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1919
- Allow passing in your own `id` prop ([#2060](https://github.com/tailwindlabs/headlessui/pull/2060))
2020
- Fix `Dialog` unmounting problem due to incorrect `transitioncancel` event in the `Transition` component on Android ([#2071](https://github.com/tailwindlabs/headlessui/pull/2071))
2121
- Ignore pointer events in Listbox, Menu, and Combobox when cursor hasn't moved ([#2069](https://github.com/tailwindlabs/headlessui/pull/2069))
22+
- Allow clicks inside dialog panel when target is inside shadow root ([#2079](https://github.com/tailwindlabs/headlessui/pull/2079))
2223

2324
## [1.7.4] - 2022-11-03
2425

packages/@headlessui-react/src/components/dialog/dialog.test.tsx

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { createElement, useRef, useState, Fragment } from 'react'
1+
import React, { createElement, useRef, useState, Fragment, useEffect } from 'react'
22
import { render } from '@testing-library/react'
33

44
import { Dialog } from './dialog'
@@ -1144,6 +1144,96 @@ describe('Mouse interactions', () => {
11441144
})
11451145
)
11461146

1147+
it(
1148+
'should be possible to click elements inside the dialog when they reside inside a shadow boundary',
1149+
suppressConsoleLogs(async () => {
1150+
let fn = jest.fn()
1151+
function ShadowChildren({ id, buttonId }: { id: string; buttonId: string }) {
1152+
let container = useRef<HTMLDivElement | null>(null)
1153+
1154+
useEffect(() => {
1155+
if (!container.current || container.current.shadowRoot) {
1156+
return
1157+
}
1158+
1159+
let shadowRoot = container.current.attachShadow({ mode: 'open' })
1160+
let button = document.createElement('button')
1161+
button.id = buttonId
1162+
button.textContent = 'Inside shadow root'
1163+
button.addEventListener('click', fn)
1164+
shadowRoot.appendChild(button)
1165+
}, [])
1166+
1167+
return <div id={id} ref={container}></div>
1168+
}
1169+
1170+
function Example() {
1171+
let [isOpen, setIsOpen] = useState(true)
1172+
1173+
return (
1174+
<div>
1175+
<button onClick={() => setIsOpen(true)}>open</button>
1176+
<Dialog open={isOpen} onClose={() => setIsOpen(false)}>
1177+
<div>
1178+
<button id="btn_outside_light" onClick={fn}>
1179+
Button
1180+
</button>
1181+
<ShadowChildren id="outside_shadow" buttonId="btn_outside_shadow" />
1182+
</div>
1183+
<Dialog.Panel>
1184+
<button id="btn_inside_light" onClick={fn}>
1185+
Button
1186+
</button>
1187+
<ShadowChildren id="inside_shadow" buttonId="btn_inside_shadow" />
1188+
</Dialog.Panel>
1189+
</Dialog>
1190+
</div>
1191+
)
1192+
}
1193+
1194+
render(<Example />)
1195+
1196+
await nextFrame()
1197+
1198+
// Verify it is open
1199+
assertDialog({ state: DialogState.Visible })
1200+
1201+
// Click the button inside the dialog (light DOM)
1202+
await click(document.querySelector('#btn_inside_light'))
1203+
1204+
// Verify the button was clicked
1205+
expect(fn).toHaveBeenCalledTimes(1)
1206+
1207+
// Verify the dialog is still open
1208+
assertDialog({ state: DialogState.Visible })
1209+
1210+
// Click the button inside the dialog (shadow DOM)
1211+
await click(
1212+
document.querySelector('#inside_shadow')?.shadowRoot?.querySelector('#btn_inside_shadow') ??
1213+
null
1214+
)
1215+
1216+
// Verify the button was clicked
1217+
expect(fn).toHaveBeenCalledTimes(2)
1218+
1219+
// Verify the dialog is still open
1220+
assertDialog({ state: DialogState.Visible })
1221+
1222+
// Click the button outside the dialog (shadow DOM)
1223+
await click(
1224+
document
1225+
.querySelector('#outside_shadow')
1226+
?.shadowRoot?.querySelector('#btn_outside_shadow') ?? null
1227+
)
1228+
1229+
// Verify the button was clicked
1230+
expect(fn).toHaveBeenCalledTimes(3)
1231+
1232+
// Verify the dialog is closed
1233+
assertDialog({ state: DialogState.InvisibleUnmounted })
1234+
})
1235+
)
1236+
11471237
it(
11481238
'should close the Dialog if we click outside the Dialog.Panel',
11491239
suppressConsoleLogs(async () => {

packages/@headlessui-react/src/hooks/use-outside-click.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,12 @@ export function useOutsideClick(
6969
if (domNode?.contains(target)) {
7070
return
7171
}
72+
73+
// If the click crossed a shadow boundary, we need to check if the container
74+
// is inside the tree by using `composedPath` to "pierce" the shadow boundary
75+
if (event.composed && event.composedPath().includes(domNode as EventTarget)) {
76+
return
77+
}
7278
}
7379

7480
// This allows us to check whether the event was defaultPrevented when you are nesting this

packages/@headlessui-vue/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1818
- 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))
1919
- Improve SSR for Tabs in Vue ([#2068](https://github.com/tailwindlabs/headlessui/pull/2068))
2020
- Ignore pointer events in Listbox, Menu, and Combobox when cursor hasn't moved ([#2069](https://github.com/tailwindlabs/headlessui/pull/2069))
21+
- Allow clicks inside dialog panel when target is inside shadow root ([#2079](https://github.com/tailwindlabs/headlessui/pull/2079))
2122

2223
## [1.7.4] - 2022-11-03
2324

packages/@headlessui-vue/src/components/dialog/dialog.test.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1481,6 +1481,107 @@ describe('Mouse interactions', () => {
14811481
})
14821482
)
14831483

1484+
it(
1485+
'should be possible to click elements inside the dialog when they reside inside a shadow boundary',
1486+
suppressConsoleLogs(async () => {
1487+
let fn = jest.fn()
1488+
1489+
let ShadowChildren = defineComponent({
1490+
props: ['id', 'buttonId'],
1491+
setup(props) {
1492+
let container = ref<HTMLDivElement | null>(null)
1493+
1494+
onMounted(() => {
1495+
if (!container.value || container.value.shadowRoot) {
1496+
return
1497+
}
1498+
1499+
let shadowRoot = container.value.attachShadow({ mode: 'open' })
1500+
let button = document.createElement('button')
1501+
button.id = props.buttonId
1502+
button.textContent = 'Inside shadow root'
1503+
button.addEventListener('click', fn)
1504+
shadowRoot.appendChild(button)
1505+
})
1506+
1507+
return () => h('div', { id: props.id, ref: container })
1508+
},
1509+
})
1510+
1511+
renderTemplate({
1512+
components: { ShadowChildren },
1513+
template: `
1514+
<div>
1515+
<button @click="setIsOpen(true)">open</button>
1516+
<Dialog :open="isOpen" @close="setIsOpen(false)">
1517+
<div>
1518+
<button id="btn_outside_light" @click="fn">
1519+
Button
1520+
</button>
1521+
<ShadowChildren id="outside_shadow" buttonId="btn_outside_shadow" />
1522+
</div>
1523+
<DialogPanel>
1524+
<button id="btn_inside_light" @click="fn">
1525+
Button
1526+
</button>
1527+
<ShadowChildren id="inside_shadow" buttonId="btn_inside_shadow" />
1528+
</DialogPanel>
1529+
</Dialog>
1530+
</div>
1531+
`,
1532+
setup() {
1533+
let isOpen = ref(true)
1534+
return {
1535+
fn,
1536+
isOpen,
1537+
setIsOpen(value: boolean) {
1538+
isOpen.value = value
1539+
},
1540+
}
1541+
},
1542+
})
1543+
1544+
await nextFrame()
1545+
1546+
// Verify it is open
1547+
assertDialog({ state: DialogState.Visible })
1548+
1549+
// Click the button inside the dialog (light DOM)
1550+
await click(document.querySelector('#btn_inside_light'))
1551+
1552+
// Verify the button was clicked
1553+
expect(fn).toHaveBeenCalledTimes(1)
1554+
1555+
// Verify the dialog is still open
1556+
assertDialog({ state: DialogState.Visible })
1557+
1558+
// Click the button inside the dialog (shadow DOM)
1559+
await click(
1560+
document.querySelector('#inside_shadow')?.shadowRoot?.querySelector('#btn_inside_shadow') ??
1561+
null
1562+
)
1563+
1564+
// Verify the button was clicked
1565+
expect(fn).toHaveBeenCalledTimes(2)
1566+
1567+
// Verify the dialog is still open
1568+
assertDialog({ state: DialogState.Visible })
1569+
1570+
// Click the button outside the dialog (shadow DOM)
1571+
await click(
1572+
document
1573+
.querySelector('#outside_shadow')
1574+
?.shadowRoot?.querySelector('#btn_outside_shadow') ?? null
1575+
)
1576+
1577+
// Verify the button was clicked
1578+
expect(fn).toHaveBeenCalledTimes(3)
1579+
1580+
// Verify the dialog is closed
1581+
assertDialog({ state: DialogState.InvisibleUnmounted })
1582+
})
1583+
)
1584+
14841585
it(
14851586
'should close the Dialog if we click outside the DialogPanel',
14861587
suppressConsoleLogs(async () => {

packages/@headlessui-vue/src/hooks/use-outside-click.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@ export function useOutsideClick(
5555
if (domNode?.contains(target)) {
5656
return
5757
}
58+
59+
// If the click crossed a shadow boundary, we need to check if the container
60+
// is inside the tree by using `composedPath` to "pierce" the shadow boundary
61+
if (event.composed && event.composedPath().includes(domNode as EventTarget)) {
62+
return
63+
}
5864
}
5965

6066
// This allows us to check whether the event was defaultPrevented when you are nesting this

0 commit comments

Comments
 (0)