@@ -500,6 +500,8 @@ function renderActionLink(itemConfig) {
500500 var newLink = document . createElement ( "a" ) ;
501501 newLink . textContent = itemConfig . label ;
502502 newLink . href = itemConfig . href || "#" ;
503+ newLink . setAttribute ( "role" , "menuitem" ) ;
504+ newLink . tabIndex = - 1 ;
503505 if ( itemConfig . onClick ) {
504506 newLink . addEventListener ( "click" , itemConfig . onClick ) ;
505507 }
@@ -512,9 +514,36 @@ const initDatasetteTable = function (manager) {
512514 if ( ! window . URLSearchParams ) {
513515 return ;
514516 }
515- function closeMenu ( ) {
517+ function closeMenu ( options ) {
516518 menu . style . display = "none" ;
517519 menu . classList . remove ( "anim-scale-in" ) ;
520+ if ( menu . _trigger ) {
521+ menu . _trigger . setAttribute ( "aria-expanded" , "false" ) ;
522+ if ( options && options . restoreFocus ) {
523+ menu . _trigger . focus ( ) ;
524+ }
525+ menu . _trigger = null ;
526+ }
527+ }
528+
529+ // Move keyboard focus between the open menu's items (wraps around).
530+ function focusMenuItem ( index ) {
531+ var items = menu . querySelectorAll ( '[role="menuitem"]' ) ;
532+ if ( ! items . length ) {
533+ return ;
534+ }
535+ var i = ( index + items . length ) % items . length ;
536+ items [ i ] . focus ( ) ;
537+ }
538+
539+ // Keyboard support on the column cog trigger: open with Enter/Space/ArrowDown
540+ // and move focus onto the first item, per the ARIA menu button pattern.
541+ function onTriggerKeydown ( ev ) {
542+ if ( ev . key === "Enter" || ev . key === " " || ev . key === "ArrowDown" ) {
543+ ev . preventDefault ( ) ;
544+ onTableHeaderClick ( ev ) ;
545+ focusMenuItem ( 0 ) ;
546+ }
518547 }
519548
520549 const tableWrapper = document . querySelector ( manager . selectors . tableWrapper ) ;
@@ -535,6 +564,7 @@ const initDatasetteTable = function (manager) {
535564 function onTableHeaderClick ( ev ) {
536565 ev . preventDefault ( ) ;
537566 ev . stopPropagation ( ) ;
567+ var trigger = ev . currentTarget ;
538568 menu . innerHTML = DROPDOWN_HTML ;
539569 var th = ev . target ;
540570 while ( th . nodeName != "TH" ) {
@@ -560,9 +590,11 @@ const initDatasetteTable = function (manager) {
560590 } ,
561591 } ) ;
562592 var menuList = menu . querySelector ( "ul.dropdown-actions" ) ;
593+ menuList . setAttribute ( "role" , "menu" ) ;
563594 menuList . innerHTML = "" ;
564595 actionState . actionItems . forEach ( ( itemConfig ) => {
565596 var menuItem = document . createElement ( "li" ) ;
597+ menuItem . setAttribute ( "role" , "none" ) ;
566598 menuItem . appendChild ( renderActionLink ( itemConfig ) ) ;
567599 menuList . appendChild ( menuItem ) ;
568600 } ) ;
@@ -607,8 +639,35 @@ const initDatasetteTable = function (manager) {
607639 // And move hook tip as well
608640 hook . style . left = menuWidth - 13 + "px" ;
609641 }
642+
643+ menu . _trigger = trigger ;
644+ trigger . setAttribute ( "aria-expanded" , "true" ) ;
610645 }
611646
647+ // Keyboard navigation within the open menu (ARIA menu button pattern).
648+ menu . addEventListener ( "keydown" , function ( ev ) {
649+ var items = Array . from ( menu . querySelectorAll ( '[role="menuitem"]' ) ) ;
650+ var currentIndex = items . indexOf ( document . activeElement ) ;
651+ if ( ev . key === "ArrowDown" ) {
652+ ev . preventDefault ( ) ;
653+ focusMenuItem ( currentIndex + 1 ) ;
654+ } else if ( ev . key === "ArrowUp" ) {
655+ ev . preventDefault ( ) ;
656+ focusMenuItem ( currentIndex - 1 ) ;
657+ } else if ( ev . key === "Home" ) {
658+ ev . preventDefault ( ) ;
659+ focusMenuItem ( 0 ) ;
660+ } else if ( ev . key === "End" ) {
661+ ev . preventDefault ( ) ;
662+ focusMenuItem ( items . length - 1 ) ;
663+ } else if ( ev . key === "Escape" ) {
664+ ev . preventDefault ( ) ;
665+ closeMenu ( { restoreFocus : true } ) ;
666+ } else if ( ev . key === "Tab" ) {
667+ closeMenu ( ) ;
668+ }
669+ } ) ;
670+
612671 var svg = document . createElement ( "div" ) ;
613672 svg . innerHTML = DROPDOWN_ICON_SVG ;
614673 svg = svg . querySelector ( "*" ) ;
@@ -624,11 +683,21 @@ const initDatasetteTable = function (manager) {
624683 document . querySelectorAll ( manager . selectors . tableHeaders ) ,
625684 ) ;
626685 ths . forEach ( ( th ) => {
627- if ( ! th . querySelector ( "a" ) ) {
686+ var columnLink = th . querySelector ( "a" ) ;
687+ if ( ! columnLink ) {
628688 return ;
629689 }
630690 var icon = svg . cloneNode ( true ) ;
691+ icon . setAttribute ( "role" , "button" ) ;
692+ icon . setAttribute ( "tabindex" , "0" ) ;
693+ icon . setAttribute ( "aria-haspopup" , "menu" ) ;
694+ icon . setAttribute ( "aria-expanded" , "false" ) ;
695+ icon . setAttribute (
696+ "aria-label" ,
697+ "Column actions for " + columnLink . textContent . trim ( ) ,
698+ ) ;
631699 icon . addEventListener ( "click" , onTableHeaderClick ) ;
700+ icon . addEventListener ( "keydown" , onTriggerKeydown ) ;
632701 th . appendChild ( icon ) ;
633702 } ) ;
634703} ;
0 commit comments