@@ -32,8 +32,10 @@ const DATA_KEY = 'coreui.time-picker'
3232const EVENT_KEY = `.${ DATA_KEY } `
3333const DATA_API_KEY = '.data-api'
3434
35+ const END_KEY = 'End'
3536const ENTER_KEY = 'Enter'
3637const ESCAPE_KEY = 'Escape'
38+ const HOME_KEY = 'Home'
3739const SPACE_KEY = 'Space'
3840const TAB_KEY = 'Tab'
3941const ARROW_UP_KEY = 'ArrowUp'
@@ -80,11 +82,15 @@ const SELECTOR_DATA_TOGGLE =
8082 '[data-coreui-toggle="time-picker"]:not(.disabled):not(:disabled)'
8183const SELECTOR_DATA_TOGGLE_SHOWN = `${ SELECTOR_DATA_TOGGLE } .${ CLASS_NAME_SHOW } `
8284const 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"] '
8486const SELECTOR_ROLL_COL = '.time-picker-roll-col'
8587const SELECTOR_WAS_VALIDATED = 'form.was-validated'
8688
8789const Default = {
90+ ariaSelectHoursLabel : 'Select hours' ,
91+ ariaSelectMeridiemLabel : 'Select AM/PM' ,
92+ ariaSelectMinutesLabel : 'Select minutes' ,
93+ ariaSelectSecondsLabel : 'Select seconds' ,
8894 cancelButton : 'Cancel' ,
8995 cancelButtonClasses : [ 'btn' , 'btn-sm' , 'btn-ghost-primary' ] ,
9096 cleaner : true ,
@@ -112,6 +118,10 @@ const Default = {
112118}
113119
114120const DefaultType = {
121+ ariaSelectHoursLabel : 'string' ,
122+ ariaSelectMeridiemLabel : 'string' ,
123+ ariaSelectMinutesLabel : 'string' ,
124+ ariaSelectSecondsLabel : 'string' ,
115125 cancelButton : '(boolean|string)' ,
116126 cancelButtonClasses : '(array|string)' ,
117127 cleaner : 'boolean' ,
@@ -291,6 +301,42 @@ class TimePicker extends BaseComponent {
291301 } )
292302 }
293303
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+
294340 _addEventListeners ( ) {
295341 EventHandler . on ( this . _indicatorElement , EVENT_CLICK , ( ) => {
296342 if ( ! this . _config . disabled ) {
@@ -319,8 +365,10 @@ class TimePicker extends BaseComponent {
319365 } )
320366
321367 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+ }
324372 } )
325373
326374 EventHandler . on ( this . _timePickerBody , EVENT_KEYDOWN , SELECTOR_ROLL_CELL , event => {
@@ -333,38 +381,37 @@ class TimePicker extends BaseComponent {
333381 return
334382 }
335383
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
337390 }
338391
339- if ( event . key === ARROW_LEFT_KEY || event . key === ARROW_RIGHT_KEY ) {
392+ if ( event . key === HOME_KEY || event . key === END_KEY ) {
340393 event . preventDefault ( )
341394 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 )
359396
360- if ( selectedCell ) {
361- selectedCell . focus ( )
362- return
363- }
397+ if ( ! items . length ) {
398+ return
399+ }
364400
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+ }
366405
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 )
368415 }
369416 }
370417 } )
@@ -561,17 +608,19 @@ class TimePicker extends BaseComponent {
561608
562609 if ( this . _config . variant === 'roll' ) {
563610 timePickerBodyEl . classList . add ( CLASS_NAME_ROLL )
611+ timePickerBodyEl . setAttribute ( 'role' , 'group' )
564612 }
565613
566614 this . _timePickerBody = timePickerBodyEl
567615
568616 return timePickerBodyEl
569617 }
570618
571- _createTimePickerInlineSelect ( className , options ) {
619+ _createTimePickerInlineSelect ( className , options , ariaLabel ) {
572620 const selectEl = document . createElement ( 'select' )
573621 selectEl . classList . add ( CLASS_NAME_INLINE_SELECT , className )
574622 selectEl . disabled = this . _config . disabled
623+ selectEl . setAttribute ( 'aria-label' , ariaLabel )
575624 selectEl . addEventListener ( 'change' , event =>
576625 this . _handleTimeChange ( className , event . target . value )
577626 )
@@ -595,7 +644,8 @@ class TimePicker extends BaseComponent {
595644 this . _timePickerBody . append (
596645 this . _createTimePickerInlineSelect (
597646 'hours' ,
598- this . _localizedTimePartials . listOfHours
647+ this . _localizedTimePartials . listOfHours ,
648+ this . _config . ariaSelectHoursLabel
599649 )
600650 )
601651
@@ -604,7 +654,8 @@ class TimePicker extends BaseComponent {
604654 timeSeparatorEl . cloneNode ( true ) ,
605655 this . _createTimePickerInlineSelect (
606656 'minutes' ,
607- this . _localizedTimePartials . listOfMinutes
657+ this . _localizedTimePartials . listOfMinutes ,
658+ this . _config . ariaSelectMinutesLabel
608659 )
609660 )
610661 }
@@ -614,7 +665,8 @@ class TimePicker extends BaseComponent {
614665 timeSeparatorEl ,
615666 this . _createTimePickerInlineSelect (
616667 'seconds' ,
617- this . _localizedTimePartials . listOfSeconds
668+ this . _localizedTimePartials . listOfSeconds ,
669+ this . _config . ariaSelectSecondsLabel
618670 )
619671 )
620672 }
@@ -627,8 +679,7 @@ class TimePicker extends BaseComponent {
627679 { value : 'am' , label : 'AM' } ,
628680 { value : 'pm' , label : 'PM' }
629681 ] ,
630- '_selectAmPm' ,
631- this . _ampm
682+ this . _config . ariaSelectMeridiemLabel
632683 )
633684 )
634685 }
@@ -638,15 +689,17 @@ class TimePicker extends BaseComponent {
638689 this . _timePickerBody . append (
639690 this . _createTimePickerRollCol (
640691 this . _localizedTimePartials . listOfHours ,
641- 'hours'
692+ 'hours' ,
693+ this . _config . ariaSelectHoursLabel
642694 )
643695 )
644696
645697 if ( this . _config . minutes ) {
646698 this . _timePickerBody . append (
647699 this . _createTimePickerRollCol (
648700 this . _localizedTimePartials . listOfMinutes ,
649- 'minutes'
701+ 'minutes' ,
702+ this . _config . ariaSelectMinutesLabel
650703 )
651704 )
652705 }
@@ -655,7 +708,8 @@ class TimePicker extends BaseComponent {
655708 this . _timePickerBody . append (
656709 this . _createTimePickerRollCol (
657710 this . _localizedTimePartials . listOfSeconds ,
658- 'seconds'
711+ 'seconds' ,
712+ this . _config . ariaSelectSecondsLabel
659713 )
660714 )
661715 }
@@ -668,21 +722,27 @@ class TimePicker extends BaseComponent {
668722 { value : 'pm' , label : 'PM' }
669723 ] ,
670724 'toggle' ,
671- this . _ampm
725+ this . _config . ariaSelectMeridiemLabel
672726 )
673727 )
674728 }
675729 }
676730
677- _createTimePickerRollCol ( options , part ) {
731+ _createTimePickerRollCol ( options , part , ariaLabel ) {
678732 const timePickerRollColEl = document . createElement ( 'div' )
679733 timePickerRollColEl . classList . add ( CLASS_NAME_ROLL_COL )
734+ timePickerRollColEl . setAttribute ( 'role' , 'listbox' )
735+ timePickerRollColEl . setAttribute ( 'aria-label' , ariaLabel )
680736
681- for ( const option of options ) {
737+ for ( const [ index , option ] of options . entries ( ) ) {
682738 const timePickerRollCellEl = document . createElement ( 'div' )
683739 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+
686746 timePickerRollCellEl . innerHTML = option . label
687747 timePickerRollCellEl . addEventListener ( 'click' , ( ) => {
688748 this . _handleTimeChange ( part , option . value )
@@ -691,6 +751,7 @@ class TimePicker extends BaseComponent {
691751 if ( event . code === SPACE_KEY || event . key === ENTER_KEY ) {
692752 event . preventDefault ( )
693753 this . _handleTimeChange ( part , option . value )
754+ this . _moveFocusToNextColumn ( event )
694755 }
695756 } )
696757
@@ -747,29 +808,41 @@ class TimePicker extends BaseComponent {
747808 }
748809
749810 _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 )
769826 }
770827 }
771828 }
772829
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+
773846 _setInputValue ( date , input = this . _input ) {
774847 input . value = date instanceof Date ?
775848 date . toLocaleTimeString ( this . _config . locale , {
0 commit comments