Skip to content

Commit

Permalink
Merge pull request #327 from 10up/fix/use-on-click-outside-hook-to-wo…
Browse files Browse the repository at this point in the history
…rk-across-iframed-editor-boundaries

Fix: ensure `useOnClickOutside` hook correctly detects clicks across iframed editor boundaries
  • Loading branch information
fabiankaegy authored Jun 7, 2024
2 parents 2083983 + 083d09d commit a6464a2
Showing 1 changed file with 41 additions and 15 deletions.
56 changes: 41 additions & 15 deletions hooks/use-on-click-outside.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,61 @@
import { useEffect, useRef } from '@wordpress/element';
import { useRefEffect } from '@wordpress/compose';

/**
* useOnClickOutside
*
* Note: This hook is only intended to be used in the WordPress backend/block editor.
*
* @param {Function} onClickOutside callback that will get invoked when the user clicks outside of the target
* @returns {object} ref to the target element
*/
export function useOnClickOutside(onClickOutside: (event: MouseEvent | TouchEvent) => void) {
const ref = useRef<HTMLElement>();
useEffect(
() => {
const ref = useRefEffect(
(element) => {
if (!element) {
return;
}
const listener = (event: MouseEvent | TouchEvent) => {
// Do nothing if clicking ref's element or descendent elements
if (!ref.current || ref.current.contains(event.target as HTMLElement)) {
if (!element || element.contains(event.target as HTMLElement)) {
return;
}
onClickOutside(event);
};
document.addEventListener('mousedown', listener);
document.addEventListener('touchstart', listener);

const ownerDocument = element.ownerDocument || document;
const isRenderedInsideIframe = ownerDocument !== document;

const editorCanvasIframe = document.querySelector<HTMLIFrameElement>('[name="editor-canvas"]');
const editorCanvasDocument = editorCanvasIframe?.contentDocument;

ownerDocument.addEventListener('mousedown', listener);
ownerDocument.addEventListener('touchstart', listener);

// If the element is rendered inside an iframe, we need to listen to events on the parent document
// as well to detect clicks outside the iframe.
if (isRenderedInsideIframe) {
document.addEventListener('mousedown', listener);
document.addEventListener('touchstart', listener);

// If the element is rendered outside the editor canvas iframe, we need to listen to events on the editor canvas
// document as well to detect clicks inside the editor canvas.
} else if (editorCanvasDocument) {
editorCanvasDocument.addEventListener('mousedown', listener);
editorCanvasDocument.addEventListener('touchstart', listener);
}
return () => {
document.removeEventListener('mousedown', listener);
document.removeEventListener('touchstart', listener);
ownerDocument.removeEventListener('mousedown', listener);
ownerDocument.removeEventListener('touchstart', listener);
if (isRenderedInsideIframe) {
document.removeEventListener('mousedown', listener);
document.removeEventListener('touchstart', listener);
} else if (editorCanvasDocument) {
editorCanvasDocument.removeEventListener('mousedown', listener);
editorCanvasDocument.removeEventListener('touchstart', listener);
}
};
},
// Add ref and handler to effect dependencies
// It's worth noting that because passed in handler is a new function on every
// render that will cause this effect callback/cleanup to run every render.
// It's not a big deal but to optimize you can wrap handler in useCallback before
// passing it into this hook.
[ref, onClickOutside],
[onClickOutside],
);

return ref;
Expand Down

0 comments on commit a6464a2

Please sign in to comment.