33// BSD-style license that can be found in the LICENSE file.
44
55import 'dart:async' ;
6+ import 'dart:html' ;
67
78import 'package:angular/angular.dart' ;
89import 'package:angular_components/content/deferred_content.dart' ;
@@ -16,8 +17,11 @@ import 'package:angular_components/material_menu/menu_root.dart';
1617import 'package:angular_components/material_popup/material_popup.dart' ;
1718import 'package:angular_components/material_tooltip/material_tooltip.dart' ;
1819import 'package:angular_components/mixins/track_layout_changes.dart' ;
20+ import 'package:angular_components/model/a11y/keyboard_handler_mixin.dart' ;
1921import 'package:angular_components/model/menu/menu.dart' ;
2022import '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
2226import 'menu_item_groups.dart' ;
2327
@@ -45,8 +49,9 @@ import 'menu_item_groups.dart';
4549 preserveWhitespace: true ,
4650)
4751class 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 ;
0 commit comments