@@ -45,7 +45,27 @@ export const KeyboardNavigationMixin = (superClass) =>
4545 } ,
4646
4747 /** @private */
48- _focusedColumnOrder : Number
48+ _focusedColumnOrder : Number ,
49+
50+ /**
51+ * Indicates whether the grid is currently in interaction mode.
52+ * In interaction mode the user is currently interacting with a control,
53+ * such as an input or a select, within a cell.
54+ * In interaction mode keyboard navigation between cells is disabled.
55+ * Interaction mode also prevents the focus target cell of that section of
56+ * the grid from receiving focus, allowing the user to switch focus to
57+ * controls in adjacent cells, rather than focussing the outer cell
58+ * itself.
59+ * @type {boolean }
60+ * @private
61+ */
62+ interacting : {
63+ type : Boolean ,
64+ value : false ,
65+ reflectToAttribute : true ,
66+ readOnly : true ,
67+ observer : '_interactingChanged'
68+ }
4969 } ;
5070 }
5171
@@ -82,10 +102,18 @@ export const KeyboardNavigationMixin = (superClass) =>
82102 oldFocusable . setAttribute ( 'tabindex' , '-1' ) ;
83103 }
84104 if ( focusable ) {
85- focusable . setAttribute ( 'tabindex' , '0' ) ;
105+ this . _updateGridSectionFocusTarget ( focusable ) ;
86106 }
87107 }
88108
109+ /** @private */
110+ _interactingChanged ( ) {
111+ // Update focus targets when entering / exiting interaction mode
112+ this . _updateGridSectionFocusTarget ( this . _headerFocusable ) ;
113+ this . _updateGridSectionFocusTarget ( this . _itemsFocusable ) ;
114+ this . _updateGridSectionFocusTarget ( this . _footerFocusable ) ;
115+ }
116+
89117 /**
90118 * @param {!KeyboardEvent } e
91119 * @protected
@@ -119,7 +147,7 @@ export const KeyboardNavigationMixin = (superClass) =>
119147 }
120148
121149 this . _detectInteracting ( e ) ;
122- if ( this . hasAttribute ( ' interacting' ) && keyGroup !== 'Interaction' ) {
150+ if ( this . interacting && keyGroup !== 'Interaction' ) {
123151 // When in the interacting mode, only the “Interaction” keys are handled.
124152 keyGroup = undefined ;
125153 }
@@ -334,32 +362,32 @@ export const KeyboardNavigationMixin = (superClass) =>
334362 let wantInteracting ;
335363 switch ( key ) {
336364 case 'Enter' :
337- wantInteracting = this . hasAttribute ( ' interacting' ) ? ! localTargetIsTextInput : true ;
365+ wantInteracting = this . interacting ? ! localTargetIsTextInput : true ;
338366 break ;
339367 case 'Escape' :
340368 wantInteracting = false ;
341369 break ;
342370 case 'F2' :
343- wantInteracting = ! this . hasAttribute ( ' interacting' ) ;
371+ wantInteracting = ! this . interacting ;
344372 break ;
345373 }
346374
347375 const { cell } = this . _parseEventPath ( e . composedPath ( ) ) ;
348376
349- if ( this . hasAttribute ( ' interacting' ) !== wantInteracting ) {
377+ if ( this . interacting !== wantInteracting ) {
350378 if ( wantInteracting ) {
351379 const focusTarget = cell . _content . querySelector ( '[focus-target]' ) || cell . _content . firstElementChild ;
352380 if ( focusTarget ) {
353381 e . preventDefault ( ) ;
354382 focusTarget . focus ( ) ;
355- this . _toggleAttribute ( 'interacting' , true , this ) ;
383+ this . _setInteracting ( true ) ;
356384 this . _toggleAttribute ( 'navigating' , false , this ) ;
357385 }
358386 } else {
359387 e . preventDefault ( ) ;
360388 this . _focusedColumnOrder = undefined ;
361389 cell . focus ( ) ;
362- this . _toggleAttribute ( 'interacting' , false , this ) ;
390+ this . _setInteracting ( false ) ;
363391 this . _toggleAttribute ( 'navigating' , true , this ) ;
364392 }
365393 }
@@ -469,7 +497,7 @@ export const KeyboardNavigationMixin = (superClass) =>
469497 // tabbed (shift-tabbed) into the grid. Move the focus to
470498 // the first (the last) focusable.
471499 this . _predictFocusStepTarget ( rootTarget , rootTarget === this . $ . table ? 1 : - 1 ) . focus ( ) ;
472- this . _toggleAttribute ( 'interacting' , false , this ) ;
500+ this . _setInteracting ( false ) ;
473501 } else {
474502 this . _detectInteracting ( e ) ;
475503 }
@@ -486,16 +514,17 @@ export const KeyboardNavigationMixin = (superClass) =>
486514
487515 /** @private */
488516 _onCellFocusIn ( e ) {
517+ const location = this . _getCellFocusEventLocation ( e ) ;
489518 this . _detectInteracting ( e ) ;
490519
491- if ( e . composedPath ( ) . indexOf ( this . $ . table ) === 3 ) {
492- const cell = e . composedPath ( ) [ 0 ] ;
493- this . _activeRowGroup = cell . parentNode . parentNode ;
494- if ( this . _activeRowGroup === this . $ . header ) {
520+ if ( location ) {
521+ const { section , cell } = location ;
522+ this . _activeRowGroup = section ;
523+ if ( this . $ . header === section ) {
495524 this . _headerFocusable = cell ;
496- } else if ( this . _activeRowGroup === this . $ . items ) {
525+ } else if ( this . $ . items === section ) {
497526 this . _itemsFocusable = cell ;
498- } else if ( this . _activeRowGroup === this . $ . footer ) {
527+ } else if ( this . $ . footer === section ) {
499528 this . _footerFocusable = cell ;
500529 }
501530 // Inform cell content of the focus (used in <vaadin-grid-sorter>)
@@ -517,13 +546,14 @@ export const KeyboardNavigationMixin = (superClass) =>
517546 }
518547 }
519548
520- /** @private */
549+ /** @private
550+ * Enables interaction mode if a cells descendant receives focus or keyboard
551+ * input. Disables it if the event is not related to cell content.
552+ * @param {!KeyboardEvent|!FocusEvent } e
553+ */
521554 _detectInteracting ( e ) {
522- this . _toggleAttribute (
523- 'interacting' ,
524- e . composedPath ( ) . some ( ( el ) => el . localName === 'vaadin-grid-cell-content' ) ,
525- this
526- ) ;
555+ const isInteracting = e . composedPath ( ) . some ( ( el ) => el . localName === 'vaadin-grid-cell-content' ) ;
556+ this . _setInteracting ( isInteracting ) ;
527557 }
528558
529559 /** @private */
@@ -534,6 +564,21 @@ export const KeyboardNavigationMixin = (superClass) =>
534564 }
535565 }
536566
567+ /** @private
568+ * Enables or disables the focus target cell of the containing section of the
569+ * grid from receiving focus, based on whether the user is interacting with
570+ * that section of the grid.
571+ * @param {HTMLTableCellElement } focusTargetCell
572+ */
573+ _updateGridSectionFocusTarget ( focusTargetCell ) {
574+ if ( ! focusTargetCell ) return ;
575+
576+ const section = this . _getGridSectionFromFocusTarget ( focusTargetCell ) ;
577+ const isInteractingWithinActiveSection = this . interacting && section === this . _activeRowGroup ;
578+
579+ focusTargetCell . tabIndex = isInteractingWithinActiveSection ? - 1 : 0 ;
580+ }
581+
537582 /**
538583 * @param {!HTMLTableRowElement } row
539584 * @param {number } index
@@ -639,4 +684,47 @@ export const KeyboardNavigationMixin = (superClass) =>
639684 _elementMatches ( el , query ) {
640685 return el . matches ? el . matches ( query ) : Array . from ( el . parentNode . querySelectorAll ( query ) ) . indexOf ( el ) !== - 1 ;
641686 }
687+
688+ /**
689+ * @typedef {Object } CellFocusEventLocation
690+ * @property {HTMLTableSectionElement } section - The grid section element that contains the focused cell (header, body, or footer)
691+ * @property {HTMLElement } cell - The cell element that received focus or is ancestor of the element that received focus
692+ * @private
693+ */
694+ /**
695+ * Takes a focus event and returns a location object describing in which
696+ * section of the grid and in or on which cell the focus event occurred.
697+ * The focus event may either target the cell itself or contents of the cell.
698+ * If the event does not target a cell then null is returned.
699+ * @param {FocusEvent } e
700+ * @returns {CellFocusEventLocation | null }
701+ * @private
702+ */
703+ _getCellFocusEventLocation ( e ) {
704+ const path = e . composedPath ( ) ;
705+ const tableIndex = path . indexOf ( this . $ . table ) ;
706+ // Assuming ascending path to table is: [...,] th|td, tr, thead|tbody, table [,...]
707+ const section = tableIndex >= 2 ? path [ tableIndex - 1 ] : null ;
708+ const cell = tableIndex >= 3 ? path [ tableIndex - 3 ] : null ;
709+
710+ if ( ! section || ! cell ) return null ;
711+
712+ return {
713+ section,
714+ cell
715+ } ;
716+ }
717+
718+ /**
719+ * Helper method that maps a focus target cell to the containing grid section
720+ * @param {HTMLTableCellElement } focusTargetCell
721+ * @returns {HTMLTableSectionElement | null }
722+ * @private
723+ */
724+ _getGridSectionFromFocusTarget ( focusTargetCell ) {
725+ if ( focusTargetCell === this . _headerFocusable ) return this . $ . header ;
726+ if ( focusTargetCell === this . _itemsFocusable ) return this . $ . items ;
727+ if ( focusTargetCell === this . _footerFocusable ) return this . $ . footer ;
728+ return null ;
729+ }
642730 } ;
0 commit comments