From b4a9b0e3a1084077ed276ec54bab2823da45470b Mon Sep 17 00:00:00 2001 From: Rohit Raj Date: Tue, 24 Jun 2025 16:37:30 +0530 Subject: [PATCH 1/2] feat(Backdrop): add support for transparent background and mount callback --- src/Shared/Components/Backdrop/Backdrop.tsx | 8 ++++++-- src/Shared/Components/Backdrop/types.ts | 12 ++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/Shared/Components/Backdrop/Backdrop.tsx b/src/Shared/Components/Backdrop/Backdrop.tsx index db654a021..d396e8a53 100644 --- a/src/Shared/Components/Backdrop/Backdrop.tsx +++ b/src/Shared/Components/Backdrop/Backdrop.tsx @@ -25,7 +25,7 @@ import { getUniqueId, preventBodyScroll, preventOutsideFocus } from '@Shared/Hel import { BackdropProps } from './types' import { createPortalContainerAndAppendToDOM } from './utils' -const Backdrop = ({ children, onEscape, onClick }: BackdropProps) => { +const Backdrop = ({ children, onEscape, onClick, hasClearBackground = false, onBackdropMount }: BackdropProps) => { // STATES const [portalContainer, setPortalContainer] = useState(null) @@ -59,6 +59,10 @@ const Backdrop = ({ children, onEscape, onClick }: BackdropProps) => { } }, []) + useEffect(() => { + onBackdropMount?.(!!portalContainer) + }, [portalContainer]) + /** * Manages a dedicated DOM node for rendering a portal backdrop. * @@ -106,7 +110,7 @@ const Backdrop = ({ children, onEscape, onClick }: BackdropProps) => { initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0, transition: { duration: 0.35 } }} - className="backdrop dc__position-fixed dc__top-0 dc__left-0 full-height-width flexbox dc__content-center dc__align-items-center dc__overflow-hidden" + className={`backdrop ${hasClearBackground ? 'backdrop--transparent' : ''} dc__position-fixed dc__top-0 dc__left-0 full-height-width flexbox dc__content-center dc__align-items-center dc__overflow-hidden`} onClick={onClick} > {children} diff --git a/src/Shared/Components/Backdrop/types.ts b/src/Shared/Components/Backdrop/types.ts index 6bf984344..a2939c4c5 100644 --- a/src/Shared/Components/Backdrop/types.ts +++ b/src/Shared/Components/Backdrop/types.ts @@ -36,4 +36,16 @@ export interface BackdropProps { * @param e - The mouse event object from the click interaction */ onClick?: (e: MouseEvent) => void + /** + * Determines if the backdrop should be transparent. + * When true, the backdrop will not have any background color or blur filter. + * @default false + */ + hasClearBackground?: boolean + /** + * Callback function that gets triggered when the backdrop component mounts or unmounts. + * This can be used to perform side effects or state updates when the backdrop's visibility changes. + * @param isMounted - A boolean indicating whether the backdrop is currently mounted (true) or not (false) + */ + onBackdropMount?: (isMounted: boolean) => void } From 60029e41cae56a2933fdedc34feea19c6ae7583f Mon Sep 17 00:00:00 2001 From: Rohit Raj Date: Tue, 24 Jun 2025 16:45:33 +0530 Subject: [PATCH 2/2] feat(Popover): integrate Backdrop component and remove custom overlay styles --- .../Components/Popover/Popover.component.tsx | 7 +++--- src/Shared/Components/Popover/popover.scss | 8 ------- src/Shared/Components/Popover/types.ts | 5 ++-- .../Components/Popover/usePopover.hook.ts | 24 +++++++++++-------- 4 files changed, 20 insertions(+), 24 deletions(-) delete mode 100644 src/Shared/Components/Popover/popover.scss diff --git a/src/Shared/Components/Popover/Popover.component.tsx b/src/Shared/Components/Popover/Popover.component.tsx index d332af886..f7e027e13 100644 --- a/src/Shared/Components/Popover/Popover.component.tsx +++ b/src/Shared/Components/Popover/Popover.component.tsx @@ -1,10 +1,9 @@ import { AnimatePresence, motion } from 'framer-motion' +import { Backdrop } from '../Backdrop' import { Button } from '../Button' import { PopoverProps } from './types' -import './popover.scss' - /** * Popover Component \ * This component serves as a base for creating popovers. It is not intended to be used directly. @@ -25,14 +24,14 @@ export const Popover = ({ {open && ( -
+
{children}
-
+
)} diff --git a/src/Shared/Components/Popover/popover.scss b/src/Shared/Components/Popover/popover.scss deleted file mode 100644 index 26caeb6e0..000000000 --- a/src/Shared/Components/Popover/popover.scss +++ /dev/null @@ -1,8 +0,0 @@ -.popover-overlay { - position: fixed; - top:0; - right:0; - bottom:0; - left:0; - z-index: var(--modal-index); -} diff --git a/src/Shared/Components/Popover/types.ts b/src/Shared/Components/Popover/types.ts index aff9c3406..b46f54c38 100644 --- a/src/Shared/Components/Popover/types.ts +++ b/src/Shared/Components/Popover/types.ts @@ -1,6 +1,7 @@ import { DetailedHTMLProps, KeyboardEvent, LegacyRef, MutableRefObject, ReactElement } from 'react' import { HTMLMotionProps } from 'framer-motion' +import { BackdropProps } from '../Backdrop' import { ButtonProps } from '../Button' export interface UsePopoverProps { @@ -77,9 +78,9 @@ export interface UsePopoverReturnType { } /** * Props to be spread onto the overlay element of the popover. - * These props include standard HTML attributes for a `div` element. + * These props include backdrop properties. */ - overlayProps: DetailedHTMLProps, HTMLDivElement> + overlayProps: Omit /** * Props to be spread onto the popover element itself. * Includes motion-related props for animations and a `ref` to the popover's `div` element. diff --git a/src/Shared/Components/Popover/usePopover.hook.ts b/src/Shared/Components/Popover/usePopover.hook.ts index 7afba4329..a1fe3d594 100644 --- a/src/Shared/Components/Popover/usePopover.hook.ts +++ b/src/Shared/Components/Popover/usePopover.hook.ts @@ -1,4 +1,4 @@ -import { MouseEvent, useLayoutEffect, useRef, useState } from 'react' +import { useLayoutEffect, useRef, useState } from 'react' import { UsePopoverProps, UsePopoverReturnType } from './types' import { @@ -23,6 +23,7 @@ export const usePopover = ({ const [actualPosition, setActualPosition] = useState(position) const [actualAlignment, setActualAlignment] = useState(alignment) const [triggerBounds, setTriggerBounds] = useState(null) + const [isBackdropMounted, setIsBackdropMounted] = useState(false) // CONSTANTS const isAutoWidth = width === 'auto' @@ -53,14 +54,12 @@ export const usePopover = ({ const handlePopoverKeyDown = (e: React.KeyboardEvent) => onPopoverKeyDown?.(e, open, closePopover) - const handleOverlayClick = (e: MouseEvent) => { - if (!popover.current?.contains(e.target as Node)) { - closePopover() - } + const handleOverlayClick = () => { + closePopover() } useLayoutEffect(() => { - if (!open || !triggerRef.current || !popover.current || !scrollableRef.current) { + if (!open || !isBackdropMounted || !triggerRef.current || !popover.current || !scrollableRef.current) { return } @@ -88,6 +87,9 @@ export const usePopover = ({ // update position on open updatePopoverPosition() + // focus on popover when it is opened, so that keyboard navigation works as expected + popover.current.focus() + // prevent scroll propagation unless scrollable const handleWheel = (e: WheelEvent) => { e.stopPropagation() @@ -110,7 +112,7 @@ export const usePopover = ({ scrollableRef.current.removeEventListener('wheel', handleWheel) window.removeEventListener('resize', updatePopoverPosition) } - }, [open, position, alignment]) + }, [open, position, alignment, isBackdropMounted]) return { open, @@ -124,15 +126,17 @@ export const usePopover = ({ bounds: triggerBounds ?? { left: 0, top: 0, height: 0, width: 0 }, }, overlayProps: { - role: 'dialog', + hasClearBackground: true, onClick: handleOverlayClick, - className: 'popover-overlay', + onEscape: closePopover, + onBackdropMount: setIsBackdropMounted, }, popoverProps: { id, ref: popover, role: 'listbox', - className: `dc__position-abs ${variant === 'menu' ? 'bg__menu--primary shadow__menu' : 'bg__overlay--primary shadow__overlay'} border__primary br-6 dc__overflow-hidden ${isAutoWidth ? 'dc_width-max-content dc__mxw-250' : ''}`, + tabIndex: 0, + className: `dc__position-abs dc__outline-none-imp ${variant === 'menu' ? 'bg__menu--primary shadow__menu' : 'bg__overlay--primary shadow__overlay'} border__primary br-6 dc__overflow-hidden ${isAutoWidth ? 'dc_width-max-content dc__mxw-250' : ''}`, onKeyDown: handlePopoverKeyDown, style: { width: !isAutoWidth ? `${width}px` : undefined,