@@ -32,8 +32,10 @@ const DATA_KEY = 'coreui.time-picker'
32
32
const EVENT_KEY = `.${ DATA_KEY } `
33
33
const DATA_API_KEY = '.data-api'
34
34
35
+ const END_KEY = 'End'
35
36
const ENTER_KEY = 'Enter'
36
37
const ESCAPE_KEY = 'Escape'
38
+ const HOME_KEY = 'Home'
37
39
const SPACE_KEY = 'Space'
38
40
const TAB_KEY = 'Tab'
39
41
const ARROW_UP_KEY = 'ArrowUp'
@@ -80,11 +82,15 @@ const SELECTOR_DATA_TOGGLE =
80
82
'[data-coreui-toggle="time-picker"]:not(.disabled):not(:disabled)'
81
83
const SELECTOR_DATA_TOGGLE_SHOWN = `${ SELECTOR_DATA_TOGGLE } .${ CLASS_NAME_SHOW } `
82
84
const SELECTOR_ROLL_CELL = '.time-picker-roll-cell'
83
- const SELECTOR_ROLL_CELL_SELECTED = '.time-picker-roll-cell.selected '
85
+ const SELECTOR_ROLL_CELL_FOCUSABLE = '.time-picker-roll-cell[tabindex="0"] '
84
86
const SELECTOR_ROLL_COL = '.time-picker-roll-col'
85
87
const SELECTOR_WAS_VALIDATED = 'form.was-validated'
86
88
87
89
const Default = {
90
+ ariaSelectHoursLabel : 'Select hours' ,
91
+ ariaSelectMeridiemLabel : 'Select AM/PM' ,
92
+ ariaSelectMinutesLabel : 'Select minutes' ,
93
+ ariaSelectSecondsLabel : 'Select seconds' ,
88
94
cancelButton : 'Cancel' ,
89
95
cancelButtonClasses : [ 'btn' , 'btn-sm' , 'btn-ghost-primary' ] ,
90
96
cleaner : true ,
@@ -112,6 +118,10 @@ const Default = {
112
118
}
113
119
114
120
const DefaultType = {
121
+ ariaSelectHoursLabel : 'string' ,
122
+ ariaSelectMeridiemLabel : 'string' ,
123
+ ariaSelectMinutesLabel : 'string' ,
124
+ ariaSelectSecondsLabel : 'string' ,
115
125
cancelButton : '(boolean|string)' ,
116
126
cancelButtonClasses : '(array|string)' ,
117
127
cleaner : 'boolean' ,
@@ -291,6 +301,42 @@ class TimePicker extends BaseComponent {
291
301
} )
292
302
}
293
303
304
+ _moveFocusToNextColumn ( event ) {
305
+ if ( ! this . _timePickerBody ) {
306
+ return
307
+ }
308
+
309
+ const { target } = event
310
+ const columnElement = target . parentElement
311
+
312
+ const columns = SelectorEngine . find ( SELECTOR_ROLL_COL , this . _timePickerBody )
313
+ const currentColumnIndex = columns . indexOf ( columnElement )
314
+
315
+ if ( currentColumnIndex < columns . length - 1 ) {
316
+ const firstFocusableCell = SelectorEngine . findOne ( SELECTOR_ROLL_CELL_FOCUSABLE , columns [ currentColumnIndex + 1 ] )
317
+
318
+ firstFocusableCell . focus ( )
319
+ }
320
+ }
321
+
322
+ _moveFocusToPreviousColumn ( event ) {
323
+ if ( ! this . _timePickerBody ) {
324
+ return
325
+ }
326
+
327
+ const { target } = event
328
+ const columnElement = target . parentElement
329
+
330
+ const columns = SelectorEngine . find ( SELECTOR_ROLL_COL , this . _timePickerBody )
331
+ const currentColumnIndex = columns . indexOf ( columnElement )
332
+
333
+ if ( currentColumnIndex > 0 ) {
334
+ const firstFocusableCell = SelectorEngine . findOne ( SELECTOR_ROLL_CELL_FOCUSABLE , columns [ currentColumnIndex - 1 ] )
335
+
336
+ firstFocusableCell . focus ( )
337
+ }
338
+ }
339
+
294
340
_addEventListeners ( ) {
295
341
EventHandler . on ( this . _indicatorElement , EVENT_CLICK , ( ) => {
296
342
if ( ! this . _config . disabled ) {
@@ -319,8 +365,10 @@ class TimePicker extends BaseComponent {
319
365
} )
320
366
321
367
if ( this . _config . variant === 'roll' ) {
322
- EventHandler . on ( this . _timePickerBody , EVENT_FOCUSOUT , SELECTOR_ROLL_COL , ( ) => {
323
- this . _setUpRolls ( false )
368
+ EventHandler . on ( this . _timePickerBody , EVENT_FOCUSOUT , SELECTOR_ROLL_COL , event => {
369
+ if ( ! event . delegateTarget . contains ( event . relatedTarget ) ) {
370
+ this . _setUpRolls ( false )
371
+ }
324
372
} )
325
373
326
374
EventHandler . on ( this . _timePickerBody , EVENT_KEYDOWN , SELECTOR_ROLL_CELL , event => {
@@ -333,38 +381,37 @@ class TimePicker extends BaseComponent {
333
381
return
334
382
}
335
383
336
- getNextActiveElement ( items , target , key === ARROW_DOWN_KEY , ! items . includes ( target ) ) . focus ( )
384
+ const nextElement = getNextActiveElement ( items , target , key === ARROW_DOWN_KEY , ! items . includes ( target ) )
385
+ if ( nextElement ) {
386
+ nextElement . focus ( )
387
+ }
388
+
389
+ return
337
390
}
338
391
339
- if ( event . key === ARROW_LEFT_KEY || event . key === ARROW_RIGHT_KEY ) {
392
+ if ( event . key === HOME_KEY || event . key === END_KEY ) {
340
393
event . preventDefault ( )
341
394
const { key, target } = event
342
- const columnElement = target . parentElement
343
-
344
- if ( this . _timePickerBody ) {
345
- const columns = SelectorEngine . find ( SELECTOR_ROLL_COL , this . _timePickerBody )
346
- const currentColumnIndex = columns . indexOf ( columnElement )
347
-
348
- let targetColumnIndex
349
- const isRtl = isRTL ( )
350
- const shouldGoLeft = ( key === ARROW_LEFT_KEY && ! isRtl ) || ( key === ARROW_RIGHT_KEY && isRtl )
351
- if ( shouldGoLeft ) {
352
- targetColumnIndex = currentColumnIndex > 0 ? currentColumnIndex - 1 : columns . length - 1
353
- } else {
354
- targetColumnIndex = currentColumnIndex < columns . length - 1 ? currentColumnIndex + 1 : 0
355
- }
356
-
357
- const targetColumn = columns [ targetColumnIndex ]
358
- const selectedCell = SelectorEngine . findOne ( SELECTOR_ROLL_CELL_SELECTED , targetColumn )
395
+ const items = SelectorEngine . find ( SELECTOR_ROLL_CELL , target . parentElement )
359
396
360
- if ( selectedCell ) {
361
- selectedCell . focus ( )
362
- return
363
- }
397
+ if ( ! items . length ) {
398
+ return
399
+ }
364
400
365
- const firstFocusableCell = SelectorEngine . findOne ( SELECTOR_ROLL_CELL , targetColumn )
401
+ const index = key === HOME_KEY ? 0 : items . length - 1
402
+ items [ index ] . focus ( )
403
+ return
404
+ }
366
405
367
- firstFocusableCell . focus ( )
406
+ if ( event . key === ARROW_LEFT_KEY || event . key === ARROW_RIGHT_KEY ) {
407
+ event . preventDefault ( )
408
+ const { key } = event
409
+ const isRtl = isRTL ( )
410
+ const shouldGoLeft = ( key === ARROW_LEFT_KEY && ! isRtl ) || ( key === ARROW_RIGHT_KEY && isRtl )
411
+ if ( shouldGoLeft ) {
412
+ this . _moveFocusToPreviousColumn ( event )
413
+ } else {
414
+ this . _moveFocusToNextColumn ( event )
368
415
}
369
416
}
370
417
} )
@@ -561,17 +608,19 @@ class TimePicker extends BaseComponent {
561
608
562
609
if ( this . _config . variant === 'roll' ) {
563
610
timePickerBodyEl . classList . add ( CLASS_NAME_ROLL )
611
+ timePickerBodyEl . setAttribute ( 'role' , 'group' )
564
612
}
565
613
566
614
this . _timePickerBody = timePickerBodyEl
567
615
568
616
return timePickerBodyEl
569
617
}
570
618
571
- _createTimePickerInlineSelect ( className , options ) {
619
+ _createTimePickerInlineSelect ( className , options , ariaLabel ) {
572
620
const selectEl = document . createElement ( 'select' )
573
621
selectEl . classList . add ( CLASS_NAME_INLINE_SELECT , className )
574
622
selectEl . disabled = this . _config . disabled
623
+ selectEl . setAttribute ( 'aria-label' , ariaLabel )
575
624
selectEl . addEventListener ( 'change' , event =>
576
625
this . _handleTimeChange ( className , event . target . value )
577
626
)
@@ -595,7 +644,8 @@ class TimePicker extends BaseComponent {
595
644
this . _timePickerBody . append (
596
645
this . _createTimePickerInlineSelect (
597
646
'hours' ,
598
- this . _localizedTimePartials . listOfHours
647
+ this . _localizedTimePartials . listOfHours ,
648
+ this . _config . ariaSelectHoursLabel
599
649
)
600
650
)
601
651
@@ -604,7 +654,8 @@ class TimePicker extends BaseComponent {
604
654
timeSeparatorEl . cloneNode ( true ) ,
605
655
this . _createTimePickerInlineSelect (
606
656
'minutes' ,
607
- this . _localizedTimePartials . listOfMinutes
657
+ this . _localizedTimePartials . listOfMinutes ,
658
+ this . _config . ariaSelectMinutesLabel
608
659
)
609
660
)
610
661
}
@@ -614,7 +665,8 @@ class TimePicker extends BaseComponent {
614
665
timeSeparatorEl ,
615
666
this . _createTimePickerInlineSelect (
616
667
'seconds' ,
617
- this . _localizedTimePartials . listOfSeconds
668
+ this . _localizedTimePartials . listOfSeconds ,
669
+ this . _config . ariaSelectSecondsLabel
618
670
)
619
671
)
620
672
}
@@ -627,8 +679,7 @@ class TimePicker extends BaseComponent {
627
679
{ value : 'am' , label : 'AM' } ,
628
680
{ value : 'pm' , label : 'PM' }
629
681
] ,
630
- '_selectAmPm' ,
631
- this . _ampm
682
+ this . _config . ariaSelectMeridiemLabel
632
683
)
633
684
)
634
685
}
@@ -638,15 +689,17 @@ class TimePicker extends BaseComponent {
638
689
this . _timePickerBody . append (
639
690
this . _createTimePickerRollCol (
640
691
this . _localizedTimePartials . listOfHours ,
641
- 'hours'
692
+ 'hours' ,
693
+ this . _config . ariaSelectHoursLabel
642
694
)
643
695
)
644
696
645
697
if ( this . _config . minutes ) {
646
698
this . _timePickerBody . append (
647
699
this . _createTimePickerRollCol (
648
700
this . _localizedTimePartials . listOfMinutes ,
649
- 'minutes'
701
+ 'minutes' ,
702
+ this . _config . ariaSelectMinutesLabel
650
703
)
651
704
)
652
705
}
@@ -655,7 +708,8 @@ class TimePicker extends BaseComponent {
655
708
this . _timePickerBody . append (
656
709
this . _createTimePickerRollCol (
657
710
this . _localizedTimePartials . listOfSeconds ,
658
- 'seconds'
711
+ 'seconds' ,
712
+ this . _config . ariaSelectSecondsLabel
659
713
)
660
714
)
661
715
}
@@ -668,21 +722,27 @@ class TimePicker extends BaseComponent {
668
722
{ value : 'pm' , label : 'PM' }
669
723
] ,
670
724
'toggle' ,
671
- this . _ampm
725
+ this . _config . ariaSelectMeridiemLabel
672
726
)
673
727
)
674
728
}
675
729
}
676
730
677
- _createTimePickerRollCol ( options , part ) {
731
+ _createTimePickerRollCol ( options , part , ariaLabel ) {
678
732
const timePickerRollColEl = document . createElement ( 'div' )
679
733
timePickerRollColEl . classList . add ( CLASS_NAME_ROLL_COL )
734
+ timePickerRollColEl . setAttribute ( 'role' , 'listbox' )
735
+ timePickerRollColEl . setAttribute ( 'aria-label' , ariaLabel )
680
736
681
- for ( const option of options ) {
737
+ for ( const [ index , option ] of options . entries ( ) ) {
682
738
const timePickerRollCellEl = document . createElement ( 'div' )
683
739
timePickerRollCellEl . classList . add ( CLASS_NAME_ROLL_CELL )
684
- timePickerRollCellEl . setAttribute ( 'role' , 'button' )
685
- timePickerRollCellEl . tabIndex = 0
740
+
741
+ timePickerRollCellEl . setAttribute ( 'role' , 'option' )
742
+ timePickerRollCellEl . tabIndex = index === 0 ? 0 : - 1
743
+ timePickerRollCellEl . setAttribute ( 'aria-label' , option . label . toString ( ) )
744
+ timePickerRollCellEl . setAttribute ( 'aria-selected' , 'false' )
745
+
686
746
timePickerRollCellEl . innerHTML = option . label
687
747
timePickerRollCellEl . addEventListener ( 'click' , ( ) => {
688
748
this . _handleTimeChange ( part , option . value )
@@ -691,6 +751,7 @@ class TimePicker extends BaseComponent {
691
751
if ( event . code === SPACE_KEY || event . key === ENTER_KEY ) {
692
752
event . preventDefault ( )
693
753
this . _handleTimeChange ( part , option . value )
754
+ this . _moveFocusToNextColumn ( event )
694
755
}
695
756
} )
696
757
@@ -747,29 +808,41 @@ class TimePicker extends BaseComponent {
747
808
}
748
809
749
810
_setUpRolls ( initial = false ) {
750
- for ( const part of Array . from ( [ 'hours' , 'minutes' , 'seconds' , 'toggle' ] ) ) {
751
- for ( const element of SelectorEngine . find (
752
- `[data-coreui-${ part } ]` ,
753
- this . _element
754
- ) ) {
755
- if (
756
- this . _getPartOfTime ( part ) ===
757
- Manipulator . getDataAttribute ( element , part )
758
- ) {
759
- element . classList . add ( CLASS_NAME_SELECTED )
760
- this . _scrollTo ( element . parentElement , element , initial )
761
-
762
- for ( const sibling of element . parentElement . children ) {
763
- // eslint-disable-next-line max-depth
764
- if ( sibling !== element ) {
765
- sibling . classList . remove ( CLASS_NAME_SELECTED )
766
- }
767
- }
768
- }
811
+ const parts = [ 'hours' , 'minutes' , 'seconds' , 'toggle' ]
812
+
813
+ for ( const part of parts ) {
814
+ const partValue = this . _getPartOfTime ( part )
815
+ if ( partValue === null ) {
816
+ continue
817
+ }
818
+
819
+ const elements = SelectorEngine . find ( `[data-coreui-${ part } ]` , this . _element )
820
+ const selectedElement = elements . find ( element =>
821
+ partValue === Manipulator . getDataAttribute ( element , part )
822
+ )
823
+
824
+ if ( selectedElement ) {
825
+ this . _selectRollElement ( selectedElement , initial )
769
826
}
770
827
}
771
828
}
772
829
830
+ _selectRollElement ( element , initial = false ) {
831
+ const { parentElement } = element
832
+
833
+ const currentSelected = SelectorEngine . findOne ( SELECTOR_ROLL_CELL_FOCUSABLE , parentElement )
834
+ if ( currentSelected && currentSelected !== element ) {
835
+ currentSelected . classList . remove ( CLASS_NAME_SELECTED )
836
+ currentSelected . tabIndex = - 1
837
+ currentSelected . setAttribute ( 'aria-selected' , 'false' )
838
+ }
839
+
840
+ element . classList . add ( CLASS_NAME_SELECTED )
841
+ element . tabIndex = 0
842
+ element . setAttribute ( 'aria-selected' , 'true' )
843
+ this . _scrollTo ( parentElement , element , initial )
844
+ }
845
+
773
846
_setInputValue ( date , input = this . _input ) {
774
847
input . value = date instanceof Date ?
775
848
date . toLocaleTimeString ( this . _config . locale , {
0 commit comments