Skip to content

Commit

Permalink
fix: defer open/close when touch event is detected
Browse files Browse the repository at this point in the history
fix #45
  • Loading branch information
jedwards1211 committed Apr 17, 2021
1 parent a652a0e commit 6e3d1ee
Show file tree
Hide file tree
Showing 4 changed files with 94 additions and 39 deletions.
9 changes: 7 additions & 2 deletions demo/examples/CascadingHoverMenus.hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ import {
const ParentPopupState = React.createContext(null)

const CascadingHoverMenus = () => {
const popupState = usePopupState({ popupId: 'demoMenu', variant: 'popover' })
const popupState = usePopupState({
popupId: 'demoMenu',
variant: 'popover',
deferOpenClose: true,
})
return (
<div style={{ height: 600 }}>
<Button variant="contained" {...bindHover(popupState)}>
Expand Down Expand Up @@ -49,7 +53,7 @@ const CascadingHoverMenus = () => {

export default CascadingHoverMenus

const submenuStyles = theme => ({
const submenuStyles = (theme) => ({
menu: {
marginTop: theme.spacing(-1),
},
Expand All @@ -70,6 +74,7 @@ const Submenu = withStyles(submenuStyles)(
popupId,
variant: 'popover',
parentPopupState,
deferOpenClose: true,
})
return (
<ParentPopupState.Provider value={popupState}>
Expand Down
4 changes: 2 additions & 2 deletions demo/examples/TriggerMenu.hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
bindMenu,
} from 'material-ui-popup-state/hooks'

const Triggernu = () => {
const TriggerMenu = () => {
const popupState = usePopupState({ variant: 'popover', popupId: 'demoMenu' })
return (
<div>
Expand All @@ -28,4 +28,4 @@ const Triggernu = () => {
)
}

export default Triggernu
export default TriggerMenu
2 changes: 2 additions & 0 deletions src/core.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export type CoreState = {
anchorEl: HTMLElement | undefined
hovered: boolean
_childPopupState: PopupState | undefined
_deferNextOpen: boolean
_deferNextClose: boolean
}

export const initCoreState: CoreState
Expand Down
118 changes: 83 additions & 35 deletions src/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ export type CoreState = {
anchorEl: ?HTMLElement,
hovered: boolean,
_childPopupState: ?PopupState,
_deferNextOpen: boolean,
_deferNextClose: boolean,
}

export const initCoreState: CoreState = {
Expand All @@ -47,6 +49,8 @@ export const initCoreState: CoreState = {
anchorEl: null,
hovered: false,
_childPopupState: null,
_deferNextOpen: false,
_deferNextClose: false,
}

export function createPopupState({
Expand All @@ -64,7 +68,15 @@ export function createPopupState({
parentPopupState?: ?PopupState,
disableAutoFocus?: ?boolean,
}): PopupState {
const { isOpen, setAnchorElUsed, anchorEl, hovered, _childPopupState } = state
const {
isOpen,
setAnchorElUsed,
anchorEl,
hovered,
_childPopupState,
_deferNextOpen,
_deferNextClose,
} = state

// use lastState to workaround cases where setState is called multiple times
// in a single render (e.g. because of refs being called multiple times)
Expand All @@ -80,50 +92,80 @@ export function createPopupState({
}

const toggle = (eventOrAnchorEl?: SyntheticEvent<any> | HTMLElement) => {
if (isOpen) close()
if (isOpen) close(eventOrAnchorEl)
else open(eventOrAnchorEl)
}

const open = (eventOrAnchorEl?: SyntheticEvent<any> | HTMLElement) => {
if (!eventOrAnchorEl && !setAnchorElUsed) {
warn(
'missingEventOrAnchorEl',
'eventOrAnchorEl should be defined if setAnchorEl is not used'
)
}
const eventType = eventOrAnchorEl && (eventOrAnchorEl: any).type
const currentTarget =
eventOrAnchorEl && (eventOrAnchorEl: any).currentTarget

if (parentPopupState) {
if (!parentPopupState.isOpen) return
parentPopupState._setChildPopupState(popupState)
}
if (
!disableAutoFocus &&
typeof document === 'object' &&
document.activeElement
) {
document.activeElement.blur()
if (eventType === 'touchstart') {
setState({ _deferNextOpen: true })
return
}

const newState: $Shape<CoreState> = {
isOpen: true,
hovered: eventOrAnchorEl && (eventOrAnchorEl: any).type === 'mouseover',
}
const doOpen = () => {
if (!eventOrAnchorEl && !setAnchorElUsed) {
warn(
'missingEventOrAnchorEl',
'eventOrAnchorEl should be defined if setAnchorEl is not used'
)
}

if (eventOrAnchorEl && eventOrAnchorEl.currentTarget) {
if (!setAnchorElUsed) {
newState.anchorEl = (eventOrAnchorEl.currentTarget: any)
if (parentPopupState) {
if (!parentPopupState.isOpen) return
parentPopupState._setChildPopupState(popupState)
}
if (
!disableAutoFocus &&
typeof document === 'object' &&
document.activeElement
) {
document.activeElement.blur()
}

const newState: $Shape<CoreState> = {
isOpen: true,
hovered: eventType === 'mouseover',
}

if (currentTarget) {
if (!setAnchorElUsed) {
newState.anchorEl = (currentTarget: any)
}
} else if (eventOrAnchorEl) {
newState.anchorEl = (eventOrAnchorEl: any)
}
} else if (eventOrAnchorEl) {
newState.anchorEl = (eventOrAnchorEl: any)
}

setState(newState)
setState(newState)
}
if (_deferNextOpen) {
setState({ _deferNextOpen: false })
setTimeout(doOpen, 0)
} else {
doOpen()
}
}

const close = () => {
if (_childPopupState) _childPopupState.close()
if (parentPopupState) parentPopupState._setChildPopupState(null)
setState({ isOpen: false, hovered: false })
const close = (arg?: SyntheticEvent<any> | HTMLElement) => {
const eventType = arg && (arg: any).type
if (eventType === 'touchstart') {
setState({ _deferNextClose: true })
return
}
const doClose = () => {
if (_childPopupState) _childPopupState.close()
if (parentPopupState) parentPopupState._setChildPopupState(null)
setState({ isOpen: false, hovered: false })
}
if (_deferNextClose) {
setState({ _deferNextClose: false })
setTimeout(doClose, 0)
} else {
doClose()
}
}

const setOpen = (
Expand All @@ -132,13 +174,13 @@ export function createPopupState({
) => {
if (nextOpen) {
open(eventOrAnchorEl)
} else close()
} else close(eventOrAnchorEl)
}

const onMouseLeave = (event: SyntheticEvent<any>) => {
const relatedTarget: any = (event: any).relatedTarget
if (hovered && !isElementInPopup(relatedTarget, popupState)) {
close()
close(event)
}
}

Expand Down Expand Up @@ -193,6 +235,7 @@ export function bindTrigger({
'aria-describedby'?: ?string,
'aria-haspopup': ?true,
onClick: (event: SyntheticEvent<any>) => void,
onTouchStart: (event: SyntheticEvent<any>) => void,
} {
return {
// $FlowFixMe
Expand All @@ -201,6 +244,7 @@ export function bindTrigger({
: null,
'aria-haspopup': variant === 'popover' ? true : undefined,
onClick: open,
onTouchStart: open,
}
}

Expand Down Expand Up @@ -250,6 +294,7 @@ export function bindToggle({
'aria-describedby'?: ?string,
'aria-haspopup': ?true,
onClick: (event: SyntheticEvent<any>) => void,
onTouchStart: (event: SyntheticEvent<any>) => void,
} {
return {
// $FlowFixMe
Expand All @@ -258,6 +303,7 @@ export function bindToggle({
: null,
'aria-haspopup': variant === 'popover' ? true : undefined,
onClick: toggle,
onTouchStart: toggle,
}
}

Expand All @@ -277,6 +323,7 @@ export function bindHover({
'aria-controls'?: ?string,
'aria-describedby'?: ?string,
'aria-haspopup': ?true,
onTouchStart: (event: SyntheticMouseEvent<any>) => any,
onMouseOver: (event: SyntheticMouseEvent<any>) => any,
onMouseLeave: (event: SyntheticMouseEvent<any>) => any,
} {
Expand All @@ -286,6 +333,7 @@ export function bindHover({
? popupId
: null,
'aria-haspopup': variant === 'popover' ? true : undefined,
onTouchStart: open,
onMouseOver: open,
onMouseLeave,
}
Expand Down

0 comments on commit 6e3d1ee

Please sign in to comment.