Skip to content

Commit

Permalink
Detect “outside clicks” inside <iframe> elements
Browse files Browse the repository at this point in the history
  • Loading branch information
thecrypticace committed Jun 3, 2022
1 parent 979de55 commit 9e5a984
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 6 deletions.
38 changes: 38 additions & 0 deletions packages/@headlessui-react/src/components/menu/menu.test.tsx
Expand Up @@ -3070,6 +3070,44 @@ describe('Mouse interactions', () => {
})
)

// TODO: This test doesn't work — and it would be more suited for browser testing anyway
it.skip(
'should be possible to click outside of the menu into an iframe and which should close the menu',
suppressConsoleLogs(async () => {
render(
<div>
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">alice</Menu.Item>
<Menu.Item as="a">bob</Menu.Item>
<Menu.Item as="a">charlie</Menu.Item>
</Menu.Items>
</Menu>
<iframe
srcDoc={'<button>Trigger</button>'}
frameBorder="0"
width="300"
height="300"
></iframe>
</div>
)

// Open menu
await click(getMenuButton())
assertMenu({ state: MenuState.Visible })

// Click the input element in the iframe
await click(document.querySelector('iframe')?.contentDocument!.querySelector('button')!)

// Should be closed now
assertMenu({ state: MenuState.InvisibleUnmounted })

// Verify the button is focused again
assertActiveElement(getMenuButton())
})
)

it(
'should be possible to hover an item and make it active',
suppressConsoleLogs(async () => {
Expand Down
28 changes: 25 additions & 3 deletions packages/@headlessui-react/src/hooks/use-outside-click.ts
Expand Up @@ -8,7 +8,7 @@ type ContainerInput = Container | ContainerCollection

export function useOutsideClick(
containers: ContainerInput | (() => ContainerInput),
cb: (event: MouseEvent | PointerEvent, target: HTMLElement) => void,
cb: (event: MouseEvent | PointerEvent | FocusEvent, target: HTMLElement) => void,
enabled: boolean = true
) {
// TODO: remove this once the React bug has been fixed: https://github.com/facebook/react/issues/24657
Expand All @@ -26,9 +26,9 @@ export function useOutsideClick(
[enabled]
)

function handleOutsideClick<E extends MouseEvent | PointerEvent>(
function handleOutsideClick<E extends MouseEvent | PointerEvent | FocusEvent>(
event: E,
resolveTarget: (event: E) => HTMLElement
resolveTarget: (event: E) => HTMLElement | null
) {
if (!enabledRef.current) return

Expand All @@ -55,6 +55,10 @@ export function useOutsideClick(

let target = resolveTarget(event)

if (target === null) {
return
}

// Ignore if the target doesn't exist in the DOM anymore
if (!target.ownerDocument.documentElement.contains(target)) return

Expand Down Expand Up @@ -96,4 +100,22 @@ export function useOutsideClick(
// the `DialogPanel` has a `onClick(e) { e.stopPropagation() }` which would cancel this.
true
)

// When content inside an iframe is clicked `window` will receive a blur event
// This can happen when an iframe _inside_ a window is clicked
// Or, if headless UI is *in* the iframe, when a content in a window containing that iframe is clicked

// In this case we care only about the first case so we check to see if the active element is the iframe
// If so this was because of a click, focus, or other interaction with the child iframe
// and we can consider it an "outside click"
useWindowEvent(
'blur',
(event) =>
handleOutsideClick(event, () =>
window.document.activeElement instanceof HTMLIFrameElement
? window.document.activeElement
: null
),
true
)
}
33 changes: 33 additions & 0 deletions packages/@headlessui-vue/src/components/menu/menu.test.tsx
Expand Up @@ -3092,6 +3092,39 @@ describe('Mouse interactions', () => {
})
)

// TODO: This test doesn't work — and it would be more suited for browser testing anyway
it.skip(
'should be possible to click outside of the menu into an iframe and which should close the menu',
suppressConsoleLogs(async () => {
renderTemplate(`
<div>
<Menu>
<MenuButton>Trigger</MenuButton>
<MenuItems>
<menuitem as="a">alice</menuitem>
<menuitem as="a">bob</menuitem>
<menuitem as="a">charlie</menuitem>
</MenuItems>
</Menu>
<iframe :srcdoc="'<button>Trigger</button>'" frameborder="0" width="300" height="300"></iframe>
</div>
`)

// Open menu
await click(getMenuButton())
assertMenu({ state: MenuState.Visible })

// Click the input element in the iframe
await click(document.querySelector('iframe')?.contentDocument!.querySelector('button')!)

// Should be closed now
assertMenu({ state: MenuState.InvisibleUnmounted })

// Verify the button is focused again
assertActiveElement(getMenuButton())
})
)

it('should be possible to hover an item and make it active', async () => {
renderTemplate(jsx`
<Menu>
Expand Down
28 changes: 25 additions & 3 deletions packages/@headlessui-vue/src/hooks/use-outside-click.ts
Expand Up @@ -9,12 +9,12 @@ type ContainerInput = Container | ContainerCollection

export function useOutsideClick(
containers: ContainerInput | (() => ContainerInput),
cb: (event: MouseEvent | PointerEvent, target: HTMLElement) => void,
cb: (event: MouseEvent | PointerEvent | FocusEvent, target: HTMLElement) => void,
enabled: ComputedRef<boolean> = computed(() => true)
) {
function handleOutsideClick<E extends MouseEvent | PointerEvent>(
function handleOutsideClick<E extends MouseEvent | PointerEvent | FocusEvent>(
event: E,
resolveTarget: (event: E) => HTMLElement
resolveTarget: (event: E) => HTMLElement | null
) {
if (!enabled.value) return

Expand All @@ -25,6 +25,10 @@ export function useOutsideClick(

let target = resolveTarget(event)

if (target === null) {
return
}

// Ignore if the target doesn't exist in the DOM anymore
if (!target.ownerDocument.documentElement.contains(target)) return

Expand Down Expand Up @@ -82,4 +86,22 @@ export function useOutsideClick(
// the `DialogPanel` has a `onClick(e) { e.stopPropagation() }` which would cancel this.
true
)

// When content inside an iframe is clicked `window` will receive a blur event
// This can happen when an iframe _inside_ a window is clicked
// Or, if headless UI is *in* the iframe, when a content in a window containing that iframe is clicked

// In this case we care only about the first case so we check to see if the active element is the iframe
// If so this was because of a click, focus, or other interaction with the child iframe
// and we can consider it an "outside click"
useWindowEvent(
'blur',
(event) =>
handleOutsideClick(event, () =>
window.document.activeElement instanceof HTMLIFrameElement
? window.document.activeElement
: null
),
true
)
}

0 comments on commit 9e5a984

Please sign in to comment.