Skip to content

Commit

Permalink
feat: keep open when mouse moves from hover trigger to popover/popper
Browse files Browse the repository at this point in the history
  • Loading branch information
jedwards1211 committed Jan 29, 2019
1 parent 304a6b1 commit d16fb15
Show file tree
Hide file tree
Showing 2 changed files with 73 additions and 9 deletions.
44 changes: 40 additions & 4 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export type Variant = 'popover' | 'popper'
export type InjectedProps = {
open: (eventOrAnchorEl: SyntheticEvent<any> | HTMLElement) => void,
close: () => void,
onMouseLeave: (event: SyntheticEvent<any>) => void,
toggle: (eventOrAnchorEl: SyntheticEvent<any> | HTMLElement) => void,
setOpen: (
open: boolean,
Expand Down Expand Up @@ -80,7 +81,7 @@ export function bindToggle({
export function bindHover({
isOpen,
open,
close,
onMouseLeave,
popupId,
variant,
}: InjectedProps): {
Expand All @@ -96,7 +97,7 @@ export function bindHover({
: null,
'aria-haspopup': true,
onMouseEnter: open,
onMouseLeave: close,
onMouseLeave,
}
}

Expand All @@ -110,18 +111,21 @@ export function bindPopover({
isOpen,
anchorEl,
close,
onMouseLeave,
popupId,
}: InjectedProps): {
id: ?string,
anchorEl: ?HTMLElement,
open: boolean,
onClose: () => void,
onMouseLeave: (event: SyntheticEvent<any>) => void,
} {
return {
id: popupId,
anchorEl,
open: isOpen,
onClose: close,
onMouseLeave,
}
}

Expand All @@ -143,15 +147,18 @@ export function bindPopper({
isOpen,
anchorEl,
popupId,
onMouseLeave,
}: InjectedProps): {
id: ?string,
anchorEl: ?HTMLElement,
open: boolean,
onMouseLeave: (event: SyntheticEvent<any>) => void,
} {
return {
id: popupId,
anchorEl,
open: isOpen,
onMouseLeave,
}
}

Expand All @@ -163,12 +170,13 @@ export type Props = {

type State = {
anchorEl: ?HTMLElement,
hovered: boolean,
}

let eventOrAnchorElWarned: boolean = false

export default class PopupState extends React.Component<Props, State> {
state: State = { anchorEl: null }
state: State = { anchorEl: null, hovered: false }

static propTypes = {
/**
Expand Down Expand Up @@ -222,10 +230,28 @@ export default class PopupState extends React.Component<Props, State> {
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<any>) => {
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,
Expand All @@ -244,6 +270,7 @@ export default class PopupState extends React.Component<Props, State> {
const result = children({
open: this.handleOpen,
close: this.handleClose,
onMouseLeave: this.handleMouseLeave,
toggle: this.handleToggle,
setOpen: this.handleSetOpen,
isOpen,
Expand All @@ -255,3 +282,12 @@ export default class PopupState extends React.Component<Props, State> {
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
}
38 changes: 33 additions & 5 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -202,13 +202,16 @@ describe('<PopupState />', () => {
let buttonRef
let button
let popover
let content

const render = spy(popupState => (
<React.Fragment>
<Button buttonRef={c => (buttonRef = c)} {...bindHover(popupState)}>
Open Menu
</Button>
<Popover {...bindPopover(popupState)}>The popover content</Popover>
<Popover {...bindPopover(popupState)}>
<span ref={c => (content = c)}>The popover content</span>
</Popover>
</React.Fragment>
))

Expand All @@ -226,7 +229,10 @@ describe('<PopupState />', () => {
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)
Expand All @@ -240,21 +246,43 @@ describe('<PopupState />', () => {
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)
assert.strictEqual(render.args[2][0].isOpen, false)
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)
Expand Down

0 comments on commit d16fb15

Please sign in to comment.