11import {
2- useEffectEvent ,
2+ useEffect ,
33 useId ,
4- useLayoutEffect ,
54 useRef ,
5+ useState ,
66 type ReactNode ,
7+ type RefObject ,
78} from 'react' ;
89import { styled } from 'styled-components' ;
910import { transparentize } from 'polished' ;
1011import { fadeIn } from '@helpers/commonAnimations' ;
1112import { useControlLock } from '@hooks/useControlLock' ;
1213import { useDialogTreeInfo } from './Dialog/dialogContext' ;
13- import { useControllable } from '@hooks/useControlable ' ;
14+ import { useOnValueChange } from '@helpers/useOnValueChange ' ;
1415
1516export interface TriggerProps {
1617 onClick : ( ) => void ;
1718 'data-popover-target' : string ;
1819}
1920
20- export interface PopoverProps {
21- Trigger : ( props : TriggerProps ) => ReactNode ;
22- open ?: boolean ;
23- defaultOpen ?: boolean ;
24- onOpenChange : ( open : boolean ) => void ;
21+ export interface PopoverPropsFromHook {
22+ isOpen : boolean ;
23+ setIsOpen : React . Dispatch < React . SetStateAction < boolean > > ;
24+ anchorName : string ;
25+ }
26+ export interface PopoverProps extends PopoverPropsFromHook {
27+ Trigger : ReactNode ;
2528 className ?: string ;
26- noArrow ?: boolean ;
2729 noLock ?: boolean ;
28- modal ?: boolean ;
29- side ?: 'top' | 'bottom' | 'left' | 'right' ;
3030}
3131
32+ export interface UsePopoverProps {
33+ defaultOpen ?: boolean ;
34+ autoFocusElement ?: RefObject < HTMLElement | null > ;
35+ }
36+
37+ export interface UsePopoverReturn {
38+ triggerProps : {
39+ onClick : ( ) => void ;
40+ 'data-popover-target' : string ;
41+ } ;
42+ popoverProps : PopoverPropsFromHook ;
43+ openPopover : ( ) => void ;
44+ closePopover : ( ) => void ;
45+ isOpen : boolean ;
46+ }
47+
48+ export const usePopover = ( {
49+ defaultOpen = false ,
50+ autoFocusElement,
51+ } : UsePopoverProps ) : UsePopoverReturn => {
52+ const id = useId ( ) ;
53+ const [ isOpen , setIsOpen ] = useState ( defaultOpen ) ;
54+ const { setHasOpenInnerPopup } = useDialogTreeInfo ( ) ;
55+
56+ const openPopover = ( ) => {
57+ setIsOpen ( true ) ;
58+ } ;
59+
60+ const closePopover = ( ) => {
61+ setIsOpen ( false ) ;
62+ } ;
63+
64+ const triggerProps = {
65+ onClick : ( ) => setIsOpen ( prev => ! prev ) ,
66+ 'data-popover-target' : id ,
67+ } ;
68+
69+ const popoverProps = {
70+ anchorName : id ,
71+ isOpen,
72+ setIsOpen,
73+ } ;
74+
75+ useOnValueChange ( ( ) => {
76+ setHasOpenInnerPopup ( isOpen ) ;
77+ } , [ isOpen ] ) ;
78+
79+ useEffect ( ( ) => {
80+ if ( isOpen && autoFocusElement && autoFocusElement . current ) {
81+ autoFocusElement . current . focus ( ) ;
82+ }
83+ } , [ isOpen , autoFocusElement ] ) ;
84+
85+ return { triggerProps, popoverProps, openPopover, closePopover, isOpen } ;
86+ } ;
87+
3288/**
3389 * Popover component, consists of an outer dialog element and an inner content div.
3490 * To style the content div use `${CustomPopover.Content}: { ... }`
3591 */
3692export function CustomPopover ( {
3793 Trigger,
38- open : parentOpen ,
39- defaultOpen ,
40- onOpenChange ,
94+ anchorName ,
95+ isOpen ,
96+ setIsOpen ,
4197 className,
4298 noLock,
43- side = 'top' ,
44- modal,
4599 children,
46100} : React . PropsWithChildren < PopoverProps > ) {
47- const popoverRef = useRef < HTMLDialogElement > ( null ) ;
101+ const popoverRef = useRef < HTMLDivElement > ( null ) ;
48102 const contentRef = useRef < HTMLDivElement > ( null ) ;
49- const id = useId ( ) ;
50103
51- const setElementState = ( state : boolean ) => {
52- if ( state && ! popoverRef . current ?. hasAttribute ( 'open' ) ) {
53- if ( modal ) {
54- popoverRef . current ?. showModal ( ) ;
55- } else {
56- popoverRef . current ?. show ( ) ;
57- }
58- } else if ( ! state && popoverRef . current ?. hasAttribute ( 'open' ) ) {
59- popoverRef . current ?. close ( ) ;
104+ useEffect ( ( ) => {
105+ if ( isOpen && ! popoverRef . current ?. matches ( ':popover-open' ) ) {
106+ popoverRef . current ?. showPopover ( ) ;
107+ } else if ( ! isOpen && popoverRef . current ?. matches ( ':popover-open' ) ) {
108+ popoverRef . current ?. hidePopover ( ) ;
60109 }
61- } ;
62-
63- const onStateChange = ( state : boolean ) => {
64- setElementState ( state ) ;
65- setHasOpenInnerPopup ( state ) ;
66-
67- onOpenChange ?.( state ) ;
68- } ;
69-
70- const [ open , setOpen ] = useControllable ( {
71- controlledValue : parentOpen ,
72- defaultValue : defaultOpen ,
73- onChange : onStateChange ,
74- } ) ;
110+ } , [ isOpen ] ) ;
75111
76- const { setHasOpenInnerPopup } = useDialogTreeInfo ( ) ;
112+ useEffect ( ( ) => {
113+ const handleToggle = ( e : ToggleEvent ) => {
114+ if ( e . newState === 'closed' ) {
115+ setIsOpen ( false ) ;
116+ }
117+ } ;
77118
78- const handleOutsideClick = (
79- e : React . MouseEvent < HTMLDialogElement , MouseEvent > ,
80- ) => {
81- if (
82- ! contentRef . current ?. contains ( e . target as HTMLElement ) &&
83- contentRef . current !== e . target
84- ) {
85- setOpen ( false ) ;
86- }
87- } ;
119+ if ( ! popoverRef . current ) return ;
88120
89- const setElementStateEffect = useEffectEvent ( ( state : boolean ) => {
90- setElementState ( state ) ;
91- } ) ;
121+ const popover = popoverRef . current ;
122+ popover . addEventListener ( 'toggle' , handleToggle ) ;
92123
93- useLayoutEffect ( ( ) => {
94- setElementStateEffect ( ! ! open ) ;
95- } , [ open ] ) ;
124+ return ( ) => {
125+ popover . removeEventListener ( 'toggle' , handleToggle ) ;
126+ } ;
127+ } , [ setIsOpen ] ) ;
96128
97- useControlLock ( ! noLock && ! ! open ) ;
129+ useControlLock ( ! noLock && ! ! isOpen ) ;
98130
99131 return (
100- < Wrapper anchorName = { id } >
101- < Trigger
102- onClick = { ( ) => setOpen ( prev => ! prev ) }
103- data-popover-target = { id }
104- />
132+ < Wrapper anchorName = { anchorName } >
133+ { Trigger }
105134 < Popover
106- anchorName = { id }
135+ anchorName = { anchorName }
107136 popover = 'auto'
108137 ref = { popoverRef }
109- id = { id }
110- side = { side }
111- onMouseDown = { e => handleOutsideClick ( e ) }
138+ id = { anchorName }
112139 className = { className }
113140 >
114- < PopoverContent ref = { contentRef } > { open && children } </ PopoverContent >
141+ < PopoverContent ref = { contentRef } > { isOpen && children } </ PopoverContent >
115142 </ Popover >
116143 </ Wrapper >
117144 ) ;
@@ -124,12 +151,25 @@ CustomPopover.Content = PopoverContent;
124151const Wrapper = styled . div < { anchorName : string } > `
125152 display: contents;
126153
127- & button [data-popover-target='${ p => p . anchorName } '] {
154+ & * [data-popover-target='${ p => p . anchorName } '] {
128155 anchor-name: --${ p => p . anchorName } ;
129156 }
130157` ;
131158
132- const Popover = styled . dialog < { anchorName : string ; side : string } > `
159+ const Popover = styled . div < { anchorName : string } > `
160+ @position-try --top-right {
161+ position-area: top span-right;
162+ }
163+ @position-try --top-left {
164+ position-area: top span-left;
165+ }
166+ @position-try --bottom-right {
167+ position-area: bottom span-right;
168+ }
169+ @position-try --bottom-left {
170+ position-area: bottom span-left;
171+ }
172+
133173 border: none;
134174 background-color: ${ p => transparentize ( 0.2 , p . theme . colors . bgBody ) } ;
135175 backdrop-filter: blur(10px);
@@ -139,11 +179,10 @@ const Popover = styled.dialog<{ anchorName: string; side: string }>`
139179 margin: 0;
140180 padding: 0;
141181 inset: auto;
182+ position: fixed;
142183 position-anchor: --${ p => p . anchorName } ;
143- position-area: ${ p => p . side } ;
144- position-try-fallbacks: flip-block ;
184+ position-area: top center ;
185+ position-try: --top-right, --top-left, --bottom-right, --bottom-left ;
145186 max-height: unset;
146- &::backdrop {
147- background-color: transparent;
148- }
187+ min-width: max-content;
149188` ;
0 commit comments