@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
3
3
import classNames from 'classnames' ;
4
4
import scrollIntoView from 'scroll-into-view-if-needed' ;
5
5
import { isNil , focus , cancelEvent } from '../../helpers/statics' ;
6
+ import { optionMenuItemShape } from '../../helpers/customPropTypes' ;
6
7
7
8
import {
8
9
UP_KEY_CODE ,
@@ -15,21 +16,20 @@ import {
15
16
} from '../../constants' ;
16
17
17
18
import OptionMenuListItem from './OptionMenuListItem' ;
18
- import Icon from '../../library/icon' ;
19
19
20
20
const propTypes = {
21
21
id : PropTypes . string . isRequired ,
22
22
multiple : PropTypes . bool ,
23
23
autocomplete : PropTypes . bool ,
24
24
showCancel : PropTypes . bool ,
25
25
options : PropTypes . arrayOf (
26
- PropTypes . shape ( {
27
- value : PropTypes . oneOfType ( [ PropTypes . string , PropTypes . number ] )
28
- . isRequired ,
29
- label : PropTypes . node . isRequired ,
30
- icon : PropTypes . oneOf ( Icon . AVAILABLE_ICONS ) ,
31
- disabled : PropTypes . bool ,
32
- } ) ,
26
+ PropTypes . oneOfType ( [
27
+ PropTypes . shape ( optionMenuItemShape ) ,
28
+ PropTypes . shape ( {
29
+ ... optionMenuItemShape ,
30
+ value : PropTypes . arrayOf ( PropTypes . shape ( optionMenuItemShape ) ) ,
31
+ } ) ,
32
+ ] ) ,
33
33
) ,
34
34
selected : PropTypes . oneOfType ( [
35
35
PropTypes . string ,
@@ -72,10 +72,16 @@ const defaultProps = {
72
72
73
73
const getOptionId = ( id , value ) => `${ id } -${ value } ` ;
74
74
75
+ const getFocusableOptions = options =>
76
+ options . map ( opt => ( Array . isArray ( opt . value ) ? opt . value : opt ) ) . flat ( ) ;
77
+
75
78
const getFocusedId = ( focusedIndex , id , options ) =>
76
- isNil ( focusedIndex ) || focusedIndex >= options . length
79
+ typeof focusedIndex !== 'number' || focusedIndex >= options . length
77
80
? undefined
78
- : getOptionId ( id , options [ focusedIndex ] . value ) ;
81
+ : getOptionId (
82
+ id ,
83
+ getFocusableOptions ( options ) [ Math . max ( focusedIndex , 0 ) ] . value ,
84
+ ) ;
79
85
80
86
const getSelectionSet = selection =>
81
87
new Set (
@@ -134,7 +140,11 @@ class OptionMenuList extends Component {
134
140
}
135
141
136
142
onMouseEnterItem ( focusedIndex ) {
137
- this . focusItem ( focusedIndex ) ;
143
+ if ( typeof focusedIndex === 'number' ) {
144
+ this . focusItem ( focusedIndex ) ;
145
+ } else {
146
+ this . setState ( { focusedIndex : null } ) ;
147
+ }
138
148
}
139
149
140
150
onCancel ( e ) {
@@ -161,7 +171,9 @@ class OptionMenuList extends Component {
161
171
if ( isNil ( focusedIndex ) ) {
162
172
this . focusFirst ( ) ;
163
173
} else {
164
- this . focusItem ( Math . min ( options . length - 1 , focusedIndex + 1 ) ) ;
174
+ this . focusItem (
175
+ Math . min ( getFocusableOptions ( options ) . length - 1 , focusedIndex + 1 ) ,
176
+ ) ;
165
177
}
166
178
}
167
179
@@ -192,7 +204,7 @@ class OptionMenuList extends Component {
192
204
}
193
205
case SPACE_KEY_CODE :
194
206
case ENTER_KEY_CODE : {
195
- const focused = options [ focusedIndex ] ;
207
+ const focused = getFocusableOptions ( options ) [ focusedIndex ] ;
196
208
if ( focused && ! focused . disabled ) {
197
209
this . selectFocusedItem ( ) ;
198
210
onClickItem ( ) ;
@@ -251,7 +263,7 @@ class OptionMenuList extends Component {
251
263
focusLast ( ) {
252
264
const { options } = this . props ;
253
265
254
- this . focusItem ( options . length - 1 ) ;
266
+ this . focusItem ( getFocusableOptions ( options ) . length - 1 ) ;
255
267
}
256
268
257
269
focusItem ( focusedIndex ) {
@@ -296,25 +308,14 @@ class OptionMenuList extends Component {
296
308
const { options } = this . props ;
297
309
298
310
if ( ! isNil ( focusedIndex ) ) {
299
- const { value } = options [ focusedIndex ] ;
311
+ const { value } = getFocusableOptions ( options ) [ focusedIndex ] ;
300
312
301
313
this . select ( value ) ;
302
314
}
303
315
}
304
316
305
317
/* eslint-disable jsx-a11y/click-events-have-key-events */
306
318
render ( ) {
307
- const {
308
- onClickItem,
309
- onMouseEnterItem,
310
- onCancel,
311
- onKeyDown,
312
- onKeyDownInAction,
313
- onFocus,
314
- onMenuBlur,
315
- onActionBlur,
316
- } = this ;
317
- const { focusedIndex } = this . state ;
318
319
const {
319
320
id,
320
321
options,
@@ -329,7 +330,6 @@ class OptionMenuList extends Component {
329
330
className,
330
331
style,
331
332
onBlur,
332
- focusedIndex : focussed ,
333
333
onFocusItem,
334
334
footer,
335
335
onClickItem : onClick ,
@@ -340,8 +340,95 @@ class OptionMenuList extends Component {
340
340
return null ;
341
341
}
342
342
343
+ const {
344
+ onClickItem,
345
+ onMouseEnterItem,
346
+ onCancel,
347
+ onKeyDown,
348
+ onKeyDownInAction,
349
+ onFocus,
350
+ onMenuBlur,
351
+ onActionBlur,
352
+ } = this ;
353
+
343
354
const selectionSet = getSelectionSet ( selected ) ;
344
- const focusedId = getFocusedId ( focusedIndex , id , options ) ;
355
+
356
+ delete rest . focusedIndex ;
357
+ const { focusedIndex } = this . state ;
358
+ const focusedId = getFocusedId (
359
+ focusedIndex ,
360
+ id ,
361
+ getFocusableOptions ( options ) ,
362
+ ) ;
363
+
364
+ const renderListItems = ( items , offset = 0 ) => {
365
+ const list = [ ] ;
366
+
367
+ items . forEach ( item => {
368
+ if ( Array . isArray ( item . value ) ) {
369
+ const groupId = `group-${ item . value
370
+ . map ( child => child . value )
371
+ . join ( '-' ) } `;
372
+ const labelId = `${ groupId } -label` ;
373
+
374
+ list . push (
375
+ < ul
376
+ role = "group"
377
+ aria-labelledby = { labelId }
378
+ className = "rc-menu-list-group"
379
+ id = { groupId }
380
+ key = { groupId }
381
+ >
382
+ { item . label && (
383
+ < OptionMenuListItem
384
+ type = "heading"
385
+ disabled = { item . disabled }
386
+ id = { labelId }
387
+ key = { labelId }
388
+ onMouseEnter = { ( ) => onMouseEnterItem ( null ) }
389
+ >
390
+ { item . label }
391
+ </ OptionMenuListItem >
392
+ ) }
393
+ { renderListItems (
394
+ item . value . map ( child =>
395
+ Object . assign ( child , {
396
+ disabled : item . disabled || child . disabled ,
397
+ } ) ,
398
+ ) ,
399
+ list . length + offset ,
400
+ ) }
401
+ </ ul > ,
402
+ ) ;
403
+ // eslint-disable-next-line no-param-reassign
404
+ offset += item . value . length - 1 ;
405
+ } else {
406
+ const index = list . length + offset ;
407
+ list . push (
408
+ < OptionMenuListItem
409
+ id = { getOptionId ( id , item . value ) }
410
+ key = { item . value }
411
+ focused = { index === focusedIndex }
412
+ selected = { selectionSet . has ( item . value ) }
413
+ icon = { item . icon }
414
+ svg = { item . svg }
415
+ disabled = { item . disabled }
416
+ onClick = { ( ) =>
417
+ item . disabled ? undefined : onClickItem ( item . value )
418
+ }
419
+ onMouseEnter = { ( ) => onMouseEnterItem ( index ) }
420
+ ref = { option => {
421
+ this . optionRefs [ index ] = option ;
422
+ } }
423
+ >
424
+ { item . label }
425
+ </ OptionMenuListItem > ,
426
+ ) ;
427
+ }
428
+ } ) ;
429
+
430
+ return list ;
431
+ } ;
345
432
346
433
const list = (
347
434
< ul
@@ -359,24 +446,7 @@ class OptionMenuList extends Component {
359
446
} }
360
447
{ ...rest }
361
448
>
362
- { options . map ( ( { value, label, icon, svg, disabled } , index ) => (
363
- < OptionMenuListItem
364
- id = { getOptionId ( id , value ) }
365
- key = { value }
366
- focused = { index === focusedIndex }
367
- selected = { selectionSet . has ( value ) }
368
- icon = { icon }
369
- svg = { svg }
370
- disabled = { disabled }
371
- onClick = { disabled ? undefined : ( ) => onClickItem ( value ) }
372
- onMouseEnter = { ( ) => onMouseEnterItem ( index ) }
373
- ref = { option => {
374
- this . optionRefs [ index ] = option ;
375
- } }
376
- >
377
- { label }
378
- </ OptionMenuListItem >
379
- ) ) }
449
+ { renderListItems ( options ) }
380
450
</ ul >
381
451
) ;
382
452
0 commit comments