@@ -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,41 @@ 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
+ // By separating controlled and uncontrolled props, we achieve more accurate
47
+ // type inference.
48
+ type UncontrolledPopoverProps = BasePopoverProps & {
49
+ open ?: undefined ;
50
+ onOpenChange ?: undefined ;
51
+ } ;
52
+
53
+ type ControlledPopoverProps = BasePopoverProps & {
54
+ open : boolean ;
55
+ onOpenChange : ( open : boolean ) => void ;
56
+ } ;
57
+
58
+ export type PopoverProps = UncontrolledPopoverProps | ControlledPopoverProps ;
59
+
60
+ export const Popover : FC < PopoverProps > = ( props ) => {
55
61
const hookId = useId ( ) ;
56
- const [ isOpen , setIsOpen ] = useState ( isDefaultOpen ?? false ) ;
57
- const triggerRef = useRef < HTMLElement > ( null ) ;
62
+ const [ uncontrolledOpen , setUncontrolledOpen ] = useState ( false ) ;
63
+ const triggerRef : TriggerRef = useRef ( null ) ;
58
64
59
65
const value : PopoverContextValue = {
60
- isOpen,
61
- setIsOpen,
62
66
triggerRef,
63
67
id : `${ hookId } -popover` ,
64
- mode : mode ?? "click" ,
68
+ mode : props . mode ?? "click" ,
69
+ open : props . open ?? uncontrolledOpen ,
70
+ setOpen : props . onOpenChange ?? setUncontrolledOpen ,
65
71
} ;
66
72
67
73
return (
68
74
< PopoverContext . Provider value = { value } >
69
- { typeof children === "function" ? children ( value ) : children }
75
+ { props . children }
70
76
</ PopoverContext . Provider >
71
77
) ;
72
78
} ;
@@ -82,31 +88,34 @@ export const usePopover = () => {
82
88
} ;
83
89
84
90
export const PopoverTrigger = (
85
- props : HTMLAttributes < HTMLElement > & { children : TriggerElement } ,
91
+ props : HTMLAttributes < HTMLElement > & {
92
+ children : TriggerElement ;
93
+ } ,
86
94
) => {
87
95
const popover = usePopover ( ) ;
88
96
const { children, ...elementProps } = props ;
89
97
90
98
const clickProps = {
91
99
onClick : ( ) => {
92
- popover . setIsOpen ( ( isOpen ) => ! isOpen ) ;
100
+ popover . setOpen ( true ) ;
93
101
} ,
94
102
} ;
95
103
96
104
const hoverProps = {
97
105
onPointerEnter : ( ) => {
98
- popover . setIsOpen ( true ) ;
106
+ popover . setOpen ( true ) ;
99
107
} ,
100
108
onPointerLeave : ( ) => {
101
- popover . setIsOpen ( false ) ;
109
+ popover . setOpen ( false ) ;
102
110
} ,
103
111
} ;
104
112
105
113
return cloneElement ( props . children , {
106
114
...elementProps ,
107
115
...( popover . mode === "click" ? clickProps : hoverProps ) ,
108
116
"aria-haspopup" : true ,
109
- "aria-owns" : popover . isOpen ? popover . id : undefined ,
117
+ "aria-owns" : popover . id ,
118
+ "aria-expanded" : popover . open ,
110
119
ref : popover . triggerRef ,
111
120
} ) ;
112
121
} ;
@@ -125,22 +134,8 @@ export const PopoverContent: FC<PopoverContentProps> = ({
125
134
...popoverProps
126
135
} ) => {
127
136
const popover = usePopover ( ) ;
128
- const [ isReady , setIsReady ] = useState ( false ) ;
129
137
const hoverMode = popover . mode === "hover" ;
130
138
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
139
return (
145
140
< MuiPopover
146
141
disablePortal
@@ -161,8 +156,8 @@ export const PopoverContent: FC<PopoverContentProps> = ({
161
156
{ ...modeProps ( popover ) }
162
157
{ ...popoverProps }
163
158
id = { popover . id }
164
- open = { popover . isOpen }
165
- onClose = { ( ) => popover . setIsOpen ( false ) }
159
+ open = { popover . open }
160
+ onClose = { ( ) => popover . setOpen ( false ) }
166
161
anchorEl = { popover . triggerRef . current }
167
162
/>
168
163
) ;
@@ -172,10 +167,10 @@ const modeProps = (popover: PopoverContextValue) => {
172
167
if ( popover . mode === "hover" ) {
173
168
return {
174
169
onPointerEnter : ( ) => {
175
- popover . setIsOpen ( true ) ;
170
+ popover . setOpen ( true ) ;
176
171
} ,
177
172
onPointerLeave : ( ) => {
178
- popover . setIsOpen ( false ) ;
173
+ popover . setOpen ( false ) ;
179
174
} ,
180
175
} ;
181
176
}
0 commit comments