Unverified Commit 6c209ff3 authored by Kyle Keating's avatar Kyle Keating Committed by GitHub

Fix useElementShouldClose so it may work on preact components (#1683)

Fix useElementShouldClose so it may work on preact components
parent efa76f3a
...@@ -31,62 +31,99 @@ describe('hooks.useElementShouldClose', () => { ...@@ -31,62 +31,99 @@ describe('hooks.useElementShouldClose', () => {
isOpen: propTypes.bool, isOpen: propTypes.bool,
}; };
// Tests useElementShouldClose on a custom component directly
function FakeCompoundComponent({ isOpen = true }) {
function FakeCustomComponent() {
return (
<div>
<button>Hi</button>
</div>
);
}
const myRef = useRef();
useElementShouldClose(myRef, isOpen, handleClose);
return <FakeCustomComponent ref={myRef} />;
}
FakeCompoundComponent.propTypes = {
isOpen: propTypes.bool,
};
function createComponent(props) { function createComponent(props) {
return mount(<FakeComponent isOpen={true} {...props} />); return mount(<FakeComponent isOpen={true} {...props} />);
} }
function createCompoundComponent(props) {
return mount(<FakeCompoundComponent isOpen={true} {...props} />);
}
beforeEach(() => { beforeEach(() => {
handleClose = sinon.stub(); handleClose = sinon.stub();
}); });
events.forEach(event => { // Run each set of tests twice, once for a regular node and a second
it(`should invoke close callback once for events outside of element (${event.type})`, () => { // time for a custom preact component
const wrapper = createComponent(); [
{
act(() => { createWrapper: createComponent,
document.body.dispatchEvent(event); description: 'useElementShouldClose attached to a html node',
},
{
createWrapper: createCompoundComponent,
description: 'useElementShouldClose attached to a preact component',
},
].forEach(test => {
context(test.description, () => {
events.forEach(event => {
it(`should invoke close callback once for events outside of element (${event.type})`, () => {
const wrapper = test.createWrapper();
act(() => {
document.body.dispatchEvent(event);
});
wrapper.update();
assert.calledOnce(handleClose);
// Update the component to change it and re-execute the hook
wrapper.setProps({ isOpen: false });
act(() => {
document.body.dispatchEvent(event);
});
// Cleanup of hook should have removed eventListeners, so the callback
// is not called again
assert.calledOnce(handleClose);
});
}); });
wrapper.update();
assert.calledOnce(handleClose); events.forEach(event => {
it(`should not invoke close callback on events outside of element if element closed (${event.type})`, () => {
const wrapper = test.createWrapper({ isOpen: false });
// Update the component to change it and re-execute the hook act(() => {
wrapper.setProps({ isOpen: false }); document.body.dispatchEvent(event);
});
wrapper.update();
act(() => { assert.equal(handleClose.callCount, 0);
document.body.dispatchEvent(event); });
}); });
// Cleanup of hook should have removed eventListeners, so the callback events.forEach(event => {
// is not called again it(`should not invoke close callback on events inside of element (${event.type})`, () => {
assert.calledOnce(handleClose); const wrapper = test.createWrapper();
}); const button = wrapper.find('button');
});
events.forEach(event => { act(() => {
it(`should not invoke close callback on events outside of element if element closed (${event.type})`, () => { button.getDOMNode().dispatchEvent(event);
const wrapper = createComponent({ isOpen: false }); });
wrapper.update();
act(() => { assert.equal(handleClose.callCount, 0);
document.body.dispatchEvent(event); });
}); });
wrapper.update();
assert.equal(handleClose.callCount, 0);
});
});
events.forEach(event => {
it(`should not invoke close callback on events inside of element (${event.type})`, () => {
const wrapper = createComponent();
const button = wrapper.find('button');
act(() => {
button.getDOMNode().dispatchEvent(event);
});
wrapper.update();
assert.equal(handleClose.callCount, 0);
}); });
}); });
}); });
...@@ -2,6 +2,20 @@ import { useEffect } from 'preact/hooks'; ...@@ -2,6 +2,20 @@ import { useEffect } from 'preact/hooks';
import { listen } from '../../util/dom'; import { listen } from '../../util/dom';
/**
* @typedef Ref
* @prop current {Node} - HTML node
*
* A ref object attached to a HTML node.
*/
/**
* @typedef PreactRef
* @prop current {Object} - preact component object
*
* A ref object attached to a custom preact component.
*/
/** /**
* This hook adds appropriate `eventListener`s to the document when a target * This hook adds appropriate `eventListener`s to the document when a target
* element (`closeableEl`) is open. Events such as `click` and `focus` on * element (`closeableEl`) is open. Events such as `click` and `focus` on
...@@ -10,10 +24,13 @@ import { listen } from '../../util/dom'; ...@@ -10,10 +24,13 @@ import { listen } from '../../util/dom';
* to indicate that `closeableEl` should be closed. This hook also performs * to indicate that `closeableEl` should be closed. This hook also performs
* cleanup to remove `eventListener`s when appropriate. * cleanup to remove `eventListener`s when appropriate.
* *
* @param {Object} closeableEl - Preact ref object: * Limitation: This will not work when attached to a custom component that has
* Reference to a DOM element that should be * more than one element nested under a root <Fragment>
* closed when DOM elements external to it are *
* interacted with or `Esc` is pressed * @param {Ref|PreactRef} closeableEl - ref object:
* Reference to a DOM element or preat component
* that should be closed when DOM elements external
* to it are interacted with or `Esc` is pressed
* @param {bool} isOpen - Whether the element is currently open. This hook does * @param {bool} isOpen - Whether the element is currently open. This hook does
* not attach event listeners/do anything if it's not. * not attach event listeners/do anything if it's not.
* @param {() => void} handleClose - A function that will do the actual closing * @param {() => void} handleClose - A function that will do the actual closing
...@@ -24,6 +41,25 @@ export default function useElementShouldClose( ...@@ -24,6 +41,25 @@ export default function useElementShouldClose(
isOpen, isOpen,
handleClose handleClose
) { ) {
/**
* Helper to return the underlying node object whether
* `closeableEl` is attached to an HTMLNode or Preact component.
*
* @param {Preact ref} closeableEl
* @returns {Node}
*/
const getCurrentNode = closeableEl => {
// if base is present, assume its a preact component
const node = closeableEl.current.base
? closeableEl.current.base
: closeableEl.current;
if (typeof node !== 'object') {
throw new Error('useElementShouldClose can not find a node reference');
}
return node;
};
useEffect(() => { useEffect(() => {
if (!isOpen) { if (!isOpen) {
return () => {}; return () => {};
...@@ -46,7 +82,8 @@ export default function useElementShouldClose( ...@@ -46,7 +82,8 @@ export default function useElementShouldClose(
document.body, document.body,
'focus', 'focus',
event => { event => {
if (!closeableEl.current.contains(event.target)) { const current = getCurrentNode(closeableEl);
if (!current.contains(event.target)) {
handleClose(); handleClose();
} }
}, },
...@@ -59,7 +96,8 @@ export default function useElementShouldClose( ...@@ -59,7 +96,8 @@ export default function useElementShouldClose(
document.body, document.body,
['mousedown', 'click'], ['mousedown', 'click'],
event => { event => {
if (!closeableEl.current.contains(event.target)) { const current = getCurrentNode(closeableEl);
if (!current.contains(event.target)) {
handleClose(); handleClose();
} }
}, },
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment