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/model/menu/menu.dart' ;
910import 'package:angular_components/model/observable/observable.dart' ;
1011import 'package:angular_components/model/ui/accepts_width.dart' ;
1112import 'package:angular_components/utils/angular/properties/properties.dart' ;
1213
14+ /// Represents an intent to open the menu popup.
15+ ///
16+ /// This data struct is used to pass parameters along with the intent, such as
17+ /// which menu item to focus when the item is first opened.
18+ class ExpandAction {
19+ final int _focusIndexOnExpand;
20+
21+ const ExpandAction .withFirstItemFocused () : _focusIndexOnExpand = 0 ;
22+
23+ const ExpandAction .withLastItemFocused () : _focusIndexOnExpand = - 1 ;
24+
25+ const ExpandAction .withNoFocus () : _focusIndexOnExpand = null ;
26+
27+ bool get activateFirstItemOnExpand => _focusIndexOnExpand == 0 ;
28+ bool get activateLastItemOnExpand => _focusIndexOnExpand == - 1 ;
29+ }
30+
1331/// A mixin for classes that wrap a [MenuPopupComponent] .
1432class MenuPopupWrapper implements AcceptsWidth {
33+ final _expandAction = ObservableReference <ExpandAction >(null );
34+
1535 /// The displayed menu.
1636 @Input ()
1737 MenuModel menu;
1838
1939 /// Whether the menu is open.
40+ ///
41+ /// Sets the default expansion state when this input is used to control menu
42+ /// visibility. Default state means no menu items are focused when the menu
43+ /// popup is opened.
2044 @Input ()
2145 set isExpanded (value) {
22- if (_expanded.value == value) return ;
23- _expanded.value = getBool (value);
46+ if (isExpanded == value) return ;
47+
48+ if (getBool (value)) {
49+ expandAction ?? = const ExpandAction .withNoFocus ();
50+ } else {
51+ expandAction = null ;
52+ }
2453 }
2554
26- bool get isExpanded => _expanded.value;
55+ /// Expansion state of the menu popup.
56+ ///
57+ /// Null value means popup is closed, any other value means popup is open.
58+ /// This input is used to pass parameters with the open action. E.g. which
59+ /// element is selected when the popup is focused.
60+ @Input ()
61+ set expandAction (ExpandAction value) {
62+ if (_expandAction.value == value) return ;
63+
64+ _expandAction.value = value;
65+ }
66+
67+ /// True if the menu popup is expanded/open.
68+ bool get isExpanded => expandAction != null ;
69+
70+ ExpandAction get expandAction => _expandAction.value;
71+
72+ /// True if the menu item group should select the first item when the popup
73+ /// is opened.
74+ bool get activateFirstItemOnExpand =>
75+ expandAction? .activateFirstItemOnExpand ?? false ;
76+
77+ /// True if the menu item group should select the first item when the popup
78+ /// is opened.
79+ bool get activateLastItemOnExpand =>
80+ expandAction? .activateLastItemOnExpand ?? false ;
2781
2882 /// Outputs an event when the menu is expanded.
2983 @Output ()
30- Stream <bool > get isExpandedChange => _expanded.stream;
31- final _expanded = ObservableReference <bool >(false );
84+ Stream <bool > get isExpandedChange =>
85+ _expandAction.stream.map ((_) => isExpanded);
86+
87+ /// Outputs an event when the menu expansion state is changed.
88+ ///
89+ ///
90+ @Output ()
91+ Stream <ExpandAction > get expandActionChange => _expandAction.stream;
3292
3393 /// Selects 1 of 5 predefined width values for the menu.
3494 ///
@@ -52,3 +112,72 @@ class MenuPopupWrapper implements AcceptsWidth {
52112 @Input ()
53113 Iterable preferredPositions;
54114}
115+
116+ /// Provides basic accessibility-friendly methods for showing and hiding the
117+ /// menu popup described by [MenuPopupWrapper] .
118+ ///
119+ /// Basic usage:
120+ /// <div
121+ /// buttonDecorator
122+ /// (keydown)="onKeyDown($event)"
123+ /// (trigger)="handlePopupTriggerAction">
124+ /// button
125+ /// </div>
126+ ///
127+ /// Should be a mixin, but cannot because of ACX gallery compilation limitation
128+ /// See b/130170577 for details.
129+ abstract class MenuPopupTrigger {
130+ final _onTriggerAction = StreamController <void >();
131+
132+ /// If true, the menu popup will be closed if the trigger button is clicked
133+ /// while the menu is open.
134+ ///
135+ /// Otherwise, clicking the trigger button when the menu is already open will
136+ /// not do anything.
137+ @Input ()
138+ bool closeMenuOnClick = false ;
139+
140+ /// Outputs an event when the menu button is triggered.
141+ @Output ('trigger' )
142+ Stream <void > get onTrigger => _onTriggerAction.stream;
143+
144+ bool get isExpanded;
145+
146+ set expandAction (ExpandAction value);
147+
148+ // The following methods are for accessibility-friendly trigger actions.
149+ // See the 'Keyboard Interaction' section on this page:
150+ // https://www.w3.org/TR/wai-aria-practices/#menubutton
151+ void handlePopupTriggerAction (UIEvent event) {
152+ if (event is KeyboardEvent ) {
153+ _trigger (const ExpandAction .withFirstItemFocused ());
154+ } else {
155+ _trigger (const ExpandAction .withNoFocus ());
156+ }
157+ }
158+
159+ /// Provides the default implementation if parent class mixes
160+ /// in [KeyboardHandlerMixin] .
161+ void handleUpKey (KeyboardEvent event) {
162+ _trigger (const ExpandAction .withLastItemFocused ());
163+ // Prevent the scrolling associated with arrow keys.
164+ event.preventDefault ();
165+ }
166+
167+ /// Provides the default implementation if parent class mixes
168+ /// in [KeyboardHandlerMixin] .
169+ void handleDownKey (KeyboardEvent event) {
170+ _trigger (const ExpandAction .withFirstItemFocused ());
171+ // Prevent the scrolling associated with arrow keys.
172+ event.preventDefault ();
173+ }
174+
175+ void _trigger (ExpandAction action) {
176+ if (closeMenuOnClick && isExpanded) {
177+ expandAction = null ;
178+ } else {
179+ expandAction = action;
180+ }
181+ _onTriggerAction.add (null );
182+ }
183+ }
0 commit comments