fix: popover behaviour when clicking outside of popover (#1355)

This commit is contained in:
Aman Harwara
2022-07-27 20:52:08 +05:30
committed by GitHub
parent 8c364f6d58
commit 58e751f986
4 changed files with 69 additions and 7 deletions

View File

@@ -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
} }

View File

@@ -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">

View File

@@ -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 =

View File

@@ -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])
} }