Skip to content
This repository was archived by the owner on May 20, 2023. It is now read-only.

Commit 2b3694b

Browse files
Googlernshahan
authored andcommitted
[a11y] Make Material Fab Menu keyboard accessible based on WAI-ARIA best practices for menu button: https://www.w3.org/TR/wai-aria-practices/#menubutton
Behavior when button has focus: * ENTER opens the menu & places focus on the first menu item. * SPACE opens the menu and places focus on the first menu item. * DOWN arrow opens the menu and places focus on the first menu item. * UP arrow opens the menu and places focus on the first menu item. Note: when the screenreader opens the MaterialFabMenu the following dialogue is spoken: "Entered dialogue, exited main. [Label of first menu item]." PiperOrigin-RevId: 225301155
1 parent ef4ca44 commit 2b3694b

File tree

2 files changed

+49
-2
lines changed

2 files changed

+49
-2
lines changed

angular_components/lib/material_menu/material_fab_menu.dart

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// BSD-style license that can be found in the LICENSE file.
44

55
import 'dart:async';
6+
import 'dart:html';
67

78
import 'package:angular/angular.dart';
89
import 'package:angular_components/content/deferred_content.dart';
@@ -16,8 +17,11 @@ import 'package:angular_components/material_menu/menu_root.dart';
1617
import 'package:angular_components/material_popup/material_popup.dart';
1718
import 'package:angular_components/material_tooltip/material_tooltip.dart';
1819
import 'package:angular_components/mixins/track_layout_changes.dart';
20+
import 'package:angular_components/model/a11y/keyboard_handler_mixin.dart';
1921
import 'package:angular_components/model/menu/menu.dart';
2022
import 'package:angular_components/model/observable/observable.dart';
23+
import 'package:angular_components/utils/browser/events/events.dart';
24+
import 'package:angular_components/utils/id_generator/id_generator.dart';
2125

2226
import 'menu_item_groups.dart';
2327

@@ -45,8 +49,9 @@ import 'menu_item_groups.dart';
4549
preserveWhitespace: true,
4650
)
4751
class MaterialFabMenuComponent extends Object
48-
with TrackLayoutChangesMixin
52+
with KeyboardHandlerMixin, TrackLayoutChangesMixin
4953
implements OnDestroy {
54+
final ActiveMenuItemModel<MenuItem> activeModel;
5055
final ChangeDetectorRef _changeDetector;
5156

5257
/// Popup positions for the menu popup to show up in.
@@ -57,7 +62,13 @@ class MaterialFabMenuComponent extends Object
5762

5863
MaterialFabMenuModel _viewModel;
5964

60-
MaterialFabMenuComponent(this._changeDetector);
65+
factory MaterialFabMenuComponent(ChangeDetectorRef changeDetector,
66+
@Optional() IdGenerator idGenerator) =>
67+
MaterialFabMenuComponent._(
68+
changeDetector, idGenerator ?? new SequentialIdGenerator.fromUUID());
69+
70+
MaterialFabMenuComponent._(this._changeDetector, IdGenerator idGenerator)
71+
: activeModel = ActiveMenuItemModel<MenuItem>(idGenerator);
6172

6273
/// Emits when fab is opened.
6374
@Output()
@@ -119,13 +130,45 @@ class MaterialFabMenuComponent extends Object
119130

120131
bool get hasIcons => _viewModel.hasIcons;
121132

133+
/// Keypress callback is used to handle Up and Down keys.
134+
///
135+
/// Unprintable keys use this listener while printable keys
136+
/// use [handleKeyPress].
137+
/// Enabling [MaterialFabMenu] to be opened with the keyboard improves
138+
/// accessibility and improves with a11y navigation.
139+
@HostListener('keydown')
140+
void handleKeyDown(KeyboardEvent event) {
141+
if (event.keyCode == KeyCode.DOWN || event.keyCode == KeyCode.UP) {
142+
openMenuAndActivateFirstItem();
143+
}
144+
}
145+
146+
/// Keypress callback is used to handle Enter and Space keys.
147+
@HostListener('keypress')
148+
void handleKeyPress(KeyboardEvent event) {
149+
if (event.keyCode == KeyCode.ENTER || isSpaceKey(event)) {
150+
openMenuAndActivateFirstItem();
151+
}
152+
}
153+
122154
@override
123155
void ngOnDestroy() {
124156
_viewModelStreamSub?.cancel();
125157
_viewModelStreamSub = null;
126158
_onShow.close();
127159
}
128160

161+
/// Sets the [activeModel.menu] before the popup is opened so that
162+
/// the first item will already be activated when the menu is expanded.
163+
///
164+
/// [MenuItemGroupsComponent] handles the logic to activate the first item.
165+
void openMenuAndActivateFirstItem() {
166+
if (hasMenu) {
167+
activeModel.menu = menuItem.subMenu;
168+
}
169+
_viewModel.trigger();
170+
}
171+
129172
void onPopupOpened() {
130173
if (_menuVisible) return;
131174
_menuVisible = true;

angular_components/lib/material_menu/material_fab_menu.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
[tooltipPositions]="tooltipPositions"
1414
[attr.aria-label]="ariaLabel"
1515
[attr.navi-id]="naviId"
16+
(keydown)="onKeyDown($event)"
17+
(keypress)="onKeyPress($event)"
1618
(trigger)="trigger">
1719
<material-icon [icon]="glyph"></material-icon>
1820
</material-fab>
@@ -38,6 +40,8 @@
3840
<material-icon class="close-icon material-list-item-primary" icon="close"></material-icon>
3941
</material-list-item>
4042
<menu-item-groups [menu]="menuItem.subMenu"
43+
[activeModel]="activeModel"
44+
[attr.aria-activedescendant]="activeModel.activeId"
4145
class="menu-groups"
4246
autoFocus
4347
menu-root

0 commit comments

Comments
 (0)