From 6068eab67353d5dd76af62f4e727e9d4d434f3da Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 1 Jun 2022 12:52:44 +0200 Subject: [PATCH 01/11] convert dialog in playground to use Dialog.Panel --- packages/playground-react/pages/dialog/dialog.tsx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/playground-react/pages/dialog/dialog.tsx b/packages/playground-react/pages/dialog/dialog.tsx index 9e4f25f32..527484f2f 100644 --- a/packages/playground-react/pages/dialog/dialog.tsx +++ b/packages/playground-react/pages/dialog/dialog.tsx @@ -85,7 +85,12 @@ export default function Home() { beforeLeave={() => console.log('[Transition] Before leave')} afterLeave={() => console.log('[Transition] After leave')} > - + { + console.log('close') + setIsOpen(false) + }} + >
console.log('[Transition.Child] [Overlay] Before leave')} afterLeave={() => console.log('[Transition.Child] [Overlay] After leave')} > - +
​ -
+
@@ -254,7 +259,7 @@ export default function Home() { Cancel
-
+
From 1da1c8578ab696431c58b4a146e2a6b20b772e5a Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Thu, 2 Jun 2022 18:36:21 +0200 Subject: [PATCH 02/11] convert `tabs-in-dialog` example to use `Dialog.Panel` --- .../playground-react/pages/combinations/tabs-in-dialog.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/playground-react/pages/combinations/tabs-in-dialog.tsx b/packages/playground-react/pages/combinations/tabs-in-dialog.tsx index bca77d568..1bf057c9c 100644 --- a/packages/playground-react/pages/combinations/tabs-in-dialog.tsx +++ b/packages/playground-react/pages/combinations/tabs-in-dialog.tsx @@ -8,8 +8,8 @@ export default function App() { <> - -
+
+
@@ -24,7 +24,7 @@ export default function App() {
-
+
) From d383e67e2ca8d67cfd1cbd38346bc78f0f479a99 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Thu, 2 Jun 2022 18:36:42 +0200 Subject: [PATCH 03/11] add scrollable dialog example to the playground --- packages/playground-react/package.json | 1 + .../pages/dialog/scrollable-dialog.tsx | 98 +++++++++++++++++++ yarn.lock | 5 + 3 files changed, 104 insertions(+) create mode 100644 packages/playground-react/pages/dialog/scrollable-dialog.tsx diff --git a/packages/playground-react/package.json b/packages/playground-react/package.json index bd8d99a1b..bcd4afebc 100644 --- a/packages/playground-react/package.json +++ b/packages/playground-react/package.json @@ -16,6 +16,7 @@ "dependencies": { "@headlessui/react": "*", "@headlessui/tailwindcss": "*", + "@heroicons/react": "^1.0.6", "@popperjs/core": "^2.6.0", "@tailwindcss/forms": "^0.5.2", "@tailwindcss/typography": "^0.5.2", diff --git a/packages/playground-react/pages/dialog/scrollable-dialog.tsx b/packages/playground-react/pages/dialog/scrollable-dialog.tsx new file mode 100644 index 000000000..aa382e673 --- /dev/null +++ b/packages/playground-react/pages/dialog/scrollable-dialog.tsx @@ -0,0 +1,98 @@ +import { Fragment, useRef, useState } from 'react' +import { Dialog, Transition } from '@headlessui/react' +import { ExclamationIcon } from '@heroicons/react/outline' + +export default function Example() { + const [open, setOpen] = useState(false) + + const cancelButtonRef = useRef(null) + + return ( + <> +
+ +
+ + + +
+ + +
+
+ + +
+
+
+
+ + Deactivate account + +
+

+ Are you sure you want to deactivate your account? All of your data will be + permanently removed from our servers forever. This action cannot be + undone. +

+ {Array(20) + .fill(null) + .map((_, i) => ( +

+ Are you sure you want to deactivate your account? All of your data + will be permanently removed from our servers forever. This action + cannot be undone. +

+ ))} +
+
+
+
+ + +
+
+
+
+
+
+
+ + ) +} diff --git a/yarn.lock b/yarn.lock index 7aa92138f..5bf11e631 100644 --- a/yarn.lock +++ b/yarn.lock @@ -326,6 +326,11 @@ resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.4.tgz#19bf0f5af19149111c40d98bb0cf82119f5d9eeb" integrity sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw== +"@heroicons/react@^1.0.6": + version "1.0.6" + resolved "https://registry.yarnpkg.com/@heroicons/react/-/react-1.0.6.tgz#35dd26987228b39ef2316db3b1245c42eb19e324" + integrity sha512-JJCXydOFWMDpCP4q13iEplA503MQO3xLoZiKum+955ZCtHINWnx26CUxVxxFQu/uLb4LW3ge15ZpzIkXKkJ8oQ== + "@heroicons/vue@^1.0.6": version "1.0.6" resolved "https://registry.yarnpkg.com/@heroicons/vue/-/vue-1.0.6.tgz#d8b90734b436eb5a87f40cc300b64a0fb0031f7f" From 136c0281f77a439e103d2f7388c7754fccfb13ac Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Thu, 2 Jun 2022 18:37:34 +0200 Subject: [PATCH 04/11] simplify `outside click` behaviour MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Here is a little story. We used to use the `click` event listener on the window to try and detect whether we clicked outside of the main area we are working in. This all worked fine, until we got a bug report that it didn't work properly on Mobile, especially iOS. After a bit of debugging we switched this behaviour to use `pointerdown` instead of the `click` event listener. Worked great! Maybe... The reason the `click` didn't work was because of another bug fix. In React if you render a `
` and your `Dialog` contains a button without a type, (or an input where you press enter) then the form would submit... even though we portalled the `Dialog` to a different location, but it bubbled the event up via the SyntethicEvent System. To fix this, we've added a "simple" `onClick(e) { e.stopPropagation() }` to make sure that click events didn't leak out. Alright no worries, but, now that we switched to `pointerdown` we got another bug report that it didn't work on older iOS devices. Fine, let's add a `mousedown` next to the `pointerdown` event. Now this works all great! Maybe... This doesn't work quite as we expected because it could happen that both events fire and then the `onClose` of the Dialog component would fire twice. In fact, there is an open issue about this: #1490 at the time of writing this commit message. We tried to only call the close function once by checking if those events happen within the same "tick", which is not always the case... Alright, let's ignore that issue for a second, there is another issue that popped up... If you have a Dialog that is scrollable (because it is greater than the current viewport) then a wild scrollbar appears (what a weird Pokémon). The moment you try to click the scrollbar or drag it the Dialog closes. What in the world...? Well... turns out that `pointerdown` gets fired if you happen to "click" (or touch) on the scrollbar. A click event does not get fired. No worries we can fix this! Maybe... (Narrator: ... nope ...) One thing we can try is to measure the scrollbar width, and if you happen to click near the edge then we ignore this click. You can think of it like `let safeArea = viewportWidth - scrollBarWidth`. Everything works great now! Maybe... Well, let me tell you about macOS and "floating" scrollbars... you can't measure those... AAAAAAAARGHHHH Alright, scratch that, let's add an invisible 20px gap all around the viewport without measuring as a safe area. Nobody will click in the 20px gap, right, right?! Everything works great now! Maybe... Mobile devices, yep, Dialogs are used there as well and usually there is not a lot of room around those Dialogs so you almost always hit the "safe area". Should we now try and detect the device people are using...? /me takes a deep breath... Inhales... Exhales... Alright, time to start thinking again... The outside click with a "simple" click worked on Menu and Listbox not on the Dialog so this should be enough right? WAIT A MINUTE Remember this piece of code from earlier: ```js onClick(event) { event.stopPropagation() } ``` The click event never ever reaches the `window` so we can't detect the click outside... Let's move that code to the `Dialog.Panel` instead of on the `Dialog` itself, this will make sure that we stop the click event from leaking if you happen to nest a Dialog in a form and have a submitable button/input in the `Dialog.Panel`. But if you click outside of the `Dialog.Panel` the "click" event will bubble to the `window` so that we can detect a click and check whether it was outside or not. Time to start cleaning: - ☑️ Remove all the scrollbar measuring code... - Closing works on mobile now, no more safe area hack - ☑️ Remove the pointerdown & mousedown event - Outside click doesn't fire twice anymore - ☑️ Use a "simple" click event listener - We can click the scrollbar and the browser ignores it for us All issues have been fixed! (Until the next one of course...) --- .../src/hooks/use-outside-click.ts | 57 +++++++------------ .../src/hooks/use-outside-click.ts | 41 +------------ 2 files changed, 22 insertions(+), 76 deletions(-) diff --git a/packages/@headlessui-react/src/hooks/use-outside-click.ts b/packages/@headlessui-react/src/hooks/use-outside-click.ts index 8cef785e0..4866b94ec 100644 --- a/packages/@headlessui-react/src/hooks/use-outside-click.ts +++ b/packages/@headlessui-react/src/hooks/use-outside-click.ts @@ -1,29 +1,32 @@ -import { MutableRefObject, useRef } from 'react' -import { microTask } from '../utils/micro-task' -import { useEvent } from './use-event' +import { MutableRefObject, useEffect, useRef } from 'react' import { useWindowEvent } from './use-window-event' type Container = MutableRefObject | HTMLElement | null type ContainerCollection = Container[] | Set type ContainerInput = Container | ContainerCollection -export enum Features { - None = 1 << 0, - IgnoreScrollbars = 1 << 1, -} - export function useOutsideClick( containers: ContainerInput | (() => ContainerInput), cb: (event: MouseEvent | PointerEvent, target: HTMLElement) => void, - features: Features = Features.None + enabled: boolean = true ) { - let called = useRef(false) - let handler = useEvent((event: MouseEvent | PointerEvent) => { - if (called.current) return - called.current = true - microTask(() => { - called.current = false - }) + // TODO: remove this once the React bug has been fixed: https://github.com/facebook/react/issues/24657 + let enabledRef = useRef(false) + useEffect( + process.env.NODE_ENV === 'test' + ? () => { + enabledRef.current = enabled + } + : () => { + requestAnimationFrame(() => { + enabledRef.current = enabled + }) + }, + [enabled] + ) + + useWindowEvent('click', (event) => { + if (!enabledRef.current) return let _containers = (function resolve(containers): ContainerCollection { if (typeof containers === 'function') { @@ -46,25 +49,6 @@ export function useOutsideClick( // Ignore if the target doesn't exist in the DOM anymore if (!target.ownerDocument.documentElement.contains(target)) return - // Ignore scrollbars: - // This is a bit hacky, and is only necessary because we are checking for `pointerdown` and - // `mousedown` events. They _are_ being called if you click on a scrollbar. The `click` event - // is not called when clicking on a scrollbar, but we can't use that otherwise it won't work - // on mobile devices where only pointer events are being used. - if ((features & Features.IgnoreScrollbars) === Features.IgnoreScrollbars) { - // TODO: We can calculate this dynamically~is. On macOS if you have the "Automatically based - // on mouse or trackpad" setting enabled, then the scrollbar will float on top and therefore - // you can't calculate its with by checking the clientWidth and scrollWidth of the element. - // Therefore we are currently hardcoding this to be 20px. - let scrollbarWidth = 20 - - let viewport = target.ownerDocument.documentElement - if (event.clientX > viewport.clientWidth - scrollbarWidth) return - if (event.clientX < scrollbarWidth) return - if (event.clientY > viewport.clientHeight - scrollbarWidth) return - if (event.clientY < scrollbarWidth) return - } - // Ignore if the target exists in one of the containers for (let container of _containers) { if (container === null) continue @@ -76,7 +60,4 @@ export function useOutsideClick( return cb(event, target) }) - - useWindowEvent('pointerdown', handler) - useWindowEvent('mousedown', handler) } diff --git a/packages/@headlessui-vue/src/hooks/use-outside-click.ts b/packages/@headlessui-vue/src/hooks/use-outside-click.ts index fc2891b7a..726c08e5e 100644 --- a/packages/@headlessui-vue/src/hooks/use-outside-click.ts +++ b/packages/@headlessui-vue/src/hooks/use-outside-click.ts @@ -7,24 +7,11 @@ type Container = Ref | HTMLElement | null type ContainerCollection = Container[] | Set type ContainerInput = Container | ContainerCollection -export enum Features { - None = 1 << 0, - IgnoreScrollbars = 1 << 1, -} - export function useOutsideClick( containers: ContainerInput | (() => ContainerInput), - cb: (event: MouseEvent | PointerEvent, target: HTMLElement) => void, - features: Features = Features.None + cb: (event: MouseEvent | PointerEvent, target: HTMLElement) => void ) { - let called = false - function handle(event: MouseEvent | PointerEvent) { - if (called) return - called = true - microTask(() => { - called = false - }) - + useWindowEvent('click', (event) => { let target = event.target as HTMLElement // Ignore if the target doesn't exist in the DOM anymore @@ -46,25 +33,6 @@ export function useOutsideClick( return [containers] })(containers) - // Ignore scrollbars: - // This is a bit hacky, and is only necessary because we are checking for `pointerdown` and - // `mousedown` events. They _are_ being called if you click on a scrollbar. The `click` event - // is not called when clicking on a scrollbar, but we can't use that otherwise it won't work - // on mobile devices where only pointer events are being used. - if ((features & Features.IgnoreScrollbars) === Features.IgnoreScrollbars) { - // TODO: We can calculate this dynamically~is. On macOS if you have the "Automatically based - // on mouse or trackpad" setting enabled, then the scrollbar will float on top and therefore - // you can't calculate its with by checking the clientWidth and scrollWidth of the element. - // Therefore we are currently hardcoding this to be 20px. - let scrollbarWidth = 20 - - let viewport = target.ownerDocument.documentElement - if (event.clientX > viewport.clientWidth - scrollbarWidth) return - if (event.clientX < scrollbarWidth) return - if (event.clientY > viewport.clientHeight - scrollbarWidth) return - if (event.clientY < scrollbarWidth) return - } - // Ignore if the target exists in one of the containers for (let container of _containers) { if (container === null) continue @@ -75,8 +43,5 @@ export function useOutsideClick( } cb(event, target) - } - - useWindowEvent('pointerdown', handle) - useWindowEvent('mousedown', handle) + }) } From 6e787a853d291092bf653e12f8c5d70c2edf045b Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Thu, 2 Jun 2022 19:05:20 +0200 Subject: [PATCH 05/11] ensure a `Dialog.Panel` exists --- .../src/components/dialog/dialog.test.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/@headlessui-react/src/components/dialog/dialog.test.tsx b/packages/@headlessui-react/src/components/dialog/dialog.test.tsx index c59e3041c..21971b840 100644 --- a/packages/@headlessui-react/src/components/dialog/dialog.test.tsx +++ b/packages/@headlessui-react/src/components/dialog/dialog.test.tsx @@ -889,9 +889,11 @@ describe('Mouse interactions', () => { return (
- Contents - - + + Contents + + +
) From e9e2b5ba9ed528a9887844c72301883f2803ab31 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Thu, 2 Jun 2022 19:05:55 +0200 Subject: [PATCH 06/11] cleanup unnecessary code --- .../src/components/dialog/dialog.tsx | 22 +++++++++---------- .../src/components/focus-trap/focus-trap.tsx | 5 ++++- .../src/components/menu/menu.tsx | 1 - .../src/components/dialog/dialog.ts | 15 ++++++------- .../src/components/menu/menu.ts | 1 - 5 files changed, 21 insertions(+), 23 deletions(-) diff --git a/packages/@headlessui-react/src/components/dialog/dialog.tsx b/packages/@headlessui-react/src/components/dialog/dialog.tsx index 8d4b42cec..859fda748 100644 --- a/packages/@headlessui-react/src/components/dialog/dialog.tsx +++ b/packages/@headlessui-react/src/components/dialog/dialog.tsx @@ -32,7 +32,7 @@ import { Description, useDescriptions } from '../description/description' import { useOpenClosed, State } from '../../internal/open-closed' import { useServerHandoffComplete } from '../../hooks/use-server-handoff-complete' import { StackProvider, StackMessage } from '../../internal/stack-context' -import { useOutsideClick, Features as OutsideClickFeatures } from '../../hooks/use-outside-click' +import { useOutsideClick } from '../../hooks/use-outside-click' import { getOwnerDocument } from '../../utils/owner' import { useOwnerDocument } from '../../hooks/use-owner' import { useEventListener } from '../../hooks/use-event-listener' @@ -100,13 +100,7 @@ let DEFAULT_DIALOG_TAG = 'div' as const interface DialogRenderPropArg { open: boolean } -type DialogPropsWeControl = - | 'id' - | 'role' - | 'aria-modal' - | 'aria-describedby' - | 'aria-labelledby' - | 'onClick' +type DialogPropsWeControl = 'id' | 'role' | 'aria-modal' | 'aria-describedby' | 'aria-labelledby' let DialogRenderFeatures = Features.RenderStrategy | Features.Static @@ -224,7 +218,7 @@ let DialogRoot = forwardRefWithAs(function Dialog< close() }, - OutsideClickFeatures.IgnoreScrollbars + enabled ) // Handle `Escape` to close @@ -311,9 +305,6 @@ let DialogRoot = forwardRefWithAs(function Dialog< 'aria-modal': dialogState === DialogStates.Open ? true : undefined, 'aria-labelledby': state.titleId, 'aria-describedby': describedby, - onClick(event: ReactMouseEvent) { - event.stopPropagation() - }, } return ( @@ -492,10 +483,17 @@ let Panel = forwardRefWithAs(function Panel elements even if we portalled the Dialog. + let handleClick = useEvent((event: ReactMouseEvent) => { + event.stopPropagation() + }) + let theirProps = props let ourProps = { ref: panelRef, id, + onClick: handleClick, } return render({ diff --git a/packages/@headlessui-react/src/components/focus-trap/focus-trap.tsx b/packages/@headlessui-react/src/components/focus-trap/focus-trap.tsx index 0cdf662b6..bbab04b21 100644 --- a/packages/@headlessui-react/src/components/focus-trap/focus-trap.tsx +++ b/packages/@headlessui-react/src/components/focus-trap/focus-trap.tsx @@ -147,7 +147,10 @@ function useRestoreFocus({ ownerDocument }: { ownerDocument: Document | null }, useWatch(() => { if (enabled) return - focusElement(restoreElement.current) + if (ownerDocument?.activeElement === ownerDocument?.body) { + focusElement(restoreElement.current) + } + restoreElement.current = null }, [enabled]) diff --git a/packages/@headlessui-react/src/components/menu/menu.tsx b/packages/@headlessui-react/src/components/menu/menu.tsx index 1dae520d4..a46609b2e 100644 --- a/packages/@headlessui-react/src/components/menu/menu.tsx +++ b/packages/@headlessui-react/src/components/menu/menu.tsx @@ -344,7 +344,6 @@ let Button = forwardRefWithAs(function Button state.buttonRef.current?.focus({ preventScroll: true })) } else { event.preventDefault() - event.stopPropagation() dispatch({ type: ActionTypes.OpenMenu }) } }) diff --git a/packages/@headlessui-vue/src/components/dialog/dialog.ts b/packages/@headlessui-vue/src/components/dialog/dialog.ts index 0845d073f..0cf90f18d 100644 --- a/packages/@headlessui-vue/src/components/dialog/dialog.ts +++ b/packages/@headlessui-vue/src/components/dialog/dialog.ts @@ -29,7 +29,7 @@ import { ForcePortalRoot } from '../../internal/portal-force-root' import { Description, useDescriptions } from '../description/description' import { dom } from '../../utils/dom' import { useOpenClosed, State } from '../../internal/open-closed' -import { useOutsideClick, Features as OutsideClickFeatures } from '../../hooks/use-outside-click' +import { useOutsideClick } from '../../hooks/use-outside-click' import { getOwnerDocument } from '../../utils/owner' import { useEventListener } from '../../hooks/use-event-listener' import { Hidden, Features as HiddenFeatures } from '../../internal/hidden' @@ -196,8 +196,7 @@ export let Dialog = defineComponent({ api.close() nextTick(() => target?.focus()) - }, - OutsideClickFeatures.IgnoreScrollbars + } ) // Handle `Escape` to close @@ -264,10 +263,6 @@ export let Dialog = defineComponent({ onInvalidate(() => observer.disconnect()) }) - function handleClick(event: MouseEvent) { - event.stopPropagation() - } - return () => { let ourProps = { // Manually passthrough the attributes, because Vue can't automatically pass @@ -279,7 +274,6 @@ export let Dialog = defineComponent({ 'aria-modal': dialogState.value === DialogStates.Open ? true : undefined, 'aria-labelledby': titleId.value, 'aria-describedby': describedby.value, - onClick: handleClick, } let { open: _, initialFocus, ...incomingProps } = props @@ -417,10 +411,15 @@ export let DialogPanel = defineComponent({ expose({ el: api.panelRef, $el: api.panelRef }) + function handleClick(event: MouseEvent) { + event.stopPropagation() + } + return () => { let ourProps = { id, ref: api.panelRef, + onClick: handleClick, } let incomingProps = props diff --git a/packages/@headlessui-vue/src/components/menu/menu.ts b/packages/@headlessui-vue/src/components/menu/menu.ts index e34a83146..5e29c4c18 100644 --- a/packages/@headlessui-vue/src/components/menu/menu.ts +++ b/packages/@headlessui-vue/src/components/menu/menu.ts @@ -289,7 +289,6 @@ export let MenuButton = defineComponent({ nextTick(() => dom(api.buttonRef)?.focus({ preventScroll: true })) } else { event.preventDefault() - event.stopPropagation() api.openMenu() nextFrame(() => dom(api.itemsRef)?.focus({ preventScroll: true })) } From adbac391204eb32ed1341e4fed0ab78ba2472b49 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Thu, 2 Jun 2022 19:32:02 +0200 Subject: [PATCH 07/11] use capture phase for outside click behaviour --- .../src/hooks/use-outside-click.ts | 60 +++++++++------- .../src/hooks/use-outside-click.ts | 70 +++++++++++-------- 2 files changed, 73 insertions(+), 57 deletions(-) diff --git a/packages/@headlessui-react/src/hooks/use-outside-click.ts b/packages/@headlessui-react/src/hooks/use-outside-click.ts index 4866b94ec..1daa672e0 100644 --- a/packages/@headlessui-react/src/hooks/use-outside-click.ts +++ b/packages/@headlessui-react/src/hooks/use-outside-click.ts @@ -25,39 +25,47 @@ export function useOutsideClick( [enabled] ) - useWindowEvent('click', (event) => { - if (!enabledRef.current) return + useWindowEvent( + 'click', + (event) => { + if (!enabledRef.current) return - let _containers = (function resolve(containers): ContainerCollection { - if (typeof containers === 'function') { - return resolve(containers()) - } + let _containers = (function resolve(containers): ContainerCollection { + if (typeof containers === 'function') { + return resolve(containers()) + } - if (Array.isArray(containers)) { - return containers - } + if (Array.isArray(containers)) { + return containers + } - if (containers instanceof Set) { - return containers - } + if (containers instanceof Set) { + return containers + } - return [containers] - })(containers) + return [containers] + })(containers) - let target = event.target as HTMLElement + let target = event.target as HTMLElement - // Ignore if the target doesn't exist in the DOM anymore - if (!target.ownerDocument.documentElement.contains(target)) return + // Ignore if the target doesn't exist in the DOM anymore + if (!target.ownerDocument.documentElement.contains(target)) return - // Ignore if the target exists in one of the containers - for (let container of _containers) { - if (container === null) continue - let domNode = container instanceof HTMLElement ? container : container.current - if (domNode?.contains(target)) { - return + // Ignore if the target exists in one of the containers + for (let container of _containers) { + if (container === null) continue + let domNode = container instanceof HTMLElement ? container : container.current + if (domNode?.contains(target)) { + return + } } - } - return cb(event, target) - }) + return cb(event, target) + }, + // We will use the `capture` phase so that layers in between with `event.stopPropagation()` + // don't "cancel" this outside click check. E.g.: A `Menu` inside a `DialogPanel` if the `Menu` + // is open, and you click outside of it in the `DialogPanel` the `Menu` should close. However, + // the `DialogPanel` has a `onClick(e) { e.stopPropagation() }` which would cancel this. + true + ) } diff --git a/packages/@headlessui-vue/src/hooks/use-outside-click.ts b/packages/@headlessui-vue/src/hooks/use-outside-click.ts index 726c08e5e..b49fc6a05 100644 --- a/packages/@headlessui-vue/src/hooks/use-outside-click.ts +++ b/packages/@headlessui-vue/src/hooks/use-outside-click.ts @@ -11,37 +11,45 @@ export function useOutsideClick( containers: ContainerInput | (() => ContainerInput), cb: (event: MouseEvent | PointerEvent, target: HTMLElement) => void ) { - useWindowEvent('click', (event) => { - let target = event.target as HTMLElement - - // Ignore if the target doesn't exist in the DOM anymore - if (!target.ownerDocument.documentElement.contains(target)) return - - let _containers = (function resolve(containers): ContainerCollection { - if (typeof containers === 'function') { - return resolve(containers()) - } - - if (Array.isArray(containers)) { - return containers - } - - if (containers instanceof Set) { - return containers - } - - return [containers] - })(containers) - - // Ignore if the target exists in one of the containers - for (let container of _containers) { - if (container === null) continue - let domNode = container instanceof HTMLElement ? container : dom(container) - if (domNode?.contains(target)) { - return + useWindowEvent( + 'click', + (event) => { + let target = event.target as HTMLElement + + // Ignore if the target doesn't exist in the DOM anymore + if (!target.ownerDocument.documentElement.contains(target)) return + + let _containers = (function resolve(containers): ContainerCollection { + if (typeof containers === 'function') { + return resolve(containers()) + } + + if (Array.isArray(containers)) { + return containers + } + + if (containers instanceof Set) { + return containers + } + + return [containers] + })(containers) + + // Ignore if the target exists in one of the containers + for (let container of _containers) { + if (container === null) continue + let domNode = container instanceof HTMLElement ? container : dom(container) + if (domNode?.contains(target)) { + return + } } - } - cb(event, target) - }) + cb(event, target) + }, + // We will use the `capture` phase so that layers in between with `event.stopPropagation()` + // don't "cancel" this outside click check. E.g.: A `Menu` inside a `DialogPanel` if the `Menu` + // is open, and you click outside of it in the `DialogPanel` the `Menu` should close. However, + // the `DialogPanel` has a `onClick(e) { e.stopPropagation() }` which would cancel this. + true + ) } From 70764608d98ff0bd7b6aff8d47eae3ae737e42cc Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 3 Jun 2022 16:06:21 +0200 Subject: [PATCH 08/11] further improve outside click We added event.preventDefault() & event.defaultPrevented checks to make sure that we only handle 1 layer at a time. E.g.: ```js Button ... ``` If you open the Dialog, then open the Menu, pressing `Escape` will close the Menu but not the Dialog, pressing `Escape` again will close the Dialog. Now this is also applied to the outside click behaviour. If you open the Dialog, then open the Menu, clicking outside will close the Menu but not the Dialog, outside again will close the Dialog. --- .../src/hooks/use-outside-click.ts | 22 ++++++++++++++ .../src/hooks/use-outside-click.ts | 30 +++++++++++++++++-- 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/packages/@headlessui-react/src/hooks/use-outside-click.ts b/packages/@headlessui-react/src/hooks/use-outside-click.ts index 1daa672e0..3b48fc40f 100644 --- a/packages/@headlessui-react/src/hooks/use-outside-click.ts +++ b/packages/@headlessui-react/src/hooks/use-outside-click.ts @@ -1,4 +1,5 @@ import { MutableRefObject, useEffect, useRef } from 'react' +import { FocusableMode, isFocusableElement } from '../utils/focus-management' import { useWindowEvent } from './use-window-event' type Container = MutableRefObject | HTMLElement | null @@ -30,6 +31,11 @@ export function useOutsideClick( (event) => { if (!enabledRef.current) return + // Check whether the event got prevented already. This can happen if you use the + // useOutsideClick hook in both a Dialog and a Menu and the inner Menu "cancels" the default + // behaviour so that only the Menu closes and not the Dialog (yet) + if (event.defaultPrevented) return + let _containers = (function resolve(containers): ContainerCollection { if (typeof containers === 'function') { return resolve(containers()) @@ -60,6 +66,22 @@ export function useOutsideClick( } } + // This allows us to check whether the event was defaultPrevented when you are nesting this + // inside a `` for example. + if ( + // This check alllows us to know whether or not we clicked on a "focusable" element like a + // button or an input. This is a backwards compatibility check so that you can open a and click on another which should close Menu A and open Menu B. We might + // revisit that so that you will require 2 clicks instead. + !isFocusableElement(target, FocusableMode.Loose) && + // This could be improved, but the `Combobox.Button` adds tabIndex={-1} to make it + // unfocusable via the keyboard so that tabbing to the next item from the input doesn't + // first go to the button. + target.tabIndex !== -1 + ) { + event.preventDefault() + } + return cb(event, target) }, // We will use the `capture` phase so that layers in between with `event.stopPropagation()` diff --git a/packages/@headlessui-vue/src/hooks/use-outside-click.ts b/packages/@headlessui-vue/src/hooks/use-outside-click.ts index b49fc6a05..562e81d69 100644 --- a/packages/@headlessui-vue/src/hooks/use-outside-click.ts +++ b/packages/@headlessui-vue/src/hooks/use-outside-click.ts @@ -1,7 +1,7 @@ import { useWindowEvent } from './use-window-event' -import { Ref } from 'vue' +import { computed, Ref, ComputedRef } from 'vue' +import { FocusableMode, isFocusableElement } from '../utils/focus-management' import { dom } from '../utils/dom' -import { microTask } from '../utils/micro-task' type Container = Ref | HTMLElement | null type ContainerCollection = Container[] | Set @@ -9,11 +9,19 @@ type ContainerInput = Container | ContainerCollection export function useOutsideClick( containers: ContainerInput | (() => ContainerInput), - cb: (event: MouseEvent | PointerEvent, target: HTMLElement) => void + cb: (event: MouseEvent | PointerEvent, target: HTMLElement) => void, + enabled: ComputedRef = computed(() => true) ) { useWindowEvent( 'click', (event) => { + if (!enabled.value) return + + // Check whether the event got prevented already. This can happen if you use the + // useOutsideClick hook in both a Dialog and a Menu and the inner Menu "cancels" the default + // behaviour so that only the Menu closes and not the Dialog (yet) + if (event.defaultPrevented) return + let target = event.target as HTMLElement // Ignore if the target doesn't exist in the DOM anymore @@ -44,6 +52,22 @@ export function useOutsideClick( } } + // This allows us to check whether the event was defaultPrevented when you are nesting this + // inside a `` for example. + if ( + // This check alllows us to know whether or not we clicked on a "focusable" element like a + // button or an input. This is a backwards compatibility check so that you can open a and click on another which should close Menu A and open Menu B. We might + // revisit that so that you will require 2 clicks instead. + !isFocusableElement(target, FocusableMode.Loose) && + // This could be improved, but the `Combobox.Button` adds tabIndex={-1} to make it + // unfocusable via the keyboard so that tabbing to the next item from the input doesn't + // first go to the button. + target.tabIndex !== -1 + ) { + event.preventDefault() + } + cb(event, target) }, // We will use the `capture` phase so that layers in between with `event.stopPropagation()` From 068e06050775d97ccc484ede8824de4ac122d5e1 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 3 Jun 2022 16:12:08 +0200 Subject: [PATCH 09/11] add explicit `enabled` value to the `useOutsideClick` hook --- .../src/components/combobox/combobox.tsx | 12 +++++------ .../src/components/dialog/dialog.tsx | 9 ++------- .../src/components/listbox/listbox.tsx | 20 ++++++++++--------- .../src/components/menu/menu.tsx | 20 ++++++++++--------- .../src/components/popover/popover.tsx | 20 ++++++++++--------- .../src/components/combobox/combobox.ts | 9 +++++---- .../src/components/dialog/dialog.ts | 6 ++---- .../src/components/listbox/listbox.ts | 20 ++++++++++--------- .../src/components/menu/menu.ts | 20 ++++++++++--------- .../src/components/popover/popover.ts | 20 ++++++++++--------- 10 files changed, 81 insertions(+), 75 deletions(-) diff --git a/packages/@headlessui-react/src/components/combobox/combobox.tsx b/packages/@headlessui-react/src/components/combobox/combobox.tsx index 1e68c36cb..e00d55fdd 100644 --- a/packages/@headlessui-react/src/components/combobox/combobox.tsx +++ b/packages/@headlessui-react/src/components/combobox/combobox.tsx @@ -34,7 +34,7 @@ import { forwardRefWithAs, render, compact, PropsForFeatures, Features } from '. import { isDisabledReactIssue7711 } from '../../utils/bugs' import { match } from '../../utils/match' import { objectToFormEntries } from '../../utils/form' -import { sortByDomNode } from '../../utils/focus-management' +import { FocusableMode, isFocusableElement, sortByDomNode } from '../../utils/focus-management' import { Hidden, Features as HiddenFeatures } from '../../internal/hidden' import { useOpenClosed, State, OpenClosedProvider } from '../../internal/open-closed' @@ -415,11 +415,11 @@ let ComboboxRoot = forwardRefWithAs(function Combobox< }, [data]) // Handle outside click - useOutsideClick([data.buttonRef, data.inputRef, data.optionsRef], () => { - if (data.comboboxState !== ComboboxState.Open) return - - dispatch({ type: ActionTypes.CloseCombobox }) - }) + useOutsideClick( + [data.buttonRef, data.inputRef, data.optionsRef], + () => dispatch({ type: ActionTypes.CloseCombobox }), + data.comboboxState === ComboboxState.Open + ) let slot = useMemo>( () => ({ diff --git a/packages/@headlessui-react/src/components/dialog/dialog.tsx b/packages/@headlessui-react/src/components/dialog/dialog.tsx index 859fda748..4f3d0ed6f 100644 --- a/packages/@headlessui-react/src/components/dialog/dialog.tsx +++ b/packages/@headlessui-react/src/components/dialog/dialog.tsx @@ -212,13 +212,8 @@ let DialogRoot = forwardRefWithAs(function Dialog< state.panelRef.current ?? internalDialogRef.current, ] as HTMLElement[] }, - () => { - if (dialogState !== DialogStates.Open) return - if (hasNestedDialogs) return - - close() - }, - enabled + close, + enabled && !hasNestedDialogs ) // Handle `Escape` to close diff --git a/packages/@headlessui-react/src/components/listbox/listbox.tsx b/packages/@headlessui-react/src/components/listbox/listbox.tsx index b6643312d..1c1c6472f 100644 --- a/packages/@headlessui-react/src/components/listbox/listbox.tsx +++ b/packages/@headlessui-react/src/components/listbox/listbox.tsx @@ -396,16 +396,18 @@ let ListboxRoot = forwardRefWithAs(function Listbox< ) // Handle outside click - useOutsideClick([buttonRef, optionsRef], (event, target) => { - if (listboxState !== ListboxStates.Open) return - - dispatch({ type: ActionTypes.CloseListbox }) + useOutsideClick( + [buttonRef, optionsRef], + (event, target) => { + dispatch({ type: ActionTypes.CloseListbox }) - if (!isFocusableElement(target, FocusableMode.Loose)) { - event.preventDefault() - buttonRef.current?.focus() - } - }) + if (!isFocusableElement(target, FocusableMode.Loose)) { + event.preventDefault() + buttonRef.current?.focus() + } + }, + listboxState === ListboxStates.Open + ) let slot = useMemo( () => ({ open: listboxState === ListboxStates.Open, disabled }), diff --git a/packages/@headlessui-react/src/components/menu/menu.tsx b/packages/@headlessui-react/src/components/menu/menu.tsx index a46609b2e..2c7e3d1ac 100644 --- a/packages/@headlessui-react/src/components/menu/menu.tsx +++ b/packages/@headlessui-react/src/components/menu/menu.tsx @@ -239,16 +239,18 @@ let MenuRoot = forwardRefWithAs(function Menu { - if (menuState !== MenuStates.Open) return - - dispatch({ type: ActionTypes.CloseMenu }) + useOutsideClick( + [buttonRef, itemsRef], + (event, target) => { + dispatch({ type: ActionTypes.CloseMenu }) - if (!isFocusableElement(target, FocusableMode.Loose)) { - event.preventDefault() - buttonRef.current?.focus() - } - }) + if (!isFocusableElement(target, FocusableMode.Loose)) { + event.preventDefault() + buttonRef.current?.focus() + } + }, + menuState === MenuStates.Open + ) let slot = useMemo( () => ({ open: menuState === MenuStates.Open }), diff --git a/packages/@headlessui-react/src/components/popover/popover.tsx b/packages/@headlessui-react/src/components/popover/popover.tsx index a1fa990ee..9d9314fb1 100644 --- a/packages/@headlessui-react/src/components/popover/popover.tsx +++ b/packages/@headlessui-react/src/components/popover/popover.tsx @@ -258,16 +258,18 @@ let PopoverRoot = forwardRefWithAs(function Popover< ) // Handle outside click - useOutsideClick([button, panel], (event, target) => { - if (popoverState !== PopoverStates.Open) return - - dispatch({ type: ActionTypes.ClosePopover }) + useOutsideClick( + [button, panel], + (event, target) => { + dispatch({ type: ActionTypes.ClosePopover }) - if (!isFocusableElement(target, FocusableMode.Loose)) { - event.preventDefault() - button?.focus() - } - }) + if (!isFocusableElement(target, FocusableMode.Loose)) { + event.preventDefault() + button?.focus() + } + }, + popoverState === PopoverStates.Open + ) let close = useEvent((focusableElement?: HTMLElement | MutableRefObject) => { dispatch({ type: ActionTypes.ClosePopover }) diff --git a/packages/@headlessui-vue/src/components/combobox/combobox.ts b/packages/@headlessui-vue/src/components/combobox/combobox.ts index 76bb0c425..4bdb9c193 100644 --- a/packages/@headlessui-vue/src/components/combobox/combobox.ts +++ b/packages/@headlessui-vue/src/components/combobox/combobox.ts @@ -396,10 +396,11 @@ export let Combobox = defineComponent({ } // Handle outside click - useOutsideClick([inputRef, buttonRef, optionsRef], () => { - if (comboboxState.value !== ComboboxStates.Open) return - api.closeCombobox() - }) + useOutsideClick( + [inputRef, buttonRef, optionsRef], + () => api.closeCombobox(), + computed(() => comboboxState.value === ComboboxStates.Open) + ) watch([api.value, api.inputRef], () => api.syncInputValue(), { immediate: true, diff --git a/packages/@headlessui-vue/src/components/dialog/dialog.ts b/packages/@headlessui-vue/src/components/dialog/dialog.ts index 0cf90f18d..217ea2f87 100644 --- a/packages/@headlessui-vue/src/components/dialog/dialog.ts +++ b/packages/@headlessui-vue/src/components/dialog/dialog.ts @@ -191,12 +191,10 @@ export let Dialog = defineComponent({ }, (_event, target) => { - if (dialogState.value !== DialogStates.Open) return - if (hasNestedDialogs.value) return - api.close() nextTick(() => target?.focus()) - } + }, + computed(() => dialogState.value === DialogStates.Open && !hasNestedDialogs.value) ) // Handle `Escape` to close diff --git a/packages/@headlessui-vue/src/components/listbox/listbox.ts b/packages/@headlessui-vue/src/components/listbox/listbox.ts index b3a123ab3..5bd9062a1 100644 --- a/packages/@headlessui-vue/src/components/listbox/listbox.ts +++ b/packages/@headlessui-vue/src/components/listbox/listbox.ts @@ -298,16 +298,18 @@ export let Listbox = defineComponent({ } // Handle outside click - useOutsideClick([buttonRef, optionsRef], (event, target) => { - if (listboxState.value !== ListboxStates.Open) return - - api.closeListbox() + useOutsideClick( + [buttonRef, optionsRef], + (event, target) => { + api.closeListbox() - if (!isFocusableElement(target, FocusableMode.Loose)) { - event.preventDefault() - dom(buttonRef)?.focus() - } - }) + if (!isFocusableElement(target, FocusableMode.Loose)) { + event.preventDefault() + dom(buttonRef)?.focus() + } + }, + computed(() => listboxState.value === ListboxStates.Open) + ) // @ts-expect-error Types of property 'dataRef' are incompatible. provide(ListboxContext, api) diff --git a/packages/@headlessui-vue/src/components/menu/menu.ts b/packages/@headlessui-vue/src/components/menu/menu.ts index 5e29c4c18..4a4a54322 100644 --- a/packages/@headlessui-vue/src/components/menu/menu.ts +++ b/packages/@headlessui-vue/src/components/menu/menu.ts @@ -201,16 +201,18 @@ export let Menu = defineComponent({ } // Handle outside click - useOutsideClick([buttonRef, itemsRef], (event, target) => { - if (menuState.value !== MenuStates.Open) return - - api.closeMenu() + useOutsideClick( + [buttonRef, itemsRef], + (event, target) => { + api.closeMenu() - if (!isFocusableElement(target, FocusableMode.Loose)) { - event.preventDefault() - dom(buttonRef)?.focus() - } - }) + if (!isFocusableElement(target, FocusableMode.Loose)) { + event.preventDefault() + dom(buttonRef)?.focus() + } + }, + computed(() => menuState.value === MenuStates.Open) + ) // @ts-expect-error Types of property 'dataRef' are incompatible. provide(MenuContext, api) diff --git a/packages/@headlessui-vue/src/components/popover/popover.ts b/packages/@headlessui-vue/src/components/popover/popover.ts index fc9a26f21..d79f64587 100644 --- a/packages/@headlessui-vue/src/components/popover/popover.ts +++ b/packages/@headlessui-vue/src/components/popover/popover.ts @@ -211,16 +211,18 @@ export let Popover = defineComponent({ ) // Handle outside click - useOutsideClick([button, panel], (event, target) => { - if (popoverState.value !== PopoverStates.Open) return - - api.closePopover() + useOutsideClick( + [button, panel], + (event, target) => { + api.closePopover() - if (!isFocusableElement(target, FocusableMode.Loose)) { - event.preventDefault() - dom(button)?.focus() - } - }) + if (!isFocusableElement(target, FocusableMode.Loose)) { + event.preventDefault() + dom(button)?.focus() + } + }, + computed(() => popoverState.value === PopoverStates.Open) + ) return () => { let slot = { open: popoverState.value === PopoverStates.Open, close: api.close } From 4697f5d1d046a6ee15a49ce95e9af669d8f300c9 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 3 Jun 2022 16:12:23 +0200 Subject: [PATCH 10/11] ensure outside click properly works with Poratl components Usually this works out of the box, however our Portal components will render inside the Dialog component "root" to ensure that it is inside the non-inert tree and is inside the Dialog visually. This means that the Portal is not in a separate container and technically outside of the `Dialog.Panel` which means that it will close when you click on a non-interactive item inside that Portal... This fixes that and allows all Portal components. --- .../src/components/dialog/dialog.tsx | 16 ++++++++-------- .../src/components/portal/portal.test.tsx | 2 +- .../src/components/portal/portal.tsx | 1 + .../src/components/dialog/dialog.ts | 2 +- .../src/components/portal/portal.test.ts | 2 +- .../src/components/portal/portal.ts | 1 + 6 files changed, 13 insertions(+), 11 deletions(-) diff --git a/packages/@headlessui-react/src/components/dialog/dialog.tsx b/packages/@headlessui-react/src/components/dialog/dialog.tsx index 4f3d0ed6f..351c28ad2 100644 --- a/packages/@headlessui-react/src/components/dialog/dialog.tsx +++ b/packages/@headlessui-react/src/components/dialog/dialog.tsx @@ -198,14 +198,14 @@ let DialogRoot = forwardRefWithAs(function Dialog< useOutsideClick( () => { // Third party roots - let rootContainers = Array.from(ownerDocument?.querySelectorAll('body > *') ?? []).filter( - (container) => { - if (!(container instanceof HTMLElement)) return false // Skip non-HTMLElements - if (container.contains(mainTreeNode.current)) return false // Skip if it is the main app - if (state.panelRef.current && container.contains(state.panelRef.current)) return false - return true // Keep - } - ) + let rootContainers = Array.from( + ownerDocument?.querySelectorAll('body > *, [data-headlessui-portal]') ?? [] + ).filter((container) => { + if (!(container instanceof HTMLElement)) return false // Skip non-HTMLElements + if (container.contains(mainTreeNode.current)) return false // Skip if it is the main app + if (state.panelRef.current && container.contains(state.panelRef.current)) return false + return true // Keep + }) return [ ...rootContainers, diff --git a/packages/@headlessui-react/src/components/portal/portal.test.tsx b/packages/@headlessui-react/src/components/portal/portal.test.tsx index 6b21ea7a5..d434dd608 100644 --- a/packages/@headlessui-react/src/components/portal/portal.test.tsx +++ b/packages/@headlessui-react/src/components/portal/portal.test.tsx @@ -299,6 +299,6 @@ it('should be possible to force the Portal into a specific element using Portal. render() expect(document.body.innerHTML).toMatchInlineSnapshot( - `"
B
I am in the portal root
"` + `"
B
I am in the portal root
"` ) }) diff --git a/packages/@headlessui-react/src/components/portal/portal.tsx b/packages/@headlessui-react/src/components/portal/portal.tsx index c6b55d817..86bdba171 100644 --- a/packages/@headlessui-react/src/components/portal/portal.tsx +++ b/packages/@headlessui-react/src/components/portal/portal.tsx @@ -95,6 +95,7 @@ let PortalRoot = forwardRefWithAs(function Portal< // Element already exists in target, always calling target.appendChild(element) will cause a // brief unmount/remount. if (!target.contains(element)) { + element.setAttribute('data-headlessui-portal', '') target.appendChild(element) } diff --git a/packages/@headlessui-vue/src/components/dialog/dialog.ts b/packages/@headlessui-vue/src/components/dialog/dialog.ts index 217ea2f87..dd1b24c7f 100644 --- a/packages/@headlessui-vue/src/components/dialog/dialog.ts +++ b/packages/@headlessui-vue/src/components/dialog/dialog.ts @@ -179,7 +179,7 @@ export let Dialog = defineComponent({ () => { // Third party roots let rootContainers = Array.from( - ownerDocument.value?.querySelectorAll('body > *') ?? [] + ownerDocument.value?.querySelectorAll('body > *, [data-headlessui-portal]') ?? [] ).filter((container) => { if (!(container instanceof HTMLElement)) return false // Skip non-HTMLElements if (container.contains(dom(mainTreeNode))) return false // Skip if it is the main app diff --git a/packages/@headlessui-vue/src/components/portal/portal.test.ts b/packages/@headlessui-vue/src/components/portal/portal.test.ts index ff8f2c942..0425e4588 100644 --- a/packages/@headlessui-vue/src/components/portal/portal.test.ts +++ b/packages/@headlessui-vue/src/components/portal/portal.test.ts @@ -389,6 +389,6 @@ it('should be possible to force the Portal into a specific element using PortalG await new Promise(nextTick) expect(document.body.innerHTML).toMatchInlineSnapshot( - `"
B
"` + `"
B
"` ) }) diff --git a/packages/@headlessui-vue/src/components/portal/portal.ts b/packages/@headlessui-vue/src/components/portal/portal.ts index 3ee164d0b..fc8e1aabe 100644 --- a/packages/@headlessui-vue/src/components/portal/portal.ts +++ b/packages/@headlessui-vue/src/components/portal/portal.ts @@ -79,6 +79,7 @@ export let Portal = defineComponent({ let ourProps = { ref: element, + 'data-headlessui-portal': '', } return h( From 9cc1ef29ba1665d8c139fc380f1f106c68def07d Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 3 Jun 2022 16:18:03 +0200 Subject: [PATCH 11/11] update changelog --- packages/@headlessui-react/CHANGELOG.md | 1 + packages/@headlessui-vue/CHANGELOG.md | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/@headlessui-react/CHANGELOG.md b/packages/@headlessui-react/CHANGELOG.md index e40f5d50a..970b37e4c 100644 --- a/packages/@headlessui-react/CHANGELOG.md +++ b/packages/@headlessui-react/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Fix incorrect transitionend/transitioncancel events for the Transition component ([#1537](https://github.com/tailwindlabs/headlessui/pull/1537)) +- Improve outside click of `Dialog` component ([#1546](https://github.com/tailwindlabs/headlessui/pull/1546)) ## [1.6.4] - 2022-05-29 diff --git a/packages/@headlessui-vue/CHANGELOG.md b/packages/@headlessui-vue/CHANGELOG.md index 4c6b3d963..3eb8a3064 100644 --- a/packages/@headlessui-vue/CHANGELOG.md +++ b/packages/@headlessui-vue/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Support `` children when using `as="template"` ([#1548](https://github.com/tailwindlabs/headlessui/pull/1548)) +- Improve outside click of `Dialog` component ([#1546](https://github.com/tailwindlabs/headlessui/pull/1546)) ## [1.6.4] - 2022-05-29