fix: popover behaviour when clicking outside of popover (#1355)
This commit is contained in:
@@ -1,6 +1,29 @@
|
|||||||
|
import { UuidGenerator } from '@standardnotes/snjs'
|
||||||
|
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import PositionedPopoverContent from './PositionedPopoverContent'
|
import PositionedPopoverContent from './PositionedPopoverContent'
|
||||||
import { PopoverProps } from './Types'
|
import { PopoverProps } from './Types'
|
||||||
|
|
||||||
|
type PopoverContextData = {
|
||||||
|
registerChildPopover: (id: string) => void
|
||||||
|
unregisterChildPopover: (id: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const PopoverContext = createContext<PopoverContextData | null>(null)
|
||||||
|
|
||||||
|
const useRegisterPopoverToParent = (popoverId: string) => {
|
||||||
|
const parentPopoverContext = useContext(PopoverContext)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const currentId = popoverId
|
||||||
|
|
||||||
|
parentPopoverContext?.registerChildPopover(currentId)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
parentPopoverContext?.unregisterChildPopover(currentId)
|
||||||
|
}
|
||||||
|
}, [parentPopoverContext, popoverId])
|
||||||
|
}
|
||||||
|
|
||||||
type Props = PopoverProps & {
|
type Props = PopoverProps & {
|
||||||
open: boolean
|
open: boolean
|
||||||
}
|
}
|
||||||
@@ -16,20 +39,47 @@ const Popover = ({
|
|||||||
side,
|
side,
|
||||||
togglePopover,
|
togglePopover,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
|
const popoverId = useRef(UuidGenerator.GenerateUuid())
|
||||||
|
|
||||||
|
useRegisterPopoverToParent(popoverId.current)
|
||||||
|
|
||||||
|
const [childPopovers, setChildPopovers] = useState<Set<string>>(new Set())
|
||||||
|
|
||||||
|
const registerChildPopover = useCallback((id: string) => {
|
||||||
|
setChildPopovers((childPopovers) => new Set(childPopovers.add(id)))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const unregisterChildPopover = useCallback((id: string) => {
|
||||||
|
setChildPopovers((childPopovers) => {
|
||||||
|
childPopovers.delete(id)
|
||||||
|
return new Set(childPopovers)
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const contextValue = useMemo(
|
||||||
|
() => ({
|
||||||
|
registerChildPopover,
|
||||||
|
unregisterChildPopover,
|
||||||
|
}),
|
||||||
|
[registerChildPopover, unregisterChildPopover],
|
||||||
|
)
|
||||||
|
|
||||||
return open ? (
|
return open ? (
|
||||||
<>
|
<PopoverContext.Provider value={contextValue}>
|
||||||
<PositionedPopoverContent
|
<PositionedPopoverContent
|
||||||
align={align}
|
align={align}
|
||||||
anchorElement={anchorElement}
|
anchorElement={anchorElement}
|
||||||
anchorPoint={anchorPoint}
|
anchorPoint={anchorPoint}
|
||||||
|
childPopovers={childPopovers}
|
||||||
className={className}
|
className={className}
|
||||||
|
id={popoverId.current}
|
||||||
overrideZIndex={overrideZIndex}
|
overrideZIndex={overrideZIndex}
|
||||||
side={side}
|
side={side}
|
||||||
togglePopover={togglePopover}
|
togglePopover={togglePopover}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</PositionedPopoverContent>
|
</PositionedPopoverContent>
|
||||||
</>
|
</PopoverContext.Provider>
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ const PositionedPopoverContent = ({
|
|||||||
anchorElement,
|
anchorElement,
|
||||||
anchorPoint,
|
anchorPoint,
|
||||||
children,
|
children,
|
||||||
|
childPopovers,
|
||||||
className,
|
className,
|
||||||
|
id,
|
||||||
overrideZIndex,
|
overrideZIndex,
|
||||||
side = 'bottom',
|
side = 'bottom',
|
||||||
togglePopover,
|
togglePopover,
|
||||||
@@ -44,6 +46,7 @@ const PositionedPopoverContent = ({
|
|||||||
popoverElement,
|
popoverElement,
|
||||||
anchorElement,
|
anchorElement,
|
||||||
togglePopover,
|
togglePopover,
|
||||||
|
childPopovers,
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -61,7 +64,7 @@ const PositionedPopoverContent = ({
|
|||||||
ref={(node) => {
|
ref={(node) => {
|
||||||
setPopoverElement(node)
|
setPopoverElement(node)
|
||||||
}}
|
}}
|
||||||
data-popover
|
data-popover={id}
|
||||||
>
|
>
|
||||||
<div className="md:hidden">
|
<div className="md:hidden">
|
||||||
<div className="flex items-center justify-end px-3">
|
<div className="flex items-center justify-end px-3">
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ type CommonPopoverProps = {
|
|||||||
export type PopoverContentProps = CommonPopoverProps & {
|
export type PopoverContentProps = CommonPopoverProps & {
|
||||||
anchorElement?: HTMLElement | null
|
anchorElement?: HTMLElement | null
|
||||||
anchorPoint?: Point
|
anchorPoint?: Point
|
||||||
|
childPopovers: Set<string>
|
||||||
|
id: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PopoverProps =
|
export type PopoverProps =
|
||||||
|
|||||||
@@ -4,9 +4,15 @@ type Options = {
|
|||||||
popoverElement: HTMLElement | null
|
popoverElement: HTMLElement | null
|
||||||
anchorElement: HTMLElement | null | undefined
|
anchorElement: HTMLElement | null | undefined
|
||||||
togglePopover: () => void
|
togglePopover: () => void
|
||||||
|
childPopovers: Set<string>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const usePopoverCloseOnClickOutside = ({ popoverElement, anchorElement, togglePopover }: Options) => {
|
export const usePopoverCloseOnClickOutside = ({
|
||||||
|
popoverElement,
|
||||||
|
anchorElement,
|
||||||
|
togglePopover,
|
||||||
|
childPopovers,
|
||||||
|
}: Options) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const closeIfClickedOutside = (event: MouseEvent) => {
|
const closeIfClickedOutside = (event: MouseEvent) => {
|
||||||
const matchesMediumBreakpoint = matchMedia('(min-width: 768px)').matches
|
const matchesMediumBreakpoint = matchMedia('(min-width: 768px)').matches
|
||||||
@@ -19,9 +25,10 @@ export const usePopoverCloseOnClickOutside = ({ popoverElement, anchorElement, t
|
|||||||
|
|
||||||
const isDescendantOfMenu = popoverElement?.contains(target)
|
const isDescendantOfMenu = popoverElement?.contains(target)
|
||||||
const isAnchorElement = anchorElement ? anchorElement === event.target || anchorElement.contains(target) : false
|
const isAnchorElement = anchorElement ? anchorElement === event.target || anchorElement.contains(target) : false
|
||||||
const isDescendantOfPopover = target.closest('[data-popover]')
|
const closestPopoverId = target.closest('[data-popover]')?.getAttribute('data-popover')
|
||||||
|
const isDescendantOfChildPopover = closestPopoverId && childPopovers.has(closestPopoverId)
|
||||||
|
|
||||||
if (!isDescendantOfMenu && !isAnchorElement && !isDescendantOfPopover) {
|
if (!isDescendantOfMenu && !isAnchorElement && !isDescendantOfChildPopover) {
|
||||||
togglePopover()
|
togglePopover()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -32,5 +39,5 @@ export const usePopoverCloseOnClickOutside = ({ popoverElement, anchorElement, t
|
|||||||
capture: true,
|
capture: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, [anchorElement, popoverElement, togglePopover])
|
}, [anchorElement, childPopovers, popoverElement, togglePopover])
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user