@@ -6,16 +6,18 @@ import * as React from 'react'
66export type Variant = 'popover' | 'popper'
77
88export type PopupState = {
9- open : ( eventOrAnchorEl : SyntheticEvent < any > | HTMLElement ) => void ,
9+ open : ( eventOrAnchorEl ? : SyntheticEvent < any > | HTMLElement ) => void ,
1010 close : ( ) => void ,
11- toggle : ( eventOrAnchorEl : SyntheticEvent < any > | HTMLElement ) => void ,
11+ toggle : ( eventOrAnchorEl ? : SyntheticEvent < any > | HTMLElement ) => void ,
1212 onMouseLeave : ( event : SyntheticEvent < any > ) => void ,
1313 setOpen : (
1414 open : boolean ,
1515 eventOrAnchorEl ?: SyntheticEvent < any > | HTMLElement
1616 ) => void ,
1717 isOpen : boolean ,
1818 anchorEl : ?HTMLElement ,
19+ setAnchorEl : ( ?HTMLElement ) => any ,
20+ setAnchorElUsed : boolean ,
1921 popupId : ?string ,
2022 variant : Variant ,
2123 _childPopupState : ?PopupState ,
@@ -25,20 +27,24 @@ export type PopupState = {
2527let eventOrAnchorElWarned : boolean = false
2628
2729export type CoreState = {
30+ isOpen : boolean ,
31+ setAnchorElUsed : boolean ,
2832 anchorEl : ?HTMLElement ,
2933 hovered : boolean ,
3034 _childPopupState : ?PopupState ,
3135}
3236
3337export const initCoreState : CoreState = {
38+ isOpen : false ,
39+ setAnchorElUsed : false ,
3440 anchorEl : null ,
3541 hovered : false ,
3642 _childPopupState : null ,
3743}
3844
3945export function createPopupState ( {
40- state : { anchorEl , hovered , _childPopupState } ,
41- setState,
46+ state,
47+ setState : _setState ,
4248 parentPopupState,
4349 popupId,
4450 variant,
@@ -49,15 +55,34 @@ export function createPopupState({
4955 variant : Variant ,
5056 parentPopupState ?: ?PopupState ,
5157} ) : PopupState {
52- const toggle = ( eventOrAnchorEl : SyntheticEvent < any > | HTMLElement ) => {
53- if ( anchorEl ) close ( )
58+ const { isOpen, setAnchorElUsed, anchorEl, hovered, _childPopupState } = state
59+
60+ // use lastState to workaround cases where setState is called multiple times
61+ // in a single render (e.g. because of refs being called multiple times)
62+ let lastState = state
63+ const setState = ( nextState : $Shape < CoreState > ) => {
64+ if ( hasChanges ( lastState , nextState ) ) {
65+ _setState ( ( lastState = { ...lastState , ...nextState } ) )
66+ }
67+ }
68+
69+ const setAnchorEl = ( _anchorEl : ?HTMLElement ) => {
70+ setState ( { setAnchorElUsed : true , anchorEl : _anchorEl } )
71+ }
72+
73+ const toggle = ( eventOrAnchorEl ?: SyntheticEvent < any > | HTMLElement ) => {
74+ if ( isOpen ) close ( )
5475 else open ( eventOrAnchorEl )
5576 }
5677
57- const open = ( eventOrAnchorEl : SyntheticEvent < any > | HTMLElement ) => {
58- if ( ! eventOrAnchorElWarned && ! eventOrAnchorEl ) {
78+ const open = ( eventOrAnchorEl ? : SyntheticEvent < any > | HTMLElement ) => {
79+ if ( ! eventOrAnchorElWarned && ! eventOrAnchorEl && ! setAnchorElUsed ) {
5980 eventOrAnchorElWarned = true
60- console . error ( 'eventOrAnchorEl should be defined' ) // eslint-disable-line no-console
81+ /* eslint-disable no-console */
82+ console . error (
83+ 'eventOrAnchorEl should be defined if setAnchorEl is not used'
84+ )
85+ /* eslint-enable no-console */
6186 }
6287 if ( parentPopupState ) {
6388 if ( ! parentPopupState . isOpen ) return
@@ -66,29 +91,34 @@ export function createPopupState({
6691 if ( typeof document === 'object' && document . activeElement ) {
6792 document . activeElement . blur ( )
6893 }
69- setState ( {
70- anchorEl :
71- eventOrAnchorEl && eventOrAnchorEl . currentTarget
72- ? ( eventOrAnchorEl . currentTarget : any )
73- : ( eventOrAnchorEl : any ) ,
74- hovered : ( eventOrAnchorEl : any ) . type === 'mouseenter' ,
75- } )
94+
95+ const newState : $Shape < CoreState > = {
96+ isOpen : true ,
97+ hovered : eventOrAnchorEl && ( eventOrAnchorEl : any ) . type === 'mouseenter' ,
98+ }
99+
100+ if ( eventOrAnchorEl && eventOrAnchorEl . currentTarget ) {
101+ if ( ! setAnchorElUsed ) {
102+ newState . anchorEl = ( eventOrAnchorEl . currentTarget : any )
103+ }
104+ } else if ( eventOrAnchorEl ) {
105+ newState . anchorEl = ( eventOrAnchorEl : any )
106+ }
107+
108+ setState ( newState )
76109 }
77110
78111 const close = ( ) => {
79112 if ( _childPopupState ) _childPopupState . close ( )
80113 if ( parentPopupState ) parentPopupState . _setChildPopupState ( null )
81- setState ( { anchorEl : null , hovered : false } )
114+ setState ( { isOpen : false , hovered : false } )
82115 }
83116
84117 const setOpen = (
85118 nextOpen : boolean ,
86119 eventOrAnchorEl ?: SyntheticEvent < any > | HTMLElement
87120 ) => {
88121 if ( nextOpen ) {
89- if ( ! eventOrAnchorEl ) {
90- throw new Error ( 'eventOrAnchorEl must be defined when opening' )
91- }
92122 open ( eventOrAnchorEl )
93123 } else close ( )
94124 }
@@ -104,9 +134,11 @@ export function createPopupState({
104134
105135 const popupState = {
106136 anchorEl,
137+ setAnchorEl,
138+ setAnchorElUsed,
107139 popupId,
108140 variant,
109- isOpen : anchorEl != null ,
141+ isOpen,
110142 open,
111143 close,
112144 toggle,
@@ -119,6 +151,18 @@ export function createPopupState({
119151 return popupState
120152}
121153
154+ /**
155+ * Creates a ref that sets the anchorEl for the popup.
156+ *
157+ * @param {object } popupState the argument passed to the child function of
158+ * `PopupState`
159+ */
160+ export function anchorRef ( { setAnchorEl } : PopupState ) : ( ?HTMLElement ) => any {
161+ return ( el : ?HTMLElement ) => {
162+ if ( el ) setAnchorEl ( el )
163+ }
164+ }
165+
122166/**
123167 * Creates props for a component that opens the popup when clicked.
124168 *
@@ -133,14 +177,14 @@ export function bindTrigger({
133177} : PopupState ) : {
134178 'aria-owns' ?: ?string ,
135179 'aria-describedby' ?: ?string ,
136- 'aria-haspopup' : true ,
180+ 'aria-haspopup' : ? true ,
137181 onClick : ( event : SyntheticEvent < any > ) => void ,
138182} {
139183 return {
140184 [ variant === 'popover' ? 'aria-owns' : 'aria-describedby' ] : isOpen
141185 ? popupId
142186 : null ,
143- 'aria-haspopup' : true ,
187+ 'aria-haspopup' : variant === 'popover' ? true : undefined ,
144188 onClick : open ,
145189 }
146190}
@@ -159,14 +203,14 @@ export function bindToggle({
159203} : PopupState ) : {
160204 'aria-owns' ?: ?string ,
161205 'aria-describedby' ?: ?string ,
162- 'aria-haspopup' : true ,
206+ 'aria-haspopup' : ? true ,
163207 onClick : ( event : SyntheticEvent < any > ) => void ,
164208} {
165209 return {
166210 [ variant === 'popover' ? 'aria-owns' : 'aria-describedby' ] : isOpen
167211 ? popupId
168212 : null ,
169- 'aria-haspopup' : true ,
213+ 'aria-haspopup' : variant === 'popover' ? true : undefined ,
170214 onClick : toggle ,
171215 }
172216}
@@ -186,15 +230,15 @@ export function bindHover({
186230} : PopupState ) : {
187231 'aria-owns' ?: ?string ,
188232 'aria-describedby' ?: ?string ,
189- 'aria-haspopup' : true ,
233+ 'aria-haspopup' : ? true ,
190234 onMouseEnter : ( event : SyntheticEvent < any > ) => any ,
191235 onMouseLeave : ( event : SyntheticEvent < any > ) => any ,
192236} {
193237 return {
194238 [ variant === 'popover' ? 'aria-owns' : 'aria-describedby' ] : isOpen
195239 ? popupId
196240 : null ,
197- 'aria-haspopup' : true ,
241+ 'aria-haspopup' : variant === 'popover' ? true : undefined ,
198242 onMouseEnter : open ,
199243 onMouseLeave,
200244 }
@@ -284,3 +328,12 @@ function isAncestor(parent: ?Element, child: ?Element): boolean {
284328 }
285329 return false
286330}
331+
332+ function hasChanges ( state : CoreState , nextState : $Shape < CoreState > ) : boolean {
333+ for ( let key in nextState ) {
334+ if ( state . hasOwnProperty ( key ) && state [ key ] !== nextState [ key ] ) {
335+ return true
336+ }
337+ }
338+ return false
339+ }
0 commit comments