Skip to content

Commit d6b01a8

Browse files
committed
feat: Popover, ActionMenu, ProfileMenu - modify popover implementation to render on top of all elements regardless of z-index hierarchy
1 parent 7bc7b72 commit d6b01a8

File tree

6 files changed

+56
-37
lines changed

6 files changed

+56
-37
lines changed

src/Shared/Components/ActionMenu/ActionMenu.component.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import { MutableRefObject } from 'react'
2-
31
import { CustomInput } from '../CustomInput'
42
import { Popover } from '../Popover'
53
import { SelectPickerMenuListFooter } from '../SelectPicker/common'
@@ -82,7 +80,7 @@ export const ActionMenu = <T extends string | number = string | number>({
8280
</div>
8381
)}
8482
<ul
85-
ref={scrollableRef as MutableRefObject<HTMLUListElement>}
83+
ref={scrollableRef}
8684
role="menu"
8785
className="action-menu m-0 p-0 flex-grow-1 dc__overflow-auto dc__overscroll-none"
8886
>

src/Shared/Components/Header/ProfileMenu.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export const ProfileMenu = ({ user, onClick }: ProfileMenuProps) => {
1414
// HOOKS
1515
const { viewIsPipelineRBACConfiguredNode } = useMainContext()
1616

17-
const { open, overlayProps, popoverProps, triggerProps, closePopover } = usePopover({
17+
const { open, overlayProps, popoverProps, triggerProps, scrollableRef, closePopover } = usePopover({
1818
id: 'profile-menu',
1919
alignment: 'end',
2020
width: 250,
@@ -51,7 +51,7 @@ export const ProfileMenu = ({ user, onClick }: ProfileMenuProps) => {
5151
triggerElement={triggerElement}
5252
buttonProps={null}
5353
>
54-
<>
54+
<div ref={scrollableRef} className="dc__overflow-auto">
5555
<div className="p-4">
5656
<div className="flex dc__content-space dc__gap-8 px-8 py-6">
5757
<div>
@@ -71,7 +71,7 @@ export const ProfileMenu = ({ user, onClick }: ProfileMenuProps) => {
7171
<Icon name="ic-logout" color="R500" />
7272
</Link>
7373
</div>
74-
</>
74+
</div>
7575
</Popover>
7676
)
7777
}

src/Shared/Components/Popover/Popover.component.tsx

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,24 +15,25 @@ export const Popover = ({
1515
open,
1616
popoverProps,
1717
overlayProps,
18-
triggerProps,
18+
triggerProps: { bounds, ...triggerProps },
1919
buttonProps,
2020
triggerElement,
2121
children,
2222
}: PopoverProps) => (
23-
<div className="dc__position-rel dc__inline-block">
23+
<>
2424
<div {...triggerProps}>{triggerElement || <Button {...buttonProps} />}</div>
2525

2626
<AnimatePresence>
2727
{open && (
28-
<>
29-
{/* Overlay to block interactions with the background */}
30-
<div {...overlayProps} />
31-
<motion.div {...popoverProps} data-testid={popoverProps.id}>
32-
{children}
33-
</motion.div>
34-
</>
28+
<div {...overlayProps}>
29+
<div className="dc__position-abs" style={{ left: bounds.left, top: bounds.top }}>
30+
<div className="dc__visibility-hidden" style={{ width: bounds.width, height: bounds.height }} />
31+
<motion.div {...popoverProps} data-testid={popoverProps.id}>
32+
{children}
33+
</motion.div>
34+
</div>
35+
</div>
3536
)}
3637
</AnimatePresence>
37-
</div>
38+
</>
3839
)

src/Shared/Components/Popover/popover.scss

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,3 @@
66
left:0;
77
z-index: var(--modal-index);
88
}
9-
10-
.popover-content {
11-
z-index: var(--modal-index);
12-
}

src/Shared/Components/Popover/types.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { DetailedHTMLProps, KeyboardEvent, MutableRefObject, ReactElement } from 'react'
1+
import { DetailedHTMLProps, KeyboardEvent, LegacyRef, MutableRefObject, ReactElement } from 'react'
22
import { HTMLMotionProps } from 'framer-motion'
33

44
import { ButtonProps } from '../Button'
@@ -67,7 +67,9 @@ export interface UsePopoverReturnType {
6767
* Props to be spread onto the trigger element that opens the popover.
6868
* These props include standard HTML attributes for a `div` element.
6969
*/
70-
triggerProps: DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>
70+
triggerProps: DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement> & {
71+
bounds: Pick<DOMRect, 'left' | 'top' | 'height' | 'width'>
72+
}
7173
/**
7274
* Props to be spread onto the overlay element of the popover.
7375
* These props include standard HTML attributes for a `div` element.
@@ -82,7 +84,7 @@ export interface UsePopoverReturnType {
8284
* A mutable reference to the scrollable element inside the popover. \
8385
* This reference should be assigned to the element that is scrollable.
8486
*/
85-
scrollableRef: MutableRefObject<HTMLElement>
87+
scrollableRef: MutableRefObject<any> | LegacyRef<any>
8688
/**
8789
* A function to close the popover.
8890
*/

src/Shared/Components/Popover/usePopover.hook.ts

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useLayoutEffect, useRef, useState } from 'react'
1+
import { MouseEvent, useLayoutEffect, useRef, useState } from 'react'
22

33
import { UsePopoverProps, UsePopoverReturnType } from './types'
44
import {
@@ -21,6 +21,7 @@ export const usePopover = ({
2121
const [open, setOpen] = useState(false)
2222
const [actualPosition, setActualPosition] = useState<UsePopoverProps['position']>(position)
2323
const [actualAlignment, setActualAlignment] = useState<UsePopoverProps['alignment']>(alignment)
24+
const [triggerBounds, setTriggerBounds] = useState<UsePopoverReturnType['triggerProps']['bounds'] | null>(null)
2425

2526
// CONSTANTS
2627
const isAutoWidth = width === 'auto'
@@ -51,23 +52,40 @@ export const usePopover = ({
5152

5253
const handlePopoverKeyDown = (e: React.KeyboardEvent) => onPopoverKeyDown(e, open, closePopover)
5354

55+
const handleOverlayClick = (e: MouseEvent<HTMLDivElement>) => {
56+
if (!popover.current?.contains(e.target as Node)) {
57+
closePopover()
58+
}
59+
}
60+
5461
useLayoutEffect(() => {
5562
if (!open || !triggerRef.current || !popover.current || !scrollableRef.current) {
5663
return
5764
}
5865

59-
const triggerRect = triggerRef.current.getBoundingClientRect()
60-
const popoverRect = popover.current.getBoundingClientRect()
61-
62-
const { fallbackPosition, fallbackAlignment } = getPopoverActualPositionAlignment({
63-
position,
64-
alignment,
65-
triggerRect,
66-
popoverRect,
67-
})
66+
const updatePopoverPosition = () => {
67+
const triggerRect = triggerRef.current.getBoundingClientRect()
68+
const popoverRect = popover.current.getBoundingClientRect()
69+
70+
const { fallbackPosition, fallbackAlignment } = getPopoverActualPositionAlignment({
71+
position,
72+
alignment,
73+
triggerRect,
74+
popoverRect,
75+
})
76+
77+
setActualPosition(fallbackPosition)
78+
setActualAlignment(fallbackAlignment)
79+
setTriggerBounds({
80+
left: triggerRect.left,
81+
top: triggerRect.top,
82+
height: triggerRect.height,
83+
width: triggerRect.width,
84+
})
85+
}
6886

69-
setActualPosition(fallbackPosition)
70-
setActualAlignment(fallbackAlignment)
87+
// update position on open
88+
updatePopoverPosition()
7189

7290
// prevent scroll propagation unless scrollable
7391
const handleWheel = (e: WheelEvent) => {
@@ -84,9 +102,12 @@ export const usePopover = ({
84102
}
85103

86104
scrollableRef.current.addEventListener('wheel', handleWheel, { passive: false })
105+
window.addEventListener('resize', updatePopoverPosition)
106+
87107
// eslint-disable-next-line consistent-return
88108
return () => {
89109
scrollableRef.current.removeEventListener('wheel', handleWheel)
110+
window.removeEventListener('resize', updatePopoverPosition)
90111
}
91112
}, [open, position, alignment])
92113

@@ -100,17 +121,18 @@ export const usePopover = ({
100121
'aria-haspopup': 'listbox',
101122
'aria-expanded': open,
102123
tabIndex: 0,
124+
bounds: triggerBounds ?? { left: 0, top: 0, height: 0, width: 0 },
103125
},
104126
overlayProps: {
105127
role: 'dialog',
106-
onClick: closePopover,
128+
onClick: handleOverlayClick,
107129
className: 'popover-overlay',
108130
},
109131
popoverProps: {
110132
id,
111133
ref: popover,
112134
role: 'listbox',
113-
className: `popover-content dc__position-abs bg__menu--primary shadow__menu border__primary br-6 dc__overflow-hidden ${isAutoWidth ? 'dc_width-max-content dc__mxw-250' : ''}`,
135+
className: `dc__position-abs bg__menu--primary shadow__menu border__primary br-6 dc__overflow-hidden ${isAutoWidth ? 'dc_width-max-content dc__mxw-250' : ''}`,
114136
onKeyDown: handlePopoverKeyDown,
115137
style: {
116138
width: !isAutoWidth ? `${width}px` : undefined,

0 commit comments

Comments
 (0)