@theseam/ui-common 0.4.1 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/esm2020/datatable/datatable-action-menu/datatable-action-menu.component.mjs +1 -1
- package/esm2020/datatable/datatable-column-preferences-button/datatable-column-preferences-button.component.mjs +1 -1
- package/esm2020/datatable/datatable-export-button/datatable-export-button.component.mjs +1 -1
- package/esm2020/datatable-dynamic/datatable-dynamic-action-menu/datatable-dynamic-action-menu.component.mjs +1 -1
- package/esm2020/framework/schema-form-controls/schema-form-submit-split/schema-form-submit-split.component.mjs +1 -1
- package/esm2020/framework/top-bar/top-bar.component.mjs +1 -1
- package/esm2020/google-maps/google-maps/google-maps.component.mjs +32 -8
- package/esm2020/google-maps/google-maps-feature-helpers.mjs +13 -1
- package/esm2020/google-maps/google-maps.module.mjs +4 -2
- package/esm2020/google-maps/google-maps.service.mjs +96 -25
- package/esm2020/google-maps/public-api.mjs +2 -1
- package/esm2020/menu/menu-item.component.mjs +43 -15
- package/esm2020/menu/menu-toggle.directive.mjs +202 -43
- package/esm2020/menu/menu.component.mjs +37 -14
- package/esm2020/utils/geo-json/close-polygons.mjs +38 -0
- package/esm2020/utils/public-api.mjs +2 -1
- package/esm2020/widget/widget/widget.component.mjs +54 -33
- package/fesm2015/theseam-ui-common-datatable-dynamic.mjs +1 -1
- package/fesm2015/theseam-ui-common-datatable-dynamic.mjs.map +1 -1
- package/fesm2015/theseam-ui-common-datatable.mjs +3 -3
- package/fesm2015/theseam-ui-common-datatable.mjs.map +1 -1
- package/fesm2015/theseam-ui-common-framework.mjs +2 -2
- package/fesm2015/theseam-ui-common-framework.mjs.map +1 -1
- package/fesm2015/theseam-ui-common-google-maps.mjs +138 -28
- package/fesm2015/theseam-ui-common-google-maps.mjs.map +1 -1
- package/fesm2015/theseam-ui-common-menu.mjs +442 -228
- package/fesm2015/theseam-ui-common-menu.mjs.map +1 -1
- package/fesm2015/theseam-ui-common-utils.mjs +39 -1
- package/fesm2015/theseam-ui-common-utils.mjs.map +1 -1
- package/fesm2015/theseam-ui-common-widget.mjs +53 -32
- package/fesm2015/theseam-ui-common-widget.mjs.map +1 -1
- package/fesm2020/theseam-ui-common-datatable-dynamic.mjs +1 -1
- package/fesm2020/theseam-ui-common-datatable-dynamic.mjs.map +1 -1
- package/fesm2020/theseam-ui-common-datatable.mjs +3 -3
- package/fesm2020/theseam-ui-common-datatable.mjs.map +1 -1
- package/fesm2020/theseam-ui-common-framework.mjs +2 -2
- package/fesm2020/theseam-ui-common-framework.mjs.map +1 -1
- package/fesm2020/theseam-ui-common-google-maps.mjs +141 -32
- package/fesm2020/theseam-ui-common-google-maps.mjs.map +1 -1
- package/fesm2020/theseam-ui-common-menu.mjs +432 -225
- package/fesm2020/theseam-ui-common-menu.mjs.map +1 -1
- package/fesm2020/theseam-ui-common-utils.mjs +39 -1
- package/fesm2020/theseam-ui-common-utils.mjs.map +1 -1
- package/fesm2020/theseam-ui-common-widget.mjs +53 -32
- package/fesm2020/theseam-ui-common-widget.mjs.map +1 -1
- package/google-maps/google-maps/google-maps.component.d.ts +8 -1
- package/google-maps/google-maps-feature-helpers.d.ts +3 -0
- package/google-maps/google-maps.module.d.ts +1 -1
- package/google-maps/google-maps.service.d.ts +7 -0
- package/google-maps/public-api.d.ts +1 -0
- package/menu/menu-item.component.d.ts +20 -9
- package/menu/menu-toggle.directive.d.ts +34 -11
- package/menu/menu.component.d.ts +16 -2
- package/package.json +1 -1
- package/utils/geo-json/close-polygons.d.ts +9 -0
- package/utils/public-api.d.ts +1 -0
- package/widget/widget/widget.component.d.ts +10 -11
- package/widget/widget/widget.component.scss +12 -1
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
import { animation, style, group, animate, query, useAnimation, trigger, transition } from '@angular/animations';
|
|
2
2
|
import * as i0 from '@angular/core';
|
|
3
|
-
import { Component, ChangeDetectionStrategy, InjectionToken, Inject, Optional, Input, HostListener,
|
|
3
|
+
import { Component, ChangeDetectionStrategy, InjectionToken, Inject, Optional, Input, HostListener, forwardRef, EventEmitter, TemplateRef, ViewChild, Output, inject, ChangeDetectorRef, Directive, Self, HostBinding, NgModule } from '@angular/core';
|
|
4
4
|
import * as i2 from '@angular/common';
|
|
5
5
|
import { DOCUMENT, CommonModule } from '@angular/common';
|
|
6
|
-
import { Subject, Subscription, of, merge,
|
|
6
|
+
import { Subject, BehaviorSubject, Subscription, fromEvent, of, merge, filter, delay, asapScheduler, take, takeUntil as takeUntil$1 } from 'rxjs';
|
|
7
|
+
import { faCaretRight } from '@fortawesome/free-solid-svg-icons';
|
|
7
8
|
import { mixinDisabled } from '@theseam/ui-common/core';
|
|
8
9
|
import * as i1 from '@angular/cdk/a11y';
|
|
9
|
-
import {
|
|
10
|
+
import { FocusKeyManager, isFakeMousedownFromScreenReader } from '@angular/cdk/a11y';
|
|
10
11
|
import * as i3 from '@theseam/ui-common/icon';
|
|
11
12
|
import { TheSeamIconModule } from '@theseam/ui-common/icon';
|
|
12
|
-
import { UP_ARROW, DOWN_ARROW,
|
|
13
|
+
import { UP_ARROW, DOWN_ARROW, END, hasModifierKey, HOME, RIGHT_ARROW, LEFT_ARROW, ESCAPE, ENTER, SPACE } from '@angular/cdk/keycodes';
|
|
13
14
|
import { normalizePassiveListenerOptions } from '@angular/cdk/platform';
|
|
14
15
|
import { TemplatePortal } from '@angular/cdk/portal';
|
|
15
|
-
import * as i1$1 from '@angular/cdk/overlay';
|
|
16
|
-
import { OverlayModule } from '@angular/cdk/overlay';
|
|
17
16
|
import { coerceNumberProperty } from '@angular/cdk/coercion';
|
|
18
17
|
import { map, switchMap, startWith, distinctUntilChanged, takeUntil } from 'rxjs/operators';
|
|
18
|
+
import * as i1$1 from '@angular/cdk/overlay';
|
|
19
|
+
import { OverlayModule } from '@angular/cdk/overlay';
|
|
20
|
+
import * as i4 from '@angular/cdk/bidi';
|
|
19
21
|
|
|
20
22
|
const menuDropdownPanelSlideIn = animation([
|
|
21
23
|
style({
|
|
@@ -94,25 +96,25 @@ class TheSeamMenuItemBase {
|
|
|
94
96
|
}
|
|
95
97
|
const _seamMenuItemMixinBase = mixinDisabled(TheSeamMenuItemBase);
|
|
96
98
|
class MenuItemComponent extends _seamMenuItemMixinBase {
|
|
97
|
-
constructor(_elementRef,
|
|
99
|
+
constructor(_elementRef, _document, _focusMonitor, _changeDetectorRef, _parentMenu) {
|
|
98
100
|
super();
|
|
99
101
|
this._elementRef = _elementRef;
|
|
100
|
-
this.
|
|
102
|
+
this._document = _document;
|
|
101
103
|
this._focusMonitor = _focusMonitor;
|
|
104
|
+
this._changeDetectorRef = _changeDetectorRef;
|
|
102
105
|
this._parentMenu = _parentMenu;
|
|
103
106
|
/** ARIA role for the menu item. */
|
|
104
107
|
this.role = 'menuitem';
|
|
108
|
+
this.sublevelIcon = faCaretRight;
|
|
105
109
|
this.badgeTheme = 'danger';
|
|
106
110
|
/** Stream that emits when the menu item is hovered. */
|
|
107
111
|
this._hovered = new Subject();
|
|
112
|
+
/** Stream that emits when the menu item is focused. */
|
|
113
|
+
this._focused = new Subject();
|
|
108
114
|
/** Whether the menu item is highlighted. */
|
|
109
115
|
this._highlighted = false;
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
// to show the focus style for menu items only when the focus was not caused by a
|
|
113
|
-
// mouse or touch interaction.
|
|
114
|
-
_focusMonitor.monitor(this._elementRef, false);
|
|
115
|
-
}
|
|
116
|
+
/** Whether the menu item acts as a trigger for a sub-menu. */
|
|
117
|
+
this._triggersSubmenu = false;
|
|
116
118
|
// console.log(this._parentMenu)
|
|
117
119
|
if (_parentMenu && _parentMenu.addItem) {
|
|
118
120
|
_parentMenu.addItem(this);
|
|
@@ -126,6 +128,15 @@ class MenuItemComponent extends _seamMenuItemMixinBase {
|
|
|
126
128
|
this._parentMenu.removeItem(this);
|
|
127
129
|
}
|
|
128
130
|
this._hovered.complete();
|
|
131
|
+
this._focused.complete();
|
|
132
|
+
}
|
|
133
|
+
ngAfterViewInit() {
|
|
134
|
+
if (this._focusMonitor) {
|
|
135
|
+
// Start monitoring the element, so it gets the appropriate focused classes. We want
|
|
136
|
+
// to show the focus style for menu items only when the focus was not caused by a
|
|
137
|
+
// mouse or touch interaction.
|
|
138
|
+
this._focusMonitor.monitor(this._elementRef, false);
|
|
139
|
+
}
|
|
129
140
|
}
|
|
130
141
|
/** Focuses the menu item. */
|
|
131
142
|
focus(origin = 'program') {
|
|
@@ -135,6 +146,7 @@ class MenuItemComponent extends _seamMenuItemMixinBase {
|
|
|
135
146
|
else {
|
|
136
147
|
this._getHostElement().focus();
|
|
137
148
|
}
|
|
149
|
+
this._focused.next(this);
|
|
138
150
|
}
|
|
139
151
|
/** Used to set the `tabindex`. */
|
|
140
152
|
_getTabIndex() {
|
|
@@ -158,7 +170,7 @@ class MenuItemComponent extends _seamMenuItemMixinBase {
|
|
|
158
170
|
/** Gets the label to be used when determining whether the option should be focused. */
|
|
159
171
|
getLabel() {
|
|
160
172
|
const element = this._elementRef.nativeElement;
|
|
161
|
-
const textNodeType = this.
|
|
173
|
+
const textNodeType = this._document ? this._document.TEXT_NODE : 3;
|
|
162
174
|
let output = '';
|
|
163
175
|
if (element.childNodes) {
|
|
164
176
|
const length = element.childNodes.length;
|
|
@@ -173,22 +185,35 @@ class MenuItemComponent extends _seamMenuItemMixinBase {
|
|
|
173
185
|
}
|
|
174
186
|
return output.trim();
|
|
175
187
|
}
|
|
188
|
+
_setHighlighted(isHighlighted) {
|
|
189
|
+
this._highlighted = isHighlighted;
|
|
190
|
+
this._changeDetectorRef?.markForCheck();
|
|
191
|
+
}
|
|
192
|
+
_setTriggersSubmenu(triggersSubmenu) {
|
|
193
|
+
this._triggersSubmenu = triggersSubmenu;
|
|
194
|
+
this._changeDetectorRef?.markForCheck();
|
|
195
|
+
}
|
|
196
|
+
_hasFocus() {
|
|
197
|
+
return this._document && this._document.activeElement === this._getHostElement();
|
|
198
|
+
}
|
|
176
199
|
}
|
|
177
|
-
MenuItemComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.9", ngImport: i0, type: MenuItemComponent, deps: [{ token: i0.ElementRef }, { token: DOCUMENT }, { token: i1.FocusMonitor }, { token: THESEAM_MENU_PANEL, optional: true }], target: i0.ɵɵFactoryTarget.Component });
|
|
178
|
-
MenuItemComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "15.2.9", type: MenuItemComponent, selector: "[seamMenuItem]", inputs: { disabled: "disabled", role: "role", icon: "icon", iconClass: "iconClass", badgeText: "badgeText", badgeTheme: "badgeTheme" }, host: { listeners: { "click": "_checkDisabled($event)", "mouseenter": "_handleMouseEnter()" }, properties: { "attr.role": "role", "attr.tabindex": "_getTabIndex()", "attr.aria-disabled": "disabled.toString()", "attr.disabled": "disabled || null" }, classAttribute: "dropdown-item" }, exportAs: ["seamMenuItem"], usesInheritance: true, ngImport: i0, template: "<div class=\"d-flex flex-row\">\n <seam-icon *ngIf=\"icon\"\n style=\"width: 30px; height: 20px; flex: 0 0 auto;\"\n class=\"pr-2\"\n [icon]=\"icon\"\n [iconClass]=\"iconClass\"\n iconType=\"image-fill\">\n </seam-icon>\n
|
|
200
|
+
MenuItemComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.9", ngImport: i0, type: MenuItemComponent, deps: [{ token: i0.ElementRef }, { token: DOCUMENT }, { token: i1.FocusMonitor }, { token: i0.ChangeDetectorRef }, { token: THESEAM_MENU_PANEL, optional: true }], target: i0.ɵɵFactoryTarget.Component });
|
|
201
|
+
MenuItemComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "15.2.9", type: MenuItemComponent, selector: "[seamMenuItem]", inputs: { disabled: "disabled", role: "role", icon: "icon", iconClass: "iconClass", sublevelIcon: "sublevelIcon", subLevelIconClass: "subLevelIconClass", badgeText: "badgeText", badgeTheme: "badgeTheme" }, host: { listeners: { "click": "_checkDisabled($event)", "mouseenter": "_handleMouseEnter()" }, properties: { "attr.role": "role", "class.seam-menu-item-highlighted": "_highlighted", "class.seam-menu-item-submenu-trigger": "_triggersSubmenu", "attr.tabindex": "_getTabIndex()", "attr.aria-disabled": "disabled.toString()", "attr.disabled": "disabled || null" }, classAttribute: "seam-menu-item dropdown-item" }, exportAs: ["seamMenuItem"], usesInheritance: true, ngImport: i0, template: "<div class=\"d-flex flex-row\">\n <seam-icon *ngIf=\"icon\"\n style=\"width: 30px; height: 20px; flex: 0 0 auto;\"\n class=\"pr-2\"\n [icon]=\"icon\"\n [iconClass]=\"iconClass\"\n iconType=\"image-fill\">\n </seam-icon>\n <div class=\"flex-fill\">\n <div class=\"text-truncate\">\n <ng-content></ng-content>\n </div>\n\n <div *ngIf=\"badgeText\">\n <span class=\"badge badge-pill{{ badgeTheme ? ' badge-' + badgeTheme : '' }} position-relative\"\n style=\"top: -8px\"\n >{{ badgeText }}</span>\n </div>\n </div>\n <seam-icon *ngIf=\"_triggersSubmenu\"\n style=\"width: 30px; height: 20px; flex: 0 0 auto;\"\n class=\"pl-2\"\n [icon]=\"sublevelIcon\"\n [iconClass]=\"subLevelIconClass\"\n iconType=\"image-fill\">\n </seam-icon>\n</div>\n", styles: [":host.cdk-mouse-focused:not(:hover){background:transparent}:host[aria-expanded]{color:#16181b;text-decoration:none;background-color:#e9ecef}\n"], dependencies: [{ kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i3.IconComponent, selector: "seam-icon", inputs: ["grayscaleOnDisable", "disabled", "iconClass", "icon", "size", "showDefaultOnError", "defaultIcon", "iconType"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
179
202
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.9", ngImport: i0, type: MenuItemComponent, decorators: [{
|
|
180
203
|
type: Component,
|
|
181
204
|
args: [{ selector: '[seamMenuItem]', exportAs: 'seamMenuItem', inputs: ['disabled'], host: {
|
|
182
205
|
'[attr.role]': 'role',
|
|
183
|
-
'class': 'dropdown-item',
|
|
206
|
+
'class': 'seam-menu-item dropdown-item',
|
|
207
|
+
'[class.seam-menu-item-highlighted]': '_highlighted',
|
|
208
|
+
'[class.seam-menu-item-submenu-trigger]': '_triggersSubmenu',
|
|
184
209
|
'[attr.tabindex]': '_getTabIndex()',
|
|
185
210
|
'[attr.aria-disabled]': 'disabled.toString()',
|
|
186
211
|
'[attr.disabled]': 'disabled || null',
|
|
187
|
-
}, changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"d-flex flex-row\">\n <seam-icon *ngIf=\"icon\"\n style=\"width: 30px; height: 20px; flex: 0 0 auto;\"\n class=\"pr-2\"\n [icon]=\"icon\"\n [iconClass]=\"iconClass\"\n iconType=\"image-fill\">\n </seam-icon>\n
|
|
212
|
+
}, changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"d-flex flex-row\">\n <seam-icon *ngIf=\"icon\"\n style=\"width: 30px; height: 20px; flex: 0 0 auto;\"\n class=\"pr-2\"\n [icon]=\"icon\"\n [iconClass]=\"iconClass\"\n iconType=\"image-fill\">\n </seam-icon>\n <div class=\"flex-fill\">\n <div class=\"text-truncate\">\n <ng-content></ng-content>\n </div>\n\n <div *ngIf=\"badgeText\">\n <span class=\"badge badge-pill{{ badgeTheme ? ' badge-' + badgeTheme : '' }} position-relative\"\n style=\"top: -8px\"\n >{{ badgeText }}</span>\n </div>\n </div>\n <seam-icon *ngIf=\"_triggersSubmenu\"\n style=\"width: 30px; height: 20px; flex: 0 0 auto;\"\n class=\"pl-2\"\n [icon]=\"sublevelIcon\"\n [iconClass]=\"subLevelIconClass\"\n iconType=\"image-fill\">\n </seam-icon>\n</div>\n", styles: [":host.cdk-mouse-focused:not(:hover){background:transparent}:host[aria-expanded]{color:#16181b;text-decoration:none;background-color:#e9ecef}\n"] }]
|
|
188
213
|
}], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: undefined, decorators: [{
|
|
189
214
|
type: Inject,
|
|
190
215
|
args: [DOCUMENT]
|
|
191
|
-
}] }, { type: i1.FocusMonitor }, { type: undefined, decorators: [{
|
|
216
|
+
}] }, { type: i1.FocusMonitor }, { type: i0.ChangeDetectorRef }, { type: undefined, decorators: [{
|
|
192
217
|
type: Inject,
|
|
193
218
|
args: [THESEAM_MENU_PANEL]
|
|
194
219
|
}, {
|
|
@@ -199,6 +224,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.9", ngImpor
|
|
|
199
224
|
type: Input
|
|
200
225
|
}], iconClass: [{
|
|
201
226
|
type: Input
|
|
227
|
+
}], sublevelIcon: [{
|
|
228
|
+
type: Input
|
|
229
|
+
}], subLevelIconClass: [{
|
|
230
|
+
type: Input
|
|
202
231
|
}], badgeText: [{
|
|
203
232
|
type: Input
|
|
204
233
|
}], badgeTheme: [{
|
|
@@ -211,16 +240,261 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.9", ngImpor
|
|
|
211
240
|
args: ['mouseenter']
|
|
212
241
|
}] } });
|
|
213
242
|
|
|
243
|
+
let menuPanelUid = 0;
|
|
244
|
+
const LIB_MENU = {
|
|
245
|
+
provide: THESEAM_MENU_PANEL,
|
|
246
|
+
// tslint:disable-next-line:no-use-before-declare
|
|
247
|
+
useExisting: forwardRef(() => MenuComponent)
|
|
248
|
+
};
|
|
249
|
+
class MenuComponent {
|
|
250
|
+
/**
|
|
251
|
+
* Defines a width for a menu that will scale down if the window innerWidth is
|
|
252
|
+
* smaller than the value.
|
|
253
|
+
*/
|
|
254
|
+
get baseWidth() { return this._baseWidth.value; }
|
|
255
|
+
set baseWidth(value) {
|
|
256
|
+
const _val = coerceNumberProperty(value, null);
|
|
257
|
+
if (_val !== this._baseWidth.value) {
|
|
258
|
+
this._baseWidth.next(_val);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
constructor() {
|
|
262
|
+
this._ngUnsubscribe = new Subject();
|
|
263
|
+
this.panelId = `menu-panel-${menuPanelUid++}`;
|
|
264
|
+
this._footer = new BehaviorSubject(undefined);
|
|
265
|
+
this.hasFooter$ = this._footer.pipe(map(v => v !== null && v !== undefined));
|
|
266
|
+
this._header = new BehaviorSubject(undefined);
|
|
267
|
+
this.hasHeader$ = this._header.pipe(map(v => v !== null && v !== undefined));
|
|
268
|
+
/** Menu items inside the current menu. */
|
|
269
|
+
this._items = [];
|
|
270
|
+
/** Emits whenever the amount of menu items changes. */
|
|
271
|
+
this._itemChanges = new Subject();
|
|
272
|
+
/** Subscription to tab events on the menu panel */
|
|
273
|
+
this._tabSubscription = Subscription.EMPTY;
|
|
274
|
+
/** Emits whenever an animation on the menu completes. */
|
|
275
|
+
this._animationDone = new Subject();
|
|
276
|
+
/** Whether the menu is animating. */
|
|
277
|
+
this._isAnimating = false;
|
|
278
|
+
this.closed = new EventEmitter();
|
|
279
|
+
this._baseWidth = new BehaviorSubject(null);
|
|
280
|
+
this.animationType = 'slide';
|
|
281
|
+
this._menuWidth$ = this._baseWidth.pipe(switchMap(baseWidth => {
|
|
282
|
+
if (baseWidth) {
|
|
283
|
+
return fromEvent(window, 'resize').pipe(startWith(undefined), map(() => window.innerWidth < baseWidth ? `${window.innerWidth}px` : `${baseWidth}px`));
|
|
284
|
+
}
|
|
285
|
+
return of(undefined);
|
|
286
|
+
}), distinctUntilChanged(), takeUntil(this._ngUnsubscribe));
|
|
287
|
+
}
|
|
288
|
+
ngOnDestroy() {
|
|
289
|
+
this._tabSubscription.unsubscribe();
|
|
290
|
+
this.closed.complete();
|
|
291
|
+
this._ngUnsubscribe.next(undefined);
|
|
292
|
+
this._ngUnsubscribe.complete();
|
|
293
|
+
}
|
|
294
|
+
ngAfterContentInit() {
|
|
295
|
+
this._keyManager = new FocusKeyManager(this._items).withWrap().withTypeAhead();
|
|
296
|
+
this._tabSubscription = this._keyManager.tabOut.subscribe(() => this.closed.emit('tab'));
|
|
297
|
+
}
|
|
298
|
+
/** Stream that emits whenever the hovered menu item changes. */
|
|
299
|
+
_hovered() {
|
|
300
|
+
return this._itemChanges.pipe(startWith(this._items), switchMap(items => merge(...items.map(item => item._hovered))));
|
|
301
|
+
}
|
|
302
|
+
/** Handle a keyboard event from the menu, delegating to the appropriate action. */
|
|
303
|
+
_handleKeydown(event) {
|
|
304
|
+
// tslint:disable-next-line:deprecation
|
|
305
|
+
const keyCode = event.keyCode;
|
|
306
|
+
const manager = this._keyManager;
|
|
307
|
+
switch (keyCode) {
|
|
308
|
+
case ESCAPE:
|
|
309
|
+
if (!hasModifierKey(event)) {
|
|
310
|
+
event.preventDefault();
|
|
311
|
+
this.closed.emit('keydown');
|
|
312
|
+
}
|
|
313
|
+
break;
|
|
314
|
+
case LEFT_ARROW:
|
|
315
|
+
if (this.parentMenu && this.direction === 'ltr') {
|
|
316
|
+
this.closed.emit('keydown');
|
|
317
|
+
}
|
|
318
|
+
break;
|
|
319
|
+
case RIGHT_ARROW:
|
|
320
|
+
if (this.parentMenu && this.direction === 'rtl') {
|
|
321
|
+
this.closed.emit('keydown');
|
|
322
|
+
}
|
|
323
|
+
break;
|
|
324
|
+
case HOME:
|
|
325
|
+
case END:
|
|
326
|
+
if (!hasModifierKey(event)) {
|
|
327
|
+
keyCode === HOME ? manager?.setFirstItemActive() : manager?.setLastItemActive();
|
|
328
|
+
event.preventDefault();
|
|
329
|
+
}
|
|
330
|
+
break;
|
|
331
|
+
default:
|
|
332
|
+
if (keyCode === UP_ARROW || keyCode === DOWN_ARROW) {
|
|
333
|
+
manager?.setFocusOrigin('keyboard');
|
|
334
|
+
}
|
|
335
|
+
manager?.onKeydown(event);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Focus the first item in the menu.
|
|
340
|
+
* @param origin Action from which the focus originated. Used to set the correct styling.
|
|
341
|
+
*/
|
|
342
|
+
focusFirstItem(origin = 'program') {
|
|
343
|
+
this._keyManager?.setFocusOrigin(origin).setFirstItemActive();
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Resets the active item in the menu. This is used when the menu is opened, allowing
|
|
347
|
+
* the user to start from the first option when pressing the down arrow.
|
|
348
|
+
*/
|
|
349
|
+
resetActiveItem() {
|
|
350
|
+
this._keyManager?.setActiveItem(-1);
|
|
351
|
+
}
|
|
352
|
+
/** Registers a menu item with the menu. */
|
|
353
|
+
addItem(item) {
|
|
354
|
+
// We register the items through this method, rather than picking them up through
|
|
355
|
+
// `ContentChildren`, because we need the items to be picked up by their closest
|
|
356
|
+
// `seam-menu` ancestor. If we used `@ContentChildren(MenuItemComponent, {descendants: true})`,
|
|
357
|
+
// all descendant items will bleed into the top-level menu in the case where the consumer
|
|
358
|
+
// has `seam-menu` instances nested inside each other.
|
|
359
|
+
if (this._items.indexOf(item) === -1) {
|
|
360
|
+
this._items.push(item);
|
|
361
|
+
this._itemChanges.next(this._items);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
/** Removes an item from the menu. */
|
|
365
|
+
removeItem(item) {
|
|
366
|
+
const index = this._items.indexOf(item);
|
|
367
|
+
if (this._items.indexOf(item) > -1) {
|
|
368
|
+
this._items.splice(index, 1);
|
|
369
|
+
this._itemChanges.next(this._items);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
/** Sets the footer component. */
|
|
373
|
+
setFooter(footer) {
|
|
374
|
+
this._footer.next(footer);
|
|
375
|
+
}
|
|
376
|
+
/** Sets the header component. */
|
|
377
|
+
setHeader(header) {
|
|
378
|
+
this._header.next(header);
|
|
379
|
+
}
|
|
380
|
+
_dropdownMenuClick(event) {
|
|
381
|
+
// This is needed, because some menu's will get stuck open if the component
|
|
382
|
+
// managing the menu is destroyed before the menu finishes its cleanup. I
|
|
383
|
+
// may look for a fix to that eventually.
|
|
384
|
+
this.closed.emit('click');
|
|
385
|
+
}
|
|
386
|
+
/** Callback that is invoked when the panel animation completes. */
|
|
387
|
+
_onAnimationDone(event) {
|
|
388
|
+
this._animationDone.next(event);
|
|
389
|
+
this._isAnimating = false;
|
|
390
|
+
}
|
|
391
|
+
_onAnimationStart(event) {
|
|
392
|
+
this._isAnimating = true;
|
|
393
|
+
// Scroll the content element to the top as soon as the animation starts. This is necessary,
|
|
394
|
+
// because we move focus to the first item while it's still being animated, which can throw
|
|
395
|
+
// the browser off when it determines the scroll position. Alternatively we can move focus
|
|
396
|
+
// when the animation is done, however moving focus asynchronously will interrupt screen
|
|
397
|
+
// readers which are in the process of reading out the menu already. We take the `element`
|
|
398
|
+
// from the `event` since we can't use a `ViewChild` to access the pane.
|
|
399
|
+
if (event.toState === 'enter' && this._keyManager?.activeItemIndex === 0) {
|
|
400
|
+
event.element.scrollTop = 0;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
MenuComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.9", ngImport: i0, type: MenuComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
405
|
+
MenuComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "15.2.9", type: MenuComponent, selector: "seam-menu", inputs: { menuClass: "menuClass", baseWidth: "baseWidth", animationType: "animationType" }, outputs: { closed: "closed" }, providers: [LIB_MENU], viewQueries: [{ propertyName: "templateRef", first: true, predicate: TemplateRef, descendants: true }], exportAs: ["seamMenu"], ngImport: i0, template: "<ng-template>\n <div class=\"seam-menu-container\"\n @slideDown\n (@slideDown.start)=\"_onAnimationStart($event)\"\n (@slideDown.done)=\"_onAnimationDone($event)\"\n [class.seam-menu-anim--slide]=\"animationType==='slide'\"\n [class.seam-menu-anim--fade]=\"animationType==='fade'\"\n [id]=\"panelId\">\n <div class=\"dropdown-menu show position-static{{ menuClass ? ' ' + menuClass : '' }}\"\n [style.width]=\"_menuWidth$ | async\"\n [class.pt-0]=\"hasHeader$ | async\"\n [class.pb-0]=\"hasFooter$ | async\"\n (keydown)=\"_handleKeydown($event)\"\n (click)=\"_dropdownMenuClick($event)\"\n tabindex=\"-1\"\n role=\"menu\"\n >\n <ng-content></ng-content>\n </div>\n </div>\n</ng-template>\n", styles: [".seam-menu-container.seam-menu-anim--slide{overflow:hidden}.seam-menu-container.ng-animating,.dropdown-menu.ng-animating{-webkit-user-select:none;-moz-user-select:none;user-select:none;pointer-events:none}\n"], dependencies: [{ kind: "pipe", type: i2.AsyncPipe, name: "async" }], animations: [
|
|
406
|
+
trigger('slideDown', [
|
|
407
|
+
transition(':enter', useAnimation(menuDropdownPanelIn)),
|
|
408
|
+
transition(':leave', useAnimation(menuDropdownPanelOut)),
|
|
409
|
+
])
|
|
410
|
+
], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
411
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.9", ngImport: i0, type: MenuComponent, decorators: [{
|
|
412
|
+
type: Component,
|
|
413
|
+
args: [{ selector: 'seam-menu', providers: [LIB_MENU], animations: [
|
|
414
|
+
trigger('slideDown', [
|
|
415
|
+
transition(':enter', useAnimation(menuDropdownPanelIn)),
|
|
416
|
+
transition(':leave', useAnimation(menuDropdownPanelOut)),
|
|
417
|
+
])
|
|
418
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, exportAs: 'seamMenu', template: "<ng-template>\n <div class=\"seam-menu-container\"\n @slideDown\n (@slideDown.start)=\"_onAnimationStart($event)\"\n (@slideDown.done)=\"_onAnimationDone($event)\"\n [class.seam-menu-anim--slide]=\"animationType==='slide'\"\n [class.seam-menu-anim--fade]=\"animationType==='fade'\"\n [id]=\"panelId\">\n <div class=\"dropdown-menu show position-static{{ menuClass ? ' ' + menuClass : '' }}\"\n [style.width]=\"_menuWidth$ | async\"\n [class.pt-0]=\"hasHeader$ | async\"\n [class.pb-0]=\"hasFooter$ | async\"\n (keydown)=\"_handleKeydown($event)\"\n (click)=\"_dropdownMenuClick($event)\"\n tabindex=\"-1\"\n role=\"menu\"\n >\n <ng-content></ng-content>\n </div>\n </div>\n</ng-template>\n", styles: [".seam-menu-container.seam-menu-anim--slide{overflow:hidden}.seam-menu-container.ng-animating,.dropdown-menu.ng-animating{-webkit-user-select:none;-moz-user-select:none;user-select:none;pointer-events:none}\n"] }]
|
|
419
|
+
}], ctorParameters: function () { return []; }, propDecorators: { templateRef: [{
|
|
420
|
+
type: ViewChild,
|
|
421
|
+
args: [TemplateRef]
|
|
422
|
+
}], closed: [{
|
|
423
|
+
type: Output
|
|
424
|
+
}], menuClass: [{
|
|
425
|
+
type: Input
|
|
426
|
+
}], baseWidth: [{
|
|
427
|
+
type: Input
|
|
428
|
+
}], animationType: [{
|
|
429
|
+
type: Input
|
|
430
|
+
}] } });
|
|
431
|
+
|
|
214
432
|
/** Options for binding a passive event listener. */
|
|
215
433
|
const passiveEventListenerOptions = normalizePassiveListenerOptions({ passive: true });
|
|
216
434
|
class MenuToggleDirective {
|
|
435
|
+
get menu() { return this._menu; }
|
|
436
|
+
set menu(menu) {
|
|
437
|
+
if (menu === this._menu) {
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
this._menu = menu;
|
|
441
|
+
this._menuClosedSubscription.unsubscribe();
|
|
442
|
+
if (menu) {
|
|
443
|
+
if (menu === this._parentMenuComponent && (typeof ngDevMode === 'undefined' || ngDevMode)) {
|
|
444
|
+
throw Error(`seamMenuToggle: menu cannot contain its own trigger. Assign a menu that is ` +
|
|
445
|
+
`not a parent of the trigger or move the trigger outside of the menu.`);
|
|
446
|
+
}
|
|
447
|
+
this._menuClosedSubscription = menu.closed.subscribe((reason) => {
|
|
448
|
+
// this._destroyMenu(reason)
|
|
449
|
+
this.closeMenu();
|
|
450
|
+
// If a click closed the menu, we should close the entire chain of nested menus.
|
|
451
|
+
if ((reason === 'click' || reason === 'tab') && this._parentMenuComponent) {
|
|
452
|
+
this._parentMenuComponent.closed.emit(reason);
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
this._menuItemInstance?._setTriggersSubmenu(this.triggersSubmenu());
|
|
457
|
+
}
|
|
217
458
|
set positions(val) {
|
|
218
459
|
this._positions = val;
|
|
219
460
|
if (this.menuOpen()) {
|
|
220
461
|
this._overlayRef?.updatePositionStrategy(this.getOverlayPosition(this._elementRef.nativeElement));
|
|
221
462
|
}
|
|
222
463
|
}
|
|
223
|
-
get positions() {
|
|
464
|
+
get positions() {
|
|
465
|
+
const positions = this._positions;
|
|
466
|
+
if (this.triggersSubmenu()) {
|
|
467
|
+
return [
|
|
468
|
+
{
|
|
469
|
+
originX: 'start',
|
|
470
|
+
originY: 'top',
|
|
471
|
+
overlayX: 'end',
|
|
472
|
+
overlayY: 'top',
|
|
473
|
+
offsetY: -11,
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
originX: 'end',
|
|
477
|
+
originY: 'top',
|
|
478
|
+
overlayX: 'start',
|
|
479
|
+
overlayY: 'top',
|
|
480
|
+
offsetY: -11,
|
|
481
|
+
},
|
|
482
|
+
{
|
|
483
|
+
originX: 'start',
|
|
484
|
+
originY: 'bottom',
|
|
485
|
+
overlayX: 'end',
|
|
486
|
+
overlayY: 'top',
|
|
487
|
+
},
|
|
488
|
+
{
|
|
489
|
+
originX: 'start',
|
|
490
|
+
originY: 'top',
|
|
491
|
+
overlayX: 'start',
|
|
492
|
+
overlayY: 'bottom',
|
|
493
|
+
},
|
|
494
|
+
];
|
|
495
|
+
}
|
|
496
|
+
return positions;
|
|
497
|
+
}
|
|
224
498
|
_onMouseDown(event) {
|
|
225
499
|
if (!isFakeMousedownFromScreenReader(event)) {
|
|
226
500
|
// Since right or middle button clicks won't trigger the `click` event,
|
|
@@ -229,41 +503,63 @@ class MenuToggleDirective {
|
|
|
229
503
|
// Since clicking on the trigger won't close the menu if it opens a sub-menu,
|
|
230
504
|
// we should prevent focus from moving onto it via click to avoid the
|
|
231
505
|
// highlight from lingering on the menu item.
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
506
|
+
if (this.triggersSubmenu()) {
|
|
507
|
+
event.preventDefault();
|
|
508
|
+
}
|
|
235
509
|
}
|
|
236
510
|
}
|
|
237
511
|
_onKeydown(event) {
|
|
238
512
|
this._openedBy = null;
|
|
513
|
+
// console.log('keydown', event)
|
|
239
514
|
// tslint:disable-next-line:deprecation
|
|
240
515
|
const keyCode = event.keyCode;
|
|
241
516
|
if (keyCode === UP_ARROW || keyCode === DOWN_ARROW) {
|
|
242
517
|
if (this.menuOpen()) {
|
|
243
|
-
this.
|
|
518
|
+
this.menu?.focusFirstItem(this._openedBy || 'program');
|
|
244
519
|
}
|
|
245
520
|
}
|
|
521
|
+
else if (keyCode === ESCAPE) {
|
|
522
|
+
this.closeMenu();
|
|
523
|
+
}
|
|
524
|
+
// Pressing enter on the trigger will trigger the click handler later.
|
|
525
|
+
if (keyCode === ENTER || keyCode === SPACE) {
|
|
526
|
+
this._openedBy = 'keyboard';
|
|
527
|
+
}
|
|
528
|
+
if (this.triggersSubmenu() &&
|
|
529
|
+
((keyCode === RIGHT_ARROW && this.dir === 'ltr') ||
|
|
530
|
+
(keyCode === LEFT_ARROW && this.dir === 'rtl'))) {
|
|
531
|
+
this._openedBy = 'keyboard';
|
|
532
|
+
this.openMenu();
|
|
533
|
+
}
|
|
246
534
|
}
|
|
247
535
|
_onClick(event) {
|
|
248
|
-
this.
|
|
536
|
+
if (this.triggersSubmenu()) {
|
|
537
|
+
// Stop event propagation to avoid closing the parent menu.
|
|
538
|
+
event.stopPropagation();
|
|
539
|
+
this.openMenu();
|
|
540
|
+
}
|
|
541
|
+
else {
|
|
542
|
+
this.toggle();
|
|
543
|
+
}
|
|
249
544
|
}
|
|
250
545
|
_onDocumentKeydown(event) {
|
|
251
546
|
if (event.keyCode === ESCAPE) {
|
|
252
547
|
this.closeMenu();
|
|
253
548
|
}
|
|
254
549
|
}
|
|
255
|
-
|
|
256
|
-
// _onDocumentMouseDown(event: any) {
|
|
257
|
-
// console.log('outside click')
|
|
258
|
-
// }
|
|
259
|
-
constructor(_elementRef, _viewContainerRef, _overlay, _focusMonitor) {
|
|
550
|
+
constructor(_elementRef, _viewContainerRef, _overlay, _focusMonitor, _parentMenu, _menuItemInstance, _dir) {
|
|
260
551
|
this._elementRef = _elementRef;
|
|
261
552
|
this._viewContainerRef = _viewContainerRef;
|
|
262
553
|
this._overlay = _overlay;
|
|
263
554
|
this._focusMonitor = _focusMonitor;
|
|
555
|
+
this._parentMenu = _parentMenu;
|
|
556
|
+
this._menuItemInstance = _menuItemInstance;
|
|
557
|
+
this._dir = _dir;
|
|
264
558
|
this._active = false;
|
|
265
559
|
this._menuClosedSubscription = Subscription.EMPTY;
|
|
266
560
|
this._closingActionsSubscription = Subscription.EMPTY;
|
|
561
|
+
this._hoverSubscription = Subscription.EMPTY;
|
|
562
|
+
this._changeDetectorRef = inject(ChangeDetectorRef);
|
|
267
563
|
this.restoreFocus = true;
|
|
268
564
|
// Tracking input type is necessary so it's possible to only auto-focus
|
|
269
565
|
// the first item of the list when the menu is opened via the keyboard
|
|
@@ -294,11 +590,16 @@ class MenuToggleDirective {
|
|
|
294
590
|
overlayY: 'bottom',
|
|
295
591
|
},
|
|
296
592
|
];
|
|
593
|
+
/** Event emitted when the associated menu is opened. */
|
|
594
|
+
this.menuOpened = new EventEmitter();
|
|
595
|
+
/** Event emitted when the associated menu is opened. */
|
|
596
|
+
this.menuClosed = new EventEmitter();
|
|
297
597
|
/**
|
|
298
598
|
* Handles touch start events on the trigger.
|
|
299
599
|
* Needs to be an arrow function so we can easily use addEventListener and removeEventListener.
|
|
300
600
|
*/
|
|
301
601
|
this._handleTouchStart = () => this._openedBy = 'touch';
|
|
602
|
+
this._parentMenuComponent = this._parentMenu instanceof MenuComponent ? this._parentMenu : undefined;
|
|
302
603
|
this._elementRef.nativeElement.addEventListener('touchstart', this._handleTouchStart, passiveEventListenerOptions);
|
|
303
604
|
}
|
|
304
605
|
ngOnDestroy() {
|
|
@@ -306,6 +607,10 @@ class MenuToggleDirective {
|
|
|
306
607
|
this._elementRef.nativeElement.removeEventListener('touchstart', this._handleTouchStart, passiveEventListenerOptions);
|
|
307
608
|
this._menuClosedSubscription.unsubscribe();
|
|
308
609
|
this._closingActionsSubscription.unsubscribe();
|
|
610
|
+
this._hoverSubscription.unsubscribe();
|
|
611
|
+
}
|
|
612
|
+
ngAfterContentInit() {
|
|
613
|
+
this._handleHover();
|
|
309
614
|
}
|
|
310
615
|
toggle() {
|
|
311
616
|
if (this._active) {
|
|
@@ -316,23 +621,23 @@ class MenuToggleDirective {
|
|
|
316
621
|
}
|
|
317
622
|
}
|
|
318
623
|
openMenu() {
|
|
319
|
-
if (this._active || !this.
|
|
624
|
+
if (this._active || !this.menu) {
|
|
320
625
|
return;
|
|
321
626
|
}
|
|
322
627
|
this._active = true;
|
|
323
628
|
this._overlayRef = this._overlay.create({
|
|
324
|
-
hasBackdrop:
|
|
325
|
-
backdropClass: 'transparent',
|
|
629
|
+
hasBackdrop: !this._parentMenuComponent,
|
|
630
|
+
backdropClass: 'cdk-overlay-transparent-backdrop',
|
|
326
631
|
positionStrategy: this.getOverlayPosition(this._elementRef.nativeElement),
|
|
327
632
|
});
|
|
328
|
-
const tpl = this.
|
|
633
|
+
const tpl = this.menu.templateRef;
|
|
329
634
|
if (!tpl) {
|
|
330
635
|
throw Error(`Menu template not found.`);
|
|
331
636
|
}
|
|
332
637
|
this._overlayRef.attach(new TemplatePortal(tpl, this._viewContainerRef));
|
|
333
638
|
this._closingActionsSubscription = this._menuClosingActions().subscribe(() => this.closeMenu());
|
|
334
|
-
this._initMenu();
|
|
335
|
-
this._menuClosedSubscription = this.
|
|
639
|
+
this._initMenu(this.menu);
|
|
640
|
+
this._menuClosedSubscription = this.menu.closed.subscribe(v => {
|
|
336
641
|
// console.log('closed', v)
|
|
337
642
|
this.closeMenu();
|
|
338
643
|
});
|
|
@@ -340,10 +645,22 @@ class MenuToggleDirective {
|
|
|
340
645
|
// console.log('backdropClick', v)
|
|
341
646
|
// })
|
|
342
647
|
}
|
|
648
|
+
/** The text direction of the containing app. */
|
|
649
|
+
get dir() {
|
|
650
|
+
return this._dir && this._dir.value === 'rtl' ? 'rtl' : 'ltr';
|
|
651
|
+
}
|
|
652
|
+
/** Whether the menu triggers a sub-menu or a top-level one. */
|
|
653
|
+
triggersSubmenu() {
|
|
654
|
+
return !!(this._menuItemInstance && this._parentMenuComponent && this.menu);
|
|
655
|
+
}
|
|
343
656
|
closeMenu() {
|
|
344
657
|
if (!this._active) {
|
|
345
658
|
return;
|
|
346
659
|
}
|
|
660
|
+
let emitCloseEvent = false;
|
|
661
|
+
if (this.menuOpen()) {
|
|
662
|
+
emitCloseEvent = true;
|
|
663
|
+
}
|
|
347
664
|
if (this._overlayRef?.hasAttached()) {
|
|
348
665
|
this._overlayRef?.detach();
|
|
349
666
|
}
|
|
@@ -351,6 +668,9 @@ class MenuToggleDirective {
|
|
|
351
668
|
this._elementRef.nativeElement.removeEventListener('touchstart', this._handleTouchStart, passiveEventListenerOptions);
|
|
352
669
|
this._menuClosedSubscription.unsubscribe();
|
|
353
670
|
this._closingActionsSubscription.unsubscribe();
|
|
671
|
+
if (emitCloseEvent) {
|
|
672
|
+
this.menu?.closed.emit();
|
|
673
|
+
}
|
|
354
674
|
this._active = false;
|
|
355
675
|
}
|
|
356
676
|
menuOpen() {
|
|
@@ -380,19 +700,19 @@ class MenuToggleDirective {
|
|
|
380
700
|
* This method sets the menu state to open and focuses the first item if
|
|
381
701
|
* the menu was opened via the keyboard.
|
|
382
702
|
*/
|
|
383
|
-
_initMenu() {
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
// this._setMenuElevation()
|
|
387
|
-
|
|
388
|
-
|
|
703
|
+
_initMenu(menu) {
|
|
704
|
+
menu.parentMenu = this.triggersSubmenu() ? this._parentMenu : undefined;
|
|
705
|
+
menu.direction = this.dir;
|
|
706
|
+
// this._setMenuElevation(menu)
|
|
707
|
+
menu.focusFirstItem(this._openedBy || 'program');
|
|
708
|
+
this._setIsMenuOpen(true);
|
|
389
709
|
}
|
|
390
710
|
/**
|
|
391
711
|
* This method resets the menu when it's closed, most importantly restoring
|
|
392
712
|
* focus to the menu trigger if the menu was opened via the keyboard.
|
|
393
713
|
*/
|
|
394
714
|
_resetMenu() {
|
|
395
|
-
|
|
715
|
+
this._setIsMenuOpen(false);
|
|
396
716
|
// We should reset focus if the user is navigating using a keyboard or
|
|
397
717
|
// if we have a top-level trigger which might cause focus to be lost
|
|
398
718
|
// when clicking on the backdrop.
|
|
@@ -402,43 +722,96 @@ class MenuToggleDirective {
|
|
|
402
722
|
// `keyboard` so we don't have to specify which one it is.
|
|
403
723
|
this.focus();
|
|
404
724
|
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
725
|
+
else if (!this.triggersSubmenu()) {
|
|
726
|
+
this.focus(this._openedBy);
|
|
727
|
+
}
|
|
408
728
|
}
|
|
409
729
|
this._openedBy = null;
|
|
410
730
|
}
|
|
731
|
+
// set state rather than toggle to support triggers sharing a menu
|
|
732
|
+
_setIsMenuOpen(isOpen) {
|
|
733
|
+
if (isOpen !== this.menuOpen()) {
|
|
734
|
+
// this._menuOpen = isOpen
|
|
735
|
+
this.menuOpen() ? this.menuOpened.emit() : this.menuClosed.emit();
|
|
736
|
+
if (this.triggersSubmenu()) {
|
|
737
|
+
this._menuItemInstance?._setHighlighted(isOpen);
|
|
738
|
+
}
|
|
739
|
+
this._changeDetectorRef.markForCheck();
|
|
740
|
+
}
|
|
741
|
+
}
|
|
411
742
|
/** Returns a stream that emits whenever an action that should close the menu occurs. */
|
|
412
743
|
_menuClosingActions() {
|
|
413
744
|
const backdrop = this._overlayRef?.backdropClick() ?? of();
|
|
414
745
|
const detachments = this._overlayRef?.detachments() ?? of();
|
|
415
|
-
|
|
416
|
-
const
|
|
417
|
-
// const hover = this._parentMenu ? this._parentMenu._hovered().pipe(
|
|
418
|
-
// filter(active => active !== this._menuItemInstance),
|
|
419
|
-
// filter(() => this._menuOpen)
|
|
420
|
-
// ) : of()
|
|
421
|
-
const hover = of();
|
|
746
|
+
const parentClose = this._parentMenu ? this._parentMenu.closed : of();
|
|
747
|
+
const hover = this._parentMenuComponent ? this._parentMenuComponent._hovered().pipe(filter(active => active !== this._menuItemInstance), filter(() => this.menuOpen())) : of();
|
|
422
748
|
return merge(backdrop, parentClose, hover, detachments);
|
|
423
749
|
}
|
|
750
|
+
/** Handles the cases where the user hovers over the trigger. */
|
|
751
|
+
_handleHover() {
|
|
752
|
+
// Subscribe to changes in the hovered item in order to toggle the panel.
|
|
753
|
+
if (!this.triggersSubmenu() || !this._parentMenuComponent) {
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
this._hoverSubscription = this._parentMenuComponent
|
|
757
|
+
._hovered()
|
|
758
|
+
// Since we might have multiple competing triggers for the same menu (e.g. a sub-menu
|
|
759
|
+
// with different data and triggers), we have to delay it by a tick to ensure that
|
|
760
|
+
// it won't be closed immediately after it is opened.
|
|
761
|
+
.pipe(filter(active => active === this._menuItemInstance && !active.disabled), delay(0, asapScheduler))
|
|
762
|
+
.subscribe(() => {
|
|
763
|
+
this._openedBy = 'mouse';
|
|
764
|
+
// If the same menu is used between multiple triggers, it might still be animating
|
|
765
|
+
// while the new trigger tries to re-open it. Wait for the animation to finish
|
|
766
|
+
// before doing so. Also interrupt if the user moves to another item.
|
|
767
|
+
if (this.menu instanceof MenuComponent && this.menu._isAnimating) {
|
|
768
|
+
// We need the `delay(0)` here in order to avoid
|
|
769
|
+
// 'changed after checked' errors in some cases. See #12194.
|
|
770
|
+
this.menu._animationDone
|
|
771
|
+
.pipe(take(1), delay(0, asapScheduler), takeUntil$1(this._parentMenuComponent._hovered()))
|
|
772
|
+
.subscribe(() => this.openMenu());
|
|
773
|
+
}
|
|
774
|
+
else {
|
|
775
|
+
this.openMenu();
|
|
776
|
+
}
|
|
777
|
+
});
|
|
778
|
+
}
|
|
424
779
|
}
|
|
425
|
-
MenuToggleDirective.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.9", ngImport: i0, type: MenuToggleDirective, deps: [{ token: i0.ElementRef }, { token: i0.ViewContainerRef }, { token: i1$1.Overlay }, { token: i1.FocusMonitor }], target: i0.ɵɵFactoryTarget.Directive });
|
|
426
|
-
MenuToggleDirective.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "15.2.9", type: MenuToggleDirective, selector: "[seamMenuToggle]", inputs: {
|
|
780
|
+
MenuToggleDirective.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.9", ngImport: i0, type: MenuToggleDirective, deps: [{ token: i0.ElementRef }, { token: i0.ViewContainerRef }, { token: i1$1.Overlay }, { token: i1.FocusMonitor }, { token: THESEAM_MENU_PANEL, optional: true }, { token: MenuItemComponent, optional: true, self: true }, { token: i4.Directionality, optional: true }], target: i0.ɵɵFactoryTarget.Directive });
|
|
781
|
+
MenuToggleDirective.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "15.2.9", type: MenuToggleDirective, selector: "[seamMenuToggle]", inputs: { menu: ["seamMenuToggle", "menu"], positions: "positions" }, outputs: { menuOpened: "menuOpened", menuClosed: "menuClosed" }, host: { attributes: { "aria-haspopup": "true" }, listeners: { "mousedown": "_onMouseDown($event)", "keydown": "_onKeydown($event)", "click": "_onClick($event)", "document:keydown": "_onDocumentKeydown($event)" }, properties: { "attr.aria-expanded": "menuOpen() || null", "attr.aria-controls": "menuOpen() ? menu.panelId : null" }, classAttribute: "seam-menu-toggle" }, exportAs: ["seamMenuToggle"], ngImport: i0 });
|
|
427
782
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.9", ngImport: i0, type: MenuToggleDirective, decorators: [{
|
|
428
783
|
type: Directive,
|
|
429
784
|
args: [{
|
|
430
785
|
selector: '[seamMenuToggle]',
|
|
431
786
|
// tslint:disable-next-line:use-host-property-decorator
|
|
432
787
|
host: {
|
|
788
|
+
'class': 'seam-menu-toggle',
|
|
433
789
|
'aria-haspopup': 'true',
|
|
434
|
-
'[attr.aria-expanded]': 'menuOpen() || null'
|
|
790
|
+
'[attr.aria-expanded]': 'menuOpen() || null',
|
|
791
|
+
'[attr.aria-controls]': 'menuOpen() ? menu.panelId : null',
|
|
435
792
|
},
|
|
436
793
|
exportAs: 'seamMenuToggle'
|
|
437
794
|
}]
|
|
438
|
-
}], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: i0.ViewContainerRef }, { type: i1$1.Overlay }, { type: i1.FocusMonitor }
|
|
439
|
-
|
|
795
|
+
}], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: i0.ViewContainerRef }, { type: i1$1.Overlay }, { type: i1.FocusMonitor }, { type: undefined, decorators: [{
|
|
796
|
+
type: Inject,
|
|
797
|
+
args: [THESEAM_MENU_PANEL]
|
|
798
|
+
}, {
|
|
799
|
+
type: Optional
|
|
800
|
+
}] }, { type: MenuItemComponent, decorators: [{
|
|
801
|
+
type: Optional
|
|
802
|
+
}, {
|
|
803
|
+
type: Self
|
|
804
|
+
}] }, { type: i4.Directionality, decorators: [{
|
|
805
|
+
type: Optional
|
|
806
|
+
}] }]; }, propDecorators: { menu: [{
|
|
807
|
+
type: Input,
|
|
808
|
+
args: ['seamMenuToggle']
|
|
440
809
|
}], positions: [{
|
|
441
810
|
type: Input
|
|
811
|
+
}], menuOpened: [{
|
|
812
|
+
type: Output
|
|
813
|
+
}], menuClosed: [{
|
|
814
|
+
type: Output
|
|
442
815
|
}], _onMouseDown: [{
|
|
443
816
|
type: HostListener,
|
|
444
817
|
args: ['mousedown', ['$event']]
|
|
@@ -453,172 +826,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.9", ngImpor
|
|
|
453
826
|
args: ['document:keydown', ['$event']]
|
|
454
827
|
}] } });
|
|
455
828
|
|
|
456
|
-
const LIB_MENU = {
|
|
457
|
-
provide: THESEAM_MENU_PANEL,
|
|
458
|
-
// tslint:disable-next-line:no-use-before-declare
|
|
459
|
-
useExisting: forwardRef(() => MenuComponent)
|
|
460
|
-
};
|
|
461
|
-
class MenuComponent {
|
|
462
|
-
/**
|
|
463
|
-
* Defines a width for a menu that will scale down if the window innerWidth is
|
|
464
|
-
* smaller than the value.
|
|
465
|
-
*/
|
|
466
|
-
get baseWidth() { return this._baseWidth.value; }
|
|
467
|
-
set baseWidth(value) {
|
|
468
|
-
const _val = coerceNumberProperty(value, null);
|
|
469
|
-
if (_val !== this._baseWidth.value) {
|
|
470
|
-
this._baseWidth.next(_val);
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
constructor() {
|
|
474
|
-
this._ngUnsubscribe = new Subject();
|
|
475
|
-
this._footer = new BehaviorSubject(undefined);
|
|
476
|
-
this.hasFooter$ = this._footer.pipe(map(v => v !== null && v !== undefined));
|
|
477
|
-
this._header = new BehaviorSubject(undefined);
|
|
478
|
-
this.hasHeader$ = this._header.pipe(map(v => v !== null && v !== undefined));
|
|
479
|
-
/** Menu items inside the current menu. */
|
|
480
|
-
this._items = [];
|
|
481
|
-
/** Emits whenever the amount of menu items changes. */
|
|
482
|
-
this._itemChanges = new Subject();
|
|
483
|
-
/** Subscription to tab events on the menu panel */
|
|
484
|
-
this._tabSubscription = Subscription.EMPTY;
|
|
485
|
-
this.closed = new EventEmitter();
|
|
486
|
-
this._baseWidth = new BehaviorSubject(null);
|
|
487
|
-
this.animationType = 'slide';
|
|
488
|
-
this._menuWidth$ = this._baseWidth.pipe(switchMap(baseWidth => {
|
|
489
|
-
if (baseWidth) {
|
|
490
|
-
return fromEvent(window, 'resize').pipe(startWith(undefined), map(() => window.innerWidth < baseWidth ? `${window.innerWidth}px` : `${baseWidth}px`));
|
|
491
|
-
}
|
|
492
|
-
return of(undefined);
|
|
493
|
-
}), distinctUntilChanged(), takeUntil(this._ngUnsubscribe));
|
|
494
|
-
}
|
|
495
|
-
ngOnDestroy() {
|
|
496
|
-
this._tabSubscription.unsubscribe();
|
|
497
|
-
this.closed.complete();
|
|
498
|
-
this._ngUnsubscribe.next(undefined);
|
|
499
|
-
this._ngUnsubscribe.complete();
|
|
500
|
-
}
|
|
501
|
-
ngAfterContentInit() {
|
|
502
|
-
this._keyManager = new FocusKeyManager(this._items).withWrap().withTypeAhead();
|
|
503
|
-
this._tabSubscription = this._keyManager.tabOut.subscribe(() => this.closed.emit('tab'));
|
|
504
|
-
}
|
|
505
|
-
/** Stream that emits whenever the hovered menu item changes. */
|
|
506
|
-
_hovered() {
|
|
507
|
-
return this._itemChanges.pipe(startWith(this._items), switchMap(items => merge(...items.map(item => item._hovered))));
|
|
508
|
-
}
|
|
509
|
-
/** Handle a keyboard event from the menu, delegating to the appropriate action. */
|
|
510
|
-
_handleKeydown(event) {
|
|
511
|
-
// tslint:disable-next-line:deprecation
|
|
512
|
-
const keyCode = event.keyCode;
|
|
513
|
-
const manager = this._keyManager;
|
|
514
|
-
switch (keyCode) {
|
|
515
|
-
case ESCAPE:
|
|
516
|
-
if (!hasModifierKey(event)) {
|
|
517
|
-
event.preventDefault();
|
|
518
|
-
this.closed.emit('keydown');
|
|
519
|
-
}
|
|
520
|
-
break;
|
|
521
|
-
// case LEFT_ARROW:
|
|
522
|
-
// if (this.parentMenu && this.direction === 'ltr') {
|
|
523
|
-
// this.closed.emit('keydown')
|
|
524
|
-
// }
|
|
525
|
-
// break
|
|
526
|
-
// case RIGHT_ARROW:
|
|
527
|
-
// if (this.parentMenu && this.direction === 'rtl') {
|
|
528
|
-
// this.closed.emit('keydown')
|
|
529
|
-
// }
|
|
530
|
-
// break
|
|
531
|
-
case HOME:
|
|
532
|
-
case END:
|
|
533
|
-
if (!hasModifierKey(event)) {
|
|
534
|
-
keyCode === HOME ? manager?.setFirstItemActive() : manager?.setLastItemActive();
|
|
535
|
-
event.preventDefault();
|
|
536
|
-
}
|
|
537
|
-
break;
|
|
538
|
-
default:
|
|
539
|
-
if (keyCode === UP_ARROW || keyCode === DOWN_ARROW) {
|
|
540
|
-
manager?.setFocusOrigin('keyboard');
|
|
541
|
-
}
|
|
542
|
-
manager?.onKeydown(event);
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
/**
|
|
546
|
-
* Focus the first item in the menu.
|
|
547
|
-
* @param origin Action from which the focus originated. Used to set the correct styling.
|
|
548
|
-
*/
|
|
549
|
-
focusFirstItem(origin = 'program') {
|
|
550
|
-
this._keyManager?.setFocusOrigin(origin).setFirstItemActive();
|
|
551
|
-
}
|
|
552
|
-
/**
|
|
553
|
-
* Resets the active item in the menu. This is used when the menu is opened, allowing
|
|
554
|
-
* the user to start from the first option when pressing the down arrow.
|
|
555
|
-
*/
|
|
556
|
-
resetActiveItem() {
|
|
557
|
-
this._keyManager?.setActiveItem(-1);
|
|
558
|
-
}
|
|
559
|
-
/** Registers a menu item with the menu. */
|
|
560
|
-
addItem(item) {
|
|
561
|
-
// We register the items through this method, rather than picking them up through
|
|
562
|
-
// `ContentChildren`, because we need the items to be picked up by their closest
|
|
563
|
-
// `seam-menu` ancestor. If we used `@ContentChildren(MenuItemComponent, {descendants: true})`,
|
|
564
|
-
// all descendant items will bleed into the top-level menu in the case where the consumer
|
|
565
|
-
// has `seam-menu` instances nested inside each other.
|
|
566
|
-
if (this._items.indexOf(item) === -1) {
|
|
567
|
-
this._items.push(item);
|
|
568
|
-
this._itemChanges.next(this._items);
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
/** Removes an item from the menu. */
|
|
572
|
-
removeItem(item) {
|
|
573
|
-
const index = this._items.indexOf(item);
|
|
574
|
-
if (this._items.indexOf(item) > -1) {
|
|
575
|
-
this._items.splice(index, 1);
|
|
576
|
-
this._itemChanges.next(this._items);
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
/** Sets the footer component. */
|
|
580
|
-
setFooter(footer) {
|
|
581
|
-
this._footer.next(footer);
|
|
582
|
-
}
|
|
583
|
-
/** Sets the header component. */
|
|
584
|
-
setHeader(header) {
|
|
585
|
-
this._header.next(header);
|
|
586
|
-
}
|
|
587
|
-
_dropdownMenuClick(event) {
|
|
588
|
-
// This is needed, because some menu's will get stuck open if the component
|
|
589
|
-
// managing the menu is destroyed before the menu finishes its cleanup. I
|
|
590
|
-
// may look for a fix to that eventually.
|
|
591
|
-
this.closed.emit('click');
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
MenuComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.9", ngImport: i0, type: MenuComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
595
|
-
MenuComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "15.2.9", type: MenuComponent, selector: "seam-menu", inputs: { menuClass: "menuClass", baseWidth: "baseWidth", animationType: "animationType" }, outputs: { closed: "closed" }, providers: [LIB_MENU], viewQueries: [{ propertyName: "templateRef", first: true, predicate: TemplateRef, descendants: true }], exportAs: ["seamMenu"], ngImport: i0, template: "<ng-template>\n <div class=\"seam-menu-container\" @slideDown\n [class.seam-menu-anim--slide]=\"animationType==='slide'\"\n [class.seam-menu-anim--fade]=\"animationType==='fade'\">\n <div class=\"dropdown-menu show position-static{{ menuClass ? ' ' + menuClass : '' }}\"\n [style.width]=\"_menuWidth$ | async\"\n [class.pt-0]=\"hasHeader$ | async\"\n [class.pb-0]=\"hasFooter$ | async\"\n (keydown)=\"_handleKeydown($event)\"\n (click)=\"_dropdownMenuClick($event)\"\n tabindex=\"-1\"\n role=\"menu\"\n >\n <ng-content></ng-content>\n </div>\n </div>\n</ng-template>\n", styles: [".seam-menu-container.seam-menu-anim--slide{overflow:hidden}.seam-menu-container.ng-animating,.dropdown-menu.ng-animating{-webkit-user-select:none;-moz-user-select:none;user-select:none;pointer-events:none}\n"], dependencies: [{ kind: "pipe", type: i2.AsyncPipe, name: "async" }], animations: [
|
|
596
|
-
trigger('slideDown', [
|
|
597
|
-
transition(':enter', useAnimation(menuDropdownPanelIn)),
|
|
598
|
-
transition(':leave', useAnimation(menuDropdownPanelOut)),
|
|
599
|
-
])
|
|
600
|
-
], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
601
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.9", ngImport: i0, type: MenuComponent, decorators: [{
|
|
602
|
-
type: Component,
|
|
603
|
-
args: [{ selector: 'seam-menu', providers: [LIB_MENU], animations: [
|
|
604
|
-
trigger('slideDown', [
|
|
605
|
-
transition(':enter', useAnimation(menuDropdownPanelIn)),
|
|
606
|
-
transition(':leave', useAnimation(menuDropdownPanelOut)),
|
|
607
|
-
])
|
|
608
|
-
], changeDetection: ChangeDetectionStrategy.OnPush, exportAs: 'seamMenu', template: "<ng-template>\n <div class=\"seam-menu-container\" @slideDown\n [class.seam-menu-anim--slide]=\"animationType==='slide'\"\n [class.seam-menu-anim--fade]=\"animationType==='fade'\">\n <div class=\"dropdown-menu show position-static{{ menuClass ? ' ' + menuClass : '' }}\"\n [style.width]=\"_menuWidth$ | async\"\n [class.pt-0]=\"hasHeader$ | async\"\n [class.pb-0]=\"hasFooter$ | async\"\n (keydown)=\"_handleKeydown($event)\"\n (click)=\"_dropdownMenuClick($event)\"\n tabindex=\"-1\"\n role=\"menu\"\n >\n <ng-content></ng-content>\n </div>\n </div>\n</ng-template>\n", styles: [".seam-menu-container.seam-menu-anim--slide{overflow:hidden}.seam-menu-container.ng-animating,.dropdown-menu.ng-animating{-webkit-user-select:none;-moz-user-select:none;user-select:none;pointer-events:none}\n"] }]
|
|
609
|
-
}], ctorParameters: function () { return []; }, propDecorators: { templateRef: [{
|
|
610
|
-
type: ViewChild,
|
|
611
|
-
args: [TemplateRef]
|
|
612
|
-
}], closed: [{
|
|
613
|
-
type: Output
|
|
614
|
-
}], menuClass: [{
|
|
615
|
-
type: Input
|
|
616
|
-
}], baseWidth: [{
|
|
617
|
-
type: Input
|
|
618
|
-
}], animationType: [{
|
|
619
|
-
type: Input
|
|
620
|
-
}] } });
|
|
621
|
-
|
|
622
829
|
// TODO: Split up the button and anchor classes.
|
|
623
830
|
class MenuFooterActionComponent {
|
|
624
831
|
get _attrType() { return this.type; }
|