From 58e751f986564cf74cbfd869d2c43e626030b701 Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Wed, 27 Jul 2022 20:52:08 +0530 Subject: [PATCH] fix: popover behaviour when clicking outside of popover (#1355) --- .../Components/Popover/Popover.tsx | 54 ++++++++++++++++++- .../Popover/PositionedPopoverContent.tsx | 5 +- .../javascripts/Components/Popover/Types.ts | 2 + .../Utils/usePopoverCloseOnClickOutside.ts | 15 ++++-- 4 files changed, 69 insertions(+), 7 deletions(-) diff --git a/packages/web/src/javascripts/Components/Popover/Popover.tsx b/packages/web/src/javascripts/Components/Popover/Popover.tsx index 328c484af..0dd9f1831 100644 --- a/packages/web/src/javascripts/Components/Popover/Popover.tsx +++ b/packages/web/src/javascripts/Components/Popover/Popover.tsx @@ -1,6 +1,29 @@ +import { UuidGenerator } from '@standardnotes/snjs' +import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react' import PositionedPopoverContent from './PositionedPopoverContent' import { PopoverProps } from './Types' +type PopoverContextData = { + registerChildPopover: (id: string) => void + unregisterChildPopover: (id: string) => void +} + +const PopoverContext = createContext(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 & { open: boolean } @@ -16,20 +39,47 @@ const Popover = ({ side, togglePopover, }: Props) => { + const popoverId = useRef(UuidGenerator.GenerateUuid()) + + useRegisterPopoverToParent(popoverId.current) + + const [childPopovers, setChildPopovers] = useState>(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 ? ( - <> + {children} - + ) : null } diff --git a/packages/web/src/javascripts/Components/Popover/PositionedPopoverContent.tsx b/packages/web/src/javascripts/Components/Popover/PositionedPopoverContent.tsx index ef675bbc1..b0194c78e 100644 --- a/packages/web/src/javascripts/Components/Popover/PositionedPopoverContent.tsx +++ b/packages/web/src/javascripts/Components/Popover/PositionedPopoverContent.tsx @@ -15,7 +15,9 @@ const PositionedPopoverContent = ({ anchorElement, anchorPoint, children, + childPopovers, className, + id, overrideZIndex, side = 'bottom', togglePopover, @@ -44,6 +46,7 @@ const PositionedPopoverContent = ({ popoverElement, anchorElement, togglePopover, + childPopovers, }) return ( @@ -61,7 +64,7 @@ const PositionedPopoverContent = ({ ref={(node) => { setPopoverElement(node) }} - data-popover + data-popover={id} >
diff --git a/packages/web/src/javascripts/Components/Popover/Types.ts b/packages/web/src/javascripts/Components/Popover/Types.ts index face9d2db..351ea0b0d 100644 --- a/packages/web/src/javascripts/Components/Popover/Types.ts +++ b/packages/web/src/javascripts/Components/Popover/Types.ts @@ -42,6 +42,8 @@ type CommonPopoverProps = { export type PopoverContentProps = CommonPopoverProps & { anchorElement?: HTMLElement | null anchorPoint?: Point + childPopovers: Set + id: string } export type PopoverProps = diff --git a/packages/web/src/javascripts/Components/Popover/Utils/usePopoverCloseOnClickOutside.ts b/packages/web/src/javascripts/Components/Popover/Utils/usePopoverCloseOnClickOutside.ts index 79c91debb..1cc6eafab 100644 --- a/packages/web/src/javascripts/Components/Popover/Utils/usePopoverCloseOnClickOutside.ts +++ b/packages/web/src/javascripts/Components/Popover/Utils/usePopoverCloseOnClickOutside.ts @@ -4,9 +4,15 @@ type Options = { popoverElement: HTMLElement | null anchorElement: HTMLElement | null | undefined togglePopover: () => void + childPopovers: Set } -export const usePopoverCloseOnClickOutside = ({ popoverElement, anchorElement, togglePopover }: Options) => { +export const usePopoverCloseOnClickOutside = ({ + popoverElement, + anchorElement, + togglePopover, + childPopovers, +}: Options) => { useEffect(() => { const closeIfClickedOutside = (event: MouseEvent) => { const matchesMediumBreakpoint = matchMedia('(min-width: 768px)').matches @@ -19,9 +25,10 @@ export const usePopoverCloseOnClickOutside = ({ popoverElement, anchorElement, t const isDescendantOfMenu = popoverElement?.contains(target) 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() } } @@ -32,5 +39,5 @@ export const usePopoverCloseOnClickOutside = ({ popoverElement, anchorElement, t capture: true, }) } - }, [anchorElement, popoverElement, togglePopover]) + }, [anchorElement, childPopovers, popoverElement, togglePopover]) }