1
1
import matches from 'dom-helpers/query/matches' ;
2
2
import qsa from 'dom-helpers/query/querySelectorAll' ;
3
- import React from 'react' ;
4
- import ReactDOM from 'react-dom' ;
3
+ import React , { useCallback , useRef , useEffect , useMemo } from 'react' ;
5
4
import PropTypes from 'prop-types' ;
6
- import { uncontrollable } from 'uncontrollable' ;
5
+ import { useUncontrolled } from 'uncontrollable' ;
6
+ import usePrevious from '@restart/hooks/usePrevious' ;
7
+ import useCallbackRef from '@restart/hooks/useCallbackRef' ;
8
+ import useForceUpdate from '@restart/hooks/useForceUpdate' ;
9
+ import useEventCallback from '@restart/hooks/useEventCallback' ;
7
10
8
- import * as Popper from 'react-popper' ;
9
11
import DropdownContext from './DropdownContext' ;
10
12
import DropdownMenu from './DropdownMenu' ;
11
13
import DropdownToggle from './DropdownToggle' ;
@@ -58,6 +60,11 @@ const propTypes = {
58
60
*/
59
61
show : PropTypes . bool ,
60
62
63
+ /**
64
+ * Sets the initial show position of the Dropdown.
65
+ */
66
+ defaultShow : PropTypes . bool ,
67
+
61
68
/**
62
69
* A callback fired when the Dropdown wishes to change visibility. Called with the requested
63
70
* `show` value, the DOM event, and the source that fired it: `'click'`,`'keydown'`,`'rootClose'`, or `'select'`.
@@ -88,82 +95,130 @@ const defaultProps = {
88
95
* - `Dropdown.Toggle` generally a button that triggers the menu opening
89
96
* - `Dropdown.Menu` The overlaid, menu, positioned to the toggle with PopperJs
90
97
*/
91
- class Dropdown extends React . Component {
92
- static displayName = 'ReactOverlaysDropdown' ;
93
-
94
- static getDerivedStateFromProps ( { drop, alignEnd, show } , prevState ) {
95
- const lastShow = prevState . context . show ;
96
- return {
97
- lastShow,
98
- context : {
99
- ...prevState . context ,
100
- drop,
101
- show,
102
- alignEnd,
103
- } ,
104
- } ;
98
+ function Dropdown ( {
99
+ drop,
100
+ alignEnd,
101
+ defaultShow,
102
+ show : rawShow ,
103
+ onToggle : rawOnToggle ,
104
+ itemSelector,
105
+ focusFirstItemOnShow,
106
+ children,
107
+ } ) {
108
+ const forceUpdate = useForceUpdate ( ) ;
109
+ const { show, onToggle } = useUncontrolled (
110
+ { defaultShow, show : rawShow , onToggle : rawOnToggle } ,
111
+ { show : 'onToggle' } ,
112
+ ) ;
113
+
114
+ const [ toggleElement , setToggle ] = useCallbackRef ( ) ;
115
+
116
+ // We use normal refs instead of useCallbackRef in order to populate the
117
+ // the value as quickly as possible, otherwise the effect to focus the element
118
+ // may run before the state value is set
119
+ const menuRef = useRef ( ) ;
120
+ const menuElement = menuRef . current ;
121
+
122
+ const setMenu = useCallback (
123
+ ref => {
124
+ menuRef . current = ref ;
125
+ // ensure that a menu set triggers an update for consumers
126
+ forceUpdate ( ) ;
127
+ } ,
128
+ [ forceUpdate ] ,
129
+ ) ;
130
+
131
+ const lastShow = usePrevious ( show ) ;
132
+ const lastSourceEvent = useRef ( null ) ;
133
+ const focusInDropdown = useRef ( false ) ;
134
+
135
+ const toggle = useCallback (
136
+ event => {
137
+ onToggle ( ! show , event ) ;
138
+ } ,
139
+ [ onToggle , show ] ,
140
+ ) ;
141
+
142
+ const context = useMemo (
143
+ ( ) => ( {
144
+ toggle,
145
+ drop,
146
+ show,
147
+ alignEnd,
148
+ menuElement,
149
+ toggleElement,
150
+ setMenu,
151
+ setToggle,
152
+ } ) ,
153
+ [
154
+ toggle ,
155
+ drop ,
156
+ show ,
157
+ alignEnd ,
158
+ menuElement ,
159
+ toggleElement ,
160
+ setMenu ,
161
+ setToggle ,
162
+ ] ,
163
+ ) ;
164
+
165
+ if ( menuElement && lastShow && ! show ) {
166
+ focusInDropdown . current = menuElement . contains ( document . activeElement ) ;
105
167
}
106
168
107
- constructor ( ...args ) {
108
- super ( ...args ) ;
109
-
110
- this . _focusInDropdown = false ;
111
-
112
- this . menu = null ;
113
-
114
- this . state = {
115
- context : {
116
- close : this . handleClose ,
117
- toggle : this . handleClick ,
118
- menuRef : r => {
119
- this . menu = r ;
120
- } ,
121
- toggleRef : r => {
122
- const toggleNode = r && ReactDOM . findDOMNode ( r ) ;
123
- this . setState ( ( { context } ) => ( {
124
- context : { ...context , toggleNode } ,
125
- } ) ) ;
126
- } ,
127
- } ,
128
- } ;
129
- }
169
+ const focusToggle = useEventCallback ( ( ) => {
170
+ if ( toggleElement && toggleElement . focus ) {
171
+ toggleElement . focus ( ) ;
172
+ }
173
+ } ) ;
130
174
131
- componentDidUpdate ( prevProps ) {
132
- const { show } = this . props ;
133
- const prevOpen = prevProps . show ;
175
+ const maybeFocusFirst = useEventCallback ( ( ) => {
176
+ const type = lastSourceEvent . current ;
177
+ let focusType = focusFirstItemOnShow ;
134
178
135
- if ( show && ! prevOpen ) {
136
- this . maybeFocusFirst ( ) ;
179
+ if ( focusType == null ) {
180
+ focusType =
181
+ menuRef . current && matches ( menuRef . current , '[role=menu]' )
182
+ ? 'keyboard'
183
+ : false ;
137
184
}
138
- this . _lastSourceEvent = null ;
139
-
140
- if ( ! show && prevOpen ) {
141
- // if focus hasn't already moved from the menu let's return it
142
- // to the toggle
143
- if ( this . _focusInDropdown ) {
144
- this . _focusInDropdown = false ;
145
- this . focus ( ) ;
146
- }
185
+
186
+ if (
187
+ focusType === false ||
188
+ ( focusType === 'keyboard' && ! / ^ k e y .+ $ / . test ( type ) )
189
+ ) {
190
+ return ;
147
191
}
148
- }
149
192
150
- getNextFocusedChild ( current , offset ) {
151
- if ( ! this . menu ) return null ;
193
+ let first = qsa ( menuRef . current , itemSelector ) [ 0 ] ;
194
+ if ( first && first . focus ) first . focus ( ) ;
195
+ } ) ;
196
+
197
+ useEffect ( ( ) => {
198
+ if ( show ) maybeFocusFirst ( ) ;
199
+ else if ( focusInDropdown . current ) {
200
+ focusInDropdown . current = false ;
201
+ focusToggle ( ) ;
202
+ }
203
+ // only `show` should be changing
204
+ } , [ show , focusInDropdown , focusToggle , maybeFocusFirst ] ) ;
152
205
153
- const { itemSelector } = this . props ;
154
- let items = qsa ( this . menu , itemSelector ) ;
206
+ useEffect ( ( ) => {
207
+ lastSourceEvent . current = null ;
208
+ } ) ;
209
+
210
+ const getNextFocusedChild = ( current , offset ) => {
211
+ if ( ! menuRef . current ) return null ;
212
+
213
+ let items = qsa ( menuRef . current , itemSelector ) ;
155
214
156
215
let index = items . indexOf ( current ) + offset ;
157
216
index = Math . max ( 0 , Math . min ( index , items . length ) ) ;
158
217
159
218
return items [ index ] ;
160
- }
161
-
162
- handleClick = event => {
163
- this . toggleOpen ( event ) ;
164
219
} ;
165
220
166
- handleKeyDown = event => {
221
+ const handleKeyDown = event => {
167
222
const { key, target } = event ;
168
223
169
224
// Second only to https://github.com/twbs/bootstrap/blob/8cfbf6933b8a0146ac3fbc369f19e520bd1ebdac/js/src/dropdown.js#L400
@@ -172,99 +227,53 @@ class Dropdown extends React.Component {
172
227
if (
173
228
isInput &&
174
229
( key === ' ' ||
175
- ( key !== 'Escape' && this . menu && this . menu . contains ( target ) ) )
230
+ ( key !== 'Escape' &&
231
+ menuRef . current &&
232
+ menuRef . current . contains ( target ) ) )
176
233
) {
177
234
return ;
178
235
}
179
236
180
- this . _lastSourceEvent = event . type ;
237
+ lastSourceEvent . current = event . type ;
181
238
182
239
switch ( key ) {
183
240
case 'ArrowUp' : {
184
- let next = this . getNextFocusedChild ( target , - 1 ) ;
241
+ let next = getNextFocusedChild ( target , - 1 ) ;
185
242
if ( next && next . focus ) next . focus ( ) ;
186
243
event . preventDefault ( ) ;
187
244
188
245
return ;
189
246
}
190
247
case 'ArrowDown' :
191
248
event . preventDefault ( ) ;
192
- if ( ! this . props . show ) {
193
- this . toggleOpen ( event ) ;
249
+ if ( ! show ) {
250
+ toggle ( event ) ;
194
251
} else {
195
- let next = this . getNextFocusedChild ( target , 1 ) ;
252
+ let next = getNextFocusedChild ( target , 1 ) ;
196
253
if ( next && next . focus ) next . focus ( ) ;
197
254
}
198
255
return ;
199
256
case 'Escape' :
200
257
case 'Tab' :
201
- this . props . onToggle ( false , event ) ;
258
+ onToggle ( false , event ) ;
202
259
break ;
203
260
default :
204
261
}
205
262
} ;
206
263
207
- hasMenuRole ( ) {
208
- return this . menu && matches ( this . menu , '[role=menu]' ) ;
209
- }
210
-
211
- focus ( ) {
212
- const { toggleNode } = this . state . context ;
213
- if ( toggleNode && toggleNode . focus ) {
214
- toggleNode . focus ( ) ;
215
- }
216
- }
217
-
218
- maybeFocusFirst ( ) {
219
- const type = this . _lastSourceEvent ;
220
- let { focusFirstItemOnShow } = this . props ;
221
- if ( focusFirstItemOnShow == null ) {
222
- focusFirstItemOnShow = this . hasMenuRole ( ) ? 'keyboard' : false ;
223
- }
224
-
225
- if (
226
- focusFirstItemOnShow === false ||
227
- ( focusFirstItemOnShow === 'keyboard' && ! / ^ k e y .+ $ / . test ( type ) )
228
- ) {
229
- return ;
230
- }
231
-
232
- const { itemSelector } = this . props ;
233
- let first = qsa ( this . menu , itemSelector ) [ 0 ] ;
234
- if ( first && first . focus ) first . focus ( ) ;
235
- }
236
-
237
- toggleOpen ( event ) {
238
- let show = ! this . props . show ;
239
-
240
- this . props . onToggle ( show , event ) ;
241
- }
242
-
243
- render ( ) {
244
- const { children, ...props } = this . props ;
245
-
246
- delete props . onToggle ;
247
-
248
- if ( this . menu && this . state . lastShow && ! this . props . show ) {
249
- this . _focusInDropdown = this . menu . contains ( document . activeElement ) ;
250
- }
251
-
252
- return (
253
- < DropdownContext . Provider value = { this . state . context } >
254
- < Popper . Manager >
255
- { children ( { props : { onKeyDown : this . handleKeyDown } } ) }
256
- </ Popper . Manager >
257
- </ DropdownContext . Provider >
258
- ) ;
259
- }
264
+ return (
265
+ < DropdownContext . Provider value = { context } >
266
+ { children ( { props : { onKeyDown : handleKeyDown } } ) }
267
+ </ DropdownContext . Provider >
268
+ ) ;
260
269
}
261
270
271
+ Dropdown . displayName = 'ReactOverlaysDropdown' ;
272
+
262
273
Dropdown . propTypes = propTypes ;
263
274
Dropdown . defaultProps = defaultProps ;
264
275
265
- const UncontrolledDropdown = uncontrollable ( Dropdown , { show : 'onToggle' } ) ;
266
-
267
- UncontrolledDropdown . Menu = DropdownMenu ;
268
- UncontrolledDropdown . Toggle = DropdownToggle ;
276
+ Dropdown . Menu = DropdownMenu ;
277
+ Dropdown . Toggle = DropdownToggle ;
269
278
270
- export default UncontrolledDropdown ;
279
+ export default Dropdown ;
0 commit comments