From d16fb151550191aae08366e7bfa28e86f5d5043a Mon Sep 17 00:00:00 2001 From: Andy Edwards Date: Mon, 28 Jan 2019 20:58:23 -0600 Subject: [PATCH] feat: keep open when mouse moves from hover trigger to popover/popper --- src/index.js | 44 ++++++++++++++++++++++++++++++++++++++++---- test/index.js | 38 +++++++++++++++++++++++++++++++++----- 2 files changed, 73 insertions(+), 9 deletions(-) diff --git a/src/index.js b/src/index.js index fbfdbca..642e8ae 100644 --- a/src/index.js +++ b/src/index.js @@ -8,6 +8,7 @@ export type Variant = 'popover' | 'popper' export type InjectedProps = { open: (eventOrAnchorEl: SyntheticEvent | HTMLElement) => void, close: () => void, + onMouseLeave: (event: SyntheticEvent) => void, toggle: (eventOrAnchorEl: SyntheticEvent | HTMLElement) => void, setOpen: ( open: boolean, @@ -80,7 +81,7 @@ export function bindToggle({ export function bindHover({ isOpen, open, - close, + onMouseLeave, popupId, variant, }: InjectedProps): { @@ -96,7 +97,7 @@ export function bindHover({ : null, 'aria-haspopup': true, onMouseEnter: open, - onMouseLeave: close, + onMouseLeave, } } @@ -110,18 +111,21 @@ export function bindPopover({ isOpen, anchorEl, close, + onMouseLeave, popupId, }: InjectedProps): { id: ?string, anchorEl: ?HTMLElement, open: boolean, onClose: () => void, + onMouseLeave: (event: SyntheticEvent) => void, } { return { id: popupId, anchorEl, open: isOpen, onClose: close, + onMouseLeave, } } @@ -143,15 +147,18 @@ export function bindPopper({ isOpen, anchorEl, popupId, + onMouseLeave, }: InjectedProps): { id: ?string, anchorEl: ?HTMLElement, open: boolean, + onMouseLeave: (event: SyntheticEvent) => void, } { return { id: popupId, anchorEl, open: isOpen, + onMouseLeave, } } @@ -163,12 +170,13 @@ export type Props = { type State = { anchorEl: ?HTMLElement, + hovered: boolean, } let eventOrAnchorElWarned: boolean = false export default class PopupState extends React.Component { - state: State = { anchorEl: null } + state: State = { anchorEl: null, hovered: false } static propTypes = { /** @@ -222,10 +230,28 @@ export default class PopupState extends React.Component { eventOrAnchorEl && eventOrAnchorEl.currentTarget ? (eventOrAnchorEl.currentTarget: any) : (eventOrAnchorEl: any), + hovered: (eventOrAnchorEl: any).type === 'mouseenter', }) } - handleClose = () => this.setState({ anchorEl: null }) + handleClose = () => this.setState({ anchorEl: null, hovered: false }) + + handleMouseLeave = (event: SyntheticEvent) => { + const { popupId } = this.props + const { hovered, anchorEl } = this.state + const popup = + popupId && typeof document !== 'undefined' + ? document.getElementById(popupId) // eslint-disable-line no-undef + : null + const { relatedTarget } = (event: any) + if ( + hovered && + !isAncestor(popup, relatedTarget) && + !isAncestor(anchorEl, relatedTarget) + ) { + this.handleClose() + } + } handleSetOpen = ( open: boolean, @@ -244,6 +270,7 @@ export default class PopupState extends React.Component { const result = children({ open: this.handleOpen, close: this.handleClose, + onMouseLeave: this.handleMouseLeave, toggle: this.handleToggle, setOpen: this.handleSetOpen, isOpen, @@ -255,3 +282,12 @@ export default class PopupState extends React.Component { return result } } + +function isAncestor(parent: ?Element, child: ?Element): boolean { + if (!parent) return false + while (child) { + if (child === parent) return true + child = child.parentElement + } + return false +} diff --git a/test/index.js b/test/index.js index 86432f7..77196bd 100644 --- a/test/index.js +++ b/test/index.js @@ -202,13 +202,16 @@ describe('', () => { let buttonRef let button let popover + let content const render = spy(popupState => ( - The popover content + + (content = c)}>The popover content + )) @@ -226,7 +229,10 @@ describe('', () => { assert.strictEqual(button.prop('aria-owns'), null) assert.strictEqual(button.prop('aria-haspopup'), true) assert.strictEqual(button.prop('onMouseEnter'), render.args[0][0].open) - assert.strictEqual(button.prop('onMouseLeave'), render.args[0][0].close) + assert.strictEqual( + button.prop('onMouseLeave'), + render.args[0][0].onMouseLeave + ) assert.strictEqual(popover.prop('id'), 'popover') assert.strictEqual(popover.prop('anchorEl'), null) assert.strictEqual(popover.prop('open'), false) @@ -240,13 +246,32 @@ describe('', () => { assert.strictEqual(button.prop('aria-owns'), 'popover') assert.strictEqual(button.prop('aria-haspopup'), true) assert.strictEqual(button.prop('onMouseEnter'), render.args[1][0].open) - assert.strictEqual(button.prop('onMouseLeave'), render.args[1][0].close) + assert.strictEqual( + button.prop('onMouseLeave'), + render.args[1][0].onMouseLeave + ) assert.strictEqual(popover.prop('id'), 'popover') assert.strictEqual(popover.prop('anchorEl'), buttonRef) assert.strictEqual(popover.prop('open'), true) assert.strictEqual(popover.prop('onClose'), render.args[1][0].close) - button.simulate('mouseleave') + button.simulate('mouseleave', { relatedTarget: content }) + wrapper.update() + button = wrapper.find(Button) + popover = wrapper.find(Popover) + assert.strictEqual(button.prop('aria-owns'), 'popover') + assert.strictEqual(button.prop('aria-haspopup'), true) + assert.strictEqual(button.prop('onMouseEnter'), render.args[1][0].open) + assert.strictEqual( + button.prop('onMouseLeave'), + render.args[1][0].onMouseLeave + ) + assert.strictEqual(popover.prop('id'), 'popover') + assert.strictEqual(popover.prop('anchorEl'), buttonRef) + assert.strictEqual(popover.prop('open'), true) + assert.strictEqual(popover.prop('onClose'), render.args[1][0].close) + + popover.simulate('mouseleave') wrapper.update() button = wrapper.find(Button) popover = wrapper.find(Popover) @@ -254,7 +279,10 @@ describe('', () => { assert.strictEqual(button.prop('aria-owns'), null) assert.strictEqual(button.prop('aria-haspopup'), true) assert.strictEqual(button.prop('onMouseEnter'), render.args[2][0].open) - assert.strictEqual(button.prop('onMouseLeave'), render.args[2][0].close) + assert.strictEqual( + button.prop('onMouseLeave'), + render.args[2][0].onMouseLeave + ) assert.strictEqual(popover.prop('id'), 'popover') assert.strictEqual(popover.prop('anchorEl'), null) assert.strictEqual(popover.prop('open'), false)