@@ -12,7 +12,6 @@ import {
12
12
type ReactNode ,
13
13
type RefObject ,
14
14
useContext ,
15
- useEffect ,
16
15
useId ,
17
16
useRef ,
18
17
useState ,
@@ -25,14 +24,12 @@ type TriggerRef = RefObject<HTMLElement>;
25
24
type TriggerElement = ReactElement < {
26
25
ref : TriggerRef ;
27
26
onClick ?: ( ) => void ;
28
- "aria-haspopup" ?: boolean ;
29
- "aria-owns" ?: string | undefined ;
30
27
} > ;
31
28
32
29
type PopoverContextValue = {
33
30
id : string ;
34
- isOpen : boolean ;
35
- setIsOpen : React . Dispatch < React . SetStateAction < boolean > > ;
31
+ open : boolean ;
32
+ setOpen : ( open : boolean ) => void ;
36
33
triggerRef : TriggerRef ;
37
34
mode : TriggerMode ;
38
35
} ;
@@ -41,32 +38,40 @@ const PopoverContext = createContext<PopoverContextValue | undefined>(
41
38
undefined ,
42
39
) ;
43
40
44
- export interface PopoverProps {
45
- children : ReactNode | ( ( popover : PopoverContextValue ) => ReactNode ) ; // Allows inline usage
41
+ type BasePopoverProps = {
42
+ children : ReactNode ;
46
43
mode ?: TriggerMode ;
47
- isDefaultOpen ?: boolean ;
48
- }
44
+ } ;
49
45
50
- export const Popover : FC < PopoverProps > = ( {
51
- children,
52
- mode,
53
- isDefaultOpen,
54
- } ) => {
46
+ type UncontrolledPopoverProps = BasePopoverProps & {
47
+ open ?: undefined ;
48
+ onOpenChange ?: undefined ;
49
+ } ;
50
+
51
+ type ControlledPopoverProps = BasePopoverProps & {
52
+ open : boolean ;
53
+ onOpenChange : ( open : boolean ) => void ;
54
+ } ;
55
+
56
+ export type PopoverProps = UncontrolledPopoverProps | ControlledPopoverProps ;
57
+
58
+ export const Popover : FC < PopoverProps > = ( props ) => {
55
59
const hookId = useId ( ) ;
56
- const [ isOpen , setIsOpen ] = useState ( isDefaultOpen ?? false ) ;
57
- const triggerRef = useRef < HTMLElement > ( null ) ;
60
+ const [ open , setOpen ] = useState ( false ) ;
61
+ const triggerRef : TriggerRef = useRef ( null ) ;
62
+ const isControlled = props . open !== undefined ;
58
63
59
64
const value : PopoverContextValue = {
60
- isOpen,
61
- setIsOpen,
62
65
triggerRef,
63
66
id : `${ hookId } -popover` ,
64
- mode : mode ?? "click" ,
67
+ mode : props . mode ?? "click" ,
68
+ open : isControlled ? props . open : open ,
69
+ setOpen : isControlled ? props . onOpenChange : setOpen ,
65
70
} ;
66
71
67
72
return (
68
73
< PopoverContext . Provider value = { value } >
69
- { typeof children === "function" ? children ( value ) : children }
74
+ { props . children }
70
75
</ PopoverContext . Provider >
71
76
) ;
72
77
} ;
@@ -82,31 +87,33 @@ export const usePopover = () => {
82
87
} ;
83
88
84
89
export const PopoverTrigger = (
85
- props : HTMLAttributes < HTMLElement > & { children : TriggerElement } ,
90
+ props : HTMLAttributes < HTMLElement > & {
91
+ children : TriggerElement ;
92
+ } ,
86
93
) => {
87
94
const popover = usePopover ( ) ;
88
95
const { children, ...elementProps } = props ;
89
96
90
97
const clickProps = {
91
98
onClick : ( ) => {
92
- popover . setIsOpen ( ( isOpen ) => ! isOpen ) ;
99
+ popover . setOpen ( true ) ;
93
100
} ,
94
101
} ;
95
102
96
103
const hoverProps = {
97
104
onPointerEnter : ( ) => {
98
- popover . setIsOpen ( true ) ;
105
+ popover . setOpen ( true ) ;
99
106
} ,
100
107
onPointerLeave : ( ) => {
101
- popover . setIsOpen ( false ) ;
108
+ popover . setOpen ( false ) ;
102
109
} ,
103
110
} ;
104
111
105
112
return cloneElement ( props . children , {
106
113
...elementProps ,
107
114
...( popover . mode === "click" ? clickProps : hoverProps ) ,
108
115
"aria-haspopup" : true ,
109
- "aria-owns" : popover . isOpen ? popover . id : undefined ,
116
+ "aria-owns" : popover . open ? popover . id : undefined ,
110
117
ref : popover . triggerRef ,
111
118
} ) ;
112
119
} ;
@@ -125,22 +132,8 @@ export const PopoverContent: FC<PopoverContentProps> = ({
125
132
...popoverProps
126
133
} ) => {
127
134
const popover = usePopover ( ) ;
128
- const [ isReady , setIsReady ] = useState ( false ) ;
129
135
const hoverMode = popover . mode === "hover" ;
130
136
131
- // This is a hack to make sure the popover is not rendered until the trigger
132
- // is ready. This is a limitation on MUI that does not support defaultIsOpen
133
- // on Popover but we need it to storybook the component.
134
- useEffect ( ( ) => {
135
- if ( ! isReady && popover . triggerRef . current !== null ) {
136
- setIsReady ( true ) ;
137
- }
138
- } , [ isReady , popover . triggerRef ] ) ;
139
-
140
- if ( ! popover . triggerRef . current ) {
141
- return null ;
142
- }
143
-
144
137
return (
145
138
< MuiPopover
146
139
disablePortal
@@ -161,8 +154,8 @@ export const PopoverContent: FC<PopoverContentProps> = ({
161
154
{ ...modeProps ( popover ) }
162
155
{ ...popoverProps }
163
156
id = { popover . id }
164
- open = { popover . isOpen }
165
- onClose = { ( ) => popover . setIsOpen ( false ) }
157
+ open = { popover . open }
158
+ onClose = { ( ) => popover . setOpen ( false ) }
166
159
anchorEl = { popover . triggerRef . current }
167
160
/>
168
161
) ;
@@ -172,10 +165,10 @@ const modeProps = (popover: PopoverContextValue) => {
172
165
if ( popover . mode === "hover" ) {
173
166
return {
174
167
onPointerEnter : ( ) => {
175
- popover . setIsOpen ( true ) ;
168
+ popover . setOpen ( true ) ;
176
169
} ,
177
170
onPointerLeave : ( ) => {
178
- popover . setIsOpen ( false ) ;
171
+ popover . setOpen ( false ) ;
179
172
} ,
180
173
} ;
181
174
}
0 commit comments