@truenas/ui-components 0.1.66 → 0.1.68
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.
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { Overlay, OverlayPositionBuilder, OverlayModule } from '@angular/cdk/overlay';
|
|
2
|
+
import { TemplatePortal, ComponentPortal, PortalModule } from '@angular/cdk/portal';
|
|
1
3
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { InjectionToken, inject, Renderer2, ElementRef, input, effect, Directive, output, viewChild,
|
|
4
|
+
import { InjectionToken, inject, Renderer2, ElementRef, input, effect, Directive, ViewContainerRef, output, viewChild, computed, signal, forwardRef, Component, model, afterNextRender, ChangeDetectionStrategy, Injectable, isDevMode, ViewEncapsulation, contentChildren, HostListener, contentChild, ChangeDetectorRef, TemplateRef, DestroyRef, isSignal, untracked, IterableDiffers, Pipe, ApplicationRef, EnvironmentInjector, createComponent, PLATFORM_ID } from '@angular/core';
|
|
3
5
|
import * as i2 from '@angular/forms';
|
|
4
6
|
import { NG_VALUE_ACCESSOR, FormsModule, NgControl } from '@angular/forms';
|
|
5
7
|
import { ComponentHarness, HarnessPredicate, TestKey } from '@angular/cdk/testing';
|
|
@@ -13,8 +15,6 @@ import { DomSanitizer } from '@angular/platform-browser';
|
|
|
13
15
|
import { HttpClient } from '@angular/common/http';
|
|
14
16
|
import { firstValueFrom, BehaviorSubject, merge, Subject, take } from 'rxjs';
|
|
15
17
|
import { RouterLink } from '@angular/router';
|
|
16
|
-
import { Overlay, OverlayPositionBuilder, OverlayModule } from '@angular/cdk/overlay';
|
|
17
|
-
import { ComponentPortal, TemplatePortal, PortalModule } from '@angular/cdk/portal';
|
|
18
18
|
import { CdkMenu, CdkMenuItem, CdkMenuTrigger } from '@angular/cdk/menu';
|
|
19
19
|
import { trigger, state, transition, style, animate } from '@angular/animations';
|
|
20
20
|
import { SPACE, ENTER, END, HOME, DOWN_ARROW, UP_ARROW, RIGHT_ARROW, LEFT_ARROW } from '@angular/cdk/keycodes';
|
|
@@ -96,6 +96,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
96
96
|
let nextId$1 = 0;
|
|
97
97
|
class TnAutocompleteComponent {
|
|
98
98
|
elementRef = inject(ElementRef);
|
|
99
|
+
overlay = inject(Overlay);
|
|
100
|
+
viewContainerRef = inject(ViewContainerRef);
|
|
99
101
|
/** Unique instance ID for ARIA linkage */
|
|
100
102
|
uid = `tn-autocomplete-${nextId$1++}`;
|
|
101
103
|
/** All available options */
|
|
@@ -112,14 +114,34 @@ class TnAutocompleteComponent {
|
|
|
112
114
|
filterFn = input(undefined, ...(ngDevMode ? [{ debugName: "filterFn" }] : []));
|
|
113
115
|
/** Text shown when no options match the search */
|
|
114
116
|
noResultsText = input('No results found', ...(ngDevMode ? [{ debugName: "noResultsText" }] : []));
|
|
115
|
-
/**
|
|
116
|
-
|
|
117
|
+
/**
|
|
118
|
+
* Maximum number of options to render.
|
|
119
|
+
*
|
|
120
|
+
* Defaults to `Infinity` so browsing the open dropdown without a search term
|
|
121
|
+
* never silently truncates a long list. Pass an explicit cap if you want to
|
|
122
|
+
* limit how many filtered results are rendered at once.
|
|
123
|
+
*/
|
|
124
|
+
maxResults = input(Infinity, ...(ngDevMode ? [{ debugName: "maxResults" }] : []));
|
|
125
|
+
/**
|
|
126
|
+
* Max height of the dropdown panel before it scrolls. Accepts a number
|
|
127
|
+
* (interpreted as `px`) or any CSS length string (e.g. `'320px'`,
|
|
128
|
+
* `'min(320px, 40vh)'`). Surfaced as the `--tn-autocomplete-panel-max-height`
|
|
129
|
+
* custom property so it can also be themed in CSS.
|
|
130
|
+
*/
|
|
131
|
+
panelMaxHeight = input('320px', ...(ngDevMode ? [{ debugName: "panelMaxHeight" }] : []));
|
|
117
132
|
/** Test ID attribute */
|
|
118
133
|
testId = input('', ...(ngDevMode ? [{ debugName: "testId" }] : []));
|
|
119
134
|
/** Emits when an option is selected */
|
|
120
135
|
optionSelected = output();
|
|
121
136
|
/** Reference to the input element */
|
|
122
137
|
inputEl = viewChild('inputEl', ...(ngDevMode ? [{ debugName: "inputEl" }] : []));
|
|
138
|
+
/** Template for the dropdown panel, portaled into a CDK overlay on open. */
|
|
139
|
+
dropdownTemplate = viewChild.required('dropdownTemplate');
|
|
140
|
+
/** Normalized panel max-height as a CSS length string. */
|
|
141
|
+
panelMaxHeightValue = computed(() => {
|
|
142
|
+
const value = this.panelMaxHeight();
|
|
143
|
+
return typeof value === 'number' ? `${value}px` : value;
|
|
144
|
+
}, ...(ngDevMode ? [{ debugName: "panelMaxHeightValue" }] : []));
|
|
123
145
|
/** Current search term typed by the user */
|
|
124
146
|
searchTerm = signal('', ...(ngDevMode ? [{ debugName: "searchTerm" }] : []));
|
|
125
147
|
/** Whether the dropdown is open */
|
|
@@ -152,24 +174,14 @@ class TnAutocompleteComponent {
|
|
|
152
174
|
hasResults = computed(() => this.filteredOptions().length > 0, ...(ngDevMode ? [{ debugName: "hasResults" }] : []));
|
|
153
175
|
onChange = (_value) => { };
|
|
154
176
|
onTouched = () => { };
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
};
|
|
164
|
-
setTimeout(() => {
|
|
165
|
-
document.addEventListener('click', listener);
|
|
166
|
-
}, 0);
|
|
167
|
-
return () => {
|
|
168
|
-
document.removeEventListener('click', listener);
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
return undefined;
|
|
172
|
-
});
|
|
177
|
+
/** Live overlay holding the dropdown panel, or undefined when closed. */
|
|
178
|
+
overlayRef;
|
|
179
|
+
/** Subscriptions tied to the current overlay; torn down on close. */
|
|
180
|
+
overlaySubs = [];
|
|
181
|
+
ngOnDestroy() {
|
|
182
|
+
// If the component is destroyed while the dropdown is open (e.g. router
|
|
183
|
+
// navigates away), close() never runs — clean up the overlay directly.
|
|
184
|
+
this.detachOverlay();
|
|
173
185
|
}
|
|
174
186
|
// ── ControlValueAccessor ──
|
|
175
187
|
writeValue(value) {
|
|
@@ -196,12 +208,17 @@ class TnAutocompleteComponent {
|
|
|
196
208
|
this.searchTerm.set(value);
|
|
197
209
|
this.highlightedIndex.set(-1);
|
|
198
210
|
if (!this.isOpen()) {
|
|
199
|
-
this.
|
|
211
|
+
this.open();
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
// Filtering changes the panel's height; nudge CDK to re-evaluate the
|
|
215
|
+
// anchored position so it stays attached to the input.
|
|
216
|
+
this.overlayRef?.updatePosition();
|
|
200
217
|
}
|
|
201
218
|
}
|
|
202
219
|
onFocus() {
|
|
203
220
|
if (!this.isDisabled()) {
|
|
204
|
-
this.
|
|
221
|
+
this.open();
|
|
205
222
|
}
|
|
206
223
|
}
|
|
207
224
|
onBlur() {
|
|
@@ -236,7 +253,7 @@ class TnAutocompleteComponent {
|
|
|
236
253
|
case 'ArrowDown':
|
|
237
254
|
event.preventDefault();
|
|
238
255
|
if (!this.isOpen()) {
|
|
239
|
-
this.
|
|
256
|
+
this.open();
|
|
240
257
|
}
|
|
241
258
|
else {
|
|
242
259
|
this.highlightedIndex.update((i) => i < options.length - 1 ? i + 1 : 0);
|
|
@@ -271,26 +288,81 @@ class TnAutocompleteComponent {
|
|
|
271
288
|
this.optionSelected.emit(option);
|
|
272
289
|
this.close();
|
|
273
290
|
}
|
|
291
|
+
open() {
|
|
292
|
+
if (this.isDisabled() || this.isOpen()) {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
this.isOpen.set(true);
|
|
296
|
+
this.attachOverlay();
|
|
297
|
+
}
|
|
274
298
|
close() {
|
|
275
299
|
this.isOpen.set(false);
|
|
276
300
|
this.highlightedIndex.set(-1);
|
|
301
|
+
this.detachOverlay();
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Attach the dropdown panel as a CDK overlay anchored to the input.
|
|
305
|
+
*
|
|
306
|
+
* Why an overlay (vs. an inline absolutely-positioned panel): the panel is
|
|
307
|
+
* appended to the overlay container on `document.body`, so it can never be
|
|
308
|
+
* clipped by an ancestor's `overflow: hidden`/`auto` (e.g. a surrounding
|
|
309
|
+
* card). CDK also flips the panel above the input near the viewport edge and
|
|
310
|
+
* matches its width to the input. Mirrors TnSelectComponent's approach.
|
|
311
|
+
*/
|
|
312
|
+
attachOverlay() {
|
|
313
|
+
const anchor = this.inputEl()?.nativeElement;
|
|
314
|
+
if (!anchor) {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const positionStrategy = this.overlay
|
|
318
|
+
.position()
|
|
319
|
+
.flexibleConnectedTo(anchor)
|
|
320
|
+
.withPositions([
|
|
321
|
+
{ originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top', offsetY: 4 },
|
|
322
|
+
{ originX: 'start', originY: 'top', overlayX: 'start', overlayY: 'bottom', offsetY: -4 },
|
|
323
|
+
]);
|
|
324
|
+
this.overlayRef = this.overlay.create({
|
|
325
|
+
positionStrategy,
|
|
326
|
+
scrollStrategy: this.overlay.scrollStrategies.reposition(),
|
|
327
|
+
hasBackdrop: false,
|
|
328
|
+
width: anchor.offsetWidth,
|
|
329
|
+
});
|
|
330
|
+
this.overlayRef.attach(new TemplatePortal(this.dropdownTemplate(), this.viewContainerRef));
|
|
331
|
+
// Non-intercepting click-outside: the pointer event still reaches whatever
|
|
332
|
+
// the user clicked; we just notice and close. Ignore targets inside the
|
|
333
|
+
// host (the input itself) so we don't fight the focus/typing handlers.
|
|
334
|
+
this.overlaySubs.push(this.overlayRef.outsidePointerEvents().subscribe((event) => {
|
|
335
|
+
const target = event.target;
|
|
336
|
+
if (target && this.elementRef.nativeElement.contains(target)) {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
this.close();
|
|
340
|
+
}));
|
|
341
|
+
}
|
|
342
|
+
detachOverlay() {
|
|
343
|
+
this.overlaySubs.forEach((sub) => sub.unsubscribe());
|
|
344
|
+
this.overlaySubs = [];
|
|
345
|
+
this.overlayRef?.dispose();
|
|
346
|
+
this.overlayRef = undefined;
|
|
277
347
|
}
|
|
278
348
|
scrollToHighlighted() {
|
|
279
349
|
const idx = this.highlightedIndex();
|
|
280
|
-
|
|
281
|
-
|
|
350
|
+
// The panel lives in the CDK overlay (outside the host), so scope the query
|
|
351
|
+
// to the overlay element rather than the host element.
|
|
352
|
+
const overlayEl = this.overlayRef?.overlayElement;
|
|
353
|
+
const options = overlayEl?.querySelectorAll('.tn-autocomplete__option');
|
|
282
354
|
if (options?.[idx]?.scrollIntoView) {
|
|
283
355
|
options[idx].scrollIntoView({ block: 'nearest' });
|
|
284
356
|
}
|
|
285
357
|
}
|
|
286
358
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnAutocompleteComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
287
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnAutocompleteComponent, isStandalone: true, selector: "tn-autocomplete", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, displayWith: { classPropertyName: "displayWith", publicName: "displayWith", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, requireSelection: { classPropertyName: "requireSelection", publicName: "requireSelection", isSignal: true, isRequired: false, transformFunction: null }, filterFn: { classPropertyName: "filterFn", publicName: "filterFn", isSignal: true, isRequired: false, transformFunction: null }, noResultsText: { classPropertyName: "noResultsText", publicName: "noResultsText", isSignal: true, isRequired: false, transformFunction: null }, maxResults: { classPropertyName: "maxResults", publicName: "maxResults", isSignal: true, isRequired: false, transformFunction: null }, testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { optionSelected: "optionSelected" }, providers: [
|
|
359
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnAutocompleteComponent, isStandalone: true, selector: "tn-autocomplete", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, displayWith: { classPropertyName: "displayWith", publicName: "displayWith", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, requireSelection: { classPropertyName: "requireSelection", publicName: "requireSelection", isSignal: true, isRequired: false, transformFunction: null }, filterFn: { classPropertyName: "filterFn", publicName: "filterFn", isSignal: true, isRequired: false, transformFunction: null }, noResultsText: { classPropertyName: "noResultsText", publicName: "noResultsText", isSignal: true, isRequired: false, transformFunction: null }, maxResults: { classPropertyName: "maxResults", publicName: "maxResults", isSignal: true, isRequired: false, transformFunction: null }, panelMaxHeight: { classPropertyName: "panelMaxHeight", publicName: "panelMaxHeight", isSignal: true, isRequired: false, transformFunction: null }, testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { optionSelected: "optionSelected" }, providers: [
|
|
288
360
|
{
|
|
289
361
|
provide: NG_VALUE_ACCESSOR,
|
|
290
362
|
useExisting: forwardRef(() => TnAutocompleteComponent),
|
|
291
363
|
multi: true,
|
|
292
364
|
},
|
|
293
|
-
], viewQueries: [{ propertyName: "inputEl", first: true, predicate: ["inputEl"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"tn-autocomplete\" [tnTestId]=\"testId()\">\n <input\n #inputEl\n type=\"text\"\n class=\"tn-autocomplete__input\"\n role=\"combobox\"\n autocomplete=\"off\"\n [class.open]=\"isOpen()\"\n [class.disabled]=\"isDisabled()\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"isDisabled()\"\n [value]=\"searchTerm()\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-autocomplete]=\"'list'\"\n [attr.aria-controls]=\"isOpen() ? uid + '-dropdown' : null\"\n [attr.aria-activedescendant]=\"highlightedIndex() >= 0 ? uid + '-option-' + highlightedIndex() : null\"\n (input)=\"onInput($event)\"\n (focus)=\"onFocus()\"\n (blur)=\"onBlur()\"\n (keydown)=\"onKeydown($event)\" />\n\n
|
|
365
|
+
], viewQueries: [{ propertyName: "inputEl", first: true, predicate: ["inputEl"], descendants: true, isSignal: true }, { propertyName: "dropdownTemplate", first: true, predicate: ["dropdownTemplate"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"tn-autocomplete\" [tnTestId]=\"testId()\">\n <input\n #inputEl\n type=\"text\"\n class=\"tn-autocomplete__input\"\n role=\"combobox\"\n autocomplete=\"off\"\n [class.open]=\"isOpen()\"\n [class.disabled]=\"isDisabled()\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"isDisabled()\"\n [value]=\"searchTerm()\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-autocomplete]=\"'list'\"\n [attr.aria-controls]=\"isOpen() ? uid + '-dropdown' : null\"\n [attr.aria-owns]=\"isOpen() ? uid + '-dropdown' : null\"\n [attr.aria-activedescendant]=\"highlightedIndex() >= 0 ? uid + '-option-' + highlightedIndex() : null\"\n (input)=\"onInput($event)\"\n (focus)=\"onFocus()\"\n (blur)=\"onBlur()\"\n (keydown)=\"onKeydown($event)\" />\n</div>\n\n<!-- Dropdown panel \u2014 portaled via CDK Overlay so it escapes parent\n overflow/clipping. Attach/detach (driven by open()/close()) controls\n visibility; the input keeps DOM focus throughout. -->\n<ng-template #dropdownTemplate>\n <div\n class=\"tn-autocomplete__dropdown\"\n role=\"listbox\"\n [style.--tn-autocomplete-panel-max-height]=\"panelMaxHeightValue()\"\n [attr.id]=\"uid + '-dropdown'\">\n\n @if (hasResults()) {\n @for (option of filteredOptions(); track $index) {\n <div\n class=\"tn-autocomplete__option\"\n role=\"option\"\n tabindex=\"-1\"\n [class.highlighted]=\"highlightedIndex() === $index\"\n [attr.id]=\"uid + '-option-' + $index\"\n [attr.aria-selected]=\"highlightedIndex() === $index\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"onOptionClick(option)\"\n (keydown.enter)=\"onOptionClick(option)\">\n {{ displayWith()(option) }}\n </div>\n }\n } @else {\n <div class=\"tn-autocomplete__no-results\">\n {{ noResultsText() }}\n </div>\n }\n </div>\n</ng-template>\n", styles: [".tn-autocomplete{position:relative;width:100%;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif}.tn-autocomplete__input{display:block;width:100%;min-height:2.5rem;padding:.5rem .75rem;background-color:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;color:var(--tn-fg1, #212529);font-size:1rem;font-family:inherit;outline:none;box-sizing:border-box;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}.tn-autocomplete__input::placeholder{color:var(--tn-alt-fg1, #999)}.tn-autocomplete__input:hover:not(.disabled){border-color:var(--tn-primary, #007bff)}.tn-autocomplete__input:focus-visible,.tn-autocomplete__input.open{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px color-mix(in srgb,var(--tn-primary, #007bff) 25%,transparent)}.tn-autocomplete__input.disabled{background-color:var(--tn-alt-bg1, #f8f9fa);color:var(--tn-fg2, #6c757d);cursor:not-allowed;opacity:.6}.tn-autocomplete__dropdown{background-color:var(--tn-bg2, #f5f5f5);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;box-shadow:0 4px 6px -1px #0000001a,0 2px 4px -1px #0000000f;max-height:var(--tn-autocomplete-panel-max-height, 320px);overflow-y:auto;padding:.25rem 0}.tn-autocomplete__option{display:flex;align-items:center;padding:.5rem .75rem;cursor:pointer;color:var(--tn-fg1, #212529);transition:background-color .15s ease-in-out}.tn-autocomplete__option:hover{background-color:var(--tn-alt-bg2, #f8f9fa)}.tn-autocomplete__option.highlighted{background-color:var(--tn-alt-bg1, #f8f9fa)}.tn-autocomplete__no-results{padding:1rem .75rem;text-align:center;color:var(--tn-fg2, #6c757d);font-style:italic}@media(prefers-reduced-motion:reduce){.tn-autocomplete__input,.tn-autocomplete__option{transition:none}}\n"], dependencies: [{ kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId"] }] });
|
|
294
366
|
}
|
|
295
367
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnAutocompleteComponent, decorators: [{
|
|
296
368
|
type: Component,
|
|
@@ -300,8 +372,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
300
372
|
useExisting: forwardRef(() => TnAutocompleteComponent),
|
|
301
373
|
multi: true,
|
|
302
374
|
},
|
|
303
|
-
], template: "<div class=\"tn-autocomplete\" [tnTestId]=\"testId()\">\n <input\n #inputEl\n type=\"text\"\n class=\"tn-autocomplete__input\"\n role=\"combobox\"\n autocomplete=\"off\"\n [class.open]=\"isOpen()\"\n [class.disabled]=\"isDisabled()\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"isDisabled()\"\n [value]=\"searchTerm()\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-autocomplete]=\"'list'\"\n [attr.aria-controls]=\"isOpen() ? uid + '-dropdown' : null\"\n [attr.aria-activedescendant]=\"highlightedIndex() >= 0 ? uid + '-option-' + highlightedIndex() : null\"\n (input)=\"onInput($event)\"\n (focus)=\"onFocus()\"\n (blur)=\"onBlur()\"\n (keydown)=\"onKeydown($event)\" />\n\n
|
|
304
|
-
}],
|
|
375
|
+
], template: "<div class=\"tn-autocomplete\" [tnTestId]=\"testId()\">\n <input\n #inputEl\n type=\"text\"\n class=\"tn-autocomplete__input\"\n role=\"combobox\"\n autocomplete=\"off\"\n [class.open]=\"isOpen()\"\n [class.disabled]=\"isDisabled()\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"isDisabled()\"\n [value]=\"searchTerm()\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-autocomplete]=\"'list'\"\n [attr.aria-controls]=\"isOpen() ? uid + '-dropdown' : null\"\n [attr.aria-owns]=\"isOpen() ? uid + '-dropdown' : null\"\n [attr.aria-activedescendant]=\"highlightedIndex() >= 0 ? uid + '-option-' + highlightedIndex() : null\"\n (input)=\"onInput($event)\"\n (focus)=\"onFocus()\"\n (blur)=\"onBlur()\"\n (keydown)=\"onKeydown($event)\" />\n</div>\n\n<!-- Dropdown panel \u2014 portaled via CDK Overlay so it escapes parent\n overflow/clipping. Attach/detach (driven by open()/close()) controls\n visibility; the input keeps DOM focus throughout. -->\n<ng-template #dropdownTemplate>\n <div\n class=\"tn-autocomplete__dropdown\"\n role=\"listbox\"\n [style.--tn-autocomplete-panel-max-height]=\"panelMaxHeightValue()\"\n [attr.id]=\"uid + '-dropdown'\">\n\n @if (hasResults()) {\n @for (option of filteredOptions(); track $index) {\n <div\n class=\"tn-autocomplete__option\"\n role=\"option\"\n tabindex=\"-1\"\n [class.highlighted]=\"highlightedIndex() === $index\"\n [attr.id]=\"uid + '-option-' + $index\"\n [attr.aria-selected]=\"highlightedIndex() === $index\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"onOptionClick(option)\"\n (keydown.enter)=\"onOptionClick(option)\">\n {{ displayWith()(option) }}\n </div>\n }\n } @else {\n <div class=\"tn-autocomplete__no-results\">\n {{ noResultsText() }}\n </div>\n }\n </div>\n</ng-template>\n", styles: [".tn-autocomplete{position:relative;width:100%;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif}.tn-autocomplete__input{display:block;width:100%;min-height:2.5rem;padding:.5rem .75rem;background-color:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;color:var(--tn-fg1, #212529);font-size:1rem;font-family:inherit;outline:none;box-sizing:border-box;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}.tn-autocomplete__input::placeholder{color:var(--tn-alt-fg1, #999)}.tn-autocomplete__input:hover:not(.disabled){border-color:var(--tn-primary, #007bff)}.tn-autocomplete__input:focus-visible,.tn-autocomplete__input.open{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px color-mix(in srgb,var(--tn-primary, #007bff) 25%,transparent)}.tn-autocomplete__input.disabled{background-color:var(--tn-alt-bg1, #f8f9fa);color:var(--tn-fg2, #6c757d);cursor:not-allowed;opacity:.6}.tn-autocomplete__dropdown{background-color:var(--tn-bg2, #f5f5f5);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;box-shadow:0 4px 6px -1px #0000001a,0 2px 4px -1px #0000000f;max-height:var(--tn-autocomplete-panel-max-height, 320px);overflow-y:auto;padding:.25rem 0}.tn-autocomplete__option{display:flex;align-items:center;padding:.5rem .75rem;cursor:pointer;color:var(--tn-fg1, #212529);transition:background-color .15s ease-in-out}.tn-autocomplete__option:hover{background-color:var(--tn-alt-bg2, #f8f9fa)}.tn-autocomplete__option.highlighted{background-color:var(--tn-alt-bg1, #f8f9fa)}.tn-autocomplete__no-results{padding:1rem .75rem;text-align:center;color:var(--tn-fg2, #6c757d);font-style:italic}@media(prefers-reduced-motion:reduce){.tn-autocomplete__input,.tn-autocomplete__option{transition:none}}\n"] }]
|
|
376
|
+
}], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], displayWith: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayWith", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], requireSelection: [{ type: i0.Input, args: [{ isSignal: true, alias: "requireSelection", required: false }] }], filterFn: [{ type: i0.Input, args: [{ isSignal: true, alias: "filterFn", required: false }] }], noResultsText: [{ type: i0.Input, args: [{ isSignal: true, alias: "noResultsText", required: false }] }], maxResults: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxResults", required: false }] }], panelMaxHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "panelMaxHeight", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], optionSelected: [{ type: i0.Output, args: ["optionSelected"] }], inputEl: [{ type: i0.ViewChild, args: ['inputEl', { isSignal: true }] }], dropdownTemplate: [{ type: i0.ViewChild, args: ['dropdownTemplate', { isSignal: true }] }] } });
|
|
305
377
|
|
|
306
378
|
/**
|
|
307
379
|
* Harness for interacting with tn-autocomplete in tests.
|
|
@@ -415,8 +487,11 @@ class TnAutocompleteHarness extends ComponentHarness {
|
|
|
415
487
|
* ```
|
|
416
488
|
*/
|
|
417
489
|
async isOpen() {
|
|
418
|
-
|
|
419
|
-
|
|
490
|
+
// The dropdown panel is rendered in a CDK overlay (outside the host), so we
|
|
491
|
+
// read the open state from the `.open` class the input carries while the
|
|
492
|
+
// panel is attached rather than looking for the panel in the host subtree.
|
|
493
|
+
const input = await this._input();
|
|
494
|
+
return input.hasClass('open');
|
|
420
495
|
}
|
|
421
496
|
/**
|
|
422
497
|
* Checks whether the autocomplete input is disabled.
|
|
@@ -448,7 +523,9 @@ class TnAutocompleteHarness extends ComponentHarness {
|
|
|
448
523
|
* ```
|
|
449
524
|
*/
|
|
450
525
|
async getOptions() {
|
|
451
|
-
|
|
526
|
+
// Options render inside a CDK overlay (outside the host element), so search
|
|
527
|
+
// the document root rather than the harness-local subtree.
|
|
528
|
+
const options = await this.documentRootLocatorFactory().locatorForAll('.tn-autocomplete__option')();
|
|
452
529
|
const labels = [];
|
|
453
530
|
for (const option of options) {
|
|
454
531
|
labels.push((await option.text()).trim());
|
|
@@ -476,7 +553,9 @@ class TnAutocompleteHarness extends ComponentHarness {
|
|
|
476
553
|
async selectOption(filter) {
|
|
477
554
|
const input = await this._input();
|
|
478
555
|
await input.focus();
|
|
479
|
-
|
|
556
|
+
// Options render inside a CDK overlay (outside the host element), so search
|
|
557
|
+
// the document root rather than the harness-local subtree.
|
|
558
|
+
const options = await this.documentRootLocatorFactory().locatorForAll('.tn-autocomplete__option')();
|
|
480
559
|
for (const option of options) {
|
|
481
560
|
const text = (await option.text()).trim();
|
|
482
561
|
const matches = filter instanceof RegExp ? filter.test(text) : text === filter;
|
|
@@ -3173,52 +3252,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
3173
3252
|
}]
|
|
3174
3253
|
}], propDecorators: { menu: [{ type: i0.Input, args: [{ isSignal: true, alias: "tnMenuTriggerFor", required: true }] }], tnMenuPosition: [{ type: i0.Input, args: [{ isSignal: true, alias: "tnMenuPosition", required: false }] }] } });
|
|
3175
3254
|
|
|
3176
|
-
/**
|
|
3177
|
-
* Renders a single `TnMenuItem` (separator, leaf button, or submenu trigger).
|
|
3178
|
-
*
|
|
3179
|
-
* Used recursively by `<tn-menu>` so the same authoring covers root items and
|
|
3180
|
-
* arbitrarily-nested submenus — adding a new menu-item property (badge,
|
|
3181
|
-
* tooltip, etc.) only needs to be wired up here, not duplicated across every
|
|
3182
|
-
* nesting level.
|
|
3183
|
-
*
|
|
3184
|
-
* **Why a component, not an ng-template:** earlier prototypes used a recursive
|
|
3185
|
-
* `<ng-template #menuItem>` outletted via `[ngTemplateOutlet]`. That fails
|
|
3186
|
-
* because the embedded view's DI chain resolves through the template's
|
|
3187
|
-
* declaration site, not the outlet anchor — `CdkMenuItem` then can't find
|
|
3188
|
-
* `MENU_STACK`, which is provided per-`CdkMenu`. A real component's DI walks
|
|
3189
|
-
* up through its host element instead, so each renderer instance sees the
|
|
3190
|
-
* `CdkMenu` it's actually rendered under (root or any nested submenu).
|
|
3191
|
-
*
|
|
3192
|
-
* @internal Implementation detail of `<tn-menu>`. Not re-exported from the
|
|
3193
|
-
* menu barrel or `public-api.ts` because it depends on injecting
|
|
3194
|
-
* `TnMenuComponent` from the host DI scope and cannot be used standalone.
|
|
3195
|
-
*/
|
|
3196
|
-
class TnMenuItemRendererComponent {
|
|
3197
|
-
item = input.required(...(ngDevMode ? [{ debugName: "item" }] : []));
|
|
3198
|
-
// The renderer is rendered inside a CDK overlay portal, but the portal is
|
|
3199
|
-
// created from a template defined inside `<tn-menu>` — so the view-DI chain
|
|
3200
|
-
// still walks back to the host `<tn-menu>` component, which means inject()
|
|
3201
|
-
// here resolves it correctly.
|
|
3202
|
-
menu = inject(TnMenuComponent);
|
|
3203
|
-
onClick() {
|
|
3204
|
-
this.menu.onMenuItemClick(this.item());
|
|
3205
|
-
}
|
|
3206
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnMenuItemRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3207
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnMenuItemRendererComponent, isStandalone: true, selector: "tn-menu-item-renderer", inputs: { item: { classPropertyName: "item", publicName: "item", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: "@if (item().separator) {\n <div class=\"tn-menu-separator\" role=\"separator\"></div>\n} @else if (!item().children || item().children!.length === 0) {\n <button\n cdkMenuItem\n class=\"tn-menu-item\"\n type=\"button\"\n [tnTestId]=\"item().testId ?? 'menu-item-' + item().id\"\n [disabled]=\"item().disabled\"\n [cdkMenuItemDisabled]=\"!!item().disabled\"\n [class.disabled]=\"item().disabled\"\n [class.tn-menu-item--selected]=\"item().selected\"\n [attr.aria-current]=\"item().selected ? 'true' : null\"\n (click)=\"onClick()\"\n >\n @if (item().icon) {\n <tn-icon size=\"sm\" class=\"tn-menu-item-icon\" [name]=\"item().icon!\" [library]=\"item().iconLibrary\" />\n }\n <span class=\"tn-menu-item-label\">{{ item().label }}</span>\n @if (item().shortcut) {\n <span class=\"tn-menu-item-shortcut\">{{ item().shortcut }}</span>\n }\n </button>\n} @else {\n <button\n cdkMenuItem\n class=\"tn-menu-item tn-menu-item--nested\"\n type=\"button\"\n [tnTestId]=\"item().testId ?? 'menu-item-' + item().id\"\n [cdkMenuTriggerFor]=\"submenuTpl\"\n [disabled]=\"item().disabled\"\n [cdkMenuItemDisabled]=\"!!item().disabled\"\n [class.disabled]=\"item().disabled\"\n [class.tn-menu-item--selected]=\"item().selected\"\n [attr.aria-current]=\"item().selected ? 'true' : null\"\n >\n @if (item().icon) {\n <tn-icon size=\"sm\" class=\"tn-menu-item-icon\" [name]=\"item().icon!\" [library]=\"item().iconLibrary\" />\n }\n <span class=\"tn-menu-item-label\">{{ item().label }}</span>\n @if (item().shortcut) {\n <span class=\"tn-menu-item-shortcut\">{{ item().shortcut }}</span>\n }\n <span class=\"tn-menu-item-arrow\">\u25B6</span>\n </button>\n\n <ng-template #submenuTpl>\n <div class=\"tn-menu tn-menu--nested\" cdkMenu>\n @for (child of item().children!; track child.id) {\n <tn-menu-item-renderer [item]=\"child\" />\n }\n </div>\n </ng-template>\n}\n", styles: [".tn-menu-container{display:inline-block;position:relative}.tn-menu-container.tn-menu-container--context{display:block;width:100%;height:100%;cursor:context-menu}.tn-menu-trigger{display:flex;align-items:center;gap:8px;padding:8px 16px;background:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #e0e0e0);border-radius:4px;color:var(--tn-fg1, #333333);cursor:pointer;font-size:14px;transition:all .2s ease}.tn-menu-trigger:hover:not(.disabled){background:var(--tn-bg2, #f5f5f5);border-color:var(--tn-lines, #cccccc)}.tn-menu-trigger:focus{outline:2px solid var(--tn-primary, #007bff);outline-offset:2px}.tn-menu-trigger.disabled{opacity:.5;cursor:not-allowed}.tn-menu-arrow{font-size:12px;transition:transform .2s ease}.tn-menu-arrow.tn-menu-arrow--up{transform:rotate(180deg)}.tn-menu{display:flex;flex-direction:column;background:var(--tn-bg2, #f5f5f5);border:1px solid var(--tn-lines, #e0e0e0);border-radius:4px;box-shadow:0 8px 24px #00000026,0 4px 8px #0000001a;min-width:160px;max-width:300px;padding:4px 0;z-index:1000}.tn-menu-item{display:flex;align-items:center;gap:8px;width:100%;padding:8px 16px;border:none;background:transparent;color:var(--tn-fg1, #333333);cursor:pointer;font-size:14px;text-align:left;transition:background-color .2s ease}.tn-menu-item:hover:not(.disabled){background:var(--tn-alt-bg2, #e8f4fd)!important}.tn-menu-item:focus{outline:none;background:var(--tn-alt-bg2, #e8f4fd)}.tn-menu-item[aria-selected=true]{background:var(--tn-alt-bg2, #e8f4fd)}.tn-menu-item.tn-menu-item--selected{background:var(--tn-alt-bg2, #e8f4fd);font-weight:600;color:var(--tn-primary, #007bff)}.tn-menu-item.disabled{opacity:.5;cursor:not-allowed}.tn-menu-item.disabled:hover{background:transparent!important}.tn-menu-item-icon{font-size:1rem;width:16px;text-align:center}.tn-menu-item-label{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.tn-menu-item-shortcut{font-size:12px;color:var(--tn-fg2, #666666);margin-left:auto;padding-left:16px;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;font-weight:400;opacity:.7}.tn-menu-item-arrow{font-size:10px;margin-left:8px;color:var(--tn-fg2, #666666);transition:transform .2s ease;flex-shrink:0}.tn-menu-item--nested{position:relative}.tn-menu-item--nested:hover .tn-menu-item-arrow{color:var(--tn-fg1, #333333)}.tn-menu-separator{height:1px;background:var(--tn-lines, #e0e0e0);margin:4px 0}.tn-menu--nested{margin-left:4px;box-shadow:0 8px 24px #00000026,0 4px 8px #0000001a;border-radius:4px}.tn-menu-context-content{width:100%;height:100%}.tn-menu-context-content:hover:before{content:\"\";position:absolute;inset:0;background:rgba(var(--tn-primary-rgb, 0, 123, 255),.05);pointer-events:none;border:1px dashed rgba(var(--tn-primary-rgb, 0, 123, 255),.3);border-radius:4px}\n"], dependencies: [{ kind: "component", type: i0.forwardRef(() => TnMenuItemRendererComponent), selector: "tn-menu-item-renderer", inputs: ["item"] }, { kind: "ngmodule", type: i0.forwardRef(() => CommonModule) }, { kind: "directive", type: i0.forwardRef(() => CdkMenu), selector: "[cdkMenu]", outputs: ["closed"], exportAs: ["cdkMenu"] }, { kind: "directive", type: i0.forwardRef(() => CdkMenuItem), selector: "[cdkMenuItem]", inputs: ["cdkMenuItemDisabled", "cdkMenuitemTypeaheadLabel"], outputs: ["cdkMenuItemTriggered"], exportAs: ["cdkMenuItem"] }, { kind: "directive", type: i0.forwardRef(() => CdkMenuTrigger), selector: "[cdkMenuTriggerFor]", inputs: ["cdkMenuTriggerFor", "cdkMenuPosition", "cdkMenuTriggerData", "cdkMenuTriggerTransformOriginOn"], outputs: ["cdkMenuOpened", "cdkMenuClosed"], exportAs: ["cdkMenuTriggerFor"] }, { kind: "component", type: i0.forwardRef(() => TnIconComponent), selector: "tn-icon", inputs: ["name", "size", "color", "tooltip", "ariaLabel", "library", "fullSize", "customSize"] }, { kind: "directive", type: i0.forwardRef(() => TnTestIdDirective), selector: "[tnTestId]", inputs: ["tnTestId"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3208
|
-
}
|
|
3209
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnMenuItemRendererComponent, decorators: [{
|
|
3210
|
-
type: Component,
|
|
3211
|
-
args: [{ selector: 'tn-menu-item-renderer', standalone: true, imports: [
|
|
3212
|
-
CommonModule,
|
|
3213
|
-
CdkMenu,
|
|
3214
|
-
CdkMenuItem,
|
|
3215
|
-
CdkMenuTrigger,
|
|
3216
|
-
TnIconComponent,
|
|
3217
|
-
TnTestIdDirective,
|
|
3218
|
-
forwardRef(() => TnMenuItemRendererComponent),
|
|
3219
|
-
], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (item().separator) {\n <div class=\"tn-menu-separator\" role=\"separator\"></div>\n} @else if (!item().children || item().children!.length === 0) {\n <button\n cdkMenuItem\n class=\"tn-menu-item\"\n type=\"button\"\n [tnTestId]=\"item().testId ?? 'menu-item-' + item().id\"\n [disabled]=\"item().disabled\"\n [cdkMenuItemDisabled]=\"!!item().disabled\"\n [class.disabled]=\"item().disabled\"\n [class.tn-menu-item--selected]=\"item().selected\"\n [attr.aria-current]=\"item().selected ? 'true' : null\"\n (click)=\"onClick()\"\n >\n @if (item().icon) {\n <tn-icon size=\"sm\" class=\"tn-menu-item-icon\" [name]=\"item().icon!\" [library]=\"item().iconLibrary\" />\n }\n <span class=\"tn-menu-item-label\">{{ item().label }}</span>\n @if (item().shortcut) {\n <span class=\"tn-menu-item-shortcut\">{{ item().shortcut }}</span>\n }\n </button>\n} @else {\n <button\n cdkMenuItem\n class=\"tn-menu-item tn-menu-item--nested\"\n type=\"button\"\n [tnTestId]=\"item().testId ?? 'menu-item-' + item().id\"\n [cdkMenuTriggerFor]=\"submenuTpl\"\n [disabled]=\"item().disabled\"\n [cdkMenuItemDisabled]=\"!!item().disabled\"\n [class.disabled]=\"item().disabled\"\n [class.tn-menu-item--selected]=\"item().selected\"\n [attr.aria-current]=\"item().selected ? 'true' : null\"\n >\n @if (item().icon) {\n <tn-icon size=\"sm\" class=\"tn-menu-item-icon\" [name]=\"item().icon!\" [library]=\"item().iconLibrary\" />\n }\n <span class=\"tn-menu-item-label\">{{ item().label }}</span>\n @if (item().shortcut) {\n <span class=\"tn-menu-item-shortcut\">{{ item().shortcut }}</span>\n }\n <span class=\"tn-menu-item-arrow\">\u25B6</span>\n </button>\n\n <ng-template #submenuTpl>\n <div class=\"tn-menu tn-menu--nested\" cdkMenu>\n @for (child of item().children!; track child.id) {\n <tn-menu-item-renderer [item]=\"child\" />\n }\n </div>\n </ng-template>\n}\n", styles: [".tn-menu-container{display:inline-block;position:relative}.tn-menu-container.tn-menu-container--context{display:block;width:100%;height:100%;cursor:context-menu}.tn-menu-trigger{display:flex;align-items:center;gap:8px;padding:8px 16px;background:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #e0e0e0);border-radius:4px;color:var(--tn-fg1, #333333);cursor:pointer;font-size:14px;transition:all .2s ease}.tn-menu-trigger:hover:not(.disabled){background:var(--tn-bg2, #f5f5f5);border-color:var(--tn-lines, #cccccc)}.tn-menu-trigger:focus{outline:2px solid var(--tn-primary, #007bff);outline-offset:2px}.tn-menu-trigger.disabled{opacity:.5;cursor:not-allowed}.tn-menu-arrow{font-size:12px;transition:transform .2s ease}.tn-menu-arrow.tn-menu-arrow--up{transform:rotate(180deg)}.tn-menu{display:flex;flex-direction:column;background:var(--tn-bg2, #f5f5f5);border:1px solid var(--tn-lines, #e0e0e0);border-radius:4px;box-shadow:0 8px 24px #00000026,0 4px 8px #0000001a;min-width:160px;max-width:300px;padding:4px 0;z-index:1000}.tn-menu-item{display:flex;align-items:center;gap:8px;width:100%;padding:8px 16px;border:none;background:transparent;color:var(--tn-fg1, #333333);cursor:pointer;font-size:14px;text-align:left;transition:background-color .2s ease}.tn-menu-item:hover:not(.disabled){background:var(--tn-alt-bg2, #e8f4fd)!important}.tn-menu-item:focus{outline:none;background:var(--tn-alt-bg2, #e8f4fd)}.tn-menu-item[aria-selected=true]{background:var(--tn-alt-bg2, #e8f4fd)}.tn-menu-item.tn-menu-item--selected{background:var(--tn-alt-bg2, #e8f4fd);font-weight:600;color:var(--tn-primary, #007bff)}.tn-menu-item.disabled{opacity:.5;cursor:not-allowed}.tn-menu-item.disabled:hover{background:transparent!important}.tn-menu-item-icon{font-size:1rem;width:16px;text-align:center}.tn-menu-item-label{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.tn-menu-item-shortcut{font-size:12px;color:var(--tn-fg2, #666666);margin-left:auto;padding-left:16px;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;font-weight:400;opacity:.7}.tn-menu-item-arrow{font-size:10px;margin-left:8px;color:var(--tn-fg2, #666666);transition:transform .2s ease;flex-shrink:0}.tn-menu-item--nested{position:relative}.tn-menu-item--nested:hover .tn-menu-item-arrow{color:var(--tn-fg1, #333333)}.tn-menu-separator{height:1px;background:var(--tn-lines, #e0e0e0);margin:4px 0}.tn-menu--nested{margin-left:4px;box-shadow:0 8px 24px #00000026,0 4px 8px #0000001a;border-radius:4px}.tn-menu-context-content{width:100%;height:100%}.tn-menu-context-content:hover:before{content:\"\";position:absolute;inset:0;background:rgba(var(--tn-primary-rgb, 0, 123, 255),.05);pointer-events:none;border:1px dashed rgba(var(--tn-primary-rgb, 0, 123, 255),.3);border-radius:4px}\n"] }]
|
|
3220
|
-
}], propDecorators: { item: [{ type: i0.Input, args: [{ isSignal: true, alias: "item", required: true }] }] } });
|
|
3221
|
-
|
|
3222
3255
|
/**
|
|
3223
3256
|
* Projection-based menu item for use inside `<tn-menu>`.
|
|
3224
3257
|
*
|
|
@@ -3286,27 +3319,42 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
3286
3319
|
}], propDecorators: { id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], iconLibrary: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconLibrary", required: false }] }], shortcut: [{ type: i0.Input, args: [{ isSignal: true, alias: "shortcut", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], selected: [{ type: i0.Input, args: [{ isSignal: true, alias: "selected", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], itemClick: [{ type: i0.Output, args: ["itemClick"] }], content: [{ type: i0.ViewChild, args: ['content', { isSignal: true }] }] } });
|
|
3287
3320
|
|
|
3288
3321
|
/**
|
|
3289
|
-
* Activates CDK menu hover-to-open behavior
|
|
3322
|
+
* Activates CDK menu hover-to-open behavior — and keyboard focus entry — for a
|
|
3323
|
+
* root menu opened via a custom overlay instead of CDK's own `CdkMenuTrigger`.
|
|
3290
3324
|
*
|
|
3291
3325
|
* CDK's hover-to-open for submenu triggers is guarded by `!menuStack.isEmpty()`.
|
|
3292
|
-
* When a CdkMenu is opened via TnMenuTriggerDirective (custom overlay)
|
|
3293
|
-
*
|
|
3294
|
-
* with the stack, disabling hover for its submenu triggers
|
|
3295
|
-
*
|
|
3296
|
-
*
|
|
3297
|
-
*
|
|
3298
|
-
*
|
|
3326
|
+
* When a `CdkMenu` is opened via `TnMenuTriggerDirective` (custom overlay)
|
|
3327
|
+
* rather than `CdkMenuTrigger`, the menu is considered "inline" and doesn't
|
|
3328
|
+
* register with the stack, disabling hover for its submenu triggers and leaving
|
|
3329
|
+
* keyboard focus outside the panel.
|
|
3330
|
+
*
|
|
3331
|
+
* This directive pushes that root menu onto its own stack and moves focus to
|
|
3332
|
+
* the first (or selected) item so arrow-key navigation works immediately.
|
|
3333
|
+
* Submenus — opened by `CdkMenuTrigger` — already push themselves and are
|
|
3334
|
+
* focused by CDK, so for them this directive only installs the skip-disabled
|
|
3335
|
+
* predicate and otherwise stays out of the way.
|
|
3299
3336
|
*/
|
|
3300
3337
|
class TnMenuActivateHoverDirective {
|
|
3301
3338
|
cdkMenu = inject(CdkMenu);
|
|
3302
3339
|
elementRef = inject((ElementRef));
|
|
3303
3340
|
ngAfterContentInit() {
|
|
3341
|
+
// A root inline menu (opened via our custom overlay, not CdkMenuTrigger)
|
|
3342
|
+
// hasn't been pushed onto the stack yet. Submenus opened by CdkMenuTrigger
|
|
3343
|
+
// push themselves in CdkMenuBase.ngAfterContentInit, so the stack is already
|
|
3344
|
+
// non-empty by the time this runs for them — which is how we tell the two
|
|
3345
|
+
// apart without reaching for more CDK internals.
|
|
3304
3346
|
const stack = this.cdkMenu.menuStack;
|
|
3305
|
-
|
|
3347
|
+
const isRootInline = stack.isEmpty();
|
|
3348
|
+
if (isRootInline) {
|
|
3306
3349
|
stack.push(this.cdkMenu);
|
|
3307
3350
|
}
|
|
3308
3351
|
const km = this.cdkMenu.keyManager;
|
|
3309
3352
|
km?.skipPredicate?.((item) => item.disabled);
|
|
3353
|
+
// Only the root inline menu needs manual focus entry. CdkMenuTrigger already
|
|
3354
|
+
// focuses the first item of any submenu it opens, so we must not fight it.
|
|
3355
|
+
if (!isRootInline) {
|
|
3356
|
+
return;
|
|
3357
|
+
}
|
|
3310
3358
|
// Prefer the marked "selected" item when one exists — matches user
|
|
3311
3359
|
// expectation for option pickers (export format, sort key, etc.) where
|
|
3312
3360
|
// reopening the menu should land focus on the current choice, not always
|
|
@@ -3336,6 +3384,76 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
3336
3384
|
standalone: true,
|
|
3337
3385
|
}]
|
|
3338
3386
|
}] });
|
|
3387
|
+
|
|
3388
|
+
/**
|
|
3389
|
+
* Renders one complete menu panel: the `<div cdkMenu>` container plus every
|
|
3390
|
+
* item in it (separators, leaf buttons, and submenu triggers). Used for the
|
|
3391
|
+
* root menu and recursively for each nested submenu.
|
|
3392
|
+
*
|
|
3393
|
+
* **Why this owns the `<div cdkMenu>` (and the items aren't rendered by
|
|
3394
|
+
* `<tn-menu>` directly):** `CdkMenu` discovers its items via
|
|
3395
|
+
* `@ContentChildren(CdkMenuItem, {descendants: true})`, which only sees nodes
|
|
3396
|
+
* within the *same view* as the `cdkMenu` element — it does **not** cross into
|
|
3397
|
+
* a child component's view. An earlier design kept the `<div cdkMenu>` in
|
|
3398
|
+
* `<tn-menu>`'s template and rendered each item through a separate
|
|
3399
|
+
* `<tn-menu-item-renderer>` component; those item buttons lived in the
|
|
3400
|
+
* renderer's view, so `CdkMenu`'s query never found them. The result: an empty
|
|
3401
|
+
* `FocusKeyManager`, no focus on open, and dead arrow keys for the entire
|
|
3402
|
+
* `[items]` API. Keeping the container and its buttons together in this one
|
|
3403
|
+
* component's view is what makes keyboard navigation work.
|
|
3404
|
+
*
|
|
3405
|
+
* **Why a component, not a recursive `<ng-template>`:** an embedded view from
|
|
3406
|
+
* `*ngTemplateOutlet` resolves DI through the template's *declaration* site,
|
|
3407
|
+
* so a nested submenu item would inject the root menu's `MENU_STACK` instead of
|
|
3408
|
+
* its own. A real component resolves DI through its host's insertion point, so
|
|
3409
|
+
* each panel — root or any nesting depth — sees the `CdkMenu` it's actually
|
|
3410
|
+
* rendered under.
|
|
3411
|
+
*
|
|
3412
|
+
* **Projected `<tn-menu-item>` entries** (`projectedItems`) are only passed at
|
|
3413
|
+
* the root; submenus are always driven by `item.children`. They render as
|
|
3414
|
+
* sibling `cdkMenuItem` buttons so they share the same keyboard navigation.
|
|
3415
|
+
*
|
|
3416
|
+
* @internal Implementation detail of `<tn-menu>`. Not re-exported from the
|
|
3417
|
+
* menu barrel or `public-api.ts` because it depends on injecting
|
|
3418
|
+
* `TnMenuComponent` from the host DI scope and cannot be used standalone.
|
|
3419
|
+
*/
|
|
3420
|
+
class TnMenuPanelComponent {
|
|
3421
|
+
/** Items for this menu level (the root array, or a submenu's `children`). */
|
|
3422
|
+
items = input([], ...(ngDevMode ? [{ debugName: "items" }] : []));
|
|
3423
|
+
/** Projected `<tn-menu-item>` children — only supplied for the root panel. */
|
|
3424
|
+
projectedItems = input([], ...(ngDevMode ? [{ debugName: "projectedItems" }] : []));
|
|
3425
|
+
/** Applies the nested-submenu styling/offset. */
|
|
3426
|
+
nested = input(false, ...(ngDevMode ? [{ debugName: "nested" }] : []));
|
|
3427
|
+
// The panel is rendered inside a CDK overlay portal, but the portal's
|
|
3428
|
+
// template is declared inside `<tn-menu>` — so the view-DI chain still walks
|
|
3429
|
+
// back to the host `<tn-menu>` component and inject() resolves it.
|
|
3430
|
+
menu = inject(TnMenuComponent);
|
|
3431
|
+
onItemClick(item) {
|
|
3432
|
+
this.menu.onMenuItemClick(item);
|
|
3433
|
+
}
|
|
3434
|
+
onProjectedClick(item, event) {
|
|
3435
|
+
this.menu.onProjectedItemClick(item, event);
|
|
3436
|
+
}
|
|
3437
|
+
trackByItemId(index, item) {
|
|
3438
|
+
return item.id;
|
|
3439
|
+
}
|
|
3440
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnMenuPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3441
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnMenuPanelComponent, isStandalone: true, selector: "tn-menu-panel", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, projectedItems: { classPropertyName: "projectedItems", publicName: "projectedItems", isSignal: true, isRequired: false, transformFunction: null }, nested: { classPropertyName: "nested", publicName: "nested", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"tn-menu\" cdkMenu tnMenuActivateHover [class.tn-menu--nested]=\"nested()\">\n <!--\n Projected <tn-menu-item> entries (root panel only) render as sibling\n cdkMenuItem buttons so they participate in CdkMenu's keyboard navigation\n (arrow keys, Home/End, type-ahead) alongside the items() array below.\n -->\n @for (projected of projectedItems(); track projected) {\n <button\n cdkMenuItem\n class=\"tn-menu-item\"\n type=\"button\"\n [tnTestId]=\"projected.resolvedTestId()\"\n [disabled]=\"projected.disabled()\"\n [cdkMenuItemDisabled]=\"projected.disabled()\"\n [class.disabled]=\"projected.disabled()\"\n [class.tn-menu-item--selected]=\"projected.selected()\"\n [attr.aria-current]=\"projected.selected() ? 'true' : null\"\n (click)=\"onProjectedClick(projected, $event)\"\n >\n @if (projected.label() !== undefined) {\n @if (projected.icon()) {\n <tn-icon size=\"sm\" class=\"tn-menu-item-icon\" [name]=\"projected.icon()!\" [library]=\"projected.iconLibrary()\" />\n }\n <span class=\"tn-menu-item-label\">{{ projected.label() }}</span>\n @if (projected.shortcut()) {\n <span class=\"tn-menu-item-shortcut\">{{ projected.shortcut() }}</span>\n }\n } @else {\n <ng-container [ngTemplateOutlet]=\"projected.content()\" />\n }\n </button>\n }\n\n @for (item of items(); track trackByItemId($index, item)) {\n @if (item.separator) {\n <div class=\"tn-menu-separator\" role=\"separator\"></div>\n } @else if (!item.children || item.children.length === 0) {\n <button\n cdkMenuItem\n class=\"tn-menu-item\"\n type=\"button\"\n [tnTestId]=\"item.testId ?? 'menu-item-' + item.id\"\n [disabled]=\"item.disabled\"\n [cdkMenuItemDisabled]=\"!!item.disabled\"\n [class.disabled]=\"item.disabled\"\n [class.tn-menu-item--selected]=\"item.selected\"\n [attr.aria-current]=\"item.selected ? 'true' : null\"\n (click)=\"onItemClick(item)\"\n >\n @if (item.icon) {\n <tn-icon size=\"sm\" class=\"tn-menu-item-icon\" [name]=\"item.icon!\" [library]=\"item.iconLibrary\" />\n }\n <span class=\"tn-menu-item-label\">{{ item.label }}</span>\n @if (item.shortcut) {\n <span class=\"tn-menu-item-shortcut\">{{ item.shortcut }}</span>\n }\n </button>\n } @else {\n <button\n cdkMenuItem\n class=\"tn-menu-item tn-menu-item--nested\"\n type=\"button\"\n [tnTestId]=\"item.testId ?? 'menu-item-' + item.id\"\n [cdkMenuTriggerFor]=\"submenuTpl\"\n [disabled]=\"item.disabled\"\n [cdkMenuItemDisabled]=\"!!item.disabled\"\n [class.disabled]=\"item.disabled\"\n [class.tn-menu-item--selected]=\"item.selected\"\n [attr.aria-current]=\"item.selected ? 'true' : null\"\n >\n @if (item.icon) {\n <tn-icon size=\"sm\" class=\"tn-menu-item-icon\" [name]=\"item.icon!\" [library]=\"item.iconLibrary\" />\n }\n <span class=\"tn-menu-item-label\">{{ item.label }}</span>\n @if (item.shortcut) {\n <span class=\"tn-menu-item-shortcut\">{{ item.shortcut }}</span>\n }\n <span class=\"tn-menu-item-arrow\">\u25B6</span>\n </button>\n\n <ng-template #submenuTpl>\n <tn-menu-panel [items]=\"item.children!\" [nested]=\"true\" />\n </ng-template>\n }\n }\n</div>\n", styles: [".tn-menu-container{display:inline-block;position:relative}.tn-menu-container.tn-menu-container--context{display:block;width:100%;height:100%;cursor:context-menu}.tn-menu-trigger{display:flex;align-items:center;gap:8px;padding:8px 16px;background:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #e0e0e0);border-radius:4px;color:var(--tn-fg1, #333333);cursor:pointer;font-size:14px;transition:all .2s ease}.tn-menu-trigger:hover:not(.disabled){background:var(--tn-bg2, #f5f5f5);border-color:var(--tn-lines, #cccccc)}.tn-menu-trigger:focus{outline:2px solid var(--tn-primary, #007bff);outline-offset:2px}.tn-menu-trigger.disabled{opacity:.5;cursor:not-allowed}.tn-menu-arrow{font-size:12px;transition:transform .2s ease}.tn-menu-arrow.tn-menu-arrow--up{transform:rotate(180deg)}.tn-menu{display:flex;flex-direction:column;background:var(--tn-bg2, #f5f5f5);border:1px solid var(--tn-lines, #e0e0e0);border-radius:4px;box-shadow:0 8px 24px #00000026,0 4px 8px #0000001a;min-width:160px;max-width:300px;padding:4px 0;z-index:1000}.tn-menu-item{display:flex;align-items:center;gap:8px;width:100%;padding:8px 16px;border:none;background:transparent;color:var(--tn-fg1, #333333);cursor:pointer;font-size:14px;text-align:left;transition:background-color .2s ease}.tn-menu-item:hover:not(.disabled){background:var(--tn-alt-bg2, #e8f4fd)!important}.tn-menu-item:focus{outline:none;background:var(--tn-alt-bg2, #e8f4fd)}.tn-menu-item[aria-selected=true]{background:var(--tn-alt-bg2, #e8f4fd)}.tn-menu-item.tn-menu-item--selected{background:var(--tn-alt-bg2, #e8f4fd);font-weight:600;color:var(--tn-primary, #007bff)}.tn-menu-item.disabled{opacity:.5;cursor:not-allowed}.tn-menu-item.disabled:hover{background:transparent!important}.tn-menu-item-icon{font-size:1rem;width:16px;text-align:center}.tn-menu-item-label{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.tn-menu-item-shortcut{font-size:12px;color:var(--tn-fg2, #666666);margin-left:auto;padding-left:16px;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;font-weight:400;opacity:.7}.tn-menu-item-arrow{font-size:10px;margin-left:8px;color:var(--tn-fg2, #666666);transition:transform .2s ease;flex-shrink:0}.tn-menu-item--nested{position:relative}.tn-menu-item--nested:hover .tn-menu-item-arrow{color:var(--tn-fg1, #333333)}.tn-menu-separator{height:1px;background:var(--tn-lines, #e0e0e0);margin:4px 0}.tn-menu--nested{margin-left:4px;box-shadow:0 8px 24px #00000026,0 4px 8px #0000001a;border-radius:4px}.tn-menu-context-content{width:100%;height:100%}.tn-menu-context-content:hover:before{content:\"\";position:absolute;inset:0;background:rgba(var(--tn-primary-rgb, 0, 123, 255),.05);pointer-events:none;border:1px dashed rgba(var(--tn-primary-rgb, 0, 123, 255),.3);border-radius:4px}\n"], dependencies: [{ kind: "component", type: i0.forwardRef(() => TnMenuPanelComponent), selector: "tn-menu-panel", inputs: ["items", "projectedItems", "nested"] }, { kind: "ngmodule", type: i0.forwardRef(() => CommonModule) }, { kind: "directive", type: i0.forwardRef(() => i1$2.NgTemplateOutlet), selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i0.forwardRef(() => CdkMenu), selector: "[cdkMenu]", outputs: ["closed"], exportAs: ["cdkMenu"] }, { kind: "directive", type: i0.forwardRef(() => CdkMenuItem), selector: "[cdkMenuItem]", inputs: ["cdkMenuItemDisabled", "cdkMenuitemTypeaheadLabel"], outputs: ["cdkMenuItemTriggered"], exportAs: ["cdkMenuItem"] }, { kind: "directive", type: i0.forwardRef(() => CdkMenuTrigger), selector: "[cdkMenuTriggerFor]", inputs: ["cdkMenuTriggerFor", "cdkMenuPosition", "cdkMenuTriggerData", "cdkMenuTriggerTransformOriginOn"], outputs: ["cdkMenuOpened", "cdkMenuClosed"], exportAs: ["cdkMenuTriggerFor"] }, { kind: "component", type: i0.forwardRef(() => TnIconComponent), selector: "tn-icon", inputs: ["name", "size", "color", "tooltip", "ariaLabel", "library", "fullSize", "customSize"] }, { kind: "directive", type: i0.forwardRef(() => TnTestIdDirective), selector: "[tnTestId]", inputs: ["tnTestId"] }, { kind: "directive", type: i0.forwardRef(() => TnMenuActivateHoverDirective), selector: "[tnMenuActivateHover]" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3442
|
+
}
|
|
3443
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnMenuPanelComponent, decorators: [{
|
|
3444
|
+
type: Component,
|
|
3445
|
+
args: [{ selector: 'tn-menu-panel', standalone: true, imports: [
|
|
3446
|
+
CommonModule,
|
|
3447
|
+
CdkMenu,
|
|
3448
|
+
CdkMenuItem,
|
|
3449
|
+
CdkMenuTrigger,
|
|
3450
|
+
TnIconComponent,
|
|
3451
|
+
TnTestIdDirective,
|
|
3452
|
+
TnMenuActivateHoverDirective,
|
|
3453
|
+
forwardRef(() => TnMenuPanelComponent),
|
|
3454
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"tn-menu\" cdkMenu tnMenuActivateHover [class.tn-menu--nested]=\"nested()\">\n <!--\n Projected <tn-menu-item> entries (root panel only) render as sibling\n cdkMenuItem buttons so they participate in CdkMenu's keyboard navigation\n (arrow keys, Home/End, type-ahead) alongside the items() array below.\n -->\n @for (projected of projectedItems(); track projected) {\n <button\n cdkMenuItem\n class=\"tn-menu-item\"\n type=\"button\"\n [tnTestId]=\"projected.resolvedTestId()\"\n [disabled]=\"projected.disabled()\"\n [cdkMenuItemDisabled]=\"projected.disabled()\"\n [class.disabled]=\"projected.disabled()\"\n [class.tn-menu-item--selected]=\"projected.selected()\"\n [attr.aria-current]=\"projected.selected() ? 'true' : null\"\n (click)=\"onProjectedClick(projected, $event)\"\n >\n @if (projected.label() !== undefined) {\n @if (projected.icon()) {\n <tn-icon size=\"sm\" class=\"tn-menu-item-icon\" [name]=\"projected.icon()!\" [library]=\"projected.iconLibrary()\" />\n }\n <span class=\"tn-menu-item-label\">{{ projected.label() }}</span>\n @if (projected.shortcut()) {\n <span class=\"tn-menu-item-shortcut\">{{ projected.shortcut() }}</span>\n }\n } @else {\n <ng-container [ngTemplateOutlet]=\"projected.content()\" />\n }\n </button>\n }\n\n @for (item of items(); track trackByItemId($index, item)) {\n @if (item.separator) {\n <div class=\"tn-menu-separator\" role=\"separator\"></div>\n } @else if (!item.children || item.children.length === 0) {\n <button\n cdkMenuItem\n class=\"tn-menu-item\"\n type=\"button\"\n [tnTestId]=\"item.testId ?? 'menu-item-' + item.id\"\n [disabled]=\"item.disabled\"\n [cdkMenuItemDisabled]=\"!!item.disabled\"\n [class.disabled]=\"item.disabled\"\n [class.tn-menu-item--selected]=\"item.selected\"\n [attr.aria-current]=\"item.selected ? 'true' : null\"\n (click)=\"onItemClick(item)\"\n >\n @if (item.icon) {\n <tn-icon size=\"sm\" class=\"tn-menu-item-icon\" [name]=\"item.icon!\" [library]=\"item.iconLibrary\" />\n }\n <span class=\"tn-menu-item-label\">{{ item.label }}</span>\n @if (item.shortcut) {\n <span class=\"tn-menu-item-shortcut\">{{ item.shortcut }}</span>\n }\n </button>\n } @else {\n <button\n cdkMenuItem\n class=\"tn-menu-item tn-menu-item--nested\"\n type=\"button\"\n [tnTestId]=\"item.testId ?? 'menu-item-' + item.id\"\n [cdkMenuTriggerFor]=\"submenuTpl\"\n [disabled]=\"item.disabled\"\n [cdkMenuItemDisabled]=\"!!item.disabled\"\n [class.disabled]=\"item.disabled\"\n [class.tn-menu-item--selected]=\"item.selected\"\n [attr.aria-current]=\"item.selected ? 'true' : null\"\n >\n @if (item.icon) {\n <tn-icon size=\"sm\" class=\"tn-menu-item-icon\" [name]=\"item.icon!\" [library]=\"item.iconLibrary\" />\n }\n <span class=\"tn-menu-item-label\">{{ item.label }}</span>\n @if (item.shortcut) {\n <span class=\"tn-menu-item-shortcut\">{{ item.shortcut }}</span>\n }\n <span class=\"tn-menu-item-arrow\">\u25B6</span>\n </button>\n\n <ng-template #submenuTpl>\n <tn-menu-panel [items]=\"item.children!\" [nested]=\"true\" />\n </ng-template>\n }\n }\n</div>\n", styles: [".tn-menu-container{display:inline-block;position:relative}.tn-menu-container.tn-menu-container--context{display:block;width:100%;height:100%;cursor:context-menu}.tn-menu-trigger{display:flex;align-items:center;gap:8px;padding:8px 16px;background:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #e0e0e0);border-radius:4px;color:var(--tn-fg1, #333333);cursor:pointer;font-size:14px;transition:all .2s ease}.tn-menu-trigger:hover:not(.disabled){background:var(--tn-bg2, #f5f5f5);border-color:var(--tn-lines, #cccccc)}.tn-menu-trigger:focus{outline:2px solid var(--tn-primary, #007bff);outline-offset:2px}.tn-menu-trigger.disabled{opacity:.5;cursor:not-allowed}.tn-menu-arrow{font-size:12px;transition:transform .2s ease}.tn-menu-arrow.tn-menu-arrow--up{transform:rotate(180deg)}.tn-menu{display:flex;flex-direction:column;background:var(--tn-bg2, #f5f5f5);border:1px solid var(--tn-lines, #e0e0e0);border-radius:4px;box-shadow:0 8px 24px #00000026,0 4px 8px #0000001a;min-width:160px;max-width:300px;padding:4px 0;z-index:1000}.tn-menu-item{display:flex;align-items:center;gap:8px;width:100%;padding:8px 16px;border:none;background:transparent;color:var(--tn-fg1, #333333);cursor:pointer;font-size:14px;text-align:left;transition:background-color .2s ease}.tn-menu-item:hover:not(.disabled){background:var(--tn-alt-bg2, #e8f4fd)!important}.tn-menu-item:focus{outline:none;background:var(--tn-alt-bg2, #e8f4fd)}.tn-menu-item[aria-selected=true]{background:var(--tn-alt-bg2, #e8f4fd)}.tn-menu-item.tn-menu-item--selected{background:var(--tn-alt-bg2, #e8f4fd);font-weight:600;color:var(--tn-primary, #007bff)}.tn-menu-item.disabled{opacity:.5;cursor:not-allowed}.tn-menu-item.disabled:hover{background:transparent!important}.tn-menu-item-icon{font-size:1rem;width:16px;text-align:center}.tn-menu-item-label{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.tn-menu-item-shortcut{font-size:12px;color:var(--tn-fg2, #666666);margin-left:auto;padding-left:16px;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;font-weight:400;opacity:.7}.tn-menu-item-arrow{font-size:10px;margin-left:8px;color:var(--tn-fg2, #666666);transition:transform .2s ease;flex-shrink:0}.tn-menu-item--nested{position:relative}.tn-menu-item--nested:hover .tn-menu-item-arrow{color:var(--tn-fg1, #333333)}.tn-menu-separator{height:1px;background:var(--tn-lines, #e0e0e0);margin:4px 0}.tn-menu--nested{margin-left:4px;box-shadow:0 8px 24px #00000026,0 4px 8px #0000001a;border-radius:4px}.tn-menu-context-content{width:100%;height:100%}.tn-menu-context-content:hover:before{content:\"\";position:absolute;inset:0;background:rgba(var(--tn-primary-rgb, 0, 123, 255),.05);pointer-events:none;border:1px dashed rgba(var(--tn-primary-rgb, 0, 123, 255),.3);border-radius:4px}\n"] }]
|
|
3455
|
+
}], propDecorators: { items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: false }] }], projectedItems: [{ type: i0.Input, args: [{ isSignal: true, alias: "projectedItems", required: false }] }], nested: [{ type: i0.Input, args: [{ isSignal: true, alias: "nested", required: false }] }] } });
|
|
3456
|
+
|
|
3339
3457
|
class TnMenuComponent {
|
|
3340
3458
|
items = input([], ...(ngDevMode ? [{ debugName: "items" }] : []));
|
|
3341
3459
|
contextMenu = input(false, ...(ngDevMode ? [{ debugName: "contextMenu" }] : [])); // Enable context menu mode (right-click)
|
|
@@ -3449,15 +3567,12 @@ class TnMenuComponent {
|
|
|
3449
3567
|
this.openContextMenuAt(event.clientX, event.clientY);
|
|
3450
3568
|
}
|
|
3451
3569
|
}
|
|
3452
|
-
trackByItemId(index, item) {
|
|
3453
|
-
return item.id;
|
|
3454
|
-
}
|
|
3455
3570
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnMenuComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3456
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnMenuComponent, isStandalone: true, selector: "tn-menu", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, contextMenu: { classPropertyName: "contextMenu", publicName: "contextMenu", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { menuItemClick: "menuItemClick", menuOpen: "menuOpen", menuClose: "menuClose" }, queries: [{ propertyName: "contentItems", predicate: TnMenuItemComponent, isSignal: true }], viewQueries: [{ propertyName: "menuTemplate", first: true, predicate: ["menuTemplate"], descendants: true, isSignal: true }, { propertyName: "contextMenuTemplate", first: true, predicate: ["contextMenuTemplate"], descendants: true, isSignal: true }], ngImport: i0, template: "<!-- Context menu content slot: anything that isn't a projected <tn-menu-item> -->\n@if (contextMenu()) {\n <div class=\"tn-menu-context-content\" (contextmenu)=\"onContextMenu($event)\">\n <ng-content />\n </div>\n}\n\n<!-- Context menu template for overlay -->\n<ng-template #contextMenuTemplate>\n <
|
|
3571
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnMenuComponent, isStandalone: true, selector: "tn-menu", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, contextMenu: { classPropertyName: "contextMenu", publicName: "contextMenu", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { menuItemClick: "menuItemClick", menuOpen: "menuOpen", menuClose: "menuClose" }, queries: [{ propertyName: "contentItems", predicate: TnMenuItemComponent, isSignal: true }], viewQueries: [{ propertyName: "menuTemplate", first: true, predicate: ["menuTemplate"], descendants: true, isSignal: true }, { propertyName: "contextMenuTemplate", first: true, predicate: ["contextMenuTemplate"], descendants: true, isSignal: true }], ngImport: i0, template: "<!-- Context menu content slot: anything that isn't a projected <tn-menu-item> -->\n@if (contextMenu()) {\n <div class=\"tn-menu-context-content\" (contextmenu)=\"onContextMenu($event)\">\n <ng-content />\n </div>\n}\n\n<!-- Context menu template for overlay -->\n<ng-template #contextMenuTemplate>\n <tn-menu-panel [items]=\"items()\" />\n</ng-template>\n\n<!-- Regular menu template -->\n<ng-template #menuTemplate>\n <tn-menu-panel [items]=\"items()\" [projectedItems]=\"contentItems()\" />\n</ng-template>\n", styles: [".tn-menu-container{display:inline-block;position:relative}.tn-menu-container.tn-menu-container--context{display:block;width:100%;height:100%;cursor:context-menu}.tn-menu-trigger{display:flex;align-items:center;gap:8px;padding:8px 16px;background:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #e0e0e0);border-radius:4px;color:var(--tn-fg1, #333333);cursor:pointer;font-size:14px;transition:all .2s ease}.tn-menu-trigger:hover:not(.disabled){background:var(--tn-bg2, #f5f5f5);border-color:var(--tn-lines, #cccccc)}.tn-menu-trigger:focus{outline:2px solid var(--tn-primary, #007bff);outline-offset:2px}.tn-menu-trigger.disabled{opacity:.5;cursor:not-allowed}.tn-menu-arrow{font-size:12px;transition:transform .2s ease}.tn-menu-arrow.tn-menu-arrow--up{transform:rotate(180deg)}.tn-menu{display:flex;flex-direction:column;background:var(--tn-bg2, #f5f5f5);border:1px solid var(--tn-lines, #e0e0e0);border-radius:4px;box-shadow:0 8px 24px #00000026,0 4px 8px #0000001a;min-width:160px;max-width:300px;padding:4px 0;z-index:1000}.tn-menu-item{display:flex;align-items:center;gap:8px;width:100%;padding:8px 16px;border:none;background:transparent;color:var(--tn-fg1, #333333);cursor:pointer;font-size:14px;text-align:left;transition:background-color .2s ease}.tn-menu-item:hover:not(.disabled){background:var(--tn-alt-bg2, #e8f4fd)!important}.tn-menu-item:focus{outline:none;background:var(--tn-alt-bg2, #e8f4fd)}.tn-menu-item[aria-selected=true]{background:var(--tn-alt-bg2, #e8f4fd)}.tn-menu-item.tn-menu-item--selected{background:var(--tn-alt-bg2, #e8f4fd);font-weight:600;color:var(--tn-primary, #007bff)}.tn-menu-item.disabled{opacity:.5;cursor:not-allowed}.tn-menu-item.disabled:hover{background:transparent!important}.tn-menu-item-icon{font-size:1rem;width:16px;text-align:center}.tn-menu-item-label{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.tn-menu-item-shortcut{font-size:12px;color:var(--tn-fg2, #666666);margin-left:auto;padding-left:16px;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;font-weight:400;opacity:.7}.tn-menu-item-arrow{font-size:10px;margin-left:8px;color:var(--tn-fg2, #666666);transition:transform .2s ease;flex-shrink:0}.tn-menu-item--nested{position:relative}.tn-menu-item--nested:hover .tn-menu-item-arrow{color:var(--tn-fg1, #333333)}.tn-menu-separator{height:1px;background:var(--tn-lines, #e0e0e0);margin:4px 0}.tn-menu--nested{margin-left:4px;box-shadow:0 8px 24px #00000026,0 4px 8px #0000001a;border-radius:4px}.tn-menu-context-content{width:100%;height:100%}.tn-menu-context-content:hover:before{content:\"\";position:absolute;inset:0;background:rgba(var(--tn-primary-rgb, 0, 123, 255),.05);pointer-events:none;border:1px dashed rgba(var(--tn-primary-rgb, 0, 123, 255),.3);border-radius:4px}\n"], dependencies: [{ kind: "component", type: TnMenuPanelComponent, selector: "tn-menu-panel", inputs: ["items", "projectedItems", "nested"] }] });
|
|
3457
3572
|
}
|
|
3458
3573
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnMenuComponent, decorators: [{
|
|
3459
3574
|
type: Component,
|
|
3460
|
-
args: [{ selector: 'tn-menu', standalone: true, imports: [
|
|
3575
|
+
args: [{ selector: 'tn-menu', standalone: true, imports: [TnMenuPanelComponent], template: "<!-- Context menu content slot: anything that isn't a projected <tn-menu-item> -->\n@if (contextMenu()) {\n <div class=\"tn-menu-context-content\" (contextmenu)=\"onContextMenu($event)\">\n <ng-content />\n </div>\n}\n\n<!-- Context menu template for overlay -->\n<ng-template #contextMenuTemplate>\n <tn-menu-panel [items]=\"items()\" />\n</ng-template>\n\n<!-- Regular menu template -->\n<ng-template #menuTemplate>\n <tn-menu-panel [items]=\"items()\" [projectedItems]=\"contentItems()\" />\n</ng-template>\n", styles: [".tn-menu-container{display:inline-block;position:relative}.tn-menu-container.tn-menu-container--context{display:block;width:100%;height:100%;cursor:context-menu}.tn-menu-trigger{display:flex;align-items:center;gap:8px;padding:8px 16px;background:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #e0e0e0);border-radius:4px;color:var(--tn-fg1, #333333);cursor:pointer;font-size:14px;transition:all .2s ease}.tn-menu-trigger:hover:not(.disabled){background:var(--tn-bg2, #f5f5f5);border-color:var(--tn-lines, #cccccc)}.tn-menu-trigger:focus{outline:2px solid var(--tn-primary, #007bff);outline-offset:2px}.tn-menu-trigger.disabled{opacity:.5;cursor:not-allowed}.tn-menu-arrow{font-size:12px;transition:transform .2s ease}.tn-menu-arrow.tn-menu-arrow--up{transform:rotate(180deg)}.tn-menu{display:flex;flex-direction:column;background:var(--tn-bg2, #f5f5f5);border:1px solid var(--tn-lines, #e0e0e0);border-radius:4px;box-shadow:0 8px 24px #00000026,0 4px 8px #0000001a;min-width:160px;max-width:300px;padding:4px 0;z-index:1000}.tn-menu-item{display:flex;align-items:center;gap:8px;width:100%;padding:8px 16px;border:none;background:transparent;color:var(--tn-fg1, #333333);cursor:pointer;font-size:14px;text-align:left;transition:background-color .2s ease}.tn-menu-item:hover:not(.disabled){background:var(--tn-alt-bg2, #e8f4fd)!important}.tn-menu-item:focus{outline:none;background:var(--tn-alt-bg2, #e8f4fd)}.tn-menu-item[aria-selected=true]{background:var(--tn-alt-bg2, #e8f4fd)}.tn-menu-item.tn-menu-item--selected{background:var(--tn-alt-bg2, #e8f4fd);font-weight:600;color:var(--tn-primary, #007bff)}.tn-menu-item.disabled{opacity:.5;cursor:not-allowed}.tn-menu-item.disabled:hover{background:transparent!important}.tn-menu-item-icon{font-size:1rem;width:16px;text-align:center}.tn-menu-item-label{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.tn-menu-item-shortcut{font-size:12px;color:var(--tn-fg2, #666666);margin-left:auto;padding-left:16px;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;font-weight:400;opacity:.7}.tn-menu-item-arrow{font-size:10px;margin-left:8px;color:var(--tn-fg2, #666666);transition:transform .2s ease;flex-shrink:0}.tn-menu-item--nested{position:relative}.tn-menu-item--nested:hover .tn-menu-item-arrow{color:var(--tn-fg1, #333333)}.tn-menu-separator{height:1px;background:var(--tn-lines, #e0e0e0);margin:4px 0}.tn-menu--nested{margin-left:4px;box-shadow:0 8px 24px #00000026,0 4px 8px #0000001a;border-radius:4px}.tn-menu-context-content{width:100%;height:100%}.tn-menu-context-content:hover:before{content:\"\";position:absolute;inset:0;background:rgba(var(--tn-primary-rgb, 0, 123, 255),.05);pointer-events:none;border:1px dashed rgba(var(--tn-primary-rgb, 0, 123, 255),.3);border-radius:4px}\n"] }]
|
|
3461
3576
|
}], propDecorators: { items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: false }] }], contextMenu: [{ type: i0.Input, args: [{ isSignal: true, alias: "contextMenu", required: false }] }], menuItemClick: [{ type: i0.Output, args: ["menuItemClick"] }], menuOpen: [{ type: i0.Output, args: ["menuOpen"] }], menuClose: [{ type: i0.Output, args: ["menuClose"] }], menuTemplate: [{ type: i0.ViewChild, args: ['menuTemplate', { isSignal: true }] }], contextMenuTemplate: [{ type: i0.ViewChild, args: ['contextMenuTemplate', { isSignal: true }] }], contentItems: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => TnMenuItemComponent), { isSignal: true }] }] } });
|
|
3462
3577
|
|
|
3463
3578
|
class TnSlideToggleComponent {
|