@nexterajs/ui 0.0.2 → 0.1.0
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/README.md +279 -34
- package/fesm2022/nexterajs-ui-charts.mjs +1096 -0
- package/fesm2022/nexterajs-ui-charts.mjs.map +1 -0
- package/fesm2022/nexterajs-ui-core.mjs +652 -12
- package/fesm2022/nexterajs-ui-core.mjs.map +1 -1
- package/fesm2022/nexterajs-ui-display.mjs +1142 -0
- package/fesm2022/nexterajs-ui-display.mjs.map +1 -0
- package/fesm2022/nexterajs-ui-dropdowns.mjs +321 -33
- package/fesm2022/nexterajs-ui-dropdowns.mjs.map +1 -1
- package/fesm2022/nexterajs-ui-file-uploader.mjs +572 -10
- package/fesm2022/nexterajs-ui-file-uploader.mjs.map +1 -1
- package/fesm2022/nexterajs-ui-forms.mjs +2027 -136
- package/fesm2022/nexterajs-ui-forms.mjs.map +1 -1
- package/fesm2022/nexterajs-ui-grid.mjs +1222 -188
- package/fesm2022/nexterajs-ui-grid.mjs.map +1 -1
- package/fesm2022/nexterajs-ui-intl.mjs +103 -11
- package/fesm2022/nexterajs-ui-intl.mjs.map +1 -1
- package/fesm2022/nexterajs-ui-navigation.mjs +1054 -0
- package/fesm2022/nexterajs-ui-navigation.mjs.map +1 -0
- package/fesm2022/nexterajs-ui-notify.mjs +46 -44
- package/fesm2022/nexterajs-ui-notify.mjs.map +1 -1
- package/fesm2022/nexterajs-ui-overlay.mjs +1671 -0
- package/fesm2022/nexterajs-ui-overlay.mjs.map +1 -0
- package/fesm2022/nexterajs-ui-pivot.mjs +51 -26
- package/fesm2022/nexterajs-ui-pivot.mjs.map +1 -1
- package/fesm2022/nexterajs-ui-shell.mjs +542 -0
- package/fesm2022/nexterajs-ui-shell.mjs.map +1 -0
- package/fesm2022/nexterajs-ui-tree-view.mjs +5 -2
- package/fesm2022/nexterajs-ui-tree-view.mjs.map +1 -1
- package/fesm2022/nexterajs-ui-validation.mjs +42 -11
- package/fesm2022/nexterajs-ui-validation.mjs.map +1 -1
- package/fesm2022/nexterajs-ui.mjs +5 -0
- package/fesm2022/nexterajs-ui.mjs.map +1 -1
- package/package.json +33 -2
- package/styles/_dropdown.scss +5 -5
- package/styles/_editor.scss +52 -17
- package/styles/_mixins.scss +1 -1
- package/styles/_scrollbar.scss +5 -5
- package/styles/_status.scss +21 -0
- package/styles/_toast.scss +14 -14
- package/styles/_tokens.scss +420 -3
- package/styles/_widget.scss +14 -3
- package/styles/index.scss +6 -4
- package/types/nexterajs-ui-charts.d.ts +446 -0
- package/types/nexterajs-ui-core.d.ts +387 -22
- package/types/nexterajs-ui-display.d.ts +720 -0
- package/types/nexterajs-ui-dropdowns.d.ts +191 -48
- package/types/nexterajs-ui-file-uploader.d.ts +236 -7
- package/types/nexterajs-ui-forms.d.ts +675 -30
- package/types/nexterajs-ui-grid.d.ts +448 -93
- package/types/nexterajs-ui-intl.d.ts +39 -4
- package/types/nexterajs-ui-navigation.d.ts +531 -0
- package/types/nexterajs-ui-notify.d.ts +4 -2
- package/types/nexterajs-ui-overlay.d.ts +703 -0
- package/types/nexterajs-ui-pivot.d.ts +56 -48
- package/types/nexterajs-ui-shell.d.ts +294 -0
- package/types/nexterajs-ui-tree-view.d.ts +6 -3
- package/types/nexterajs-ui-validation.d.ts +20 -11
- package/types/nexterajs-ui.d.ts +5 -0
|
@@ -0,0 +1,1671 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { InjectionToken, signal, input, Directive, inject, viewChild, Injector, TemplateRef, ViewEncapsulation, ChangeDetectionStrategy, Component, Injectable, ViewContainerRef, ElementRef, booleanAttribute, output, contentChildren, computed, afterNextRender, Renderer2, numberAttribute, DestroyRef, model } from '@angular/core';
|
|
3
|
+
import { ReplaySubject } from 'rxjs';
|
|
4
|
+
import { DIALOG_DATA, Dialog } from '@angular/cdk/dialog';
|
|
5
|
+
import { hasModifierKey } from '@angular/cdk/keycodes';
|
|
6
|
+
import { nxePane } from '@nexterajs/ui/styling';
|
|
7
|
+
import { NgComponentOutlet, NgTemplateOutlet } from '@angular/common';
|
|
8
|
+
import { NxeMessageService } from '@nexterajs/ui/core';
|
|
9
|
+
import { NxeButtonComponent } from '@nexterajs/ui/forms';
|
|
10
|
+
import { FocusKeyManager, AriaDescriber, FocusMonitor } from '@angular/cdk/a11y';
|
|
11
|
+
import { NXE_MENU } from '@nexterajs/ui/dropdowns';
|
|
12
|
+
import { Overlay } from '@angular/cdk/overlay';
|
|
13
|
+
import { TemplatePortal, ComponentPortal } from '@angular/cdk/portal';
|
|
14
|
+
import { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop';
|
|
15
|
+
|
|
16
|
+
/** The data a dialog was opened with, injected into the content component. */
|
|
17
|
+
const NXE_DIALOG_DATA = new InjectionToken('NXE_DIALOG_DATA');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Handle on an open dialog. Injectable in the content component (`inject(NxeDialogRef)`), and
|
|
21
|
+
* returned by `NxeDialogService.open`.
|
|
22
|
+
*/
|
|
23
|
+
class NxeDialogRef {
|
|
24
|
+
id;
|
|
25
|
+
titleId;
|
|
26
|
+
cdkRef = null;
|
|
27
|
+
outlet = null;
|
|
28
|
+
pendingClose = null;
|
|
29
|
+
titleClaimed = false;
|
|
30
|
+
closedSubject = new ReplaySubject(1);
|
|
31
|
+
resultSignal = signal(undefined, /* @ts-ignore */
|
|
32
|
+
...(ngDevMode ? [{ debugName: "resultSignal" }] : /* istanbul ignore next */ []));
|
|
33
|
+
closedSignal = signal(false, /* @ts-ignore */
|
|
34
|
+
...(ngDevMode ? [{ debugName: "closedSignal" }] : /* istanbul ignore next */ []));
|
|
35
|
+
dismissable = true;
|
|
36
|
+
/** The result the dialog closed with; `undefined` until it closes (and if it closed without one). */
|
|
37
|
+
result = this.resultSignal.asReadonly();
|
|
38
|
+
/** `true` once the dialog has closed. */
|
|
39
|
+
isClosed = this.closedSignal.asReadonly();
|
|
40
|
+
constructor(
|
|
41
|
+
/** Unique id; the dialog's DOM id. */
|
|
42
|
+
id,
|
|
43
|
+
/** Id the dialog's title element takes, and the dialog's `aria-labelledby`. */
|
|
44
|
+
titleId) {
|
|
45
|
+
this.id = id;
|
|
46
|
+
this.titleId = titleId;
|
|
47
|
+
}
|
|
48
|
+
/** Whether Esc and the backdrop may dismiss the dialog. Set `true` while the form is dirty. Explicit `close()` still works. */
|
|
49
|
+
get disableClose() {
|
|
50
|
+
return !this.dismissable;
|
|
51
|
+
}
|
|
52
|
+
set disableClose(value) {
|
|
53
|
+
this.dismissable = !value;
|
|
54
|
+
}
|
|
55
|
+
/** The content component instance; `null` for a template, or before it is created. */
|
|
56
|
+
get componentInstance() {
|
|
57
|
+
return this.outlet?.() ?? null;
|
|
58
|
+
}
|
|
59
|
+
/** Close with a result. Safe to call more than once. */
|
|
60
|
+
close(result) {
|
|
61
|
+
if (this.closedSignal()) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (!this.cdkRef) {
|
|
65
|
+
this.pendingClose = { result };
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
this.cdkRef.close(result);
|
|
69
|
+
}
|
|
70
|
+
/** Emits the result once, when the dialog closes, then completes. Late subscribers still get it. */
|
|
71
|
+
afterClosed() {
|
|
72
|
+
return this.closedSubject.asObservable();
|
|
73
|
+
}
|
|
74
|
+
/** The same as {@link afterClosed}, as a promise. */
|
|
75
|
+
whenClosed() {
|
|
76
|
+
return new Promise((resolve) => this.closedSubject.subscribe({ next: resolve, complete: () => resolve(undefined) }));
|
|
77
|
+
}
|
|
78
|
+
/** @internal Claimed by the first `nxe-dialog-title` in the dialog, which then carries {@link titleId}. */
|
|
79
|
+
_claimTitle() {
|
|
80
|
+
if (this.titleClaimed) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
this.titleClaimed = true;
|
|
84
|
+
return this.titleId;
|
|
85
|
+
}
|
|
86
|
+
/** @internal */
|
|
87
|
+
_attach(cdkRef) {
|
|
88
|
+
this.cdkRef = cdkRef;
|
|
89
|
+
cdkRef.closed.subscribe((result) => {
|
|
90
|
+
this.resultSignal.set(result);
|
|
91
|
+
this.closedSignal.set(true);
|
|
92
|
+
this.closedSubject.next(result);
|
|
93
|
+
this.closedSubject.complete();
|
|
94
|
+
});
|
|
95
|
+
if (this.pendingClose) {
|
|
96
|
+
const { result } = this.pendingClose;
|
|
97
|
+
this.pendingClose = null;
|
|
98
|
+
cdkRef.close(result);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** @internal */
|
|
102
|
+
_setOutlet(outlet) {
|
|
103
|
+
this.outlet = outlet;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The dialog's button row, pinned to the bottom of the dialog. `align` is where the buttons sit
|
|
109
|
+
* (`end` by default, the primary action last).
|
|
110
|
+
*
|
|
111
|
+
* ```html
|
|
112
|
+
* <div nxe-dialog-actions>
|
|
113
|
+
* <nxe-button text="Batal" (onClick)="ref.close()" />
|
|
114
|
+
* <nxe-button type="default" text="Simpan" (onClick)="save()" />
|
|
115
|
+
* </div>
|
|
116
|
+
* ```
|
|
117
|
+
*/
|
|
118
|
+
class NxeDialogActionsDirective {
|
|
119
|
+
align = input('end', /* @ts-ignore */
|
|
120
|
+
...(ngDevMode ? [{ debugName: "align" }] : /* istanbul ignore next */ []));
|
|
121
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeDialogActionsDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
122
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.5", type: NxeDialogActionsDirective, isStandalone: true, selector: "[nxe-dialog-actions], [nxeDialogActions]", inputs: { align: { classPropertyName: "align", publicName: "align", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class.nxe-dialog-actions-start": "align() === 'start'", "class.nxe-dialog-actions-between": "align() === 'between'" }, classAttribute: "nxe-dialog-actions" }, ngImport: i0 });
|
|
123
|
+
}
|
|
124
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeDialogActionsDirective, decorators: [{
|
|
125
|
+
type: Directive,
|
|
126
|
+
args: [{
|
|
127
|
+
selector: '[nxe-dialog-actions], [nxeDialogActions]',
|
|
128
|
+
host: {
|
|
129
|
+
class: 'nxe-dialog-actions',
|
|
130
|
+
'[class.nxe-dialog-actions-start]': "align() === 'start'",
|
|
131
|
+
'[class.nxe-dialog-actions-between]': "align() === 'between'",
|
|
132
|
+
},
|
|
133
|
+
}]
|
|
134
|
+
}], propDecorators: { align: [{ type: i0.Input, args: [{ isSignal: true, alias: "align", required: false }] }] } });
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Marks the dialog's heading. It takes the dialog's title id, so the dialog is named by it
|
|
138
|
+
* (`aria-labelledby`). Use it in content that declares its own chrome instead of passing `title`.
|
|
139
|
+
*
|
|
140
|
+
* ```html
|
|
141
|
+
* <h2 nxe-dialog-title>Tolak faktur</h2>
|
|
142
|
+
* ```
|
|
143
|
+
*
|
|
144
|
+
* A dialog has one title: the first one claims the id. Outside a dialog it only adds the class.
|
|
145
|
+
*/
|
|
146
|
+
class NxeDialogTitleDirective {
|
|
147
|
+
id = inject(NxeDialogRef, { optional: true })?._claimTitle() ?? null;
|
|
148
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeDialogTitleDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
149
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.5", type: NxeDialogTitleDirective, isStandalone: true, selector: "[nxe-dialog-title], [nxeDialogTitle]", host: { properties: { "attr.id": "id" }, classAttribute: "nxe-dialog-title" }, ngImport: i0 });
|
|
150
|
+
}
|
|
151
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeDialogTitleDirective, decorators: [{
|
|
152
|
+
type: Directive,
|
|
153
|
+
args: [{
|
|
154
|
+
selector: '[nxe-dialog-title], [nxeDialogTitle]',
|
|
155
|
+
host: {
|
|
156
|
+
class: 'nxe-dialog-title',
|
|
157
|
+
'[attr.id]': 'id',
|
|
158
|
+
},
|
|
159
|
+
}]
|
|
160
|
+
}] });
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The inside of every dialog: optional title, the scrolling body that hosts the content, optional
|
|
164
|
+
* actions bar and the corner close button. @internal
|
|
165
|
+
*/
|
|
166
|
+
class NxeDialogFrameComponent {
|
|
167
|
+
frame = inject(DIALOG_DATA);
|
|
168
|
+
messages = inject(NxeMessageService);
|
|
169
|
+
outlet = viewChild(NgComponentOutlet, /* @ts-ignore */
|
|
170
|
+
...(ngDevMode ? [{ debugName: "outlet" }] : /* istanbul ignore next */ []));
|
|
171
|
+
/** Both kinds of content look up the ref and the data here. */
|
|
172
|
+
contentInjector = Injector.create({
|
|
173
|
+
providers: [
|
|
174
|
+
{ provide: NxeDialogRef, useValue: this.frame.ref },
|
|
175
|
+
{ provide: NXE_DIALOG_DATA, useValue: this.frame.data },
|
|
176
|
+
...(this.frame.providers ?? []),
|
|
177
|
+
],
|
|
178
|
+
parent: this.frame.injector ?? inject(Injector),
|
|
179
|
+
});
|
|
180
|
+
template = this.frame.content instanceof TemplateRef ? this.frame.content : null;
|
|
181
|
+
component = this.template ? null : this.frame.content;
|
|
182
|
+
templateContext = { $implicit: this.frame.data, dialogRef: this.frame.ref };
|
|
183
|
+
constructor() {
|
|
184
|
+
this.frame.ref._setOutlet(() => this.outlet()?.componentInstance ?? null);
|
|
185
|
+
}
|
|
186
|
+
run(action) {
|
|
187
|
+
if (action.disabled) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (action.onClick?.(this.frame.ref) === false) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
this.frame.ref.close(action.result);
|
|
194
|
+
}
|
|
195
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeDialogFrameComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
196
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: NxeDialogFrameComponent, isStandalone: true, selector: "nxe-dialog-frame", host: { classAttribute: "nxe-dialog-frame nxe-widget" }, providers: [{ provide: NxeDialogRef, useFactory: () => inject(DIALOG_DATA).ref }], viewQueries: [{ propertyName: "outlet", first: true, predicate: NgComponentOutlet, descendants: true, isSignal: true }], ngImport: i0, template: "@if (frame.title) {\r\n <header class=\"nxe-dialog-header\">\r\n <h2 nxe-dialog-title>{{ frame.title }}</h2>\r\n </header>\r\n}\r\n<div class=\"nxe-dialog-body\">\r\n @if (template) {\r\n <ng-container *ngTemplateOutlet=\"template; context: templateContext; injector: contentInjector\" />\r\n } @else {\r\n <ng-container *ngComponentOutlet=\"component; injector: contentInjector\" />\r\n }\r\n</div>\r\n@if (frame.actions.length) {\r\n <div nxe-dialog-actions>\r\n @for (action of frame.actions; track $index) {\r\n <nxe-button\r\n [text]=\"action.text\"\r\n [type]=\"action.type ?? 'normal'\"\r\n [stylingMode]=\"action.stylingMode ?? 'contained'\"\r\n [disabled]=\"!!action.disabled\"\r\n [attr.cdkFocusInitial]=\"action.autofocus ? '' : null\"\r\n (onClick)=\"run(action)\" />\r\n }\r\n </div>\r\n}\r\n@if (frame.showClose) {\r\n <button type=\"button\" class=\"nxe-dialog-close\" [attr.aria-label]=\"messages.t('common.close')\" (click)=\"frame.ref.close()\">\r\n <svg viewBox=\"0 0 16 16\" aria-hidden=\"true\" focusable=\"false\">\r\n <path d=\"M3.5 3.5l9 9m0-9l-9 9\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" />\r\n </svg>\r\n </button>\r\n}\r\n", styles: [".cdk-overlay-pane.nxe-dialog-pane{box-sizing:border-box;max-width:calc(100vw - var(--nxe-ui-space-4) * 2);max-height:calc(100vh - var(--nxe-ui-space-4) * 2);max-height:calc(100dvh - var(--nxe-ui-space-4) * 2)}.cdk-overlay-pane.nxe-dialog-pane.nxe-dialog-sm{width:400px}.cdk-overlay-pane.nxe-dialog-pane.nxe-dialog-md{width:640px}.cdk-overlay-pane.nxe-dialog-pane.nxe-dialog-lg{width:960px}.cdk-overlay-pane.nxe-dialog-pane.nxe-dialog-xl{width:1200px}.cdk-overlay-pane.nxe-dialog-pane.nxe-dialog-fullscreen{width:100vw;height:100vh;height:100dvh;max-width:100vw;max-height:100vh;max-height:100dvh}.cdk-overlay-pane.nxe-dialog-pane.nxe-dialog-fullscreen .cdk-dialog-container{border-radius:0}.cdk-overlay-pane.nxe-dialog-pane .cdk-dialog-container{display:flex;flex-direction:column;box-sizing:border-box;width:100%;height:100%;max-height:inherit;overflow:hidden;background:var(--nxe-ui-surface);color:var(--nxe-ui-text);border:1px solid var(--nxe-ui-border);border-radius:var(--nxe-ui-radius-lg);box-shadow:var(--nxe-ui-shadow-lg);outline:none}.cdk-overlay-backdrop.nxe-dialog-backdrop{background:color-mix(in srgb,var(--nxe-ui-toast-bg) 45%,transparent)}nxe-dialog-frame.nxe-dialog-frame{position:relative;display:flex;flex-direction:column;flex:1 1 auto;min-height:0;font-size:var(--nxe-ui-font-size-md);line-height:var(--nxe-ui-line-height-md)}nxe-dialog-frame.nxe-dialog-frame .nxe-dialog-header{flex:none;padding:var(--nxe-ui-space-4) var(--nxe-ui-space-8) var(--nxe-ui-space-2) var(--nxe-ui-space-4)}nxe-dialog-frame.nxe-dialog-frame .nxe-dialog-body{display:flex;flex-direction:column;flex:1 1 auto;min-height:0;overflow:auto}nxe-dialog-frame.nxe-dialog-frame .nxe-dialog-close{position:absolute;top:var(--nxe-ui-space-2);right:var(--nxe-ui-space-2);z-index:3;display:inline-flex;align-items:center;justify-content:center;width:var(--nxe-ui-space-6);height:var(--nxe-ui-space-6);padding:0;border:0;border-radius:var(--nxe-ui-radius-sm);background:transparent;color:var(--nxe-ui-neutral-600);cursor:pointer}nxe-dialog-frame.nxe-dialog-frame .nxe-dialog-close svg{width:14px;height:14px}nxe-dialog-frame.nxe-dialog-frame .nxe-dialog-close:hover{background:var(--nxe-ui-surface-alt);color:var(--nxe-ui-text)}nxe-dialog-frame.nxe-dialog-frame .nxe-dialog-close:focus-visible{outline:none;box-shadow:var(--nxe-ui-focus-ring)}.nxe-dialog-title{margin:0;font-size:var(--nxe-ui-font-size-xl);font-weight:var(--nxe-ui-font-weight-semibold);line-height:var(--nxe-ui-line-height-xl);color:var(--nxe-ui-text)}.nxe-dialog-body>.nxe-dialog-title,.nxe-dialog-body .nxe-dialog-title:is(h1,h2,h3){padding:var(--nxe-ui-space-4) var(--nxe-ui-space-8) var(--nxe-ui-space-2) var(--nxe-ui-space-4)}.nxe-dialog-content{display:block;padding:var(--nxe-ui-space-2) var(--nxe-ui-space-4) var(--nxe-ui-space-4)}.nxe-dialog-actions{--nxe-ui-button-bg-danger: var(--nxe-ui-status-danger-fg);position:sticky;bottom:0;z-index:2;display:flex;flex:none;flex-wrap:wrap;justify-content:flex-end;gap:var(--nxe-ui-space-2);padding:var(--nxe-ui-space-3) var(--nxe-ui-space-4);border-top:1px solid var(--nxe-ui-border);background:var(--nxe-ui-surface)}.nxe-dialog-actions.nxe-dialog-actions-start{justify-content:flex-start}.nxe-dialog-actions.nxe-dialog-actions-between{justify-content:space-between}\n"], dependencies: [{ kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: NxeButtonComponent, selector: "nxe-button", inputs: ["text", "type", "stylingMode", "size", "loading", "block", "disabled", "validationGroup", "focusStateEnabled", "icon", "height", "width"], outputs: ["onClick"] }, { kind: "directive", type: NxeDialogTitleDirective, selector: "[nxe-dialog-title], [nxeDialogTitle]" }, { kind: "directive", type: NxeDialogActionsDirective, selector: "[nxe-dialog-actions], [nxeDialogActions]", inputs: ["align"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
197
|
+
}
|
|
198
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeDialogFrameComponent, decorators: [{
|
|
199
|
+
type: Component,
|
|
200
|
+
args: [{ selector: 'nxe-dialog-frame', imports: [NgComponentOutlet, NgTemplateOutlet, NxeButtonComponent, NxeDialogTitleDirective, NxeDialogActionsDirective], host: { class: 'nxe-dialog-frame nxe-widget' }, providers: [{ provide: NxeDialogRef, useFactory: () => inject(DIALOG_DATA).ref }], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: "@if (frame.title) {\r\n <header class=\"nxe-dialog-header\">\r\n <h2 nxe-dialog-title>{{ frame.title }}</h2>\r\n </header>\r\n}\r\n<div class=\"nxe-dialog-body\">\r\n @if (template) {\r\n <ng-container *ngTemplateOutlet=\"template; context: templateContext; injector: contentInjector\" />\r\n } @else {\r\n <ng-container *ngComponentOutlet=\"component; injector: contentInjector\" />\r\n }\r\n</div>\r\n@if (frame.actions.length) {\r\n <div nxe-dialog-actions>\r\n @for (action of frame.actions; track $index) {\r\n <nxe-button\r\n [text]=\"action.text\"\r\n [type]=\"action.type ?? 'normal'\"\r\n [stylingMode]=\"action.stylingMode ?? 'contained'\"\r\n [disabled]=\"!!action.disabled\"\r\n [attr.cdkFocusInitial]=\"action.autofocus ? '' : null\"\r\n (onClick)=\"run(action)\" />\r\n }\r\n </div>\r\n}\r\n@if (frame.showClose) {\r\n <button type=\"button\" class=\"nxe-dialog-close\" [attr.aria-label]=\"messages.t('common.close')\" (click)=\"frame.ref.close()\">\r\n <svg viewBox=\"0 0 16 16\" aria-hidden=\"true\" focusable=\"false\">\r\n <path d=\"M3.5 3.5l9 9m0-9l-9 9\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" />\r\n </svg>\r\n </button>\r\n}\r\n", styles: [".cdk-overlay-pane.nxe-dialog-pane{box-sizing:border-box;max-width:calc(100vw - var(--nxe-ui-space-4) * 2);max-height:calc(100vh - var(--nxe-ui-space-4) * 2);max-height:calc(100dvh - var(--nxe-ui-space-4) * 2)}.cdk-overlay-pane.nxe-dialog-pane.nxe-dialog-sm{width:400px}.cdk-overlay-pane.nxe-dialog-pane.nxe-dialog-md{width:640px}.cdk-overlay-pane.nxe-dialog-pane.nxe-dialog-lg{width:960px}.cdk-overlay-pane.nxe-dialog-pane.nxe-dialog-xl{width:1200px}.cdk-overlay-pane.nxe-dialog-pane.nxe-dialog-fullscreen{width:100vw;height:100vh;height:100dvh;max-width:100vw;max-height:100vh;max-height:100dvh}.cdk-overlay-pane.nxe-dialog-pane.nxe-dialog-fullscreen .cdk-dialog-container{border-radius:0}.cdk-overlay-pane.nxe-dialog-pane .cdk-dialog-container{display:flex;flex-direction:column;box-sizing:border-box;width:100%;height:100%;max-height:inherit;overflow:hidden;background:var(--nxe-ui-surface);color:var(--nxe-ui-text);border:1px solid var(--nxe-ui-border);border-radius:var(--nxe-ui-radius-lg);box-shadow:var(--nxe-ui-shadow-lg);outline:none}.cdk-overlay-backdrop.nxe-dialog-backdrop{background:color-mix(in srgb,var(--nxe-ui-toast-bg) 45%,transparent)}nxe-dialog-frame.nxe-dialog-frame{position:relative;display:flex;flex-direction:column;flex:1 1 auto;min-height:0;font-size:var(--nxe-ui-font-size-md);line-height:var(--nxe-ui-line-height-md)}nxe-dialog-frame.nxe-dialog-frame .nxe-dialog-header{flex:none;padding:var(--nxe-ui-space-4) var(--nxe-ui-space-8) var(--nxe-ui-space-2) var(--nxe-ui-space-4)}nxe-dialog-frame.nxe-dialog-frame .nxe-dialog-body{display:flex;flex-direction:column;flex:1 1 auto;min-height:0;overflow:auto}nxe-dialog-frame.nxe-dialog-frame .nxe-dialog-close{position:absolute;top:var(--nxe-ui-space-2);right:var(--nxe-ui-space-2);z-index:3;display:inline-flex;align-items:center;justify-content:center;width:var(--nxe-ui-space-6);height:var(--nxe-ui-space-6);padding:0;border:0;border-radius:var(--nxe-ui-radius-sm);background:transparent;color:var(--nxe-ui-neutral-600);cursor:pointer}nxe-dialog-frame.nxe-dialog-frame .nxe-dialog-close svg{width:14px;height:14px}nxe-dialog-frame.nxe-dialog-frame .nxe-dialog-close:hover{background:var(--nxe-ui-surface-alt);color:var(--nxe-ui-text)}nxe-dialog-frame.nxe-dialog-frame .nxe-dialog-close:focus-visible{outline:none;box-shadow:var(--nxe-ui-focus-ring)}.nxe-dialog-title{margin:0;font-size:var(--nxe-ui-font-size-xl);font-weight:var(--nxe-ui-font-weight-semibold);line-height:var(--nxe-ui-line-height-xl);color:var(--nxe-ui-text)}.nxe-dialog-body>.nxe-dialog-title,.nxe-dialog-body .nxe-dialog-title:is(h1,h2,h3){padding:var(--nxe-ui-space-4) var(--nxe-ui-space-8) var(--nxe-ui-space-2) var(--nxe-ui-space-4)}.nxe-dialog-content{display:block;padding:var(--nxe-ui-space-2) var(--nxe-ui-space-4) var(--nxe-ui-space-4)}.nxe-dialog-actions{--nxe-ui-button-bg-danger: var(--nxe-ui-status-danger-fg);position:sticky;bottom:0;z-index:2;display:flex;flex:none;flex-wrap:wrap;justify-content:flex-end;gap:var(--nxe-ui-space-2);padding:var(--nxe-ui-space-3) var(--nxe-ui-space-4);border-top:1px solid var(--nxe-ui-border);background:var(--nxe-ui-surface)}.nxe-dialog-actions.nxe-dialog-actions-start{justify-content:flex-start}.nxe-dialog-actions.nxe-dialog-actions-between{justify-content:space-between}\n"] }]
|
|
201
|
+
}], ctorParameters: () => [], propDecorators: { outlet: [{ type: i0.ViewChild, args: [i0.forwardRef(() => NgComponentOutlet), { isSignal: true }] }] } });
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Opens modal dialogs.
|
|
205
|
+
*
|
|
206
|
+
* ```ts
|
|
207
|
+
* const ref = this.dialog.open(RejectInvoiceDialogComponent, {
|
|
208
|
+
* title: 'Tolak faktur',
|
|
209
|
+
* size: 'sm',
|
|
210
|
+
* data: { invoiceId },
|
|
211
|
+
* actions: [
|
|
212
|
+
* { text: 'Batal', stylingMode: 'outlined', result: false },
|
|
213
|
+
* { text: 'Tolak', type: 'danger', result: true },
|
|
214
|
+
* ],
|
|
215
|
+
* });
|
|
216
|
+
* ref.afterClosed().subscribe((rejected) => rejected && this.reload());
|
|
217
|
+
* ```
|
|
218
|
+
*
|
|
219
|
+
* The content is a component (it injects `NXE_DIALOG_DATA` and `NxeDialogRef`) or a template (it
|
|
220
|
+
* receives `{ $implicit: data, dialogRef }`). Focus is trapped, starts on the first tabbable
|
|
221
|
+
* element (or `[cdkFocusInitial]`) and returns to the opener when the dialog closes. Esc and a
|
|
222
|
+
* backdrop click dismiss it unless `disableClose` is set; with dialogs stacked, Esc closes only
|
|
223
|
+
* the top one. The page behind does not scroll.
|
|
224
|
+
*/
|
|
225
|
+
class NxeDialogService {
|
|
226
|
+
cdk = inject(Dialog);
|
|
227
|
+
sequence = 0;
|
|
228
|
+
open(content, config = {}) {
|
|
229
|
+
const id = `nxe-dialog-${++this.sequence}`;
|
|
230
|
+
const ref = new NxeDialogRef(id, `${id}-title`);
|
|
231
|
+
ref.disableClose = !!config.disableClose;
|
|
232
|
+
const frame = {
|
|
233
|
+
ref: ref,
|
|
234
|
+
content: content,
|
|
235
|
+
data: config.data,
|
|
236
|
+
title: config.title,
|
|
237
|
+
actions: (config.actions ?? []),
|
|
238
|
+
showClose: config.showClose ?? true,
|
|
239
|
+
injector: config.injector,
|
|
240
|
+
};
|
|
241
|
+
const cdkRef = this.cdk.open(NxeDialogFrameComponent, {
|
|
242
|
+
id,
|
|
243
|
+
role: config.role ?? 'dialog',
|
|
244
|
+
ariaModal: true,
|
|
245
|
+
ariaLabel: config.ariaLabel ?? null,
|
|
246
|
+
ariaLabelledBy: config.ariaLabel ? null : ref.titleId,
|
|
247
|
+
ariaDescribedBy: config.ariaDescribedBy ?? null,
|
|
248
|
+
autoFocus: config.autoFocus ?? 'first-tabbable',
|
|
249
|
+
restoreFocus: true,
|
|
250
|
+
hasBackdrop: true,
|
|
251
|
+
backdropClass: 'nxe-dialog-backdrop',
|
|
252
|
+
panelClass: nxePane('nxe-dialog-pane', `nxe-dialog-${config.size ?? 'md'}`, ...[config.panelClass ?? []].flat()),
|
|
253
|
+
viewContainerRef: config.viewContainerRef,
|
|
254
|
+
// Dismissal is decided below, so Esc and the backdrop can follow different policies and
|
|
255
|
+
// `ref.disableClose` can change while the dialog is open.
|
|
256
|
+
disableClose: true,
|
|
257
|
+
data: frame,
|
|
258
|
+
});
|
|
259
|
+
ref._attach(cdkRef);
|
|
260
|
+
// The overlay keyboard dispatcher only delivers a key to the topmost overlay, so with dialogs
|
|
261
|
+
// stacked (or a menu open above one) Esc reaches exactly one of them.
|
|
262
|
+
cdkRef.keydownEvents.subscribe((event) => {
|
|
263
|
+
if (event.key === 'Escape' && !ref.disableClose && !hasModifierKey(event)) {
|
|
264
|
+
event.preventDefault();
|
|
265
|
+
ref.close();
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
cdkRef.backdropClick.subscribe(() => {
|
|
269
|
+
if (!ref.disableClose && config.backdropClose !== false) {
|
|
270
|
+
ref.close();
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
return ref;
|
|
274
|
+
}
|
|
275
|
+
/** Closes every open dialog, with no result. */
|
|
276
|
+
closeAll() {
|
|
277
|
+
this.cdk.closeAll();
|
|
278
|
+
}
|
|
279
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeDialogService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
280
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeDialogService, providedIn: 'root' });
|
|
281
|
+
}
|
|
282
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeDialogService, decorators: [{
|
|
283
|
+
type: Injectable,
|
|
284
|
+
args: [{ providedIn: 'root' }]
|
|
285
|
+
}] });
|
|
286
|
+
|
|
287
|
+
/** The dialog's body: padded, and the part that scrolls (the title and actions stay in view). */
|
|
288
|
+
class NxeDialogContentDirective {
|
|
289
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeDialogContentDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
290
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.5", type: NxeDialogContentDirective, isStandalone: true, selector: "[nxe-dialog-content], [nxeDialogContent]", host: { classAttribute: "nxe-dialog-content" }, ngImport: i0 });
|
|
291
|
+
}
|
|
292
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeDialogContentDirective, decorators: [{
|
|
293
|
+
type: Directive,
|
|
294
|
+
args: [{
|
|
295
|
+
selector: '[nxe-dialog-content], [nxeDialogContent]',
|
|
296
|
+
host: { class: 'nxe-dialog-content' },
|
|
297
|
+
}]
|
|
298
|
+
}] });
|
|
299
|
+
|
|
300
|
+
/** The message of a confirm dialog. It is the dialog's accessible description. @internal */
|
|
301
|
+
class NxeConfirmContentComponent {
|
|
302
|
+
data = inject(NXE_DIALOG_DATA);
|
|
303
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeConfirmContentComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
304
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.1.5", type: NxeConfirmContentComponent, isStandalone: true, selector: "nxe-confirm-content", host: { classAttribute: "nxe-confirm-content" }, ngImport: i0, template: "<p nxe-dialog-content class=\"nxe-confirm-message\" [id]=\"data.descriptionId\">{{ data.message }}</p>\r\n", styles: ["nxe-confirm-content.nxe-confirm-content{display:block}nxe-confirm-content.nxe-confirm-content .nxe-confirm-message{margin:0;color:var(--nxe-ui-text);font-size:var(--nxe-ui-font-size-md);line-height:var(--nxe-ui-line-height-md);white-space:pre-line}\n"], dependencies: [{ kind: "directive", type: NxeDialogContentDirective, selector: "[nxe-dialog-content], [nxeDialogContent]" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
305
|
+
}
|
|
306
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeConfirmContentComponent, decorators: [{
|
|
307
|
+
type: Component,
|
|
308
|
+
args: [{ selector: 'nxe-confirm-content', imports: [NxeDialogContentDirective], host: { class: 'nxe-confirm-content' }, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: "<p nxe-dialog-content class=\"nxe-confirm-message\" [id]=\"data.descriptionId\">{{ data.message }}</p>\r\n", styles: ["nxe-confirm-content.nxe-confirm-content{display:block}nxe-confirm-content.nxe-confirm-content .nxe-confirm-message{margin:0;color:var(--nxe-ui-text);font-size:var(--nxe-ui-font-size-md);line-height:var(--nxe-ui-line-height-md);white-space:pre-line}\n"] }]
|
|
309
|
+
}] });
|
|
310
|
+
|
|
311
|
+
let confirmSequence = 0;
|
|
312
|
+
/**
|
|
313
|
+
* Asks the user to confirm before something destructive or irreversible. Resolves `true` on
|
|
314
|
+
* confirm, `false` on cancel or Esc.
|
|
315
|
+
*
|
|
316
|
+
* ```ts
|
|
317
|
+
* const go = await this.confirm.confirm({
|
|
318
|
+
* title: 'Batalkan PO-0042?',
|
|
319
|
+
* message: 'PO ini tidak dapat dibuka kembali.',
|
|
320
|
+
* confirmText: 'Batalkan PO',
|
|
321
|
+
* danger: true,
|
|
322
|
+
* });
|
|
323
|
+
* ```
|
|
324
|
+
*
|
|
325
|
+
* It is an `alertdialog` described by the message. A backdrop click does not dismiss it, so an
|
|
326
|
+
* answer is always deliberate.
|
|
327
|
+
*/
|
|
328
|
+
class NxeConfirmService {
|
|
329
|
+
dialog = inject(NxeDialogService);
|
|
330
|
+
messages = inject(NxeMessageService);
|
|
331
|
+
async confirm(options) {
|
|
332
|
+
const descriptionId = `nxe-confirm-${++confirmSequence}-description`;
|
|
333
|
+
const data = { message: options.message ?? '', descriptionId };
|
|
334
|
+
const ref = this.dialog.open(NxeConfirmContentComponent, {
|
|
335
|
+
title: options.title,
|
|
336
|
+
size: options.size ?? 'sm',
|
|
337
|
+
role: 'alertdialog',
|
|
338
|
+
data,
|
|
339
|
+
ariaDescribedBy: options.message ? descriptionId : undefined,
|
|
340
|
+
backdropClose: false,
|
|
341
|
+
showClose: false,
|
|
342
|
+
actions: [
|
|
343
|
+
{
|
|
344
|
+
text: options.cancelText ?? this.messages.t('common.cancel'),
|
|
345
|
+
stylingMode: 'outlined',
|
|
346
|
+
result: false,
|
|
347
|
+
autofocus: !!options.danger,
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
text: options.confirmText ?? this.messages.t('common.confirm'),
|
|
351
|
+
type: options.danger ? 'danger' : 'default',
|
|
352
|
+
result: true,
|
|
353
|
+
autofocus: !options.danger,
|
|
354
|
+
},
|
|
355
|
+
],
|
|
356
|
+
});
|
|
357
|
+
return (await ref.whenClosed()) === true;
|
|
358
|
+
}
|
|
359
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeConfirmService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
360
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeConfirmService, providedIn: 'root' });
|
|
361
|
+
}
|
|
362
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeConfirmService, decorators: [{
|
|
363
|
+
type: Injectable,
|
|
364
|
+
args: [{ providedIn: 'root' }]
|
|
365
|
+
}] });
|
|
366
|
+
|
|
367
|
+
let triggerSequence$1 = 0;
|
|
368
|
+
/** Below the trigger, left edges aligned; flips above, then aligns right edges. */
|
|
369
|
+
const ROOT_POSITIONS = [
|
|
370
|
+
{ originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top' },
|
|
371
|
+
{ originX: 'start', originY: 'top', overlayX: 'start', overlayY: 'bottom' },
|
|
372
|
+
{ originX: 'end', originY: 'bottom', overlayX: 'end', overlayY: 'top' },
|
|
373
|
+
{ originX: 'end', originY: 'top', overlayX: 'end', overlayY: 'bottom' },
|
|
374
|
+
];
|
|
375
|
+
/** Beside the item, tops aligned; flips to the left. */
|
|
376
|
+
const SUBMENU_POSITIONS = [
|
|
377
|
+
{ originX: 'end', originY: 'top', overlayX: 'start', overlayY: 'top' },
|
|
378
|
+
{ originX: 'start', originY: 'top', overlayX: 'end', overlayY: 'top' },
|
|
379
|
+
{ originX: 'end', originY: 'bottom', overlayX: 'start', overlayY: 'bottom' },
|
|
380
|
+
{ originX: 'start', originY: 'bottom', overlayX: 'end', overlayY: 'bottom' },
|
|
381
|
+
];
|
|
382
|
+
/** Hosts where Enter and Space already produce a click. */
|
|
383
|
+
const NATIVELY_ACTIVATABLE$1 = new Set(['BUTTON', 'A', 'INPUT', 'SUMMARY']);
|
|
384
|
+
/**
|
|
385
|
+
* Opens an `nxe-menu` from the element it sits on.
|
|
386
|
+
*
|
|
387
|
+
* ```html
|
|
388
|
+
* <nxe-button icon="more" aria-label="Aksi" [nxeMenuTriggerFor]="menu" />
|
|
389
|
+
* <nxe-menu #menu>…</nxe-menu>
|
|
390
|
+
* ```
|
|
391
|
+
*
|
|
392
|
+
* The element gets `aria-haspopup="menu"`, `aria-expanded` and `aria-controls`. Click toggles;
|
|
393
|
+
* ArrowDown opens on the first item and ArrowUp on the last; Enter and Space open from hosts that
|
|
394
|
+
* are not buttons (`nxe-button`). The panel opens below the element and flips above (or aligns to
|
|
395
|
+
* the other edge) when there is no room. On an `nxe-menu-item` it opens a submenu to the side.
|
|
396
|
+
* Closing with Esc or by choosing an item returns focus to the element; clicking away does not.
|
|
397
|
+
*/
|
|
398
|
+
class NxeMenuTriggerForDirective {
|
|
399
|
+
overlay = inject(Overlay);
|
|
400
|
+
viewContainer = inject(ViewContainerRef);
|
|
401
|
+
element = inject(ElementRef).nativeElement;
|
|
402
|
+
/** @internal The menu this trigger sits in, when it opens a submenu. */
|
|
403
|
+
parentMenu = inject(NXE_MENU, { optional: true });
|
|
404
|
+
/** The menu to open. */
|
|
405
|
+
menu = input.required({ ...(ngDevMode ? { debugName: "menu" } : /* istanbul ignore next */ {}), alias: 'nxeMenuTriggerFor' });
|
|
406
|
+
/** @internal Id of the trigger; the panel is labelled by it. */
|
|
407
|
+
id = this.element.id || `nxe-menu-trigger-${++triggerSequence$1}`;
|
|
408
|
+
openState = signal(false, /* @ts-ignore */
|
|
409
|
+
...(ngDevMode ? [{ debugName: "openState" }] : /* istanbul ignore next */ []));
|
|
410
|
+
/** Whether the menu is showing. */
|
|
411
|
+
isOpen = this.openState.asReadonly();
|
|
412
|
+
overlayRef = null;
|
|
413
|
+
/** Opens the menu and moves focus to its first (or last) item. No-op while open. */
|
|
414
|
+
open(focus = 'first') {
|
|
415
|
+
if (this.openState()) {
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
const menu = this.menu();
|
|
419
|
+
this.overlayRef ??= this.createOverlay(!!this.parentMenu);
|
|
420
|
+
menu.trigger = this;
|
|
421
|
+
this.overlayRef.attach(new TemplatePortal(menu.template(), this.viewContainer));
|
|
422
|
+
this.openState.set(true);
|
|
423
|
+
menu._opened(focus);
|
|
424
|
+
}
|
|
425
|
+
/** Closes the menu. `restoreFocus` returns focus to the trigger element (Esc, or an item was chosen). */
|
|
426
|
+
close(restoreFocus = false) {
|
|
427
|
+
if (!this.openState()) {
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
const menu = this.menu();
|
|
431
|
+
this.openState.set(false);
|
|
432
|
+
menu._closed();
|
|
433
|
+
this.overlayRef?.detach();
|
|
434
|
+
if (menu.trigger === this) {
|
|
435
|
+
menu.trigger = null;
|
|
436
|
+
}
|
|
437
|
+
if (restoreFocus) {
|
|
438
|
+
this.element.focus();
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
toggle() {
|
|
442
|
+
if (this.openState()) {
|
|
443
|
+
this.close(true);
|
|
444
|
+
}
|
|
445
|
+
else {
|
|
446
|
+
this.open();
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
ngOnDestroy() {
|
|
450
|
+
if (this.openState()) {
|
|
451
|
+
const menu = this.menu();
|
|
452
|
+
menu._closed();
|
|
453
|
+
if (menu.trigger === this) {
|
|
454
|
+
menu.trigger = null;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
this.overlayRef?.dispose();
|
|
458
|
+
this.overlayRef = null;
|
|
459
|
+
}
|
|
460
|
+
onClick(event) {
|
|
461
|
+
// A disabled `nxe-menu-item` swallows its own click before it gets here.
|
|
462
|
+
if (event.defaultPrevented) {
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
if (this.parentMenu) {
|
|
466
|
+
// A submenu item opens its menu; the pointer is already on it, so a second click leaves it open.
|
|
467
|
+
this.open('first');
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
this.toggle();
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
onKeydown(event) {
|
|
474
|
+
if (event.ctrlKey || event.metaKey || event.altKey) {
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
// A submenu item is opened by ArrowRight, which its menu handles.
|
|
478
|
+
if (this.parentMenu) {
|
|
479
|
+
if (this.element.getAttribute('aria-disabled') !== 'true' &&
|
|
480
|
+
!NATIVELY_ACTIVATABLE$1.has(this.element.tagName) &&
|
|
481
|
+
(event.key === 'Enter' || event.key === ' ')) {
|
|
482
|
+
event.preventDefault();
|
|
483
|
+
this.open('first');
|
|
484
|
+
}
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
switch (event.key) {
|
|
488
|
+
case 'ArrowDown':
|
|
489
|
+
event.preventDefault();
|
|
490
|
+
this.open('first');
|
|
491
|
+
break;
|
|
492
|
+
case 'ArrowUp':
|
|
493
|
+
event.preventDefault();
|
|
494
|
+
this.open('last');
|
|
495
|
+
break;
|
|
496
|
+
case 'Enter':
|
|
497
|
+
case ' ':
|
|
498
|
+
// nxe-button is a div: it reports Enter and Space through its onClick output, not a DOM click.
|
|
499
|
+
if (!NATIVELY_ACTIVATABLE$1.has(this.element.tagName)) {
|
|
500
|
+
event.preventDefault();
|
|
501
|
+
this.toggle();
|
|
502
|
+
}
|
|
503
|
+
break;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
createOverlay(isSubmenu) {
|
|
507
|
+
const menu = this.menu();
|
|
508
|
+
const ref = this.overlay.create({
|
|
509
|
+
positionStrategy: this.overlay
|
|
510
|
+
.position()
|
|
511
|
+
.flexibleConnectedTo(this.element)
|
|
512
|
+
.withPositions(isSubmenu ? SUBMENU_POSITIONS : ROOT_POSITIONS)
|
|
513
|
+
.withFlexibleDimensions(false)
|
|
514
|
+
.withPush(true)
|
|
515
|
+
.withViewportMargin(8),
|
|
516
|
+
scrollStrategy: this.overlay.scrollStrategies.reposition(),
|
|
517
|
+
// A transparent backdrop catches the click-away. A submenu rides on its parent's.
|
|
518
|
+
hasBackdrop: !isSubmenu,
|
|
519
|
+
backdropClass: 'cdk-overlay-transparent-backdrop',
|
|
520
|
+
panelClass: nxePane('nxe-menu-pane', ...[menu.panelClass()].flat()),
|
|
521
|
+
});
|
|
522
|
+
ref.backdropClick().subscribe(() => this.close(false));
|
|
523
|
+
// Esc is normally handled by the panel; this covers a menu opened without focus inside it.
|
|
524
|
+
ref.keydownEvents().subscribe((event) => {
|
|
525
|
+
if (event.key === 'Escape') {
|
|
526
|
+
event.preventDefault();
|
|
527
|
+
this.close(true);
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
return ref;
|
|
531
|
+
}
|
|
532
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeMenuTriggerForDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
533
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.5", type: NxeMenuTriggerForDirective, isStandalone: true, selector: "[nxeMenuTriggerFor]", inputs: { menu: { classPropertyName: "menu", publicName: "nxeMenuTriggerFor", isSignal: true, isRequired: true, transformFunction: null } }, host: { attributes: { "aria-haspopup": "menu" }, listeners: { "click": "onClick($event)", "keydown": "onKeydown($event)" }, properties: { "attr.id": "id", "attr.aria-expanded": "isOpen()", "attr.aria-controls": "isOpen() ? menu().panelId : null" } }, exportAs: ["nxeMenuTrigger"], ngImport: i0 });
|
|
534
|
+
}
|
|
535
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeMenuTriggerForDirective, decorators: [{
|
|
536
|
+
type: Directive,
|
|
537
|
+
args: [{
|
|
538
|
+
selector: '[nxeMenuTriggerFor]',
|
|
539
|
+
exportAs: 'nxeMenuTrigger',
|
|
540
|
+
host: {
|
|
541
|
+
'aria-haspopup': 'menu',
|
|
542
|
+
'[attr.id]': 'id',
|
|
543
|
+
'[attr.aria-expanded]': 'isOpen()',
|
|
544
|
+
'[attr.aria-controls]': 'isOpen() ? menu().panelId : null',
|
|
545
|
+
'(click)': 'onClick($event)',
|
|
546
|
+
'(keydown)': 'onKeydown($event)',
|
|
547
|
+
},
|
|
548
|
+
}]
|
|
549
|
+
}], propDecorators: { menu: [{ type: i0.Input, args: [{ isSignal: true, alias: "nxeMenuTriggerFor", required: true }] }] } });
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* One action in an `nxe-menu`. Use it as an element (recommended, and the only way for apps that ban
|
|
553
|
+
* native buttons and `(click)` on non-interactive elements), or as an attribute on a
|
|
554
|
+
* `<button type="button">` or an `<a>`.
|
|
555
|
+
*
|
|
556
|
+
* ```html
|
|
557
|
+
* <nxe-menu-item (itemClick)="signOut()" [danger]="true">Sign out</nxe-menu-item>
|
|
558
|
+
* <nxe-menu-item routerLink="/profile">Profile</nxe-menu-item>
|
|
559
|
+
* <nxe-menu-item [nxeMenuTriggerFor]="more">Lainnya</nxe-menu-item>
|
|
560
|
+
*
|
|
561
|
+
* <!-- attribute form -->
|
|
562
|
+
* <button type="button" nxe-menu-item (click)="edit()">
|
|
563
|
+
* <svg nxeMenuItemIcon viewBox="0 0 16 16">…</svg>
|
|
564
|
+
* Ubah
|
|
565
|
+
* </button>
|
|
566
|
+
* <button type="button" nxe-menu-item danger (click)="remove()">Hapus</button>
|
|
567
|
+
* <button type="button" nxe-menu-item [nxeMenuTriggerFor]="more">Lainnya</button>
|
|
568
|
+
* ```
|
|
569
|
+
*
|
|
570
|
+
* `disabled` keeps the item visible but unreachable by arrow keys and ignored by clicks. `danger`
|
|
571
|
+
* colours a destructive item. `[nxeMenuItemIcon]` projects an icon before the label. An item with
|
|
572
|
+
* `[nxeMenuTriggerFor]` opens a submenu instead of closing the menu.
|
|
573
|
+
*
|
|
574
|
+
* The element form has `role="menuitem"`, a roving `tabindex`, Enter and Space activation, and
|
|
575
|
+
* `aria-disabled` when disabled. `itemClick` fires on every activation (pointer, Enter, Space) of an
|
|
576
|
+
* enabled item, and never on a disabled one; the attribute form emits it too.
|
|
577
|
+
*/
|
|
578
|
+
class NxeMenuItemComponent {
|
|
579
|
+
element = inject(ElementRef).nativeElement;
|
|
580
|
+
/** @internal The menu this item sits in. */
|
|
581
|
+
menu = inject(NXE_MENU, { optional: true });
|
|
582
|
+
/** @internal Set when the item opens a submenu. */
|
|
583
|
+
trigger = inject(NxeMenuTriggerForDirective, { optional: true, self: true });
|
|
584
|
+
isButton = this.element.tagName === 'BUTTON';
|
|
585
|
+
/** Buttons and links turn Enter (and, for a button, Space) into a click themselves. */
|
|
586
|
+
isNative = this.element.tagName === 'BUTTON' || this.element.tagName === 'A';
|
|
587
|
+
/** Shown but not actionable. */
|
|
588
|
+
disabledInput = input(false, { ...(ngDevMode ? { debugName: "disabledInput" } : /* istanbul ignore next */ {}), alias: 'disabled', transform: booleanAttribute });
|
|
589
|
+
/** Destructive action: coloured with the danger status. */
|
|
590
|
+
danger = input(false, { ...(ngDevMode ? { debugName: "danger" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
591
|
+
/** Emits when an enabled item is activated, before the menu closes. */
|
|
592
|
+
itemClick = output();
|
|
593
|
+
/** `FocusableOption`: the key manager skips disabled items. */
|
|
594
|
+
get disabled() {
|
|
595
|
+
return this.disabledInput();
|
|
596
|
+
}
|
|
597
|
+
focus() {
|
|
598
|
+
this.element.focus();
|
|
599
|
+
}
|
|
600
|
+
/** Typeahead matches the start of this. */
|
|
601
|
+
getLabel() {
|
|
602
|
+
return (this.element.querySelector('.nxe-menu-item-label')?.textContent ?? '').trim();
|
|
603
|
+
}
|
|
604
|
+
onClick(event) {
|
|
605
|
+
if (this.disabled) {
|
|
606
|
+
event.preventDefault();
|
|
607
|
+
event.stopImmediatePropagation();
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
this.itemClick.emit();
|
|
611
|
+
// A submenu trigger opens its menu; choosing a plain item closes the one it is in.
|
|
612
|
+
if (!this.trigger) {
|
|
613
|
+
this.menu?.closeMenu();
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
onKeydown(event) {
|
|
617
|
+
// Buttons and links already turn Enter into a click (and a button, Space); other hosts do not.
|
|
618
|
+
if (this.isNative || event.defaultPrevented || event.ctrlKey || event.metaKey || event.altKey) {
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
622
|
+
event.preventDefault();
|
|
623
|
+
this.element.click();
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeMenuItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
627
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: NxeMenuItemComponent, isStandalone: true, selector: "nxe-menu-item, [nxe-menu-item]", inputs: { disabledInput: { classPropertyName: "disabledInput", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, danger: { classPropertyName: "danger", publicName: "danger", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { itemClick: "itemClick" }, host: { attributes: { "role": "menuitem", "tabindex": "-1" }, listeners: { "click": "onClick($event)", "keydown": "onKeydown($event)", "mouseenter": "menu?._hovered(this)" }, properties: { "attr.aria-disabled": "disabled ? true : null", "attr.disabled": "disabled && isButton ? '' : null", "class.nxe-menu-item-danger": "danger()", "class.nxe-menu-item-disabled": "disabled" }, classAttribute: "nxe-menu-item" }, ngImport: i0, template: "<span class=\"nxe-menu-item-icon\"><ng-content select=\"[nxeMenuItemIcon]\" /></span>\r\n<span class=\"nxe-menu-item-label\"><ng-content /></span>\r\n@if (trigger) {\r\n <svg class=\"nxe-menu-item-chevron\" viewBox=\"0 0 16 16\" aria-hidden=\"true\" focusable=\"false\">\r\n <path d=\"M6 3.5 10.5 8 6 12.5\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\" />\r\n </svg>\r\n}\r\n", styles: [".nxe-menu-item{box-sizing:border-box;display:flex;flex:none;align-items:center;gap:var(--nxe-ui-space-2);width:100%;margin:0;padding:var(--nxe-ui-space-2) var(--nxe-ui-space-3);border:0;border-radius:0;background:transparent;color:var(--nxe-ui-text);font:inherit;text-align:left;text-decoration:none;cursor:pointer;-webkit-user-select:none;user-select:none}.nxe-menu-item:hover,.nxe-menu-item:focus{background:var(--nxe-ui-surface-alt);outline:none}.nxe-menu-item:focus-visible{box-shadow:inset 0 0 0 2px var(--nxe-ui-focus-ring-color)}.nxe-menu-item .nxe-menu-item-icon{display:inline-flex;flex:none;align-items:center;justify-content:center;width:var(--nxe-ui-space-4);height:var(--nxe-ui-space-4);color:var(--nxe-ui-neutral-600)}.nxe-menu-item .nxe-menu-item-icon:empty{display:none}.nxe-menu-item .nxe-menu-item-icon>svg,.nxe-menu-item .nxe-menu-item-icon>.nxe-icon{width:100%;height:100%;fill:currentColor}.nxe-menu-item .nxe-menu-item-label{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.nxe-menu-item .nxe-menu-item-chevron{flex:none;width:14px;height:14px;color:var(--nxe-ui-neutral-600)}.nxe-menu-item.nxe-menu-item-danger{color:var(--nxe-ui-status-danger-fg)}.nxe-menu-item.nxe-menu-item-danger .nxe-menu-item-icon{color:inherit}.nxe-menu-item.nxe-menu-item-disabled{color:var(--nxe-ui-text-disabled);cursor:default}.nxe-menu-item.nxe-menu-item-disabled:hover{background:transparent}.nxe-menu-item.nxe-menu-item-disabled .nxe-menu-item-icon,.nxe-menu-item.nxe-menu-item-disabled .nxe-menu-item-chevron{color:inherit}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
628
|
+
}
|
|
629
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeMenuItemComponent, decorators: [{
|
|
630
|
+
type: Component,
|
|
631
|
+
args: [{ selector: 'nxe-menu-item, [nxe-menu-item]', host: {
|
|
632
|
+
class: 'nxe-menu-item',
|
|
633
|
+
role: 'menuitem',
|
|
634
|
+
tabindex: '-1',
|
|
635
|
+
'[attr.aria-disabled]': 'disabled ? true : null',
|
|
636
|
+
'[attr.disabled]': "disabled && isButton ? '' : null",
|
|
637
|
+
'[class.nxe-menu-item-danger]': 'danger()',
|
|
638
|
+
'[class.nxe-menu-item-disabled]': 'disabled',
|
|
639
|
+
'(click)': 'onClick($event)',
|
|
640
|
+
'(keydown)': 'onKeydown($event)',
|
|
641
|
+
'(mouseenter)': 'menu?._hovered(this)',
|
|
642
|
+
}, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: "<span class=\"nxe-menu-item-icon\"><ng-content select=\"[nxeMenuItemIcon]\" /></span>\r\n<span class=\"nxe-menu-item-label\"><ng-content /></span>\r\n@if (trigger) {\r\n <svg class=\"nxe-menu-item-chevron\" viewBox=\"0 0 16 16\" aria-hidden=\"true\" focusable=\"false\">\r\n <path d=\"M6 3.5 10.5 8 6 12.5\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\" />\r\n </svg>\r\n}\r\n", styles: [".nxe-menu-item{box-sizing:border-box;display:flex;flex:none;align-items:center;gap:var(--nxe-ui-space-2);width:100%;margin:0;padding:var(--nxe-ui-space-2) var(--nxe-ui-space-3);border:0;border-radius:0;background:transparent;color:var(--nxe-ui-text);font:inherit;text-align:left;text-decoration:none;cursor:pointer;-webkit-user-select:none;user-select:none}.nxe-menu-item:hover,.nxe-menu-item:focus{background:var(--nxe-ui-surface-alt);outline:none}.nxe-menu-item:focus-visible{box-shadow:inset 0 0 0 2px var(--nxe-ui-focus-ring-color)}.nxe-menu-item .nxe-menu-item-icon{display:inline-flex;flex:none;align-items:center;justify-content:center;width:var(--nxe-ui-space-4);height:var(--nxe-ui-space-4);color:var(--nxe-ui-neutral-600)}.nxe-menu-item .nxe-menu-item-icon:empty{display:none}.nxe-menu-item .nxe-menu-item-icon>svg,.nxe-menu-item .nxe-menu-item-icon>.nxe-icon{width:100%;height:100%;fill:currentColor}.nxe-menu-item .nxe-menu-item-label{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.nxe-menu-item .nxe-menu-item-chevron{flex:none;width:14px;height:14px;color:var(--nxe-ui-neutral-600)}.nxe-menu-item.nxe-menu-item-danger{color:var(--nxe-ui-status-danger-fg)}.nxe-menu-item.nxe-menu-item-danger .nxe-menu-item-icon{color:inherit}.nxe-menu-item.nxe-menu-item-disabled{color:var(--nxe-ui-text-disabled);cursor:default}.nxe-menu-item.nxe-menu-item-disabled:hover{background:transparent}.nxe-menu-item.nxe-menu-item-disabled .nxe-menu-item-icon,.nxe-menu-item.nxe-menu-item-disabled .nxe-menu-item-chevron{color:inherit}\n"] }]
|
|
643
|
+
}], propDecorators: { disabledInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], danger: [{ type: i0.Input, args: [{ isSignal: true, alias: "danger", required: false }] }], itemClick: [{ type: i0.Output, args: ["itemClick"] }] } });
|
|
644
|
+
|
|
645
|
+
let menuSequence = 0;
|
|
646
|
+
/**
|
|
647
|
+
* `nxe-menu` is the list of actions a trigger opens. It draws nothing where it is declared; the
|
|
648
|
+
* panel appears in a CDK overlay when a `[nxeMenuTriggerFor]` element opens it.
|
|
649
|
+
*
|
|
650
|
+
* ```html
|
|
651
|
+
* <nxe-button icon="more" aria-label="Aksi baris" [nxeMenuTriggerFor]="rowMenu" />
|
|
652
|
+
*
|
|
653
|
+
* <nxe-menu #rowMenu>
|
|
654
|
+
* <button type="button" nxe-menu-item (click)="edit(row)">Ubah</button>
|
|
655
|
+
* <button type="button" nxe-menu-item [disabled]="!row.canCopy" (click)="copy(row)">Salin</button>
|
|
656
|
+
* <nxe-menu-divider />
|
|
657
|
+
* <button type="button" nxe-menu-item danger (click)="remove(row)">Hapus</button>
|
|
658
|
+
* </nxe-menu>
|
|
659
|
+
* ```
|
|
660
|
+
*
|
|
661
|
+
* It follows the ARIA menu pattern. The panel is `role="menu"` and each item `role="menuitem"`;
|
|
662
|
+
* ArrowUp, ArrowDown, Home, End and typing a letter move between items (skipping disabled ones),
|
|
663
|
+
* Enter and Space choose, Esc closes and returns focus to the trigger, and Tab closes and carries on
|
|
664
|
+
* to the next control. An item inside a menu that has its own `[nxeMenuTriggerFor]` opens a submenu
|
|
665
|
+
* with ArrowRight (ArrowLeft or Esc closes it).
|
|
666
|
+
*
|
|
667
|
+
* The menu provides `NXE_MENU`, so `nxe-list` rows and `nxe-menu-item`s close it when chosen.
|
|
668
|
+
*/
|
|
669
|
+
class NxeMenuComponent {
|
|
670
|
+
injector = inject(Injector);
|
|
671
|
+
/** Accessible name. Defaults to the trigger's. */
|
|
672
|
+
ariaLabel = input(undefined, { ...(ngDevMode ? { debugName: "ariaLabel" } : /* istanbul ignore next */ {}), alias: 'aria-label' });
|
|
673
|
+
/** Extra class(es) for the overlay pane. */
|
|
674
|
+
panelClass = input('', /* @ts-ignore */
|
|
675
|
+
...(ngDevMode ? [{ debugName: "panelClass" }] : /* istanbul ignore next */ []));
|
|
676
|
+
/** @internal Id of the panel; the trigger's `aria-controls`. */
|
|
677
|
+
panelId = `nxe-menu-${++menuSequence}`;
|
|
678
|
+
/** @internal */
|
|
679
|
+
template = viewChild.required(TemplateRef, /* @ts-ignore */
|
|
680
|
+
...(ngDevMode ? [{ debugName: "template" }] : /* istanbul ignore next */ []));
|
|
681
|
+
descendantItems = contentChildren(NxeMenuItemComponent, { ...(ngDevMode ? { debugName: "descendantItems" } : /* istanbul ignore next */ {}), descendants: true });
|
|
682
|
+
/** Only this menu's own items: a submenu declared inside this one brings its own. */
|
|
683
|
+
items = computed(() => this.descendantItems().filter((item) => item.menu === this), /* @ts-ignore */
|
|
684
|
+
...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
|
|
685
|
+
/** @internal The trigger currently showing this menu. */
|
|
686
|
+
trigger = null;
|
|
687
|
+
keyManager = null;
|
|
688
|
+
/** @internal `aria-labelledby` of the panel: the trigger, unless an explicit label is given. */
|
|
689
|
+
labelledBy() {
|
|
690
|
+
return this.ariaLabel() ? null : (this.trigger?.id ?? null);
|
|
691
|
+
}
|
|
692
|
+
/** Closes the menu (and any menu it was opened from). Called by items and `nxe-list` when chosen. */
|
|
693
|
+
closeMenu() {
|
|
694
|
+
const trigger = this.trigger;
|
|
695
|
+
if (!trigger) {
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
trigger.close(true);
|
|
699
|
+
trigger.parentMenu?.closeMenu();
|
|
700
|
+
}
|
|
701
|
+
/** @internal Called by the trigger once the panel is attached. */
|
|
702
|
+
_opened(focus) {
|
|
703
|
+
this.keyManager = new FocusKeyManager(this.items, this.injector)
|
|
704
|
+
.withVerticalOrientation()
|
|
705
|
+
.withHomeAndEnd()
|
|
706
|
+
.withWrap()
|
|
707
|
+
.withTypeAhead()
|
|
708
|
+
.skipPredicate((item) => item.disabled);
|
|
709
|
+
if (focus !== 'none') {
|
|
710
|
+
afterNextRender(() => (focus === 'first' ? this.keyManager?.setFirstItemActive() : this.keyManager?.setLastItemActive()), { injector: this.injector });
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
/** @internal Called by the trigger when the panel is detached. */
|
|
714
|
+
_closed() {
|
|
715
|
+
this.closeSubmenus();
|
|
716
|
+
this.keyManager = null;
|
|
717
|
+
}
|
|
718
|
+
/** @internal Moves the active item under the pointer, and closes a sibling's submenu. */
|
|
719
|
+
_hovered(item) {
|
|
720
|
+
if (item.disabled) {
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
for (const other of this.items()) {
|
|
724
|
+
if (other !== item && other.trigger?.isOpen()) {
|
|
725
|
+
other.trigger.close(false);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
this.keyManager?.setActiveItem(item);
|
|
729
|
+
}
|
|
730
|
+
closeSubmenus() {
|
|
731
|
+
for (const item of this.items()) {
|
|
732
|
+
if (item.trigger?.isOpen()) {
|
|
733
|
+
item.trigger.close(false);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
/** @internal */
|
|
738
|
+
onKeydown(event) {
|
|
739
|
+
const manager = this.keyManager;
|
|
740
|
+
const trigger = this.trigger;
|
|
741
|
+
if (!manager || !trigger || event.defaultPrevented || event.ctrlKey || event.metaKey || event.altKey) {
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
switch (event.key) {
|
|
745
|
+
case 'Escape':
|
|
746
|
+
// Handled here, not by the overlay dispatcher, so only this level closes.
|
|
747
|
+
event.preventDefault();
|
|
748
|
+
event.stopPropagation();
|
|
749
|
+
trigger.close(true);
|
|
750
|
+
return;
|
|
751
|
+
case 'Tab':
|
|
752
|
+
// No preventDefault: focus is back on the trigger, so Tab moves on from it.
|
|
753
|
+
this.closeChain();
|
|
754
|
+
return;
|
|
755
|
+
case 'ArrowRight':
|
|
756
|
+
if (manager.activeItem?.trigger) {
|
|
757
|
+
event.preventDefault();
|
|
758
|
+
manager.activeItem.trigger.open('first');
|
|
759
|
+
}
|
|
760
|
+
return;
|
|
761
|
+
case 'ArrowLeft':
|
|
762
|
+
if (trigger.parentMenu) {
|
|
763
|
+
event.preventDefault();
|
|
764
|
+
trigger.close(true);
|
|
765
|
+
}
|
|
766
|
+
return;
|
|
767
|
+
case 'Enter':
|
|
768
|
+
case ' ':
|
|
769
|
+
// The item handles it; typeahead must not eat the space as a character.
|
|
770
|
+
if (manager.activeItem) {
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
manager.onKeydown(event);
|
|
775
|
+
}
|
|
776
|
+
closeChain() {
|
|
777
|
+
const trigger = this.trigger;
|
|
778
|
+
trigger?.close(true);
|
|
779
|
+
trigger?.parentMenu?.closeMenu();
|
|
780
|
+
}
|
|
781
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeMenuComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
782
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "22.1.5", type: NxeMenuComponent, isStandalone: true, selector: "nxe-menu", inputs: { ariaLabel: { classPropertyName: "ariaLabel", publicName: "aria-label", isSignal: true, isRequired: false, transformFunction: null }, panelClass: { classPropertyName: "panelClass", publicName: "panelClass", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "nxe-menu-host" }, providers: [{ provide: NXE_MENU, useExisting: NxeMenuComponent }], queries: [{ propertyName: "descendantItems", predicate: NxeMenuItemComponent, descendants: true, isSignal: true }], viewQueries: [{ propertyName: "template", first: true, predicate: TemplateRef, descendants: true, isSignal: true }], exportAs: ["nxeMenu"], ngImport: i0, template: "<ng-template>\r\n <div\r\n class=\"nxe-menu\"\r\n role=\"menu\"\r\n aria-orientation=\"vertical\"\r\n tabindex=\"-1\"\r\n [id]=\"panelId\"\r\n [attr.aria-label]=\"ariaLabel() || null\"\r\n [attr.aria-labelledby]=\"labelledBy()\"\r\n (keydown)=\"onKeydown($event)\">\r\n <ng-content />\r\n </div>\r\n</ng-template>\r\n", styles: ["nxe-menu.nxe-menu-host{display:none}.cdk-overlay-pane.nxe-menu-pane{margin:0}.nxe-menu{box-sizing:border-box;display:flex;flex-direction:column;min-width:180px;max-width:min(320px,100vw - var(--nxe-ui-space-4));max-height:min(480px,100vh - var(--nxe-ui-space-4) * 2);overflow-y:auto;padding:var(--nxe-ui-space-1) 0;background:var(--nxe-ui-surface);color:var(--nxe-ui-text);border:1px solid var(--nxe-ui-border);border-radius:var(--nxe-ui-popup-radius);box-shadow:var(--nxe-ui-popup-shadow);font-size:var(--nxe-ui-font-size-md);line-height:var(--nxe-ui-line-height-md);outline:none}.nxe-menu .nxe-list{color:inherit}.nxe-menu-divider{display:block;flex:none;height:0;margin:var(--nxe-ui-space-1) 0;border:0;border-top:1px solid var(--nxe-ui-border)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
783
|
+
}
|
|
784
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeMenuComponent, decorators: [{
|
|
785
|
+
type: Component,
|
|
786
|
+
args: [{ selector: 'nxe-menu', exportAs: 'nxeMenu', providers: [{ provide: NXE_MENU, useExisting: NxeMenuComponent }], host: { class: 'nxe-menu-host' }, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: "<ng-template>\r\n <div\r\n class=\"nxe-menu\"\r\n role=\"menu\"\r\n aria-orientation=\"vertical\"\r\n tabindex=\"-1\"\r\n [id]=\"panelId\"\r\n [attr.aria-label]=\"ariaLabel() || null\"\r\n [attr.aria-labelledby]=\"labelledBy()\"\r\n (keydown)=\"onKeydown($event)\">\r\n <ng-content />\r\n </div>\r\n</ng-template>\r\n", styles: ["nxe-menu.nxe-menu-host{display:none}.cdk-overlay-pane.nxe-menu-pane{margin:0}.nxe-menu{box-sizing:border-box;display:flex;flex-direction:column;min-width:180px;max-width:min(320px,100vw - var(--nxe-ui-space-4));max-height:min(480px,100vh - var(--nxe-ui-space-4) * 2);overflow-y:auto;padding:var(--nxe-ui-space-1) 0;background:var(--nxe-ui-surface);color:var(--nxe-ui-text);border:1px solid var(--nxe-ui-border);border-radius:var(--nxe-ui-popup-radius);box-shadow:var(--nxe-ui-popup-shadow);font-size:var(--nxe-ui-font-size-md);line-height:var(--nxe-ui-line-height-md);outline:none}.nxe-menu .nxe-list{color:inherit}.nxe-menu-divider{display:block;flex:none;height:0;margin:var(--nxe-ui-space-1) 0;border:0;border-top:1px solid var(--nxe-ui-border)}\n"] }]
|
|
787
|
+
}], propDecorators: { ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-label", required: false }] }], panelClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "panelClass", required: false }] }], template: [{ type: i0.ViewChild, args: [i0.forwardRef(() => TemplateRef), { isSignal: true }] }], descendantItems: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => NxeMenuItemComponent), { ...{ descendants: true }, isSignal: true }] }] } });
|
|
788
|
+
|
|
789
|
+
/** A rule between groups of items in an `nxe-menu`. */
|
|
790
|
+
class NxeMenuDividerComponent {
|
|
791
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeMenuDividerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
792
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.1.5", type: NxeMenuDividerComponent, isStandalone: true, selector: "nxe-menu-divider", host: { attributes: { "role": "separator" }, classAttribute: "nxe-menu-divider" }, ngImport: i0, template: '', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
793
|
+
}
|
|
794
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeMenuDividerComponent, decorators: [{
|
|
795
|
+
type: Component,
|
|
796
|
+
args: [{
|
|
797
|
+
selector: 'nxe-menu-divider',
|
|
798
|
+
template: '',
|
|
799
|
+
host: { class: 'nxe-menu-divider', role: 'separator' },
|
|
800
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
801
|
+
encapsulation: ViewEncapsulation.None,
|
|
802
|
+
}]
|
|
803
|
+
}] });
|
|
804
|
+
|
|
805
|
+
/** Controls a Tab can land on, as a selector. Callers still filter out the hidden ones. @internal */
|
|
806
|
+
const NXE_TABBABLE = 'a[href], button:not([disabled]), input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"]), [contenteditable="true"]';
|
|
807
|
+
|
|
808
|
+
let popoverSequence = 0;
|
|
809
|
+
/**
|
|
810
|
+
* `nxe-popover` is a small panel of content a trigger opens: a filter form, a help text, a few
|
|
811
|
+
* details. It draws nothing where it is declared; the panel appears in a CDK overlay when a
|
|
812
|
+
* `[nxePopoverTriggerFor]` element opens it.
|
|
813
|
+
*
|
|
814
|
+
* ```html
|
|
815
|
+
* <nxe-button icon="filter" text="Filter" [nxePopoverTriggerFor]="filters" />
|
|
816
|
+
*
|
|
817
|
+
* <nxe-popover #filters heading="Filter PO" closable>
|
|
818
|
+
* <nxe-form-field label="Pemasok"><nxe-text-box /></nxe-form-field>
|
|
819
|
+
* <nxe-button text="Terapkan" (onClick)="apply(); filters.close()" />
|
|
820
|
+
* </nxe-popover>
|
|
821
|
+
* ```
|
|
822
|
+
*
|
|
823
|
+
* It is a non-modal dialog: `role="dialog"` named by its heading (else `aria-label`, else the
|
|
824
|
+
* trigger), and the page behind it stays usable. Click on the trigger toggles it. Focus moves to the
|
|
825
|
+
* first focusable control in the content when there is one (else the close button, else it stays on
|
|
826
|
+
* the trigger). Esc closes and returns focus to the trigger; so does Tab off either end. Clicking
|
|
827
|
+
* outside closes it and leaves focus where the click went.
|
|
828
|
+
*/
|
|
829
|
+
class NxePopoverComponent {
|
|
830
|
+
messages = inject(NxeMessageService);
|
|
831
|
+
/** A title for the panel; it names the dialog. */
|
|
832
|
+
heading = input(undefined, /* @ts-ignore */
|
|
833
|
+
...(ngDevMode ? [{ debugName: "heading" }] : /* istanbul ignore next */ []));
|
|
834
|
+
/** Draws a close button in the header. */
|
|
835
|
+
closable = input(false, { ...(ngDevMode ? { debugName: "closable" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
836
|
+
/** Accessible name when there is no `heading`. Without either, the trigger names the panel. */
|
|
837
|
+
ariaLabel = input(undefined, { ...(ngDevMode ? { debugName: "ariaLabel" } : /* istanbul ignore next */ {}), alias: 'aria-label' });
|
|
838
|
+
/** Preferred side of the trigger. */
|
|
839
|
+
position = input('bottom', /* @ts-ignore */
|
|
840
|
+
...(ngDevMode ? [{ debugName: "position" }] : /* istanbul ignore next */ []));
|
|
841
|
+
/** Extra class(es) for the overlay pane. */
|
|
842
|
+
panelClass = input('', /* @ts-ignore */
|
|
843
|
+
...(ngDevMode ? [{ debugName: "panelClass" }] : /* istanbul ignore next */ []));
|
|
844
|
+
/** Emits when the panel opens. */
|
|
845
|
+
opened = output();
|
|
846
|
+
/** Emits when the panel closes, however it was closed. */
|
|
847
|
+
closed = output();
|
|
848
|
+
sequence = ++popoverSequence;
|
|
849
|
+
/** @internal Id of the panel; the trigger's `aria-controls`. */
|
|
850
|
+
panelId = `nxe-popover-${this.sequence}`;
|
|
851
|
+
/** @internal */
|
|
852
|
+
headingId = `nxe-popover-${this.sequence}-heading`;
|
|
853
|
+
/** @internal */
|
|
854
|
+
template = viewChild.required(TemplateRef, /* @ts-ignore */
|
|
855
|
+
...(ngDevMode ? [{ debugName: "template" }] : /* istanbul ignore next */ []));
|
|
856
|
+
/** @internal The trigger currently showing this popover. */
|
|
857
|
+
trigger = null;
|
|
858
|
+
/** Whether the panel is showing. */
|
|
859
|
+
isOpen() {
|
|
860
|
+
return this.trigger?.isOpen() ?? false;
|
|
861
|
+
}
|
|
862
|
+
/** Closes the panel and returns focus to its trigger. No-op while closed. */
|
|
863
|
+
close() {
|
|
864
|
+
this.trigger?.close(true);
|
|
865
|
+
}
|
|
866
|
+
/** @internal `aria-labelledby` of the panel: the heading, else the trigger unless an explicit label is given. */
|
|
867
|
+
labelledBy() {
|
|
868
|
+
if (this.heading()) {
|
|
869
|
+
return this.headingId;
|
|
870
|
+
}
|
|
871
|
+
return this.ariaLabel() ? null : (this.trigger?.id ?? null);
|
|
872
|
+
}
|
|
873
|
+
/** @internal Tab off either end closes the panel, as a menu does, so focus is never stranded in an overlay at the end of the page. */
|
|
874
|
+
onKeydown(event) {
|
|
875
|
+
if (event.key !== 'Tab' || event.defaultPrevented || event.ctrlKey || event.metaKey || event.altKey) {
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
const panel = event.currentTarget;
|
|
879
|
+
const tabbable = Array.from(panel.querySelectorAll(NXE_TABBABLE)).filter((el) => el.tabIndex >= 0 && !el.hasAttribute('hidden') && el.getClientRects().length > 0);
|
|
880
|
+
const active = document.activeElement;
|
|
881
|
+
const first = tabbable[0];
|
|
882
|
+
const last = tabbable[tabbable.length - 1];
|
|
883
|
+
if (event.shiftKey) {
|
|
884
|
+
if (!first || active === first || active === panel) {
|
|
885
|
+
event.preventDefault();
|
|
886
|
+
this.trigger?.close(true);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
else if (!last || active === last) {
|
|
890
|
+
// No preventDefault: focus lands back on the trigger, so Tab moves on from it.
|
|
891
|
+
this.trigger?.close(true);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxePopoverComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
895
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: NxePopoverComponent, isStandalone: true, selector: "nxe-popover", inputs: { heading: { classPropertyName: "heading", publicName: "heading", isSignal: true, isRequired: false, transformFunction: null }, closable: { classPropertyName: "closable", publicName: "closable", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "aria-label", isSignal: true, isRequired: false, transformFunction: null }, position: { classPropertyName: "position", publicName: "position", isSignal: true, isRequired: false, transformFunction: null }, panelClass: { classPropertyName: "panelClass", publicName: "panelClass", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { opened: "opened", closed: "closed" }, host: { classAttribute: "nxe-popover-host" }, viewQueries: [{ propertyName: "template", first: true, predicate: TemplateRef, descendants: true, isSignal: true }], exportAs: ["nxePopover"], ngImport: i0, template: "<ng-template>\r\n <div\r\n class=\"nxe-popover\"\r\n role=\"dialog\"\r\n tabindex=\"-1\"\r\n [id]=\"panelId\"\r\n [attr.aria-label]=\"heading() ? null : ariaLabel() || null\"\r\n [attr.aria-labelledby]=\"labelledBy()\"\r\n (keydown)=\"onKeydown($event)\">\r\n @if (heading() || closable()) {\r\n <div class=\"nxe-popover-header\">\r\n @if (heading()) {\r\n <div class=\"nxe-popover-heading\" [id]=\"headingId\">{{ heading() }}</div>\r\n }\r\n @if (closable()) {\r\n <button\r\n type=\"button\"\r\n class=\"nxe-popover-close\"\r\n [attr.aria-label]=\"messages.t('common.close')\"\r\n (click)=\"close()\">\r\n <svg viewBox=\"0 0 16 16\" focusable=\"false\" aria-hidden=\"true\">\r\n <path d=\"M3.2 2 8 6.8 12.8 2 14 3.2 9.2 8l4.8 4.8-1.2 1.2L8 9.2 3.2 14 2 12.8 6.8 8 2 3.2z\" />\r\n </svg>\r\n </button>\r\n }\r\n </div>\r\n }\r\n <div class=\"nxe-popover-body\">\r\n <ng-content />\r\n </div>\r\n </div>\r\n</ng-template>\r\n", styles: ["nxe-popover.nxe-popover-host{display:none}.cdk-overlay-pane.nxe-popover-pane{box-sizing:border-box;padding:var(--nxe-ui-space-1)}.nxe-popover{box-sizing:border-box;min-width:200px;max-width:min(420px,100vw - var(--nxe-ui-space-4));max-height:min(520px,100vh - var(--nxe-ui-space-4) * 2);overflow:auto;background:var(--nxe-ui-surface);color:var(--nxe-ui-text);border:1px solid var(--nxe-ui-border);border-radius:var(--nxe-ui-popup-radius);box-shadow:var(--nxe-ui-popup-shadow);font-size:var(--nxe-ui-font-size-md);line-height:var(--nxe-ui-line-height-md);outline:none}.nxe-popover-header{display:flex;align-items:center;gap:var(--nxe-ui-space-2);padding:var(--nxe-ui-space-2) var(--nxe-ui-space-2) 0 var(--nxe-ui-space-3)}.nxe-popover-heading{flex:1;min-width:0;font-weight:600}.nxe-popover-close{display:inline-flex;flex:none;align-items:center;justify-content:center;width:28px;height:28px;margin-inline-start:auto;padding:0;border:0;border-radius:var(--nxe-ui-radius-sm);background:transparent;color:inherit;cursor:pointer}.nxe-popover-close svg{width:14px;height:14px;fill:currentColor}.nxe-popover-close:hover{background:var(--nxe-ui-hover-bg)}.nxe-popover-close:focus-visible{outline:2px solid var(--nxe-ui-accent);outline-offset:-2px}.nxe-popover-body{padding:var(--nxe-ui-space-3)}.nxe-popover-header+.nxe-popover-body{padding-top:var(--nxe-ui-space-2)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
896
|
+
}
|
|
897
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxePopoverComponent, decorators: [{
|
|
898
|
+
type: Component,
|
|
899
|
+
args: [{ selector: 'nxe-popover', exportAs: 'nxePopover', host: { class: 'nxe-popover-host' }, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: "<ng-template>\r\n <div\r\n class=\"nxe-popover\"\r\n role=\"dialog\"\r\n tabindex=\"-1\"\r\n [id]=\"panelId\"\r\n [attr.aria-label]=\"heading() ? null : ariaLabel() || null\"\r\n [attr.aria-labelledby]=\"labelledBy()\"\r\n (keydown)=\"onKeydown($event)\">\r\n @if (heading() || closable()) {\r\n <div class=\"nxe-popover-header\">\r\n @if (heading()) {\r\n <div class=\"nxe-popover-heading\" [id]=\"headingId\">{{ heading() }}</div>\r\n }\r\n @if (closable()) {\r\n <button\r\n type=\"button\"\r\n class=\"nxe-popover-close\"\r\n [attr.aria-label]=\"messages.t('common.close')\"\r\n (click)=\"close()\">\r\n <svg viewBox=\"0 0 16 16\" focusable=\"false\" aria-hidden=\"true\">\r\n <path d=\"M3.2 2 8 6.8 12.8 2 14 3.2 9.2 8l4.8 4.8-1.2 1.2L8 9.2 3.2 14 2 12.8 6.8 8 2 3.2z\" />\r\n </svg>\r\n </button>\r\n }\r\n </div>\r\n }\r\n <div class=\"nxe-popover-body\">\r\n <ng-content />\r\n </div>\r\n </div>\r\n</ng-template>\r\n", styles: ["nxe-popover.nxe-popover-host{display:none}.cdk-overlay-pane.nxe-popover-pane{box-sizing:border-box;padding:var(--nxe-ui-space-1)}.nxe-popover{box-sizing:border-box;min-width:200px;max-width:min(420px,100vw - var(--nxe-ui-space-4));max-height:min(520px,100vh - var(--nxe-ui-space-4) * 2);overflow:auto;background:var(--nxe-ui-surface);color:var(--nxe-ui-text);border:1px solid var(--nxe-ui-border);border-radius:var(--nxe-ui-popup-radius);box-shadow:var(--nxe-ui-popup-shadow);font-size:var(--nxe-ui-font-size-md);line-height:var(--nxe-ui-line-height-md);outline:none}.nxe-popover-header{display:flex;align-items:center;gap:var(--nxe-ui-space-2);padding:var(--nxe-ui-space-2) var(--nxe-ui-space-2) 0 var(--nxe-ui-space-3)}.nxe-popover-heading{flex:1;min-width:0;font-weight:600}.nxe-popover-close{display:inline-flex;flex:none;align-items:center;justify-content:center;width:28px;height:28px;margin-inline-start:auto;padding:0;border:0;border-radius:var(--nxe-ui-radius-sm);background:transparent;color:inherit;cursor:pointer}.nxe-popover-close svg{width:14px;height:14px;fill:currentColor}.nxe-popover-close:hover{background:var(--nxe-ui-hover-bg)}.nxe-popover-close:focus-visible{outline:2px solid var(--nxe-ui-accent);outline-offset:-2px}.nxe-popover-body{padding:var(--nxe-ui-space-3)}.nxe-popover-header+.nxe-popover-body{padding-top:var(--nxe-ui-space-2)}\n"] }]
|
|
900
|
+
}], propDecorators: { heading: [{ type: i0.Input, args: [{ isSignal: true, alias: "heading", required: false }] }], closable: [{ type: i0.Input, args: [{ isSignal: true, alias: "closable", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-label", required: false }] }], position: [{ type: i0.Input, args: [{ isSignal: true, alias: "position", required: false }] }], panelClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "panelClass", required: false }] }], opened: [{ type: i0.Output, args: ["opened"] }], closed: [{ type: i0.Output, args: ["closed"] }], template: [{ type: i0.ViewChild, args: [i0.forwardRef(() => TemplateRef), { isSignal: true }] }] } });
|
|
901
|
+
|
|
902
|
+
const OPPOSITE = { top: 'bottom', bottom: 'top', start: 'end', end: 'start' };
|
|
903
|
+
function side(placement, align) {
|
|
904
|
+
switch (placement) {
|
|
905
|
+
case 'top':
|
|
906
|
+
return align === 'center'
|
|
907
|
+
? { originX: 'center', originY: 'top', overlayX: 'center', overlayY: 'bottom' }
|
|
908
|
+
: { originX: 'start', originY: 'top', overlayX: 'start', overlayY: 'bottom' };
|
|
909
|
+
case 'bottom':
|
|
910
|
+
return align === 'center'
|
|
911
|
+
? { originX: 'center', originY: 'bottom', overlayX: 'center', overlayY: 'top' }
|
|
912
|
+
: { originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top' };
|
|
913
|
+
case 'start':
|
|
914
|
+
return align === 'center'
|
|
915
|
+
? { originX: 'start', originY: 'center', overlayX: 'end', overlayY: 'center' }
|
|
916
|
+
: { originX: 'start', originY: 'top', overlayX: 'end', overlayY: 'top' };
|
|
917
|
+
case 'end':
|
|
918
|
+
return align === 'center'
|
|
919
|
+
? { originX: 'end', originY: 'center', overlayX: 'start', overlayY: 'center' }
|
|
920
|
+
: { originX: 'end', originY: 'top', overlayX: 'start', overlayY: 'top' };
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
/**
|
|
924
|
+
* Connected positions for a placement: the preferred side, then the opposite one (the flip), then
|
|
925
|
+
* the two sides across. No offsets: the gap to the trigger is padding on the overlay pane, which
|
|
926
|
+
* keeps it right for either writing direction and keeps the pointer over the pane while crossing it.
|
|
927
|
+
*
|
|
928
|
+
* `center` centres the overlay on the trigger (a tooltip); `start` aligns the edges (a popover).
|
|
929
|
+
* @internal
|
|
930
|
+
*/
|
|
931
|
+
function nxePlacementPositions(placement, align) {
|
|
932
|
+
const across = placement === 'top' || placement === 'bottom' ? ['end', 'start'] : ['bottom', 'top'];
|
|
933
|
+
return [placement, OPPOSITE[placement], ...across].map((p) => side(p, align));
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
let triggerSequence = 0;
|
|
937
|
+
/** Hosts where Enter and Space already produce a click. */
|
|
938
|
+
const NATIVELY_ACTIVATABLE = new Set(['BUTTON', 'A', 'INPUT', 'SUMMARY']);
|
|
939
|
+
/**
|
|
940
|
+
* Opens an `nxe-popover` from the element it sits on.
|
|
941
|
+
*
|
|
942
|
+
* ```html
|
|
943
|
+
* <nxe-button icon="filter" text="Filter" [nxePopoverTriggerFor]="filters" />
|
|
944
|
+
* <nxe-popover #filters heading="Filter PO">…</nxe-popover>
|
|
945
|
+
* ```
|
|
946
|
+
*
|
|
947
|
+
* The element gets `aria-haspopup="dialog"`, `aria-expanded` and `aria-controls`. Click toggles;
|
|
948
|
+
* Enter and Space do from hosts that are not buttons (`nxe-button`). The panel opens on the
|
|
949
|
+
* popover's `position` (below by default) and flips when there is no room. Opening moves focus to
|
|
950
|
+
* the first focusable control in the content, else the close button; Esc closes and returns focus
|
|
951
|
+
* to the element; a click outside the panel and the element closes it and leaves focus where the
|
|
952
|
+
* click went.
|
|
953
|
+
*/
|
|
954
|
+
class NxePopoverTriggerForDirective {
|
|
955
|
+
overlay = inject(Overlay);
|
|
956
|
+
viewContainer = inject(ViewContainerRef);
|
|
957
|
+
injector = inject(Injector);
|
|
958
|
+
element = inject(ElementRef).nativeElement;
|
|
959
|
+
/** The popover to open. */
|
|
960
|
+
popover = input.required({ ...(ngDevMode ? { debugName: "popover" } : /* istanbul ignore next */ {}), alias: 'nxePopoverTriggerFor' });
|
|
961
|
+
/** @internal Id of the trigger; the panel is labelled by it when it has no heading or label. */
|
|
962
|
+
id = this.element.id || `nxe-popover-trigger-${++triggerSequence}`;
|
|
963
|
+
openState = signal(false, /* @ts-ignore */
|
|
964
|
+
...(ngDevMode ? [{ debugName: "openState" }] : /* istanbul ignore next */ []));
|
|
965
|
+
/** Whether the popover is showing. */
|
|
966
|
+
isOpen = this.openState.asReadonly();
|
|
967
|
+
overlayRef = null;
|
|
968
|
+
/** Opens the popover and moves focus into it. No-op while open. */
|
|
969
|
+
open() {
|
|
970
|
+
if (this.openState()) {
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
const popover = this.popover();
|
|
974
|
+
this.overlayRef?.dispose();
|
|
975
|
+
this.overlayRef = this.createOverlay(popover);
|
|
976
|
+
popover.trigger = this;
|
|
977
|
+
this.overlayRef.attach(new TemplatePortal(popover.template(), this.viewContainer));
|
|
978
|
+
this.openState.set(true);
|
|
979
|
+
popover.opened.emit();
|
|
980
|
+
afterNextRender(() => this.focusInto(), { injector: this.injector });
|
|
981
|
+
}
|
|
982
|
+
/** Closes the popover. `restoreFocus` returns focus to the element (Esc, Tab off an end, the close button). */
|
|
983
|
+
close(restoreFocus = false) {
|
|
984
|
+
if (!this.openState()) {
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
const popover = this.popover();
|
|
988
|
+
this.openState.set(false);
|
|
989
|
+
this.overlayRef?.detach();
|
|
990
|
+
if (popover.trigger === this) {
|
|
991
|
+
popover.trigger = null;
|
|
992
|
+
}
|
|
993
|
+
popover.closed.emit();
|
|
994
|
+
if (restoreFocus) {
|
|
995
|
+
this.element.focus();
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
toggle() {
|
|
999
|
+
if (this.openState()) {
|
|
1000
|
+
this.close(true);
|
|
1001
|
+
}
|
|
1002
|
+
else {
|
|
1003
|
+
this.open();
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
ngOnDestroy() {
|
|
1007
|
+
if (this.openState()) {
|
|
1008
|
+
const popover = this.popover();
|
|
1009
|
+
if (popover.trigger === this) {
|
|
1010
|
+
popover.trigger = null;
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
this.overlayRef?.dispose();
|
|
1014
|
+
this.overlayRef = null;
|
|
1015
|
+
}
|
|
1016
|
+
onClick(event) {
|
|
1017
|
+
if (!event.defaultPrevented) {
|
|
1018
|
+
this.toggle();
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
onKeydown(event) {
|
|
1022
|
+
if (event.ctrlKey || event.metaKey || event.altKey) {
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
// nxe-button is a div: it reports Enter and Space through its onClick output, not a DOM click.
|
|
1026
|
+
if ((event.key === 'Enter' || event.key === ' ') && !NATIVELY_ACTIVATABLE.has(this.element.tagName)) {
|
|
1027
|
+
event.preventDefault();
|
|
1028
|
+
this.toggle();
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
/** The first control in the content, else the close button; nothing when the panel has neither. */
|
|
1032
|
+
focusInto() {
|
|
1033
|
+
const panel = this.overlayRef?.overlayElement.querySelector('.nxe-popover');
|
|
1034
|
+
if (!panel) {
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
const visible = (el) => el.tabIndex >= 0 && el.getClientRects().length > 0;
|
|
1038
|
+
const target = Array.from(panel.querySelectorAll('.nxe-popover-body')).flatMap((body) => Array.from(body.querySelectorAll(NXE_TABBABLE))).find(visible) ?? panel.querySelector('.nxe-popover-close');
|
|
1039
|
+
target?.focus();
|
|
1040
|
+
}
|
|
1041
|
+
createOverlay(popover) {
|
|
1042
|
+
const ref = this.overlay.create({
|
|
1043
|
+
positionStrategy: this.overlay
|
|
1044
|
+
.position()
|
|
1045
|
+
.flexibleConnectedTo(this.element)
|
|
1046
|
+
.withPositions(nxePlacementPositions(popover.position(), 'start'))
|
|
1047
|
+
.withFlexibleDimensions(false)
|
|
1048
|
+
.withPush(true)
|
|
1049
|
+
.withViewportMargin(8),
|
|
1050
|
+
scrollStrategy: this.overlay.scrollStrategies.reposition(),
|
|
1051
|
+
panelClass: nxePane('nxe-popover-pane', ...[popover.panelClass()].flat()),
|
|
1052
|
+
});
|
|
1053
|
+
// Esc closes this popover; an overlay opened from inside it (a drop-down) is above it and takes the key first.
|
|
1054
|
+
ref.keydownEvents().subscribe((event) => {
|
|
1055
|
+
if (event.key === 'Escape' && !event.ctrlKey && !event.altKey && !event.metaKey) {
|
|
1056
|
+
event.preventDefault();
|
|
1057
|
+
this.close(true);
|
|
1058
|
+
}
|
|
1059
|
+
});
|
|
1060
|
+
ref.outsidePointerEvents().subscribe((event) => {
|
|
1061
|
+
const target = event.target;
|
|
1062
|
+
// The trigger's own click toggles; a drop-down or calendar opened from inside the popover is not "outside" it.
|
|
1063
|
+
if (!target || this.element.contains(target) || target.closest('.cdk-overlay-pane, .cdk-overlay-backdrop')) {
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
this.close(false);
|
|
1067
|
+
});
|
|
1068
|
+
return ref;
|
|
1069
|
+
}
|
|
1070
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxePopoverTriggerForDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1071
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.5", type: NxePopoverTriggerForDirective, isStandalone: true, selector: "[nxePopoverTriggerFor]", inputs: { popover: { classPropertyName: "popover", publicName: "nxePopoverTriggerFor", isSignal: true, isRequired: true, transformFunction: null } }, host: { attributes: { "aria-haspopup": "dialog" }, listeners: { "click": "onClick($event)", "keydown": "onKeydown($event)" }, properties: { "attr.id": "id", "attr.aria-expanded": "isOpen()", "attr.aria-controls": "isOpen() ? popover().panelId : null" } }, exportAs: ["nxePopoverTrigger"], ngImport: i0 });
|
|
1072
|
+
}
|
|
1073
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxePopoverTriggerForDirective, decorators: [{
|
|
1074
|
+
type: Directive,
|
|
1075
|
+
args: [{
|
|
1076
|
+
selector: '[nxePopoverTriggerFor]',
|
|
1077
|
+
exportAs: 'nxePopoverTrigger',
|
|
1078
|
+
host: {
|
|
1079
|
+
'aria-haspopup': 'dialog',
|
|
1080
|
+
'[attr.id]': 'id',
|
|
1081
|
+
'[attr.aria-expanded]': 'isOpen()',
|
|
1082
|
+
'[attr.aria-controls]': 'isOpen() ? popover().panelId : null',
|
|
1083
|
+
'(click)': 'onClick($event)',
|
|
1084
|
+
'(keydown)': 'onKeydown($event)',
|
|
1085
|
+
},
|
|
1086
|
+
}]
|
|
1087
|
+
}], propDecorators: { popover: [{ type: i0.Input, args: [{ isSignal: true, alias: "nxePopoverTriggerFor", required: true }] }] } });
|
|
1088
|
+
|
|
1089
|
+
/**
|
|
1090
|
+
* The chip a `[nxeTooltip]` shows. Internal: it is created by the directive, not declared in a
|
|
1091
|
+
* template.
|
|
1092
|
+
* @internal
|
|
1093
|
+
*/
|
|
1094
|
+
class NxeTooltipPanelComponent {
|
|
1095
|
+
text = input('', /* @ts-ignore */
|
|
1096
|
+
...(ngDevMode ? [{ debugName: "text" }] : /* istanbul ignore next */ []));
|
|
1097
|
+
tooltipId = input(null, /* @ts-ignore */
|
|
1098
|
+
...(ngDevMode ? [{ debugName: "tooltipId" }] : /* istanbul ignore next */ []));
|
|
1099
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeTooltipPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1100
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.5", type: NxeTooltipPanelComponent, isStandalone: true, selector: "nxe-tooltip-panel", inputs: { text: { classPropertyName: "text", publicName: "text", isSignal: true, isRequired: false, transformFunction: null }, tooltipId: { classPropertyName: "tooltipId", publicName: "tooltipId", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "tooltip" }, properties: { "attr.id": "tooltipId()" }, classAttribute: "nxe-tooltip" }, ngImport: i0, template: '{{ text() }}', isInline: true, styles: [".cdk-overlay-pane.nxe-tooltip-pane{box-sizing:border-box;padding:6px;pointer-events:auto}.nxe-tooltip{display:block;box-sizing:border-box;max-width:240px;padding:var(--nxe-ui-space-1) var(--nxe-ui-space-2);border-radius:var(--nxe-ui-radius-sm);background:var(--nxe-ui-tooltip-bg);color:var(--nxe-ui-tooltip-fg);box-shadow:var(--nxe-ui-shadow-sm);font-size:var(--nxe-ui-font-size-sm);line-height:var(--nxe-ui-line-height-sm);overflow-wrap:anywhere;white-space:normal}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
1101
|
+
}
|
|
1102
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeTooltipPanelComponent, decorators: [{
|
|
1103
|
+
type: Component,
|
|
1104
|
+
args: [{ selector: 'nxe-tooltip-panel', template: '{{ text() }}', host: {
|
|
1105
|
+
class: 'nxe-tooltip',
|
|
1106
|
+
role: 'tooltip',
|
|
1107
|
+
'[attr.id]': 'tooltipId()',
|
|
1108
|
+
}, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, styles: [".cdk-overlay-pane.nxe-tooltip-pane{box-sizing:border-box;padding:6px;pointer-events:auto}.nxe-tooltip{display:block;box-sizing:border-box;max-width:240px;padding:var(--nxe-ui-space-1) var(--nxe-ui-space-2);border-radius:var(--nxe-ui-radius-sm);background:var(--nxe-ui-tooltip-bg);color:var(--nxe-ui-tooltip-fg);box-shadow:var(--nxe-ui-shadow-sm);font-size:var(--nxe-ui-font-size-sm);line-height:var(--nxe-ui-line-height-sm);overflow-wrap:anywhere;white-space:normal}\n"] }]
|
|
1109
|
+
}], propDecorators: { text: [{ type: i0.Input, args: [{ isSignal: true, alias: "text", required: false }] }], tooltipId: [{ type: i0.Input, args: [{ isSignal: true, alias: "tooltipId", required: false }] }] } });
|
|
1110
|
+
|
|
1111
|
+
let tooltipSequence = 0;
|
|
1112
|
+
/**
|
|
1113
|
+
* Adds a short text tooltip to the element it sits on.
|
|
1114
|
+
*
|
|
1115
|
+
* ```html
|
|
1116
|
+
* <nxe-button icon="edit" stylingMode="text" aria-label="Ubah" nxeTooltip="Ubah PO-0042" />
|
|
1117
|
+
* <button type="button" nxeTooltip="Salin ke papan klip" nxeTooltipPosition="bottom">Salin</button>
|
|
1118
|
+
* ```
|
|
1119
|
+
*
|
|
1120
|
+
* It shows after `nxeTooltipShowDelay` (500ms) when the pointer rests on the element, and at once when
|
|
1121
|
+
* the element takes focus from the keyboard. It hides on Esc (first, before a dialog or menu under it
|
|
1122
|
+
* sees the key), when focus leaves, when the pointer leaves for longer than `nxeTooltipHideDelay`
|
|
1123
|
+
* (100ms, long enough to move onto the tooltip itself, so a reader can keep it open), and when the
|
|
1124
|
+
* element is pressed. Touch does not show it: there is no long-press.
|
|
1125
|
+
*
|
|
1126
|
+
* The text is also the element's description. While hidden, it is a visually hidden description the
|
|
1127
|
+
* element points at through `aria-describedby`, so a screen reader reads it on focus; while shown,
|
|
1128
|
+
* `aria-describedby` points at the `role="tooltip"` element instead. The element's own name is never
|
|
1129
|
+
* touched: an icon-only `nxe-button` keeps its `aria-label`, and the tooltip adds to it.
|
|
1130
|
+
*/
|
|
1131
|
+
class NxeTooltipDirective {
|
|
1132
|
+
overlay = inject(Overlay);
|
|
1133
|
+
element = inject(ElementRef).nativeElement;
|
|
1134
|
+
ariaDescriber = inject(AriaDescriber);
|
|
1135
|
+
renderer = inject(Renderer2);
|
|
1136
|
+
focusMonitor = inject(FocusMonitor);
|
|
1137
|
+
/** The tooltip text. Empty, or a `nxeTooltipDisabled` element, shows nothing. */
|
|
1138
|
+
text = input('', { ...(ngDevMode ? { debugName: "text" } : /* istanbul ignore next */ {}), alias: 'nxeTooltip' });
|
|
1139
|
+
/** Preferred side. `start` and `end` follow the writing direction. */
|
|
1140
|
+
position = input('top', { ...(ngDevMode ? { debugName: "position" } : /* istanbul ignore next */ {}), alias: 'nxeTooltipPosition' });
|
|
1141
|
+
/** Milliseconds the pointer rests before the tooltip shows. Focus from the keyboard ignores it. */
|
|
1142
|
+
showDelay = input(500, { ...(ngDevMode ? { debugName: "showDelay" } : /* istanbul ignore next */ {}), alias: 'nxeTooltipShowDelay', transform: numberAttribute });
|
|
1143
|
+
/** Milliseconds between the pointer leaving and the tooltip hiding. */
|
|
1144
|
+
hideDelay = input(100, { ...(ngDevMode ? { debugName: "hideDelay" } : /* istanbul ignore next */ {}), alias: 'nxeTooltipHideDelay', transform: numberAttribute });
|
|
1145
|
+
disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), alias: 'nxeTooltipDisabled', transform: booleanAttribute });
|
|
1146
|
+
message = computed(() => (this.disabled() ? '' : (this.text() ?? '').trim()), /* @ts-ignore */
|
|
1147
|
+
...(ngDevMode ? [{ debugName: "message" }] : /* istanbul ignore next */ []));
|
|
1148
|
+
visibleState = signal(false, /* @ts-ignore */
|
|
1149
|
+
...(ngDevMode ? [{ debugName: "visibleState" }] : /* istanbul ignore next */ []));
|
|
1150
|
+
/** Whether the tooltip is showing. */
|
|
1151
|
+
isVisible = this.visibleState.asReadonly();
|
|
1152
|
+
/** Id of the `role="tooltip"` element while it exists. */
|
|
1153
|
+
id = `nxe-tooltip-${++tooltipSequence}`;
|
|
1154
|
+
overlayRef = null;
|
|
1155
|
+
panel = null;
|
|
1156
|
+
showTimer = null;
|
|
1157
|
+
hideTimer = null;
|
|
1158
|
+
described = '';
|
|
1159
|
+
unlisten = [];
|
|
1160
|
+
constructor() {
|
|
1161
|
+
this.focusMonitor
|
|
1162
|
+
.monitor(this.element)
|
|
1163
|
+
.pipe(takeUntilDestroyed(inject(DestroyRef)))
|
|
1164
|
+
.subscribe((origin) => {
|
|
1165
|
+
if (origin === 'keyboard') {
|
|
1166
|
+
this.show(0);
|
|
1167
|
+
}
|
|
1168
|
+
else if (origin === null) {
|
|
1169
|
+
this.hide(0);
|
|
1170
|
+
}
|
|
1171
|
+
});
|
|
1172
|
+
toObservable(this.message)
|
|
1173
|
+
.pipe(takeUntilDestroyed())
|
|
1174
|
+
.subscribe((message) => this.messageChanged(message));
|
|
1175
|
+
}
|
|
1176
|
+
/** Shows the tooltip after `delay` milliseconds. No-op without text. */
|
|
1177
|
+
show(delay = this.showDelay()) {
|
|
1178
|
+
this.clearTimers();
|
|
1179
|
+
if (!this.message() || this.visibleState()) {
|
|
1180
|
+
return;
|
|
1181
|
+
}
|
|
1182
|
+
if (delay > 0) {
|
|
1183
|
+
this.showTimer = setTimeout(() => this.attach(), delay);
|
|
1184
|
+
}
|
|
1185
|
+
else {
|
|
1186
|
+
this.attach();
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
/** Hides the tooltip after `delay` milliseconds. */
|
|
1190
|
+
hide(delay = this.hideDelay()) {
|
|
1191
|
+
this.clearTimers();
|
|
1192
|
+
if (!this.visibleState()) {
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
if (delay > 0) {
|
|
1196
|
+
this.hideTimer = setTimeout(() => this.detach(), delay);
|
|
1197
|
+
}
|
|
1198
|
+
else {
|
|
1199
|
+
this.detach();
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
ngOnDestroy() {
|
|
1203
|
+
this.clearTimers();
|
|
1204
|
+
this.unlisten.forEach((fn) => fn());
|
|
1205
|
+
if (this.visibleState()) {
|
|
1206
|
+
this.removeDescribedBy();
|
|
1207
|
+
}
|
|
1208
|
+
this.overlayRef?.dispose();
|
|
1209
|
+
this.overlayRef = null;
|
|
1210
|
+
this.ariaDescriber.removeDescription(this.element, this.described);
|
|
1211
|
+
this.described = '';
|
|
1212
|
+
this.focusMonitor.stopMonitoring(this.element);
|
|
1213
|
+
}
|
|
1214
|
+
onPointerEnter(event) {
|
|
1215
|
+
// A touch is followed by emulated mouse events; a tooltip there is a stuck tooltip.
|
|
1216
|
+
if (event.pointerType !== 'touch') {
|
|
1217
|
+
this.show();
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
onPointerLeave(event) {
|
|
1221
|
+
if (event.pointerType !== 'touch') {
|
|
1222
|
+
this.hide();
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
attach() {
|
|
1226
|
+
this.showTimer = null;
|
|
1227
|
+
const message = this.message();
|
|
1228
|
+
if (!message || this.visibleState()) {
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
const ref = (this.overlayRef ??= this.createOverlay());
|
|
1232
|
+
ref.updatePositionStrategy(this.positionStrategy());
|
|
1233
|
+
const panel = (this.panel = ref.attach(new ComponentPortal(NxeTooltipPanelComponent)));
|
|
1234
|
+
panel.setInput('text', message);
|
|
1235
|
+
panel.setInput('tooltipId', this.id);
|
|
1236
|
+
this.visibleState.set(true);
|
|
1237
|
+
// The hidden description gives way to the tooltip itself; the text is the same.
|
|
1238
|
+
this.ariaDescriber.removeDescription(this.element, this.described);
|
|
1239
|
+
this.addDescribedBy();
|
|
1240
|
+
}
|
|
1241
|
+
detach() {
|
|
1242
|
+
this.hideTimer = null;
|
|
1243
|
+
if (!this.visibleState()) {
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
this.overlayRef?.detach();
|
|
1247
|
+
this.panel = null;
|
|
1248
|
+
this.visibleState.set(false);
|
|
1249
|
+
this.removeDescribedBy();
|
|
1250
|
+
this.describe(this.message());
|
|
1251
|
+
}
|
|
1252
|
+
createOverlay() {
|
|
1253
|
+
const ref = this.overlay.create({
|
|
1254
|
+
positionStrategy: this.positionStrategy(),
|
|
1255
|
+
scrollStrategy: this.overlay.scrollStrategies.reposition(),
|
|
1256
|
+
panelClass: nxePane('nxe-tooltip-pane'),
|
|
1257
|
+
});
|
|
1258
|
+
// Esc dismisses the tooltip alone: a dialog or menu under it is not closed by the same key.
|
|
1259
|
+
ref.keydownEvents().subscribe((event) => {
|
|
1260
|
+
if (event.key === 'Escape' && !event.ctrlKey && !event.altKey && !event.metaKey && !event.shiftKey) {
|
|
1261
|
+
event.preventDefault();
|
|
1262
|
+
event.stopPropagation();
|
|
1263
|
+
this.hide(0);
|
|
1264
|
+
}
|
|
1265
|
+
});
|
|
1266
|
+
// Hoverable: reaching the tooltip keeps it, leaving it hides it.
|
|
1267
|
+
this.unlisten.push(this.renderer.listen(ref.overlayElement, 'pointerenter', () => this.clearTimers()), this.renderer.listen(ref.overlayElement, 'pointerleave', () => this.hide()));
|
|
1268
|
+
return ref;
|
|
1269
|
+
}
|
|
1270
|
+
positionStrategy() {
|
|
1271
|
+
return this.overlay
|
|
1272
|
+
.position()
|
|
1273
|
+
.flexibleConnectedTo(this.element)
|
|
1274
|
+
.withPositions(nxePlacementPositions(this.position(), 'center'))
|
|
1275
|
+
.withFlexibleDimensions(false)
|
|
1276
|
+
.withPush(true)
|
|
1277
|
+
.withViewportMargin(4);
|
|
1278
|
+
}
|
|
1279
|
+
messageChanged(message) {
|
|
1280
|
+
if (this.visibleState()) {
|
|
1281
|
+
if (message) {
|
|
1282
|
+
this.panel?.setInput('text', message);
|
|
1283
|
+
}
|
|
1284
|
+
else {
|
|
1285
|
+
this.detach();
|
|
1286
|
+
}
|
|
1287
|
+
this.described = message;
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
this.describe(message);
|
|
1291
|
+
}
|
|
1292
|
+
/** The hidden description a screen reader reads on focus while no tooltip is showing. */
|
|
1293
|
+
describe(message) {
|
|
1294
|
+
if (message === this.described) {
|
|
1295
|
+
if (message) {
|
|
1296
|
+
this.ariaDescriber.describe(this.element, message);
|
|
1297
|
+
}
|
|
1298
|
+
return;
|
|
1299
|
+
}
|
|
1300
|
+
this.ariaDescriber.removeDescription(this.element, this.described);
|
|
1301
|
+
this.described = message;
|
|
1302
|
+
if (message) {
|
|
1303
|
+
this.ariaDescriber.describe(this.element, message);
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
addDescribedBy() {
|
|
1307
|
+
const ids = this.describedByIds();
|
|
1308
|
+
if (!ids.includes(this.id)) {
|
|
1309
|
+
this.element.setAttribute('aria-describedby', [...ids, this.id].join(' '));
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
removeDescribedBy() {
|
|
1313
|
+
const ids = this.describedByIds().filter((id) => id !== this.id);
|
|
1314
|
+
if (ids.length) {
|
|
1315
|
+
this.element.setAttribute('aria-describedby', ids.join(' '));
|
|
1316
|
+
}
|
|
1317
|
+
else {
|
|
1318
|
+
this.element.removeAttribute('aria-describedby');
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
describedByIds() {
|
|
1322
|
+
return (this.element.getAttribute('aria-describedby') ?? '').split(/\s+/).filter(Boolean);
|
|
1323
|
+
}
|
|
1324
|
+
clearTimers() {
|
|
1325
|
+
if (this.showTimer !== null) {
|
|
1326
|
+
clearTimeout(this.showTimer);
|
|
1327
|
+
this.showTimer = null;
|
|
1328
|
+
}
|
|
1329
|
+
if (this.hideTimer !== null) {
|
|
1330
|
+
clearTimeout(this.hideTimer);
|
|
1331
|
+
this.hideTimer = null;
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeTooltipDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1335
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.5", type: NxeTooltipDirective, isStandalone: true, selector: "[nxeTooltip]", inputs: { text: { classPropertyName: "text", publicName: "nxeTooltip", isSignal: true, isRequired: false, transformFunction: null }, position: { classPropertyName: "position", publicName: "nxeTooltipPosition", isSignal: true, isRequired: false, transformFunction: null }, showDelay: { classPropertyName: "showDelay", publicName: "nxeTooltipShowDelay", isSignal: true, isRequired: false, transformFunction: null }, hideDelay: { classPropertyName: "hideDelay", publicName: "nxeTooltipHideDelay", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "nxeTooltipDisabled", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "pointerenter": "onPointerEnter($event)", "pointerleave": "onPointerLeave($event)", "pointerdown": "hide(0)", "click": "hide(0)", "keydown.enter": "hide(0)", "keydown.space": "hide(0)" } }, exportAs: ["nxeTooltip"], ngImport: i0 });
|
|
1336
|
+
}
|
|
1337
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeTooltipDirective, decorators: [{
|
|
1338
|
+
type: Directive,
|
|
1339
|
+
args: [{
|
|
1340
|
+
selector: '[nxeTooltip]',
|
|
1341
|
+
exportAs: 'nxeTooltip',
|
|
1342
|
+
host: {
|
|
1343
|
+
'(pointerenter)': 'onPointerEnter($event)',
|
|
1344
|
+
'(pointerleave)': 'onPointerLeave($event)',
|
|
1345
|
+
'(pointerdown)': 'hide(0)',
|
|
1346
|
+
'(click)': 'hide(0)',
|
|
1347
|
+
'(keydown.enter)': 'hide(0)',
|
|
1348
|
+
'(keydown.space)': 'hide(0)',
|
|
1349
|
+
},
|
|
1350
|
+
}]
|
|
1351
|
+
}], ctorParameters: () => [], propDecorators: { text: [{ type: i0.Input, args: [{ isSignal: true, alias: "nxeTooltip", required: false }] }], position: [{ type: i0.Input, args: [{ isSignal: true, alias: "nxeTooltipPosition", required: false }] }], showDelay: [{ type: i0.Input, args: [{ isSignal: true, alias: "nxeTooltipShowDelay", required: false }] }], hideDelay: [{ type: i0.Input, args: [{ isSignal: true, alias: "nxeTooltipHideDelay", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "nxeTooltipDisabled", required: false }] }] } });
|
|
1352
|
+
|
|
1353
|
+
/** The data a drawer was opened with, injected into the content component. */
|
|
1354
|
+
const NXE_DRAWER_DATA = new InjectionToken('NXE_DRAWER_DATA');
|
|
1355
|
+
|
|
1356
|
+
/**
|
|
1357
|
+
* Handle on an open drawer: the dialog's ref (`close(result)`, `afterClosed()`, `whenClosed()`, `result`,
|
|
1358
|
+
* `isClosed`, `disableClose`, `componentInstance`), injectable in the content as `inject(NxeDrawerRef)` and
|
|
1359
|
+
* returned by `NxeDrawerService.open`. It is also provided as `NxeDialogRef`, so the dialog directives
|
|
1360
|
+
* (`nxe-dialog-title`, `nxe-dialog-content`, `nxe-dialog-actions`) work inside a drawer.
|
|
1361
|
+
*/
|
|
1362
|
+
class NxeDrawerRef extends NxeDialogRef {
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
/**
|
|
1366
|
+
* What `NxeDrawerService` opens in the CDK dialog container: the dialog's own frame (title, scrolling body,
|
|
1367
|
+
* actions bar, close button, all injecting the same ref and data), wrapped so the drawer's pane styles load
|
|
1368
|
+
* with it. @internal
|
|
1369
|
+
*/
|
|
1370
|
+
class NxeDrawerFrameComponent {
|
|
1371
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeDrawerFrameComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1372
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.1.5", type: NxeDrawerFrameComponent, isStandalone: true, selector: "nxe-drawer-frame", host: { classAttribute: "nxe-drawer-frame-host" }, ngImport: i0, template: '<nxe-dialog-frame />', isInline: true, styles: ["nxe-drawer-frame.nxe-drawer-frame-host{display:contents}.cdk-overlay-pane.nxe-drawer-pane{--nxe-drawer-from: 100%;box-sizing:border-box;height:100%;max-width:100vw}.cdk-overlay-pane.nxe-drawer-pane.nxe-drawer-sm{width:320px}.cdk-overlay-pane.nxe-drawer-pane.nxe-drawer-md{width:420px}.cdk-overlay-pane.nxe-drawer-pane.nxe-drawer-lg{width:560px}.cdk-overlay-pane.nxe-drawer-pane.nxe-drawer-xl{width:760px}.cdk-overlay-pane.nxe-drawer-pane.nxe-drawer-full{width:100vw}.cdk-overlay-pane.nxe-drawer-pane .cdk-dialog-container{display:flex;flex-direction:column;box-sizing:border-box;width:100%;height:100%;overflow:hidden;background:var(--nxe-ui-surface);color:var(--nxe-ui-text);box-shadow:var(--nxe-ui-shadow-lg);outline:none}.cdk-overlay-pane.nxe-drawer-pane.nxe-drawer-side-end .cdk-dialog-container{border-inline-start:1px solid var(--nxe-ui-border)}.cdk-overlay-pane.nxe-drawer-pane.nxe-drawer-side-start .cdk-dialog-container{border-inline-end:1px solid var(--nxe-ui-border)}.cdk-overlay-pane.nxe-drawer-pane.nxe-drawer-side-start,[dir=rtl] .cdk-overlay-pane.nxe-drawer-pane.nxe-drawer-side-end{--nxe-drawer-from: -100%}[dir=rtl] .cdk-overlay-pane.nxe-drawer-pane.nxe-drawer-side-start{--nxe-drawer-from: 100%}.cdk-overlay-backdrop.nxe-drawer-backdrop{background:var(--nxe-ui-shell-scrim)}@media(prefers-reduced-motion:no-preference){.cdk-overlay-pane.nxe-drawer-pane{animation:nxe-drawer-in .22s ease-out}}@keyframes nxe-drawer-in{0%{transform:translate(var(--nxe-drawer-from))}to{transform:none}}.cdk-overlay-pane.nxe-drawer-pane nxe-dialog-frame.nxe-dialog-frame .nxe-dialog-header{padding-inline:var(--nxe-ui-space-4) var(--nxe-ui-space-8)}.cdk-overlay-pane.nxe-drawer-pane nxe-dialog-frame.nxe-dialog-frame .nxe-dialog-close{right:auto;inset-inline-end:var(--nxe-ui-space-2)}\n"], dependencies: [{ kind: "component", type: NxeDialogFrameComponent, selector: "nxe-dialog-frame" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
1373
|
+
}
|
|
1374
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeDrawerFrameComponent, decorators: [{
|
|
1375
|
+
type: Component,
|
|
1376
|
+
args: [{ selector: 'nxe-drawer-frame', imports: [NxeDialogFrameComponent], template: '<nxe-dialog-frame />', host: { class: 'nxe-drawer-frame-host' }, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, styles: ["nxe-drawer-frame.nxe-drawer-frame-host{display:contents}.cdk-overlay-pane.nxe-drawer-pane{--nxe-drawer-from: 100%;box-sizing:border-box;height:100%;max-width:100vw}.cdk-overlay-pane.nxe-drawer-pane.nxe-drawer-sm{width:320px}.cdk-overlay-pane.nxe-drawer-pane.nxe-drawer-md{width:420px}.cdk-overlay-pane.nxe-drawer-pane.nxe-drawer-lg{width:560px}.cdk-overlay-pane.nxe-drawer-pane.nxe-drawer-xl{width:760px}.cdk-overlay-pane.nxe-drawer-pane.nxe-drawer-full{width:100vw}.cdk-overlay-pane.nxe-drawer-pane .cdk-dialog-container{display:flex;flex-direction:column;box-sizing:border-box;width:100%;height:100%;overflow:hidden;background:var(--nxe-ui-surface);color:var(--nxe-ui-text);box-shadow:var(--nxe-ui-shadow-lg);outline:none}.cdk-overlay-pane.nxe-drawer-pane.nxe-drawer-side-end .cdk-dialog-container{border-inline-start:1px solid var(--nxe-ui-border)}.cdk-overlay-pane.nxe-drawer-pane.nxe-drawer-side-start .cdk-dialog-container{border-inline-end:1px solid var(--nxe-ui-border)}.cdk-overlay-pane.nxe-drawer-pane.nxe-drawer-side-start,[dir=rtl] .cdk-overlay-pane.nxe-drawer-pane.nxe-drawer-side-end{--nxe-drawer-from: -100%}[dir=rtl] .cdk-overlay-pane.nxe-drawer-pane.nxe-drawer-side-start{--nxe-drawer-from: 100%}.cdk-overlay-backdrop.nxe-drawer-backdrop{background:var(--nxe-ui-shell-scrim)}@media(prefers-reduced-motion:no-preference){.cdk-overlay-pane.nxe-drawer-pane{animation:nxe-drawer-in .22s ease-out}}@keyframes nxe-drawer-in{0%{transform:translate(var(--nxe-drawer-from))}to{transform:none}}.cdk-overlay-pane.nxe-drawer-pane nxe-dialog-frame.nxe-dialog-frame .nxe-dialog-header{padding-inline:var(--nxe-ui-space-4) var(--nxe-ui-space-8)}.cdk-overlay-pane.nxe-drawer-pane nxe-dialog-frame.nxe-dialog-frame .nxe-dialog-close{right:auto;inset-inline-end:var(--nxe-ui-space-2)}\n"] }]
|
|
1377
|
+
}] });
|
|
1378
|
+
|
|
1379
|
+
/**
|
|
1380
|
+
* Opens modal drawers: a panel that slides in from the screen edge over the page, for a filter panel or a
|
|
1381
|
+
* quick look at a document that should not leave the list.
|
|
1382
|
+
*
|
|
1383
|
+
* ```ts
|
|
1384
|
+
* const ref = this.drawer.open(PoQuickViewComponent, {
|
|
1385
|
+
* title: 'PO-2026-0412',
|
|
1386
|
+
* size: 'lg',
|
|
1387
|
+
* data: { id: 412 },
|
|
1388
|
+
* actions: [{ text: 'Buka halaman penuh', type: 'default', result: 'open' }],
|
|
1389
|
+
* });
|
|
1390
|
+
* ref.afterClosed().subscribe((result) => result === 'open' && this.router.navigate(['/pos', 412]));
|
|
1391
|
+
* ```
|
|
1392
|
+
*
|
|
1393
|
+
* It is the dialog service with a different shape, and uses the same parts. The content is a component (it
|
|
1394
|
+
* injects `NXE_DRAWER_DATA` and `NxeDrawerRef`, or the dialog's `NXE_DIALOG_DATA` and `NxeDialogRef`) or a
|
|
1395
|
+
* template (it receives `{ $implicit: data, dialogRef }`). Focus is trapped, starts on the first tabbable
|
|
1396
|
+
* element (or `[cdkFocusInitial]`) and returns to the opener on close; Esc and a click on the scrim dismiss it
|
|
1397
|
+
* unless `disableClose` is set; the page behind does not scroll. `side` is `end` by default and follows the
|
|
1398
|
+
* writing direction. It slides in over 0.22s, and not under `prefers-reduced-motion: reduce`.
|
|
1399
|
+
*
|
|
1400
|
+
* For a panel that is part of the page and does not block it, use `nxe-drawer` with `mode="push"`.
|
|
1401
|
+
*/
|
|
1402
|
+
class NxeDrawerService {
|
|
1403
|
+
cdk = inject(Dialog);
|
|
1404
|
+
overlay = inject(Overlay);
|
|
1405
|
+
sequence = 0;
|
|
1406
|
+
open(content, config = {}) {
|
|
1407
|
+
const id = `nxe-drawer-${++this.sequence}`;
|
|
1408
|
+
const ref = new NxeDrawerRef(id, `${id}-title`);
|
|
1409
|
+
ref.disableClose = !!config.disableClose;
|
|
1410
|
+
const side = config.side ?? 'end';
|
|
1411
|
+
const frame = {
|
|
1412
|
+
ref: ref,
|
|
1413
|
+
content: content,
|
|
1414
|
+
data: config.data,
|
|
1415
|
+
title: config.title,
|
|
1416
|
+
actions: (config.actions ?? []),
|
|
1417
|
+
showClose: config.showClose ?? true,
|
|
1418
|
+
injector: config.injector,
|
|
1419
|
+
providers: [
|
|
1420
|
+
{ provide: NxeDrawerRef, useValue: ref },
|
|
1421
|
+
{ provide: NXE_DRAWER_DATA, useValue: config.data },
|
|
1422
|
+
],
|
|
1423
|
+
};
|
|
1424
|
+
const position = this.overlay.position().global().top('0');
|
|
1425
|
+
const cdkRef = this.cdk.open(NxeDrawerFrameComponent, {
|
|
1426
|
+
id,
|
|
1427
|
+
role: 'dialog',
|
|
1428
|
+
ariaModal: true,
|
|
1429
|
+
ariaLabel: config.ariaLabel ?? null,
|
|
1430
|
+
ariaLabelledBy: config.ariaLabel ? null : ref.titleId,
|
|
1431
|
+
ariaDescribedBy: config.ariaDescribedBy ?? null,
|
|
1432
|
+
autoFocus: config.autoFocus ?? 'first-tabbable',
|
|
1433
|
+
restoreFocus: true,
|
|
1434
|
+
hasBackdrop: true,
|
|
1435
|
+
backdropClass: 'nxe-drawer-backdrop',
|
|
1436
|
+
panelClass: nxePane('nxe-drawer-pane', `nxe-drawer-${config.size ?? 'md'}`, `nxe-drawer-side-${side}`, ...[config.panelClass ?? []].flat()),
|
|
1437
|
+
positionStrategy: side === 'start' ? position.start('0') : position.end('0'),
|
|
1438
|
+
height: '100%',
|
|
1439
|
+
viewContainerRef: config.viewContainerRef,
|
|
1440
|
+
// Dismissal is decided below, as the dialog does, so `ref.disableClose` can change while it is open.
|
|
1441
|
+
disableClose: true,
|
|
1442
|
+
data: frame,
|
|
1443
|
+
});
|
|
1444
|
+
ref._attach(cdkRef);
|
|
1445
|
+
cdkRef.keydownEvents.subscribe((event) => {
|
|
1446
|
+
if (event.key === 'Escape' && !ref.disableClose && !hasModifierKey(event)) {
|
|
1447
|
+
event.preventDefault();
|
|
1448
|
+
ref.close();
|
|
1449
|
+
}
|
|
1450
|
+
});
|
|
1451
|
+
cdkRef.backdropClick.subscribe(() => {
|
|
1452
|
+
if (!ref.disableClose && config.backdropClose !== false) {
|
|
1453
|
+
ref.close();
|
|
1454
|
+
}
|
|
1455
|
+
});
|
|
1456
|
+
return ref;
|
|
1457
|
+
}
|
|
1458
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeDrawerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1459
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeDrawerService, providedIn: 'root' });
|
|
1460
|
+
}
|
|
1461
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeDrawerService, decorators: [{
|
|
1462
|
+
type: Injectable,
|
|
1463
|
+
args: [{ providedIn: 'root' }]
|
|
1464
|
+
}] });
|
|
1465
|
+
|
|
1466
|
+
let drawerSequence = 0;
|
|
1467
|
+
/**
|
|
1468
|
+
* `nxe-drawer` is a panel at the edge of the screen: a filter panel, the details of the row you clicked.
|
|
1469
|
+
*
|
|
1470
|
+
* ```html
|
|
1471
|
+
* <nxe-button text="Filter" (onClick)="filters.set(true)" />
|
|
1472
|
+
*
|
|
1473
|
+
* <nxe-drawer [(opened)]="filters" heading="Filter pesanan" side="end" size="sm">
|
|
1474
|
+
* <nxe-form-field label="Pemasok"><nxe-text-box [formControl]="supplier" /></nxe-form-field>
|
|
1475
|
+
* <div nxeDrawerFooter>
|
|
1476
|
+
* <nxe-button text="Atur ulang" (onClick)="reset()" />
|
|
1477
|
+
* <nxe-button type="default" text="Terapkan" (onClick)="apply(); filters.set(false)" />
|
|
1478
|
+
* </div>
|
|
1479
|
+
* </nxe-drawer>
|
|
1480
|
+
* ```
|
|
1481
|
+
*
|
|
1482
|
+
* **Modes.** `overlay` (default) is modal: the panel opens in a CDK overlay over a scrim, `role="dialog"` with
|
|
1483
|
+
* `aria-modal`, focus trapped and starting on the first tabbable element (`[cdkFocusInitial]` wins; `autoFocus`
|
|
1484
|
+
* changes it), Esc and a click on the scrim close it, focus returns to the control that was focused when it
|
|
1485
|
+
* opened, and the page behind does not scroll. `push` is not modal: the panel is an `<aside>` in the page's
|
|
1486
|
+
* own layout, a flex item that opens by growing from no width, so the content beside it narrows instead of
|
|
1487
|
+
* being covered. Put it inside a flex row. It takes focus when it opens and gives it back when it closes,
|
|
1488
|
+
* and Esc inside it closes it. Changing `mode` while open (a panel that is a push drawer on a desktop and an
|
|
1489
|
+
* overlay on a phone) moves the same content from one to the other.
|
|
1490
|
+
*
|
|
1491
|
+
* **Slots.** The heading is the `heading` input (the panel's accessible name; without one, set `aria-label`).
|
|
1492
|
+
* The close button, named by `common.close`, shows with `closable` (default) unless `disableClose` is on. Everything else is the body,
|
|
1493
|
+
* which scrolls, and `[nxeDrawerFooter]` is pinned under it.
|
|
1494
|
+
*
|
|
1495
|
+
* `opened` is a two-way model. `side` is `start` or `end` and follows the writing direction, `size` is `sm`,
|
|
1496
|
+
* `md` (default), `lg`, `xl` or `full`. `disableClose` stops Esc and the scrim from closing it and hides the
|
|
1497
|
+
* close button (a form that must be saved or discarded). For a drawer opened from code with a component, use
|
|
1498
|
+
* `NxeDrawerService`.
|
|
1499
|
+
*/
|
|
1500
|
+
class NxeDrawerComponent {
|
|
1501
|
+
dialog = inject(Dialog);
|
|
1502
|
+
overlay = inject(Overlay);
|
|
1503
|
+
viewContainer = inject(ViewContainerRef);
|
|
1504
|
+
injector = inject(Injector);
|
|
1505
|
+
messages = inject(NxeMessageService);
|
|
1506
|
+
panel = viewChild.required('panel', /* @ts-ignore */
|
|
1507
|
+
...(ngDevMode ? [{ debugName: "panel" }] : /* istanbul ignore next */ []));
|
|
1508
|
+
aside = viewChild('aside', /* @ts-ignore */
|
|
1509
|
+
...(ngDevMode ? [{ debugName: "aside" }] : /* istanbul ignore next */ []));
|
|
1510
|
+
/** Whether the drawer is open. Two-way: `[(opened)]`. */
|
|
1511
|
+
opened = model(false, /* @ts-ignore */
|
|
1512
|
+
...(ngDevMode ? [{ debugName: "opened" }] : /* istanbul ignore next */ []));
|
|
1513
|
+
/** `overlay` (default, modal) or `push` (in the page's layout). */
|
|
1514
|
+
mode = input('overlay', /* @ts-ignore */
|
|
1515
|
+
...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
|
|
1516
|
+
/** `start` or `end` (default). */
|
|
1517
|
+
side = input('end', /* @ts-ignore */
|
|
1518
|
+
...(ngDevMode ? [{ debugName: "side" }] : /* istanbul ignore next */ []));
|
|
1519
|
+
/** `sm`, `md` (default), `lg`, `xl` or `full`. */
|
|
1520
|
+
size = input('md', /* @ts-ignore */
|
|
1521
|
+
...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
|
|
1522
|
+
/** The title, which also names the panel. */
|
|
1523
|
+
heading = input('', /* @ts-ignore */
|
|
1524
|
+
...(ngDevMode ? [{ debugName: "heading" }] : /* istanbul ignore next */ []));
|
|
1525
|
+
/** Accessible name when there is no `heading`. */
|
|
1526
|
+
ariaLabel = input(undefined, { ...(ngDevMode ? { debugName: "ariaLabel" } : /* istanbul ignore next */ {}), alias: 'aria-label' });
|
|
1527
|
+
/** Show the close button. Default `true`. */
|
|
1528
|
+
closable = input(true, { ...(ngDevMode ? { debugName: "closable" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
1529
|
+
/** Esc and the scrim do not close it, and the close button is hidden. */
|
|
1530
|
+
disableClose = input(false, { ...(ngDevMode ? { debugName: "disableClose" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
1531
|
+
/** Whether a click on the scrim closes an overlay drawer. Default `true`; only applies while `disableClose` is off. */
|
|
1532
|
+
backdropClose = input(true, { ...(ngDevMode ? { debugName: "backdropClose" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
1533
|
+
/** What takes initial focus in overlay mode: `first-tabbable` (default), `first-heading`, `dialog` or a CSS selector. */
|
|
1534
|
+
autoFocus = input('first-tabbable', /* @ts-ignore */
|
|
1535
|
+
...(ngDevMode ? [{ debugName: "autoFocus" }] : /* istanbul ignore next */ []));
|
|
1536
|
+
/** Extra class(es) for the overlay pane. */
|
|
1537
|
+
panelClass = input('', /* @ts-ignore */
|
|
1538
|
+
...(ngDevMode ? [{ debugName: "panelClass" }] : /* istanbul ignore next */ []));
|
|
1539
|
+
uid = `nxe-drawer-${++drawerSequence}`;
|
|
1540
|
+
titleId = `${this.uid}-title`;
|
|
1541
|
+
closeLabel = computed(() => this.messages.t('common.close'), /* @ts-ignore */
|
|
1542
|
+
...(ngDevMode ? [{ debugName: "closeLabel" }] : /* istanbul ignore next */ []));
|
|
1543
|
+
pushed = computed(() => this.mode() === 'push', /* @ts-ignore */
|
|
1544
|
+
...(ngDevMode ? [{ debugName: "pushed" }] : /* istanbul ignore next */ []));
|
|
1545
|
+
/** The close button is hidden, not disabled, while `disableClose` is on. */
|
|
1546
|
+
showClose = computed(() => this.closable() && !this.disableClose(), /* @ts-ignore */
|
|
1547
|
+
...(ngDevMode ? [{ debugName: "showClose" }] : /* istanbul ignore next */ []));
|
|
1548
|
+
/** Modal and open: the part that lives in the CDK overlay. */
|
|
1549
|
+
overlayShown = computed(() => this.opened() && this.mode() === 'overlay', /* @ts-ignore */
|
|
1550
|
+
...(ngDevMode ? [{ debugName: "overlayShown" }] : /* istanbul ignore next */ []));
|
|
1551
|
+
overlayRef = null;
|
|
1552
|
+
returnFocusTo = null;
|
|
1553
|
+
constructor() {
|
|
1554
|
+
const destroyRef = inject(DestroyRef);
|
|
1555
|
+
toObservable(this.overlayShown)
|
|
1556
|
+
.pipe(takeUntilDestroyed())
|
|
1557
|
+
.subscribe((shown) => (shown ? this.attachOverlay() : this.detachOverlay()));
|
|
1558
|
+
// A push drawer takes focus when it opens and returns it when it closes; the first value (open at load) is left alone.
|
|
1559
|
+
let first = true;
|
|
1560
|
+
toObservable(computed(() => this.opened() && this.pushed()))
|
|
1561
|
+
.pipe(takeUntilDestroyed())
|
|
1562
|
+
.subscribe((shown) => {
|
|
1563
|
+
if (first) {
|
|
1564
|
+
first = false;
|
|
1565
|
+
return;
|
|
1566
|
+
}
|
|
1567
|
+
if (shown) {
|
|
1568
|
+
this.returnFocusTo = document.activeElement;
|
|
1569
|
+
afterNextRender(() => this.aside()?.nativeElement.focus({ preventScroll: true }), { injector: this.injector });
|
|
1570
|
+
}
|
|
1571
|
+
else {
|
|
1572
|
+
const aside = this.aside()?.nativeElement;
|
|
1573
|
+
const target = this.returnFocusTo;
|
|
1574
|
+
this.returnFocusTo = null;
|
|
1575
|
+
// Only take focus back if it was in the panel (or lost with it): a click elsewhere keeps its own focus.
|
|
1576
|
+
const active = document.activeElement;
|
|
1577
|
+
if (target?.isConnected && (!active || active === document.body || aside?.contains(active))) {
|
|
1578
|
+
target.focus({ preventScroll: true });
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
});
|
|
1582
|
+
destroyRef.onDestroy(() => this.detachOverlay());
|
|
1583
|
+
}
|
|
1584
|
+
open() {
|
|
1585
|
+
this.opened.set(true);
|
|
1586
|
+
}
|
|
1587
|
+
close() {
|
|
1588
|
+
this.opened.set(false);
|
|
1589
|
+
}
|
|
1590
|
+
toggle() {
|
|
1591
|
+
this.opened.update((open) => !open);
|
|
1592
|
+
}
|
|
1593
|
+
/** The close button and Esc go through here so `disableClose` is honoured in one place. */
|
|
1594
|
+
requestClose() {
|
|
1595
|
+
if (!this.disableClose()) {
|
|
1596
|
+
this.close();
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
onKeydown(event) {
|
|
1600
|
+
if (this.pushed() && event.key === 'Escape' && !event.defaultPrevented && !hasModifierKey(event)) {
|
|
1601
|
+
event.preventDefault();
|
|
1602
|
+
this.requestClose();
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
attachOverlay() {
|
|
1606
|
+
if (this.overlayRef) {
|
|
1607
|
+
return;
|
|
1608
|
+
}
|
|
1609
|
+
const side = this.side();
|
|
1610
|
+
const position = this.overlay.position().global().top('0');
|
|
1611
|
+
const ref = this.dialog.open(this.panel(), {
|
|
1612
|
+
id: this.uid,
|
|
1613
|
+
role: 'dialog',
|
|
1614
|
+
ariaModal: true,
|
|
1615
|
+
ariaLabel: this.heading() ? null : (this.ariaLabel() ?? null),
|
|
1616
|
+
ariaLabelledBy: this.heading() ? this.titleId : null,
|
|
1617
|
+
autoFocus: this.autoFocus(),
|
|
1618
|
+
restoreFocus: true,
|
|
1619
|
+
hasBackdrop: true,
|
|
1620
|
+
backdropClass: 'nxe-drawer-backdrop',
|
|
1621
|
+
panelClass: nxePane('nxe-drawer-pane', `nxe-drawer-${this.size()}`, `nxe-drawer-side-${side}`, ...[this.panelClass()].flat()),
|
|
1622
|
+
positionStrategy: side === 'start' ? position.start('0') : position.end('0'),
|
|
1623
|
+
height: '100%',
|
|
1624
|
+
viewContainerRef: this.viewContainer,
|
|
1625
|
+
// Dismissal is decided here so `disableClose` can change while it is open.
|
|
1626
|
+
disableClose: true,
|
|
1627
|
+
});
|
|
1628
|
+
this.overlayRef = ref;
|
|
1629
|
+
ref.keydownEvents.subscribe((event) => {
|
|
1630
|
+
if (event.key === 'Escape' && !this.disableClose() && !hasModifierKey(event)) {
|
|
1631
|
+
event.preventDefault();
|
|
1632
|
+
this.close();
|
|
1633
|
+
}
|
|
1634
|
+
});
|
|
1635
|
+
ref.backdropClick.subscribe(() => {
|
|
1636
|
+
if (!this.disableClose() && this.backdropClose()) {
|
|
1637
|
+
this.close();
|
|
1638
|
+
}
|
|
1639
|
+
});
|
|
1640
|
+
// Something else closed it (`Dialog.closeAll()`): the model follows.
|
|
1641
|
+
ref.closed.subscribe(() => {
|
|
1642
|
+
if (this.overlayRef === ref) {
|
|
1643
|
+
this.overlayRef = null;
|
|
1644
|
+
this.opened.set(false);
|
|
1645
|
+
}
|
|
1646
|
+
});
|
|
1647
|
+
}
|
|
1648
|
+
detachOverlay() {
|
|
1649
|
+
const ref = this.overlayRef;
|
|
1650
|
+
this.overlayRef = null;
|
|
1651
|
+
ref?.close();
|
|
1652
|
+
}
|
|
1653
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeDrawerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1654
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: NxeDrawerComponent, isStandalone: true, selector: "nxe-drawer", inputs: { opened: { classPropertyName: "opened", publicName: "opened", isSignal: true, isRequired: false, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, side: { classPropertyName: "side", publicName: "side", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, heading: { classPropertyName: "heading", publicName: "heading", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "aria-label", isSignal: true, isRequired: false, transformFunction: null }, closable: { classPropertyName: "closable", publicName: "closable", isSignal: true, isRequired: false, transformFunction: null }, disableClose: { classPropertyName: "disableClose", publicName: "disableClose", isSignal: true, isRequired: false, transformFunction: null }, backdropClose: { classPropertyName: "backdropClose", publicName: "backdropClose", isSignal: true, isRequired: false, transformFunction: null }, autoFocus: { classPropertyName: "autoFocus", publicName: "autoFocus", isSignal: true, isRequired: false, transformFunction: null }, panelClass: { classPropertyName: "panelClass", publicName: "panelClass", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { opened: "openedChange" }, host: { properties: { "class.nxe-drawer-push": "mode() === 'push'" }, classAttribute: "nxe-drawer" }, viewQueries: [{ propertyName: "panel", first: true, predicate: ["panel"], descendants: true, isSignal: true }, { propertyName: "aside", first: true, predicate: ["aside"], descendants: true, isSignal: true }], exportAs: ["nxeDrawer"], ngImport: i0, template: "<ng-template #panel>\r\n <div class=\"nxe-drawer-frame\">\r\n @if (heading() || showClose()) {\r\n <header class=\"nxe-drawer-header\">\r\n @if (heading()) {\r\n <h2 class=\"nxe-drawer-title\" [id]=\"titleId\">{{ heading() }}</h2>\r\n }\r\n @if (showClose()) {\r\n <button type=\"button\" class=\"nxe-drawer-close\" [attr.aria-label]=\"closeLabel()\" (click)=\"requestClose()\">\r\n <svg viewBox=\"0 0 16 16\" aria-hidden=\"true\" focusable=\"false\">\r\n <path d=\"M3.5 3.5l9 9m0-9l-9 9\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" />\r\n </svg>\r\n </button>\r\n }\r\n </header>\r\n }\r\n <div class=\"nxe-drawer-body\"><ng-content /></div>\r\n <div class=\"nxe-drawer-footer\"><ng-content select=\"[nxeDrawerFooter]\" /></div>\r\n </div>\r\n</ng-template>\r\n\r\n@if (pushed()) {\r\n <aside\r\n #aside\r\n class=\"nxe-drawer-aside nxe-drawer-{{ size() }} nxe-drawer-side-{{ side() }}\"\r\n [class.nxe-drawer-open]=\"opened()\"\r\n [attr.inert]=\"opened() ? null : ''\"\r\n [attr.aria-labelledby]=\"heading() ? titleId : null\"\r\n [attr.aria-label]=\"heading() ? null : (ariaLabel() ?? null)\"\r\n tabindex=\"-1\"\r\n (keydown)=\"onKeydown($event)\">\r\n <ng-container *ngTemplateOutlet=\"panel\" />\r\n </aside>\r\n}\r\n", styles: [".cdk-overlay-pane.nxe-drawer-pane{--nxe-drawer-from: 100%;box-sizing:border-box;height:100%;max-width:100vw}.cdk-overlay-pane.nxe-drawer-pane.nxe-drawer-sm{width:320px}.cdk-overlay-pane.nxe-drawer-pane.nxe-drawer-md{width:420px}.cdk-overlay-pane.nxe-drawer-pane.nxe-drawer-lg{width:560px}.cdk-overlay-pane.nxe-drawer-pane.nxe-drawer-xl{width:760px}.cdk-overlay-pane.nxe-drawer-pane.nxe-drawer-full{width:100vw}.cdk-overlay-pane.nxe-drawer-pane .cdk-dialog-container{display:flex;flex-direction:column;box-sizing:border-box;width:100%;height:100%;overflow:hidden;background:var(--nxe-ui-surface);color:var(--nxe-ui-text);box-shadow:var(--nxe-ui-shadow-lg);outline:none}.cdk-overlay-pane.nxe-drawer-pane.nxe-drawer-side-end .cdk-dialog-container{border-inline-start:1px solid var(--nxe-ui-border)}.cdk-overlay-pane.nxe-drawer-pane.nxe-drawer-side-start .cdk-dialog-container{border-inline-end:1px solid var(--nxe-ui-border)}.cdk-overlay-pane.nxe-drawer-pane.nxe-drawer-side-start,[dir=rtl] .cdk-overlay-pane.nxe-drawer-pane.nxe-drawer-side-end{--nxe-drawer-from: -100%}[dir=rtl] .cdk-overlay-pane.nxe-drawer-pane.nxe-drawer-side-start{--nxe-drawer-from: 100%}.cdk-overlay-backdrop.nxe-drawer-backdrop{background:var(--nxe-ui-shell-scrim)}@media(prefers-reduced-motion:no-preference){.cdk-overlay-pane.nxe-drawer-pane{animation:nxe-drawer-in .22s ease-out}}@keyframes nxe-drawer-in{0%{transform:translate(var(--nxe-drawer-from))}to{transform:none}}.nxe-drawer-frame{display:flex;flex:1 1 auto;flex-direction:column;min-height:0;font-family:var(--nxe-ui-font-family);font-size:var(--nxe-ui-font-size-md);line-height:var(--nxe-ui-line-height-md);color:var(--nxe-ui-text)}.nxe-drawer-frame .nxe-drawer-header{display:flex;flex:none;align-items:flex-start;justify-content:space-between;gap:var(--nxe-ui-space-3);padding-block:var(--nxe-ui-space-4) var(--nxe-ui-space-2);padding-inline:var(--nxe-ui-space-4) var(--nxe-ui-space-3)}.nxe-drawer-frame .nxe-drawer-title{margin:0;font-size:var(--nxe-ui-font-size-xl);font-weight:var(--nxe-ui-font-weight-semibold);line-height:var(--nxe-ui-line-height-xl);overflow-wrap:anywhere}.nxe-drawer-frame .nxe-drawer-close{display:inline-flex;flex:none;align-items:center;justify-content:center;width:var(--nxe-ui-space-6);height:var(--nxe-ui-space-6);margin-inline-start:auto;padding:0;border:0;border-radius:var(--nxe-ui-radius-sm);background:transparent;color:var(--nxe-ui-neutral-600);cursor:pointer}.nxe-drawer-frame .nxe-drawer-close svg{width:14px;height:14px}.nxe-drawer-frame .nxe-drawer-close:hover{background:var(--nxe-ui-surface-alt);color:var(--nxe-ui-text)}.nxe-drawer-frame .nxe-drawer-close:focus-visible{outline:none;box-shadow:var(--nxe-ui-focus-ring)}.nxe-drawer-frame .nxe-drawer-body{flex:1 1 auto;min-height:0;overflow:auto;padding:var(--nxe-ui-space-2) var(--nxe-ui-space-4) var(--nxe-ui-space-4)}.nxe-drawer-frame .nxe-drawer-footer{display:flex;flex:none;flex-wrap:wrap;justify-content:flex-end;gap:var(--nxe-ui-space-2);padding:var(--nxe-ui-space-3) var(--nxe-ui-space-4);border-top:1px solid var(--nxe-ui-border);background:var(--nxe-ui-surface)}.nxe-drawer-frame .nxe-drawer-footer:empty{display:none}nxe-drawer.nxe-drawer{display:none}nxe-drawer.nxe-drawer.nxe-drawer-push{display:contents}.nxe-drawer-aside{--nxe-drawer-width: 420px;box-sizing:border-box;display:flex;flex:none;flex-direction:column;align-self:stretch;width:0;max-width:100%;overflow:hidden;visibility:hidden;background:var(--nxe-ui-surface);color:var(--nxe-ui-text);outline:none}.nxe-drawer-aside.nxe-drawer-sm{--nxe-drawer-width: 320px}.nxe-drawer-aside.nxe-drawer-md{--nxe-drawer-width: 420px}.nxe-drawer-aside.nxe-drawer-lg{--nxe-drawer-width: 560px}.nxe-drawer-aside.nxe-drawer-xl{--nxe-drawer-width: 760px}.nxe-drawer-aside.nxe-drawer-full{--nxe-drawer-width: 100%}.nxe-drawer-aside.nxe-drawer-open{width:var(--nxe-drawer-width);visibility:visible}.nxe-drawer-aside.nxe-drawer-open.nxe-drawer-side-start{border-inline-end:1px solid var(--nxe-ui-border)}.nxe-drawer-aside.nxe-drawer-open.nxe-drawer-side-end{border-inline-start:1px solid var(--nxe-ui-border)}.nxe-drawer-aside>.nxe-drawer-frame{width:calc(var(--nxe-drawer-width) - 1px);max-width:100vw;min-width:0}@media(prefers-reduced-motion:no-preference){.nxe-drawer-aside{transition:width .22s ease,visibility 0s linear .22s}.nxe-drawer-aside.nxe-drawer-open{transition:width .22s ease,visibility 0s linear 0s}}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
1655
|
+
}
|
|
1656
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeDrawerComponent, decorators: [{
|
|
1657
|
+
type: Component,
|
|
1658
|
+
args: [{ selector: 'nxe-drawer', exportAs: 'nxeDrawer', imports: [NgTemplateOutlet], host: {
|
|
1659
|
+
class: 'nxe-drawer',
|
|
1660
|
+
'[class.nxe-drawer-push]': "mode() === 'push'",
|
|
1661
|
+
}, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: "<ng-template #panel>\r\n <div class=\"nxe-drawer-frame\">\r\n @if (heading() || showClose()) {\r\n <header class=\"nxe-drawer-header\">\r\n @if (heading()) {\r\n <h2 class=\"nxe-drawer-title\" [id]=\"titleId\">{{ heading() }}</h2>\r\n }\r\n @if (showClose()) {\r\n <button type=\"button\" class=\"nxe-drawer-close\" [attr.aria-label]=\"closeLabel()\" (click)=\"requestClose()\">\r\n <svg viewBox=\"0 0 16 16\" aria-hidden=\"true\" focusable=\"false\">\r\n <path d=\"M3.5 3.5l9 9m0-9l-9 9\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" />\r\n </svg>\r\n </button>\r\n }\r\n </header>\r\n }\r\n <div class=\"nxe-drawer-body\"><ng-content /></div>\r\n <div class=\"nxe-drawer-footer\"><ng-content select=\"[nxeDrawerFooter]\" /></div>\r\n </div>\r\n</ng-template>\r\n\r\n@if (pushed()) {\r\n <aside\r\n #aside\r\n class=\"nxe-drawer-aside nxe-drawer-{{ size() }} nxe-drawer-side-{{ side() }}\"\r\n [class.nxe-drawer-open]=\"opened()\"\r\n [attr.inert]=\"opened() ? null : ''\"\r\n [attr.aria-labelledby]=\"heading() ? titleId : null\"\r\n [attr.aria-label]=\"heading() ? null : (ariaLabel() ?? null)\"\r\n tabindex=\"-1\"\r\n (keydown)=\"onKeydown($event)\">\r\n <ng-container *ngTemplateOutlet=\"panel\" />\r\n </aside>\r\n}\r\n", styles: [".cdk-overlay-pane.nxe-drawer-pane{--nxe-drawer-from: 100%;box-sizing:border-box;height:100%;max-width:100vw}.cdk-overlay-pane.nxe-drawer-pane.nxe-drawer-sm{width:320px}.cdk-overlay-pane.nxe-drawer-pane.nxe-drawer-md{width:420px}.cdk-overlay-pane.nxe-drawer-pane.nxe-drawer-lg{width:560px}.cdk-overlay-pane.nxe-drawer-pane.nxe-drawer-xl{width:760px}.cdk-overlay-pane.nxe-drawer-pane.nxe-drawer-full{width:100vw}.cdk-overlay-pane.nxe-drawer-pane .cdk-dialog-container{display:flex;flex-direction:column;box-sizing:border-box;width:100%;height:100%;overflow:hidden;background:var(--nxe-ui-surface);color:var(--nxe-ui-text);box-shadow:var(--nxe-ui-shadow-lg);outline:none}.cdk-overlay-pane.nxe-drawer-pane.nxe-drawer-side-end .cdk-dialog-container{border-inline-start:1px solid var(--nxe-ui-border)}.cdk-overlay-pane.nxe-drawer-pane.nxe-drawer-side-start .cdk-dialog-container{border-inline-end:1px solid var(--nxe-ui-border)}.cdk-overlay-pane.nxe-drawer-pane.nxe-drawer-side-start,[dir=rtl] .cdk-overlay-pane.nxe-drawer-pane.nxe-drawer-side-end{--nxe-drawer-from: -100%}[dir=rtl] .cdk-overlay-pane.nxe-drawer-pane.nxe-drawer-side-start{--nxe-drawer-from: 100%}.cdk-overlay-backdrop.nxe-drawer-backdrop{background:var(--nxe-ui-shell-scrim)}@media(prefers-reduced-motion:no-preference){.cdk-overlay-pane.nxe-drawer-pane{animation:nxe-drawer-in .22s ease-out}}@keyframes nxe-drawer-in{0%{transform:translate(var(--nxe-drawer-from))}to{transform:none}}.nxe-drawer-frame{display:flex;flex:1 1 auto;flex-direction:column;min-height:0;font-family:var(--nxe-ui-font-family);font-size:var(--nxe-ui-font-size-md);line-height:var(--nxe-ui-line-height-md);color:var(--nxe-ui-text)}.nxe-drawer-frame .nxe-drawer-header{display:flex;flex:none;align-items:flex-start;justify-content:space-between;gap:var(--nxe-ui-space-3);padding-block:var(--nxe-ui-space-4) var(--nxe-ui-space-2);padding-inline:var(--nxe-ui-space-4) var(--nxe-ui-space-3)}.nxe-drawer-frame .nxe-drawer-title{margin:0;font-size:var(--nxe-ui-font-size-xl);font-weight:var(--nxe-ui-font-weight-semibold);line-height:var(--nxe-ui-line-height-xl);overflow-wrap:anywhere}.nxe-drawer-frame .nxe-drawer-close{display:inline-flex;flex:none;align-items:center;justify-content:center;width:var(--nxe-ui-space-6);height:var(--nxe-ui-space-6);margin-inline-start:auto;padding:0;border:0;border-radius:var(--nxe-ui-radius-sm);background:transparent;color:var(--nxe-ui-neutral-600);cursor:pointer}.nxe-drawer-frame .nxe-drawer-close svg{width:14px;height:14px}.nxe-drawer-frame .nxe-drawer-close:hover{background:var(--nxe-ui-surface-alt);color:var(--nxe-ui-text)}.nxe-drawer-frame .nxe-drawer-close:focus-visible{outline:none;box-shadow:var(--nxe-ui-focus-ring)}.nxe-drawer-frame .nxe-drawer-body{flex:1 1 auto;min-height:0;overflow:auto;padding:var(--nxe-ui-space-2) var(--nxe-ui-space-4) var(--nxe-ui-space-4)}.nxe-drawer-frame .nxe-drawer-footer{display:flex;flex:none;flex-wrap:wrap;justify-content:flex-end;gap:var(--nxe-ui-space-2);padding:var(--nxe-ui-space-3) var(--nxe-ui-space-4);border-top:1px solid var(--nxe-ui-border);background:var(--nxe-ui-surface)}.nxe-drawer-frame .nxe-drawer-footer:empty{display:none}nxe-drawer.nxe-drawer{display:none}nxe-drawer.nxe-drawer.nxe-drawer-push{display:contents}.nxe-drawer-aside{--nxe-drawer-width: 420px;box-sizing:border-box;display:flex;flex:none;flex-direction:column;align-self:stretch;width:0;max-width:100%;overflow:hidden;visibility:hidden;background:var(--nxe-ui-surface);color:var(--nxe-ui-text);outline:none}.nxe-drawer-aside.nxe-drawer-sm{--nxe-drawer-width: 320px}.nxe-drawer-aside.nxe-drawer-md{--nxe-drawer-width: 420px}.nxe-drawer-aside.nxe-drawer-lg{--nxe-drawer-width: 560px}.nxe-drawer-aside.nxe-drawer-xl{--nxe-drawer-width: 760px}.nxe-drawer-aside.nxe-drawer-full{--nxe-drawer-width: 100%}.nxe-drawer-aside.nxe-drawer-open{width:var(--nxe-drawer-width);visibility:visible}.nxe-drawer-aside.nxe-drawer-open.nxe-drawer-side-start{border-inline-end:1px solid var(--nxe-ui-border)}.nxe-drawer-aside.nxe-drawer-open.nxe-drawer-side-end{border-inline-start:1px solid var(--nxe-ui-border)}.nxe-drawer-aside>.nxe-drawer-frame{width:calc(var(--nxe-drawer-width) - 1px);max-width:100vw;min-width:0}@media(prefers-reduced-motion:no-preference){.nxe-drawer-aside{transition:width .22s ease,visibility 0s linear .22s}.nxe-drawer-aside.nxe-drawer-open{transition:width .22s ease,visibility 0s linear 0s}}\n"] }]
|
|
1662
|
+
}], ctorParameters: () => [], propDecorators: { panel: [{ type: i0.ViewChild, args: ['panel', { isSignal: true }] }], aside: [{ type: i0.ViewChild, args: ['aside', { isSignal: true }] }], opened: [{ type: i0.Input, args: [{ isSignal: true, alias: "opened", required: false }] }, { type: i0.Output, args: ["openedChange"] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], side: [{ type: i0.Input, args: [{ isSignal: true, alias: "side", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], heading: [{ type: i0.Input, args: [{ isSignal: true, alias: "heading", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-label", required: false }] }], closable: [{ type: i0.Input, args: [{ isSignal: true, alias: "closable", required: false }] }], disableClose: [{ type: i0.Input, args: [{ isSignal: true, alias: "disableClose", required: false }] }], backdropClose: [{ type: i0.Input, args: [{ isSignal: true, alias: "backdropClose", required: false }] }], autoFocus: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoFocus", required: false }] }], panelClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "panelClass", required: false }] }] } });
|
|
1663
|
+
|
|
1664
|
+
// @nexterajs/ui/overlay — public API.
|
|
1665
|
+
|
|
1666
|
+
/**
|
|
1667
|
+
* Generated bundle index. Do not edit.
|
|
1668
|
+
*/
|
|
1669
|
+
|
|
1670
|
+
export { NXE_DIALOG_DATA, NXE_DRAWER_DATA, NxeConfirmService, NxeDialogActionsDirective, NxeDialogContentDirective, NxeDialogRef, NxeDialogService, NxeDialogTitleDirective, NxeDrawerComponent, NxeDrawerRef, NxeDrawerService, NxeMenuComponent, NxeMenuDividerComponent, NxeMenuItemComponent, NxeMenuTriggerForDirective, NxePopoverComponent, NxePopoverTriggerForDirective, NxeTooltipDirective };
|
|
1671
|
+
//# sourceMappingURL=nexterajs-ui-overlay.mjs.map
|