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

Commit b3b1b94

Browse files
committed
Add Material Menu component to export to github.
PiperOrigin-RevId: 176383235
1 parent a371e31 commit b3b1b94

25 files changed

+2093
-3
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
> 5.0.0-alpha+1. The alpha tag represents the evolving nature of the AngularDart
55
> api, not code quality (5.0.0-alpha+1 is used in production Google apps).
66
7+
* Add Material Menu component.
78
* Material Auto Suggest Input: Replace ngOnChanges(\_) with ngAfterChanges.
89
* Material Button:
910
* Update dense mixin to only apply to non-icon buttons.

lib/material_menu/_mixins.scss

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
@import 'package:angular_components/material_button/mixins';
6+
@import 'package:angular_components/material_select/mixins';
7+
8+
/// Sets the height of menu-item
9+
@mixin item-height($selector, $height) {
10+
#{$selector} ::ng-deep .menu-item {
11+
height: $height;
12+
}
13+
}
14+
15+
/// Hides underline for button text from dropdown-button widget.
16+
@mixin dropdown-menu-nounderline {
17+
::ng-deep dropdown-button {
18+
@include button-look;
19+
}
20+
}
21+
22+
@mixin dropdown-menu-nounderline-no-opacity {
23+
::ng-deep dropdown-button {
24+
.button.border {
25+
border-bottom: 0;
26+
padding-bottom: 0;
27+
}
28+
}
29+
}
30+
31+
@mixin menu-item-affix-hover {
32+
::ng-deep .secondary-icon.hover-icon {
33+
opacity: inherit;
34+
}
35+
}
36+
37+
@mixin material-fab-menu-icon-size($size) {
38+
::ng-deep material-fab-menu.toolbelt-fab material-fab {
39+
@include fab-size($size);
40+
41+
top: -($mat-grid * 4) - $size / 2;
42+
}
43+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:angular/angular.dart';
6+
import 'package:angular_components/material_menu/menu_popup.dart';
7+
import 'package:angular_components/material_menu/menu_popup_wrapper.dart';
8+
import 'package:angular_components/material_popup/material_popup.dart';
9+
import 'package:angular_components/material_select/dropdown_button.dart';
10+
import 'package:angular_components/utils/disposer/disposer.dart';
11+
12+
/// The [DropdownMenuComponent] combines a [DropdownButtonComponent] with a
13+
/// [MenuPopup].
14+
@Component(
15+
selector: 'dropdown-menu',
16+
directives: const [
17+
DropdownButtonComponent,
18+
MenuPopupComponent,
19+
PopupSourceDirective
20+
],
21+
templateUrl: 'dropdown_menu.html',
22+
changeDetection: ChangeDetectionStrategy.OnPush,
23+
// TODO(google): Change preserveWhitespace to false to improve codesize.
24+
preserveWhitespace: true,
25+
)
26+
class DropdownMenuComponent extends Object
27+
with MenuPopupWrapper
28+
implements OnDestroy {
29+
final _disposer = new Disposer.oneShot();
30+
31+
DropdownMenuComponent(ChangeDetectorRef _changeDetector) {
32+
// Let Angular pick up changes to [isExpanded] in [MenuPopupWrapper] when
33+
// it's toggled programatically, e.g. TabMenuComponent.
34+
_disposer.addStreamSubscription(isExpandedChange.listen((_) {
35+
_changeDetector.markForCheck();
36+
}));
37+
}
38+
39+
@override
40+
void ngOnDestroy() {
41+
_disposer.dispose();
42+
}
43+
44+
/// Dropdown button text.
45+
@Input()
46+
String buttonText;
47+
48+
@Input()
49+
bool disabled = false;
50+
51+
bool get dropdownStyle => _dropdownStyle;
52+
bool _dropdownStyle = false;
53+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<!--
2+
Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file
3+
for details. All rights reserved. Use of this source code is governed by a
4+
BSD-style license that can be found in the LICENSE file.
5+
-->
6+
<dropdown-button
7+
(trigger)="isExpanded=true"
8+
[buttonText]="buttonText"
9+
[disabled]="disabled"
10+
popupSource
11+
#toggle="popupSource">
12+
<ng-content select="[button-content]"></ng-content>
13+
</dropdown-button>
14+
<menu-popup
15+
[(isExpanded)]="isExpanded"
16+
[menu]="menu"
17+
[popupSource]="toggle"
18+
[preferredPositions]="preferredPositions"
19+
[visible]="isExpanded"
20+
[width]="width">
21+
<ng-content></ng-content>
22+
</menu-popup>
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'dart:async';
6+
import 'dart:html';
7+
8+
import 'package:angular/angular.dart';
9+
import 'package:angular_components/content/deferred_content.dart';
10+
import 'package:angular_components/focus/focus.dart';
11+
import 'package:angular_components/glyph/glyph.dart';
12+
import 'package:angular_components/laminate/enums/alignment.dart';
13+
import 'package:angular_components/laminate/popup/popup.dart';
14+
import 'package:angular_components/material_button/material_fab.dart';
15+
import 'package:angular_components/material_list/material_list_item.dart';
16+
import 'package:angular_components/material_menu/menu_root.dart';
17+
import 'package:angular_components/material_popup/material_popup.dart';
18+
import 'package:angular_components/material_tooltip/material_tooltip.dart';
19+
import 'package:angular_components/mixins/track_layout_changes.dart';
20+
import 'package:angular_components/model/menu/menu.dart';
21+
import 'package:angular_components/model/observable/observable.dart';
22+
23+
import 'menu_item_groups.dart';
24+
25+
// Prevent menu from showing instantly after closing
26+
const _menuVisibilityGracePeriod = const Duration(milliseconds: 400);
27+
28+
/// A fab menu item with optional sub menu.
29+
@Component(
30+
selector: 'material-fab-menu',
31+
changeDetection: ChangeDetectionStrategy.OnPush,
32+
directives: const [
33+
AutoFocusDirective,
34+
DeferredContentDirective,
35+
NgFor,
36+
NgIf,
37+
GlyphComponent,
38+
MaterialFabComponent,
39+
MaterialListItemComponent,
40+
MaterialPopupComponent,
41+
MaterialTooltipDirective,
42+
MenuItemGroupsComponent,
43+
MenuRootDirective,
44+
PopupSourceDirective,
45+
],
46+
templateUrl: 'material_fab_menu.html',
47+
styleUrls: const ['material_fab_menu.scss.css'],
48+
// TODO(google): Change preserveWhitespace to false to improve codesize.
49+
preserveWhitespace: true,
50+
)
51+
class MaterialFabMenuComponent extends Object
52+
with TrackLayoutChangesMixin
53+
implements OnDestroy {
54+
final ChangeDetectorRef _changeDetector;
55+
56+
@Input()
57+
List<RelativePosition> preferredPopupPositions;
58+
59+
StreamSubscription _viewModelStreamSub;
60+
61+
MaterialFabMenuModel _viewModel;
62+
63+
MaterialFabMenuComponent(this._changeDetector);
64+
65+
/// Emits when fab is opened.
66+
@Output()
67+
Stream<Null> get onShow => _onShow.stream;
68+
final _onShow = new StreamController<Null>.broadcast();
69+
70+
/// [MenuItem] that defines the appearance and behavior of this menu.
71+
///
72+
/// If the item has sub menu with no empty item groups, a menu will be
73+
/// surfaced via clicking or hovering.
74+
@Input()
75+
set menuItem(MenuItem menuItem) {
76+
if (menuItem == null) return;
77+
78+
viewModel = new MaterialFabMenuModel(menuItem, showPopup: showPopup);
79+
}
80+
81+
/// Sets the view model for this component.
82+
@Input()
83+
set viewModel(MaterialFabMenuModel value) {
84+
if (value == null) return;
85+
86+
_viewModel = value;
87+
_viewModelStreamSub?.cancel();
88+
_viewModelStreamSub = _viewModel.onShowPopupChange.listen((_) {
89+
_changeDetector.markForCheck();
90+
});
91+
}
92+
93+
@Input()
94+
String naviId;
95+
96+
MenuItem get menuItem => _viewModel?.menuItem;
97+
98+
bool get isFabEnabled => _viewModel?.isFabEnabled ?? false;
99+
100+
String get glyph => _viewModel?.glyph;
101+
102+
String get ariaLabel => _viewModel?.ariaLabel;
103+
104+
String get tooltip => _viewModel?.tooltip;
105+
106+
bool get hasTooltip => tooltip?.isNotEmpty ?? false;
107+
108+
bool get hasMenu => _viewModel?.hasMenu ?? false;
109+
110+
bool get isFabHidden => _viewModel?.isFabHidden ?? false;
111+
112+
bool get showPopup => _viewModel?.showPopup ?? false;
113+
114+
bool get closing => showPopup && !menuVisible;
115+
116+
bool _menuVisible = false;
117+
118+
bool get menuVisible => _menuVisible;
119+
120+
@ViewChild('content')
121+
ElementRef contentElementRef;
122+
123+
@override
124+
void ngOnDestroy() {
125+
_viewModelStreamSub?.cancel();
126+
_viewModelStreamSub = null;
127+
_onShow.close();
128+
}
129+
130+
void onPopupOpened() {
131+
if (_menuVisible || contentElementRef == null) return;
132+
// Set the content wrapper large enough so as not to cut off any menu
133+
// contents. Menu contents are measured via scrolling dimensions.
134+
var e = contentElementRef.nativeElement as HtmlElement;
135+
var scrollWidth = e.scrollWidth;
136+
var scrollHeight = e.scrollHeight;
137+
e.style.width = '${scrollWidth}px';
138+
e.style.height = '${scrollHeight}px';
139+
_menuVisible = true;
140+
_onShow.add(null);
141+
}
142+
143+
void onPopupClosed() {
144+
_viewModel.closePopup();
145+
_hideMenuContent();
146+
}
147+
148+
void trigger() {
149+
_viewModel.trigger();
150+
}
151+
152+
void hideMenu() {
153+
_hideMenuContent();
154+
new Future.delayed(MaterialPopupComponent.SLIDE_DELAY, () {
155+
_viewModel.closePopup();
156+
});
157+
}
158+
159+
void _hideMenuContent() {
160+
if (!_menuVisible) return;
161+
_menuVisible = false;
162+
if (contentElementRef == null) return;
163+
var e = contentElementRef.nativeElement as HtmlElement;
164+
e.style.removeProperty('width');
165+
e.style.removeProperty('height');
166+
}
167+
168+
final tooltipPositions = const <RelativePosition>[
169+
RelativePosition.AdjacentRight,
170+
RelativePosition.AdjacentLeft,
171+
];
172+
}
173+
174+
/// View model for [MaterialFabMenu].
175+
///
176+
/// The primary use case of this view model is to expose an interface for a
177+
/// parent component to change the visibility of the menu popup without using
178+
/// a @ViewChild directive. This is due to popup visibility being dependent on
179+
/// whether a sub-menu is actually available.
180+
class MaterialFabMenuModel {
181+
/// Menu item model used to render the FAB and its sub-menus.
182+
final MenuItem menuItem;
183+
184+
final ObservableReference<bool> _showPopup;
185+
186+
MaterialFabMenuModel(this.menuItem, {bool showPopup: false})
187+
: _showPopup = new ObservableReference<bool>(showPopup);
188+
189+
Stream<Change<bool>> get onShowPopupChange => _showPopup.changes;
190+
191+
/// True if the [menuItem] exists and has at least one item.
192+
bool get hasMenu => menuItem.subMenu?.itemGroups?.isNotEmpty ?? false;
193+
194+
/// True if the FAB menu should be shown.
195+
bool get showPopup => _showPopup.value;
196+
197+
/// True if FAB is in an enabled state - can be clicked and triggered.
198+
bool get isFabEnabled => menuItem.enabled ?? false;
199+
200+
/// Name of glyph displayed within FAB circle.
201+
String get glyph => menuItem.icon?.name;
202+
203+
String get ariaLabel => menuItem.label;
204+
205+
String get tooltip => menuItem.tooltip ?? menuItem.label;
206+
207+
bool get isFabHidden => hasMenu ? _showPopup.value : false;
208+
209+
/// If the FAB has a sub-menu, then open the sub-menu popup, otherwise only
210+
/// trigger the action callback on the FAB menu item model.
211+
void trigger() {
212+
if (hasMenu) {
213+
_showPopup.value = true;
214+
} else if (menuItem.action != null) {
215+
menuItem.action();
216+
}
217+
}
218+
219+
/// Close the sub-menu, if it is open.
220+
void closePopup() {
221+
_showPopup.value = false;
222+
}
223+
}

0 commit comments

Comments
 (0)