55 * Please see LICENSE files in the repository root for full details.
66 */
77
8- import React , { type JSX , useCallback , useLayoutEffect , useMemo , useRef , useState } from "react" ;
8+ import React , { type JSX , useCallback , useMemo , useRef } from "react" ;
99import classNames from "classnames" ;
1010import {
1111 CollapseIcon ,
@@ -27,6 +27,7 @@ import {
2727} from "@vector-im/compound-design-tokens/assets/web/icons" ;
2828import { InlineSpinner } from "@vector-im/compound-web" ;
2929
30+ import { RovingTabIndexProvider } from "../../../../../core/roving" ;
3031import { useI18n } from "../../../../../core/i18n/i18nContext" ;
3132import { Flex } from "../../../../../core/utils/Flex" ;
3233import { type ViewModel , useViewModel } from "../../../../../core/viewmodel" ;
@@ -131,7 +132,6 @@ interface ActionBarViewProps {
131132 */
132133export function ActionBarView ( { vm, className } : Readonly < ActionBarViewProps > ) : JSX . Element | null {
133134 const { translate : _t } = useI18n ( ) ;
134- const [ activeIndex , setActiveIndex ] = useState ( 0 ) ;
135135 const {
136136 actions,
137137 presentation = "icon" ,
@@ -364,97 +364,30 @@ export function ActionBarView({ vm, className }: Readonly<ActionBarViewProps>):
364364 disabled : isActionDisabled ( action ) ,
365365 } ) ) ;
366366 } , [ actions , isActionDisabled ] ) ;
367-
368- // Handle RovingIndex for toolbar
369- const enabledIndices = toolbarButtons
370- . map ( ( item , index ) => ( item . disabled ? - 1 : index ) )
371- . filter ( ( index ) => index >= 0 ) ;
372- const fallbackIndex = enabledIndices [ 0 ] ?? 0 ;
373- const currentIndex =
374- toolbarButtons [ activeIndex ] && ! toolbarButtons [ activeIndex ] . disabled ? activeIndex : fallbackIndex ;
375-
376- useLayoutEffect ( ( ) => {
377- setActiveIndex ( currentIndex ) ;
378-
379- toolbarButtons . forEach ( ( { action } , index ) => {
380- const button = actionButtonRefs . current [ action ] ?? null ;
381- if ( button ) {
382- button . tabIndex = index === currentIndex ? 0 : - 1 ;
383- }
384- } ) ;
385- } , [ currentIndex , toolbarButtons ] ) ;
386-
387- const focusButtonAtIndex = ( index : number ) : void => {
388- const action = toolbarButtons [ index ] ?. action ;
389- const button = action ? ( actionButtonRefs . current [ action ] ?? null ) : null ;
390- if ( ! button ) {
391- return ;
392- }
393-
394- setActiveIndex ( index ) ;
395- button . focus ( ) ;
396- } ;
397-
398- const handleToolbarKeyDown = ( event : React . KeyboardEvent < HTMLDivElement > ) : void => {
399- if ( enabledIndices . length === 0 ) {
400- return ;
401- }
402-
403- const focusedIndex = toolbarButtons . findIndex (
404- ( { action } ) => actionButtonRefs . current [ action ] === document . activeElement ,
405- ) ;
406- const startIndex = focusedIndex >= 0 ? focusedIndex : currentIndex ;
407-
408- switch ( event . key ) {
409- case "ArrowRight" : {
410- event . preventDefault ( ) ;
411- const nextIndex = enabledIndices . find ( ( index ) => index > startIndex ) ?? enabledIndices [ 0 ] ;
412- focusButtonAtIndex ( nextIndex ) ;
413- break ;
414- }
415- case "ArrowLeft" : {
416- event . preventDefault ( ) ;
417- const previousIndex = [ ...enabledIndices ] . reverse ( ) . find ( ( index ) => index < startIndex ) ;
418- focusButtonAtIndex ( previousIndex ?? enabledIndices [ enabledIndices . length - 1 ] ) ;
419- break ;
420- }
421- case "Home" :
422- event . preventDefault ( ) ;
423- focusButtonAtIndex ( enabledIndices [ 0 ] ) ;
424- break ;
425- case "End" :
426- event . preventDefault ( ) ;
427- focusButtonAtIndex ( enabledIndices [ enabledIndices . length - 1 ] ) ;
428- break ;
429- }
430- } ;
431-
432- const handleToolbarFocusCapture = ( ) : void => {
433- const focusedIndex = toolbarButtons . findIndex (
434- ( { action } ) => actionButtonRefs . current [ action ] === document . activeElement ,
435- ) ;
436- if ( focusedIndex >= 0 && focusedIndex !== activeIndex ) {
437- setActiveIndex ( focusedIndex ) ;
438- }
439- } ;
367+ const rovingProviderKey = toolbarButtons
368+ . map ( ( { action, disabled } ) => `${ action } :${ disabled ? "1" : "0" } ` )
369+ . join ( "|" ) ;
440370
441371 if ( toolbarButtons . length === 0 ) {
442372 return null ;
443373 }
444374
445375 // aria-live=off to not have this read out automatically as navigating around timeline, gets repetitive.
446376 return (
447- < Flex
448- display = "inline-flex"
449- direction = "row"
450- role = "toolbar"
451- aria-label = { _t ( "timeline|mab|label" ) }
452- aria-live = "off"
453- onKeyDown = { handleToolbarKeyDown }
454- onFocusCapture = { handleToolbarFocusCapture }
455- className = { classNames ( className , styles . toolbar ) }
456- >
457- { toolbarButtons . map ( ( meta ) => actionButtons [ meta . action ] ) }
458- </ Flex >
377+ < RovingTabIndexProvider key = { rovingProviderKey } handleLeftRight handleHomeEnd handleLoop >
378+ { ( { onKeyDownHandler } ) => (
379+ < Flex
380+ display = "inline-flex"
381+ direction = "row"
382+ role = "toolbar"
383+ aria-label = { _t ( "timeline|mab|label" ) }
384+ aria-live = "off"
385+ onKeyDown = { onKeyDownHandler }
386+ className = { classNames ( className , styles . toolbar ) }
387+ >
388+ { toolbarButtons . map ( ( meta ) => actionButtons [ meta . action ] ) }
389+ </ Flex >
390+ ) }
391+ </ RovingTabIndexProvider >
459392 ) ;
460393}
0 commit comments