@truenas/ui-components 0.7.10 → 0.7.12
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.
|
@@ -1732,17 +1732,43 @@ class TnAutocompleteComponent {
|
|
|
1732
1732
|
}
|
|
1733
1733
|
break;
|
|
1734
1734
|
}
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1735
|
+
// Escape is not here: it is handled on this component's HOST element
|
|
1736
|
+
// instead, because it has to consume the key — see `onHostKeydown`.
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
/**
|
|
1740
|
+
* Escape, which cancels the draft, dismisses the panel, and — while the panel
|
|
1741
|
+
* is open — CONSUMES the key.
|
|
1742
|
+
*
|
|
1743
|
+
* Consuming matters because of what is usually underneath: since #322 a
|
|
1744
|
+
* `tn-side-panel` is a CDK overlay listening for Escape through
|
|
1745
|
+
* `OverlayKeyboardDispatcher`, which delivers to the top-most attached
|
|
1746
|
+
* overlay that has subscribers. Dismissing disposes this panel's overlay
|
|
1747
|
+
* mid-keystroke, taking it out of that stack — so an Escape left to carry on
|
|
1748
|
+
* reached `<body>`, found the side panel as the new top-most subscriber, and
|
|
1749
|
+
* closed the whole form along with the panel (#324). A `tn-drawer` in `over`
|
|
1750
|
+
* mode has a keydown handler of its own and closed the same way.
|
|
1751
|
+
*
|
|
1752
|
+
* With the panel already closed the key is NOT consumed: there is nothing to
|
|
1753
|
+
* dismiss and it belongs to whatever contains this field. The draft still
|
|
1754
|
+
* reverts, which is what the blur that follows would otherwise commit.
|
|
1755
|
+
*
|
|
1756
|
+
* Why this sits on the HOST rather than beside the other keys on the input:
|
|
1757
|
+
* `stopPropagation()` stops ANCESTOR listeners, not other listeners on the
|
|
1758
|
+
* same element. Called from the input it also hid Escape from a `tnTooltip`
|
|
1759
|
+
* on this component, which dismisses a visible tooltip from a host listener —
|
|
1760
|
+
* so the tooltip stayed up. From here, that directive still sees the key and
|
|
1761
|
+
* only the containers ABOVE this field stop seeing it.
|
|
1762
|
+
*/
|
|
1763
|
+
onHostKeydown(event) {
|
|
1764
|
+
if (event.key !== 'Escape') {
|
|
1765
|
+
return;
|
|
1745
1766
|
}
|
|
1767
|
+
event.preventDefault();
|
|
1768
|
+
if (this.isOpen()) {
|
|
1769
|
+
event.stopPropagation();
|
|
1770
|
+
}
|
|
1771
|
+
this.dismiss();
|
|
1746
1772
|
}
|
|
1747
1773
|
/**
|
|
1748
1774
|
* `loadMore` normally fires from a scroll event, which never happens when
|
|
@@ -2050,6 +2076,39 @@ class TnAutocompleteComponent {
|
|
|
2050
2076
|
}
|
|
2051
2077
|
this.close();
|
|
2052
2078
|
}));
|
|
2079
|
+
// Escape reaching the panel through CDK's keyboard dispatcher, which hands
|
|
2080
|
+
// the key to the top-most attached overlay that HAS subscribers and stops
|
|
2081
|
+
// there — so an overlay that subscribes is also one the key stops at,
|
|
2082
|
+
// rather than carrying on to a `tn-side-panel` underneath it (#324).
|
|
2083
|
+
//
|
|
2084
|
+
// NOT REACHABLE TODAY, and kept deliberately. It would take a keydown that
|
|
2085
|
+
// did not come from the input, and there is none: `onBlur` closes the panel
|
|
2086
|
+
// as soon as focus leaves the field, and every option row is
|
|
2087
|
+
// `tabindex="-1"` with its `mousedown` prevented. While focus IS in the
|
|
2088
|
+
// field, `onHostKeydown` consumes Escape before it can reach the
|
|
2089
|
+
// dispatcher's listener on `<body>`. This is what would keep the key
|
|
2090
|
+
// stopping here if the panel ever did take focus — the shape `tn-select`
|
|
2091
|
+
// has carried since long before #324.
|
|
2092
|
+
this.overlaySubs.push(this.overlayRef.keydownEvents().subscribe((event) => {
|
|
2093
|
+
if (event.key === 'Escape' && !event.altKey && !event.ctrlKey && !event.metaKey) {
|
|
2094
|
+
event.preventDefault();
|
|
2095
|
+
this.dismiss();
|
|
2096
|
+
}
|
|
2097
|
+
}));
|
|
2098
|
+
}
|
|
2099
|
+
/**
|
|
2100
|
+
* Escape's effect on this field: cancel the draft, then close.
|
|
2101
|
+
*
|
|
2102
|
+
* Shared by the overlay subscription above and `onHostKeydown`, which is
|
|
2103
|
+
* where Escape arrives while focus is in the field — open panel or closed.
|
|
2104
|
+
*/
|
|
2105
|
+
dismiss() {
|
|
2106
|
+
if (this.allowCustomValue()) {
|
|
2107
|
+
// Escape means "cancel the draft": revert to the committed value's text
|
|
2108
|
+
// so the upcoming blur doesn't commit the abandoned term.
|
|
2109
|
+
this.revertDisplayToValue(this.selectedValue());
|
|
2110
|
+
}
|
|
2111
|
+
this.close();
|
|
2053
2112
|
}
|
|
2054
2113
|
detachOverlay() {
|
|
2055
2114
|
this.overlaySubs.forEach((sub) => sub.unsubscribe());
|
|
@@ -2069,7 +2128,7 @@ class TnAutocompleteComponent {
|
|
|
2069
2128
|
}
|
|
2070
2129
|
}
|
|
2071
2130
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnAutocompleteComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2072
|
-
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 }, compareWith: { classPropertyName: "compareWith", publicName: "compareWith", 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 }, allowCustomValue: { classPropertyName: "allowCustomValue", publicName: "allowCustomValue", isSignal: true, isRequired: false, transformFunction: null }, dataSource: { classPropertyName: "dataSource", publicName: "dataSource", isSignal: true, isRequired: false, transformFunction: null }, dataSourceDebounce: { classPropertyName: "dataSourceDebounce", publicName: "dataSourceDebounce", isSignal: true, isRequired: false, transformFunction: null }, pageSize: { classPropertyName: "pageSize", publicName: "pageSize", isSignal: true, isRequired: false, transformFunction: null }, actionOption: { classPropertyName: "actionOption", publicName: "actionOption", isSignal: true, isRequired: false, transformFunction: null }, keepSelectedOption: { classPropertyName: "keepSelectedOption", publicName: "keepSelectedOption", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, loadingText: { classPropertyName: "loadingText", publicName: "loadingText", 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 }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null }, optionTestIdKey: { classPropertyName: "optionTestIdKey", publicName: "optionTestIdKey", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { optionSelected: "optionSelected", searchChange: "searchChange", actionSelected: "actionSelected", dataSourceError: "dataSourceError", loadMore: "loadMore", opened: "opened" }, providers: [
|
|
2131
|
+
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 }, compareWith: { classPropertyName: "compareWith", publicName: "compareWith", 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 }, allowCustomValue: { classPropertyName: "allowCustomValue", publicName: "allowCustomValue", isSignal: true, isRequired: false, transformFunction: null }, dataSource: { classPropertyName: "dataSource", publicName: "dataSource", isSignal: true, isRequired: false, transformFunction: null }, dataSourceDebounce: { classPropertyName: "dataSourceDebounce", publicName: "dataSourceDebounce", isSignal: true, isRequired: false, transformFunction: null }, pageSize: { classPropertyName: "pageSize", publicName: "pageSize", isSignal: true, isRequired: false, transformFunction: null }, actionOption: { classPropertyName: "actionOption", publicName: "actionOption", isSignal: true, isRequired: false, transformFunction: null }, keepSelectedOption: { classPropertyName: "keepSelectedOption", publicName: "keepSelectedOption", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, loadingText: { classPropertyName: "loadingText", publicName: "loadingText", 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 }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null }, optionTestIdKey: { classPropertyName: "optionTestIdKey", publicName: "optionTestIdKey", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { optionSelected: "optionSelected", searchChange: "searchChange", actionSelected: "actionSelected", dataSourceError: "dataSourceError", loadMore: "loadMore", opened: "opened" }, host: { listeners: { "keydown": "onHostKeydown($event)" } }, providers: [
|
|
2073
2132
|
{
|
|
2074
2133
|
provide: NG_VALUE_ACCESSOR,
|
|
2075
2134
|
useExisting: forwardRef(() => TnAutocompleteComponent),
|
|
@@ -2085,7 +2144,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
2085
2144
|
useExisting: forwardRef(() => TnAutocompleteComponent),
|
|
2086
2145
|
multi: true,
|
|
2087
2146
|
},
|
|
2088
|
-
],
|
|
2147
|
+
], host: {
|
|
2148
|
+
'(keydown)': 'onHostKeydown($event)',
|
|
2149
|
+
}, template: "<div class=\"tn-autocomplete\">\n <input\n #inputEl\n type=\"text\"\n class=\"tn-autocomplete__input\"\n role=\"combobox\"\n aria-haspopup=\"listbox\"\n tnTestIdType=\"autocomplete\"\n autocomplete=\"off\"\n [tnTestId]=\"resolvedTestId()\"\n [class.open]=\"isOpen()\"\n [class.disabled]=\"isDisabled()\"\n [placeholder]=\"resolvedPlaceholder()\"\n [disabled]=\"isDisabled()\"\n [value]=\"searchTerm()\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.aria-labelledby]=\"fieldAria.labelledby()\"\n [attr.aria-describedby]=\"fieldAria.describedby()\"\n [attr.aria-invalid]=\"fieldAria.invalid()\"\n [attr.aria-required]=\"fieldAria.required()\"\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 <!-- The max-height is declared here, and inherits: it bounds the panel as a\n whole AND the listbox inside it, which are two elements now. -->\n <div\n class=\"tn-autocomplete__dropdown\"\n [style.--tn-autocomplete-panel-max-height]=\"panelMaxHeightValue()\">\n\n <!-- Per ARIA, a listbox may only contain option/group children, so the\n loading and no-results rows live OUTSIDE it as live-region siblings.\n The listbox itself stays rendered (possibly empty) the whole time the\n panel is open, so the input's aria-controls always points at a real\n element \u2014 including the loading-with-no-results-yet state.\n\n The listbox is also the scrollport, and has to be (#292): axe's\n `scrollable-region-focusable` exempts a combobox popup, but it tests\n the scroll container's OWN role, so the role-less wrapper that used to\n carry the overflow was reported even though every option inside is\n `tabindex=\"-1\"` on purpose. The paging handler moves with the\n overflow \u2014 a scroll event fires on the element that scrolls \u2014 and the\n status row below stays outside the scrolling area. -->\n <div\n class=\"tn-autocomplete__listbox\"\n role=\"listbox\"\n [attr.id]=\"uid + '-dropdown'\"\n [attr.aria-busy]=\"resolvedLoading() ? true : null\"\n (scroll)=\"onPanelScroll($event)\">\n @if (hasResults()) {\n @for (option of filteredOptions(); track $index) {\n <div\n class=\"tn-autocomplete__option\"\n role=\"option\"\n tabindex=\"-1\"\n tnTestIdType=\"option\"\n [tnTestId]=\"optionTestIdParts(option)\"\n [class.highlighted]=\"highlightedIndex() === $index\"\n [class.disabled]=\"option.disabled\"\n [class.tn-autocomplete__option--action]=\"option === actionOption()\"\n [attr.id]=\"uid + '-option-' + $index\"\n [attr.aria-selected]=\"isOptionSelected(option)\"\n [attr.aria-disabled]=\"option.disabled ? true : null\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"onOptionClick(option)\"\n (keydown.enter)=\"onOptionClick(option)\">\n {{ option.label }}\n </div>\n }\n }\n </div>\n\n <div role=\"status\">\n @if (resolvedLoading()) {\n <div\n class=\"tn-autocomplete__loading\"\n tnTestIdType=\"autocomplete\"\n [tnTestId]=\"statusTestIdParts('loading')\">\n <tn-spinner [diameter]=\"16\" [strokeWidth]=\"2\" [ariaLabel]=\"resolvedLoadingText()\" />\n <span>{{ resolvedLoadingText() }}</span>\n </div>\n } @else if (!hasMatches()) {\n <div\n class=\"tn-autocomplete__no-results\"\n tnTestIdType=\"autocomplete\"\n [tnTestId]=\"statusTestIdParts('no-results')\">\n {{ resolvedNoResultsText() }}\n </div>\n }\n </div>\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;padding:.25rem 0;display:flex;flex-direction:column;max-height:var(--tn-autocomplete-panel-max-height, 320px)}.tn-autocomplete__listbox{max-height:var(--tn-autocomplete-panel-max-height, 320px);overflow-y:auto;min-height: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:not(.disabled){background-color:var(--tn-alt-bg2, #f8f9fa)}.tn-autocomplete__option.highlighted{background-color:var(--tn-alt-bg1, #f8f9fa)}.tn-autocomplete__option.disabled{color:var(--tn-fg2, #6c757d);cursor:not-allowed;opacity:.6}.tn-autocomplete__option--action{border-bottom:1px solid var(--tn-lines, #dee2e6);font-weight:500}.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}}.tn-autocomplete__loading{align-items:center;color:var(--tn-fg2, #6c757d);display:flex;font-size:.875rem;gap:.5rem;padding:.5rem .75rem}\n"] }]
|
|
2089
2150
|
}], ctorParameters: () => [], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], compareWith: [{ type: i0.Input, args: [{ isSignal: true, alias: "compareWith", 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 }] }], allowCustomValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowCustomValue", required: false }] }], dataSource: [{ type: i0.Input, args: [{ isSignal: true, alias: "dataSource", required: false }] }], dataSourceDebounce: [{ type: i0.Input, args: [{ isSignal: true, alias: "dataSourceDebounce", required: false }] }], pageSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageSize", required: false }] }], actionOption: [{ type: i0.Input, args: [{ isSignal: true, alias: "actionOption", required: false }] }], keepSelectedOption: [{ type: i0.Input, args: [{ isSignal: true, alias: "keepSelectedOption", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], loadingText: [{ type: i0.Input, args: [{ isSignal: true, alias: "loadingText", 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 }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], optionTestIdKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionTestIdKey", required: false }] }], optionSelected: [{ type: i0.Output, args: ["optionSelected"] }], searchChange: [{ type: i0.Output, args: ["searchChange"] }], actionSelected: [{ type: i0.Output, args: ["actionSelected"] }], dataSourceError: [{ type: i0.Output, args: ["dataSourceError"] }], loadMore: [{ type: i0.Output, args: ["loadMore"] }], opened: [{ type: i0.Output, args: ["opened"] }], inputEl: [{ type: i0.ViewChild, args: ['inputEl', { isSignal: true }] }], dropdownTemplate: [{ type: i0.ViewChild, args: ['dropdownTemplate', { isSignal: true }] }] } });
|
|
2090
2151
|
|
|
2091
2152
|
/**
|
|
@@ -8900,13 +8961,10 @@ class TnChipInputComponent {
|
|
|
8900
8961
|
return;
|
|
8901
8962
|
}
|
|
8902
8963
|
if (event.key === 'Escape') {
|
|
8903
|
-
|
|
8904
|
-
|
|
8905
|
-
|
|
8906
|
-
|
|
8907
|
-
this.closedByUser = true;
|
|
8908
|
-
this.close();
|
|
8909
|
-
}
|
|
8964
|
+
// Handled on this component's HOST element instead, because it has to
|
|
8965
|
+
// consume the key — see `onHostKeydown`. Returning rather than falling
|
|
8966
|
+
// through, so that a consumer who lists Escape in `separatorKeys` cannot
|
|
8967
|
+
// have it commit a chip on the way past.
|
|
8910
8968
|
return;
|
|
8911
8969
|
}
|
|
8912
8970
|
if (this.isCommitKey(event)) {
|
|
@@ -8927,6 +8985,39 @@ class TnChipInputComponent {
|
|
|
8927
8985
|
this.removeChip(this.values().length - 1);
|
|
8928
8986
|
}
|
|
8929
8987
|
}
|
|
8988
|
+
/**
|
|
8989
|
+
* Escape, which dismisses the suggestion panel and CONSUMES the key.
|
|
8990
|
+
*
|
|
8991
|
+
* Consuming matters because of what is usually underneath: since #322 a
|
|
8992
|
+
* `tn-side-panel` is a CDK overlay listening for Escape through
|
|
8993
|
+
* `OverlayKeyboardDispatcher`, which delivers to the top-most attached
|
|
8994
|
+
* overlay that has subscribers. Dismissing disposes this panel's overlay
|
|
8995
|
+
* mid-keystroke, taking it out of that stack — so an Escape left to carry on
|
|
8996
|
+
* reached `<body>`, found the side panel as the new top-most subscriber, and
|
|
8997
|
+
* closed the whole form along with the panel (#324). A `tn-drawer` in `over`
|
|
8998
|
+
* mode has a keydown handler of its own and closed the same way.
|
|
8999
|
+
*
|
|
9000
|
+
* With the panel already closed nothing happens here and the key belongs to
|
|
9001
|
+
* whatever contains this field — which is what it did before #324 too.
|
|
9002
|
+
*
|
|
9003
|
+
* Why this sits on the HOST rather than beside the other keys on the input:
|
|
9004
|
+
* `stopPropagation()` stops ANCESTOR listeners, not other listeners on the
|
|
9005
|
+
* same element. Called from the input it also hid Escape from a `tnTooltip`
|
|
9006
|
+
* on this component, which dismisses a visible tooltip from a host listener —
|
|
9007
|
+
* so the tooltip stayed up. From here, that directive still sees the key and
|
|
9008
|
+
* only the containers ABOVE this field stop seeing it.
|
|
9009
|
+
*/
|
|
9010
|
+
onHostKeydown(event) {
|
|
9011
|
+
if (event.key !== 'Escape' || !this.isOpen()) {
|
|
9012
|
+
return;
|
|
9013
|
+
}
|
|
9014
|
+
event.preventDefault();
|
|
9015
|
+
event.stopPropagation();
|
|
9016
|
+
// Latched, so neither the re-open effect nor a `dataSource` response still
|
|
9017
|
+
// in flight can undo the dismissal. See {@link closedByUser}.
|
|
9018
|
+
this.closedByUser = true;
|
|
9019
|
+
this.close();
|
|
9020
|
+
}
|
|
8930
9021
|
onSuggestionClick(option) {
|
|
8931
9022
|
this.commitValue(option.value, option.label);
|
|
8932
9023
|
this.inputEl().nativeElement.focus();
|
|
@@ -9230,6 +9321,28 @@ class TnChipInputComponent {
|
|
|
9230
9321
|
}
|
|
9231
9322
|
this.close();
|
|
9232
9323
|
}));
|
|
9324
|
+
// Escape reaching the panel through CDK's keyboard dispatcher, which hands
|
|
9325
|
+
// the key to the top-most attached overlay that HAS subscribers and stops
|
|
9326
|
+
// there — so an overlay that subscribes is also one the key stops at,
|
|
9327
|
+
// rather than carrying on to a `tn-side-panel` underneath it (#324).
|
|
9328
|
+
//
|
|
9329
|
+
// NOT REACHABLE TODAY, and kept deliberately. It would take a keydown that
|
|
9330
|
+
// did not come from the input, and there is none: `onBlur` closes the panel
|
|
9331
|
+
// as soon as focus leaves the field, and every suggestion row is
|
|
9332
|
+
// `tabindex="-1"` with its `mousedown` prevented. While focus IS in the
|
|
9333
|
+
// field, `onHostKeydown` consumes Escape before it can reach the
|
|
9334
|
+
// dispatcher's listener on `<body>`. This is what would keep the key
|
|
9335
|
+
// stopping here if the panel ever did take focus — the shape `tn-select`
|
|
9336
|
+
// has carried since long before #324.
|
|
9337
|
+
this.overlaySubs.push(this.overlayRef.keydownEvents().subscribe((event) => {
|
|
9338
|
+
if (event.key === 'Escape' && !event.altKey && !event.ctrlKey && !event.metaKey) {
|
|
9339
|
+
event.preventDefault();
|
|
9340
|
+
// Latched, so neither the re-open effect nor a `dataSource` response
|
|
9341
|
+
// still in flight can undo the dismissal. See {@link closedByUser}.
|
|
9342
|
+
this.closedByUser = true;
|
|
9343
|
+
this.close();
|
|
9344
|
+
}
|
|
9345
|
+
}));
|
|
9233
9346
|
}
|
|
9234
9347
|
detachOverlay() {
|
|
9235
9348
|
this.overlaySubs.forEach((sub) => sub.unsubscribe());
|
|
@@ -9238,7 +9351,7 @@ class TnChipInputComponent {
|
|
|
9238
9351
|
this.overlayRef = undefined;
|
|
9239
9352
|
}
|
|
9240
9353
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnChipInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
9241
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnChipInputComponent, isStandalone: true, selector: "tn-chip-input", inputs: { placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, separatorKeys: { classPropertyName: "separatorKeys", publicName: "separatorKeys", isSignal: true, isRequired: false, transformFunction: null }, addOnBlur: { classPropertyName: "addOnBlur", publicName: "addOnBlur", isSignal: true, isRequired: false, transformFunction: null }, allowCustomValue: { classPropertyName: "allowCustomValue", publicName: "allowCustomValue", isSignal: true, isRequired: false, transformFunction: null }, allowDuplicates: { classPropertyName: "allowDuplicates", publicName: "allowDuplicates", isSignal: true, isRequired: false, transformFunction: null }, maxChips: { classPropertyName: "maxChips", publicName: "maxChips", isSignal: true, isRequired: false, transformFunction: null }, suggestions: { classPropertyName: "suggestions", publicName: "suggestions", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, dataSource: { classPropertyName: "dataSource", publicName: "dataSource", isSignal: true, isRequired: false, transformFunction: null }, dataSourceDebounce: { classPropertyName: "dataSourceDebounce", publicName: "dataSourceDebounce", isSignal: true, isRequired: false, transformFunction: null }, loadingText: { classPropertyName: "loadingText", publicName: "loadingText", isSignal: true, isRequired: false, transformFunction: null }, compareWith: { classPropertyName: "compareWith", publicName: "compareWith", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null }, optionTestIdKey: { classPropertyName: "optionTestIdKey", publicName: "optionTestIdKey", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { chipAdded: "chipAdded", chipRemoved: "chipRemoved", searchChange: "searchChange", dataSourceError: "dataSourceError" }, providers: [
|
|
9354
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnChipInputComponent, isStandalone: true, selector: "tn-chip-input", inputs: { placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, separatorKeys: { classPropertyName: "separatorKeys", publicName: "separatorKeys", isSignal: true, isRequired: false, transformFunction: null }, addOnBlur: { classPropertyName: "addOnBlur", publicName: "addOnBlur", isSignal: true, isRequired: false, transformFunction: null }, allowCustomValue: { classPropertyName: "allowCustomValue", publicName: "allowCustomValue", isSignal: true, isRequired: false, transformFunction: null }, allowDuplicates: { classPropertyName: "allowDuplicates", publicName: "allowDuplicates", isSignal: true, isRequired: false, transformFunction: null }, maxChips: { classPropertyName: "maxChips", publicName: "maxChips", isSignal: true, isRequired: false, transformFunction: null }, suggestions: { classPropertyName: "suggestions", publicName: "suggestions", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, dataSource: { classPropertyName: "dataSource", publicName: "dataSource", isSignal: true, isRequired: false, transformFunction: null }, dataSourceDebounce: { classPropertyName: "dataSourceDebounce", publicName: "dataSourceDebounce", isSignal: true, isRequired: false, transformFunction: null }, loadingText: { classPropertyName: "loadingText", publicName: "loadingText", isSignal: true, isRequired: false, transformFunction: null }, compareWith: { classPropertyName: "compareWith", publicName: "compareWith", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null }, optionTestIdKey: { classPropertyName: "optionTestIdKey", publicName: "optionTestIdKey", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { chipAdded: "chipAdded", chipRemoved: "chipRemoved", searchChange: "searchChange", dataSourceError: "dataSourceError" }, host: { listeners: { "keydown": "onHostKeydown($event)" } }, providers: [
|
|
9242
9355
|
{
|
|
9243
9356
|
provide: NG_VALUE_ACCESSOR,
|
|
9244
9357
|
useExisting: forwardRef(() => TnChipInputComponent),
|
|
@@ -9254,7 +9367,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
9254
9367
|
useExisting: forwardRef(() => TnChipInputComponent),
|
|
9255
9368
|
multi: true,
|
|
9256
9369
|
},
|
|
9257
|
-
],
|
|
9370
|
+
], host: {
|
|
9371
|
+
'(keydown)': 'onHostKeydown($event)',
|
|
9372
|
+
}, template: "<!-- Click-to-focus is a pointer affordance only; keyboard users Tab straight to\n the real <input> inside, so the container itself is not a focus target. -->\n<!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n<div\n #container\n class=\"tn-chip-input\"\n [class.tn-chip-input--disabled]=\"isDisabled()\"\n (click)=\"focusInput()\"\n>\n @for (value of values(); track $index) {\n <tn-chip\n class=\"tn-chip-input__chip\"\n color=\"secondary\"\n [label]=\"displayLabel(value)\"\n [closable]=\"!isDisabled()\"\n [disabled]=\"isDisabled()\"\n [testId]=\"chipTestId(value)\"\n (onClose)=\"removeChip($index)\"\n />\n }\n\n <input\n #inputEl\n type=\"text\"\n class=\"tn-chip-input__field\"\n role=\"combobox\"\n autocomplete=\"off\"\n tnTestIdType=\"chip-input\"\n [tnTestId]=\"resolvedTestId()\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"isDisabled()\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.aria-labelledby]=\"fieldAria.labelledby()\"\n [attr.aria-describedby]=\"fieldAria.describedby()\"\n [attr.aria-invalid]=\"fieldAria.invalid()\"\n [attr.aria-required]=\"fieldAria.required()\"\n [attr.aria-autocomplete]=\"'list'\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-controls]=\"isOpen() ? uid + '-listbox' : 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</div>\n\n<!-- Suggestion dropdown \u2014 portaled via CDK overlay so it escapes ancestor\n overflow/clipping. Attach/detach is driven by open()/close(). -->\n<ng-template #dropdownTemplate>\n <div class=\"tn-chip-input__dropdown\">\n <!-- Per ARIA a listbox may only contain option children, so the loading row\n lives OUTSIDE it as a live-region sibling. The listbox is also the\n scrollport, which keeps that row pinned below the rows it describes\n rather than scrolling away with them. -->\n <div\n class=\"tn-chip-input__listbox\"\n role=\"listbox\"\n [attr.id]=\"uid + '-listbox'\"\n [attr.aria-busy]=\"loading() ? true : null\"\n >\n @for (suggestion of filteredSuggestions(); track $index) {\n <!-- Options are tabindex=\"-1\" and never focused: keyboard selection goes\n through the input's aria-activedescendant + the Enter branch in\n onKeydown, so a key handler here would be dead code. Click is a\n pointer-only affordance. -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events -->\n <div\n class=\"tn-chip-input__option\"\n role=\"option\"\n tabindex=\"-1\"\n tnTestIdType=\"option\"\n [tnTestId]=\"suggestionTestId(suggestion)\"\n [class.highlighted]=\"highlightedIndex() === $index\"\n [attr.id]=\"uid + '-option-' + $index\"\n [attr.aria-selected]=\"highlightedIndex() === $index\"\n (mousedown)=\"onSuggestionMousedown($event)\"\n (click)=\"onSuggestionClick(suggestion)\"\n >\n {{ suggestion.label }}\n </div>\n }\n </div>\n\n <div role=\"status\">\n @if (loading()) {\n <div\n class=\"tn-chip-input__loading\"\n tnTestIdType=\"chip-input\"\n [tnTestId]=\"statusTestIdParts('loading')\"\n >\n <tn-spinner [diameter]=\"16\" [strokeWidth]=\"2\" [ariaLabel]=\"resolvedLoadingText()\" />\n <span>{{ resolvedLoadingText() }}</span>\n </div>\n }\n </div>\n </div>\n</ng-template>\n", styles: [".tn-chip-input{display:flex;flex-wrap:wrap;align-items:center;gap:.375rem;min-height:2.5rem;padding:.375rem .5rem;box-sizing:border-box;width:100%;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif;background-color:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;cursor:text;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}.tn-chip-input:focus-within{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px color-mix(in srgb,var(--tn-primary, #007bff) 25%,transparent)}.tn-chip-input--disabled{background-color:var(--tn-alt-bg1, #f8f9fa);opacity:.6;cursor:not-allowed}.tn-chip-input__chip{flex-shrink:0}.tn-chip-input__field{flex:1;min-width:120px;padding:.25rem;font-size:1rem;line-height:1.5;color:var(--tn-fg1, #212529);background:transparent;border:none;outline:none}.tn-chip-input__field::placeholder{color:var(--tn-alt-fg1, #999);opacity:1}.tn-chip-input__field:disabled{cursor:not-allowed}.tn-chip-input__dropdown{display:flex;flex-direction:column;max-height:var(--tn-chip-input-panel-max-height, 320px);background-color:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;box-shadow:0 4px 6px -1px #0000001a,0 2px 4px -1px #0000000f}.tn-chip-input__listbox{max-height:inherit;overflow-y:auto;min-height:0}.tn-chip-input__loading{align-items:center;color:var(--tn-fg2, #6c757d);display:flex;font-size:.875rem;gap:.5rem;padding:.5rem .75rem}.tn-chip-input__option{padding:.5rem .75rem;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif;font-size:1rem;color:var(--tn-fg1, #212529);cursor:pointer}.tn-chip-input__option:hover,.tn-chip-input__option.highlighted{background-color:var(--tn-bg3, #f3f4f6)}@media(prefers-reduced-motion:reduce){.tn-chip-input{transition:none}}@media(prefers-contrast:high){.tn-chip-input{border-width:2px}}\n"] }]
|
|
9258
9373
|
}], ctorParameters: () => [], propDecorators: { placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], separatorKeys: [{ type: i0.Input, args: [{ isSignal: true, alias: "separatorKeys", required: false }] }], addOnBlur: [{ type: i0.Input, args: [{ isSignal: true, alias: "addOnBlur", required: false }] }], allowCustomValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowCustomValue", required: false }] }], allowDuplicates: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowDuplicates", required: false }] }], maxChips: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxChips", required: false }] }], suggestions: [{ type: i0.Input, args: [{ isSignal: true, alias: "suggestions", required: false }] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], dataSource: [{ type: i0.Input, args: [{ isSignal: true, alias: "dataSource", required: false }] }], dataSourceDebounce: [{ type: i0.Input, args: [{ isSignal: true, alias: "dataSourceDebounce", required: false }] }], loadingText: [{ type: i0.Input, args: [{ isSignal: true, alias: "loadingText", required: false }] }], compareWith: [{ type: i0.Input, args: [{ isSignal: true, alias: "compareWith", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], optionTestIdKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionTestIdKey", required: false }] }], chipAdded: [{ type: i0.Output, args: ["chipAdded"] }], chipRemoved: [{ type: i0.Output, args: ["chipRemoved"] }], searchChange: [{ type: i0.Output, args: ["searchChange"] }], dataSourceError: [{ type: i0.Output, args: ["dataSourceError"] }], container: [{ type: i0.ViewChild, args: ['container', { isSignal: true }] }], inputEl: [{ type: i0.ViewChild, args: ['inputEl', { isSignal: true }] }], dropdownTemplate: [{ type: i0.ViewChild, args: ['dropdownTemplate', { isSignal: true }] }] } });
|
|
9259
9374
|
|
|
9260
9375
|
/**
|
|
@@ -9891,6 +10006,7 @@ class TnMenuComponent {
|
|
|
9891
10006
|
contextMenuTemplate = viewChild.required('contextMenuTemplate');
|
|
9892
10007
|
contextOverlayRef;
|
|
9893
10008
|
contextBackdropSub;
|
|
10009
|
+
contextKeydownSub;
|
|
9894
10010
|
overlay = inject(Overlay);
|
|
9895
10011
|
viewContainerRef = inject(ViewContainerRef);
|
|
9896
10012
|
onMenuItemClick(item) {
|
|
@@ -9967,12 +10083,43 @@ class TnMenuComponent {
|
|
|
9967
10083
|
this.contextBackdropSub = this.contextOverlayRef.backdropClick().subscribe(() => {
|
|
9968
10084
|
this.closeContextMenu();
|
|
9969
10085
|
});
|
|
10086
|
+
// Escape, which this menu had no handling for at all. CDK's keyboard
|
|
10087
|
+
// dispatcher hands the key to the top-most attached overlay that HAS
|
|
10088
|
+
// subscribers and stops there, so subscribing is both what dismisses this
|
|
10089
|
+
// menu and what keeps the key from reaching a `tn-side-panel` or dialog
|
|
10090
|
+
// underneath it (#324).
|
|
10091
|
+
//
|
|
10092
|
+
// Focus is NOT restored here, unlike `TnMenuTriggerDirective`: a context
|
|
10093
|
+
// menu is opened by pointer at a cursor position with no trigger element
|
|
10094
|
+
// to go back to, which is also why the backdrop-click path above does not
|
|
10095
|
+
// restore it either.
|
|
10096
|
+
//
|
|
10097
|
+
// The dispatcher is the ONLY route here, where the form controls fixed
|
|
10098
|
+
// under #324 also consume Escape on their own host element. They can:
|
|
10099
|
+
// opening one leaves focus on a trigger inside that host, so a host
|
|
10100
|
+
// listener sees the key even when an ancestor would stop it before the
|
|
10101
|
+
// dispatcher's listener on `<body>` runs. A context menu has no such
|
|
10102
|
+
// element — a right-click moves focus nowhere, so Escape starts wherever
|
|
10103
|
+
// focus already was, which need not be inside this `tn-menu` at all.
|
|
10104
|
+
// Consequence, and it is a real gap: inside a `tn-drawer` in `over` mode,
|
|
10105
|
+
// whose panel calls `stopPropagation()` on Escape, the drawer closes and
|
|
10106
|
+
// this menu stays up. Closing it there needs focus management this menu
|
|
10107
|
+
// has never had (see the note on restoration above), not another
|
|
10108
|
+
// listener.
|
|
10109
|
+
this.contextKeydownSub = this.contextOverlayRef.keydownEvents().subscribe((event) => {
|
|
10110
|
+
if (event.key === 'Escape' && !event.altKey && !event.ctrlKey && !event.metaKey) {
|
|
10111
|
+
event.preventDefault();
|
|
10112
|
+
this.closeContextMenu();
|
|
10113
|
+
}
|
|
10114
|
+
});
|
|
9970
10115
|
this.onMenuOpen();
|
|
9971
10116
|
}
|
|
9972
10117
|
}
|
|
9973
10118
|
closeContextMenu() {
|
|
9974
10119
|
this.contextBackdropSub?.unsubscribe();
|
|
9975
10120
|
this.contextBackdropSub = undefined;
|
|
10121
|
+
this.contextKeydownSub?.unsubscribe();
|
|
10122
|
+
this.contextKeydownSub = undefined;
|
|
9976
10123
|
if (this.contextOverlayRef) {
|
|
9977
10124
|
this.contextOverlayRef.dispose();
|
|
9978
10125
|
this.contextOverlayRef = undefined;
|
|
@@ -9983,6 +10130,8 @@ class TnMenuComponent {
|
|
|
9983
10130
|
// Component destroyed while context menu open → clean up without notifying.
|
|
9984
10131
|
this.contextBackdropSub?.unsubscribe();
|
|
9985
10132
|
this.contextBackdropSub = undefined;
|
|
10133
|
+
this.contextKeydownSub?.unsubscribe();
|
|
10134
|
+
this.contextKeydownSub = undefined;
|
|
9986
10135
|
this.contextOverlayRef?.dispose();
|
|
9987
10136
|
this.contextOverlayRef = undefined;
|
|
9988
10137
|
}
|
|
@@ -15085,8 +15234,15 @@ class TnSelectComponent {
|
|
|
15085
15234
|
// the trigger while open also hits the backdrop — it closes here and the
|
|
15086
15235
|
// trigger's own click never fires, so there's no reopen race.
|
|
15087
15236
|
this.overlaySubs.push(this.overlayRef.backdropClick().subscribe(() => this.closeDropdown(false)));
|
|
15088
|
-
// Escape
|
|
15089
|
-
//
|
|
15237
|
+
// Escape reaching the panel through CDK's keyboard dispatcher, which hands
|
|
15238
|
+
// the key to the top-most attached overlay that HAS subscribers and stops
|
|
15239
|
+
// there — so an overlay that subscribes is also one the key stops at,
|
|
15240
|
+
// rather than carrying on to a `tn-side-panel` underneath it (#324).
|
|
15241
|
+
//
|
|
15242
|
+
// Not reachable from the trigger, which is where focus stays: `onHostKeydown`
|
|
15243
|
+
// consumes Escape before the dispatcher's listener on `<body>` sees it. This
|
|
15244
|
+
// is the fallback for focus that reaches the panel some other way, and it
|
|
15245
|
+
// predates #324.
|
|
15090
15246
|
this.overlaySubs.push(this.overlayRef.keydownEvents().subscribe((event) => {
|
|
15091
15247
|
if (event.key === 'Escape' && !event.altKey && !event.ctrlKey && !event.metaKey) {
|
|
15092
15248
|
event.preventDefault();
|
|
@@ -15339,7 +15495,9 @@ class TnSelectComponent {
|
|
|
15339
15495
|
* - **Enter / Space** opens the dropdown if closed; if open and an option
|
|
15340
15496
|
* is highlighted, selects that option (in single mode) or toggles it
|
|
15341
15497
|
* (in multiple mode).
|
|
15342
|
-
* - **Escape** closes the dropdown without changing the
|
|
15498
|
+
* - **Escape** is NOT here: it closes the dropdown without changing the
|
|
15499
|
+
* selection, from `onHostKeydown` on this component's host element,
|
|
15500
|
+
* because it also has to consume the key.
|
|
15343
15501
|
*
|
|
15344
15502
|
* All navigation keys call `event.preventDefault()` so the page does not
|
|
15345
15503
|
* scroll while the user is moving through options.
|
|
@@ -15392,12 +15550,8 @@ class TnSelectComponent {
|
|
|
15392
15550
|
this.activateFocusedOption();
|
|
15393
15551
|
}
|
|
15394
15552
|
break;
|
|
15395
|
-
|
|
15396
|
-
|
|
15397
|
-
event.preventDefault();
|
|
15398
|
-
this.closeDropdown();
|
|
15399
|
-
}
|
|
15400
|
-
break;
|
|
15553
|
+
// Escape is not here: it is handled on this component's HOST element
|
|
15554
|
+
// instead, because it has to consume the key — see `onHostKeydown`.
|
|
15401
15555
|
case 'Tab':
|
|
15402
15556
|
// Standard combobox: Tab moves focus out of the select; close first
|
|
15403
15557
|
// so the trigger stays a clean tab stop. Don't refocus — that would
|
|
@@ -15408,6 +15562,33 @@ class TnSelectComponent {
|
|
|
15408
15562
|
break;
|
|
15409
15563
|
}
|
|
15410
15564
|
}
|
|
15565
|
+
/**
|
|
15566
|
+
* Escape, which closes the dropdown and CONSUMES the key.
|
|
15567
|
+
*
|
|
15568
|
+
* Consuming matters because of what is usually underneath: since #322 a
|
|
15569
|
+
* `tn-side-panel` is a CDK overlay listening for Escape through
|
|
15570
|
+
* `OverlayKeyboardDispatcher`, which delivers to the top-most attached
|
|
15571
|
+
* overlay that has subscribers. Closing the dropdown disposes this select's
|
|
15572
|
+
* overlay mid-keystroke, taking it out of that stack — so an Escape left to
|
|
15573
|
+
* carry on reached `<body>`, found the panel as the new top-most subscriber,
|
|
15574
|
+
* and closed the whole form along with the dropdown (#324). A `tn-drawer` in
|
|
15575
|
+
* `over` mode has a keydown handler of its own and closed the same way.
|
|
15576
|
+
*
|
|
15577
|
+
* Why this sits on the HOST rather than beside the other keys on the trigger:
|
|
15578
|
+
* `stopPropagation()` stops ANCESTOR listeners, not other listeners on the
|
|
15579
|
+
* same element. Called from the trigger it also hid Escape from a `tnTooltip`
|
|
15580
|
+
* on this component, which dismisses a visible tooltip from a host listener —
|
|
15581
|
+
* so the tooltip stayed up. From here, that directive still sees the key and
|
|
15582
|
+
* only the containers ABOVE this select stop seeing it.
|
|
15583
|
+
*/
|
|
15584
|
+
onHostKeydown(event) {
|
|
15585
|
+
if (event.key !== 'Escape' || !this.isOpen()) {
|
|
15586
|
+
return;
|
|
15587
|
+
}
|
|
15588
|
+
event.preventDefault();
|
|
15589
|
+
event.stopPropagation();
|
|
15590
|
+
this.closeDropdown();
|
|
15591
|
+
}
|
|
15411
15592
|
moveFocus(target) {
|
|
15412
15593
|
const count = this.navigableOptions().length;
|
|
15413
15594
|
if (count === 0) {
|
|
@@ -15475,7 +15656,7 @@ class TnSelectComponent {
|
|
|
15475
15656
|
});
|
|
15476
15657
|
}
|
|
15477
15658
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnSelectComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
15478
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnSelectComponent, isStandalone: true, selector: "tn-select", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, optionGroups: { classPropertyName: "optionGroups", publicName: "optionGroups", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, noOptionsLabel: { classPropertyName: "noOptionsLabel", publicName: "noOptionsLabel", isSignal: true, isRequired: false, transformFunction: null }, allowEmpty: { classPropertyName: "allowEmpty", publicName: "allowEmpty", isSignal: true, isRequired: false, transformFunction: null }, emptyLabel: { classPropertyName: "emptyLabel", publicName: "emptyLabel", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, showSelectAll: { classPropertyName: "showSelectAll", publicName: "showSelectAll", isSignal: true, isRequired: false, transformFunction: null }, selectAllLabel: { classPropertyName: "selectAllLabel", publicName: "selectAllLabel", isSignal: true, isRequired: false, transformFunction: null }, testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, optionTestIdKey: { classPropertyName: "optionTestIdKey", publicName: "optionTestIdKey", isSignal: true, isRequired: false, transformFunction: null }, compareWith: { classPropertyName: "compareWith", publicName: "compareWith", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectionChange: "selectionChange", multiSelectionChange: "multiSelectionChange" }, providers: [
|
|
15659
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnSelectComponent, isStandalone: true, selector: "tn-select", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, optionGroups: { classPropertyName: "optionGroups", publicName: "optionGroups", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, noOptionsLabel: { classPropertyName: "noOptionsLabel", publicName: "noOptionsLabel", isSignal: true, isRequired: false, transformFunction: null }, allowEmpty: { classPropertyName: "allowEmpty", publicName: "allowEmpty", isSignal: true, isRequired: false, transformFunction: null }, emptyLabel: { classPropertyName: "emptyLabel", publicName: "emptyLabel", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, showSelectAll: { classPropertyName: "showSelectAll", publicName: "showSelectAll", isSignal: true, isRequired: false, transformFunction: null }, selectAllLabel: { classPropertyName: "selectAllLabel", publicName: "selectAllLabel", isSignal: true, isRequired: false, transformFunction: null }, testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, optionTestIdKey: { classPropertyName: "optionTestIdKey", publicName: "optionTestIdKey", isSignal: true, isRequired: false, transformFunction: null }, compareWith: { classPropertyName: "compareWith", publicName: "compareWith", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectionChange: "selectionChange", multiSelectionChange: "multiSelectionChange" }, host: { listeners: { "keydown": "onHostKeydown($event)" } }, providers: [
|
|
15479
15660
|
{
|
|
15480
15661
|
provide: NG_VALUE_ACCESSOR,
|
|
15481
15662
|
useExisting: forwardRef(() => TnSelectComponent),
|
|
@@ -15491,7 +15672,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
15491
15672
|
useExisting: forwardRef(() => TnSelectComponent),
|
|
15492
15673
|
multi: true
|
|
15493
15674
|
}
|
|
15494
|
-
], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"tn-select-container\">\n <!-- Select Trigger -->\n <div\n #triggerEl\n class=\"tn-select-trigger\"\n role=\"combobox\"\n tnTestIdType=\"select\"\n [tnTestId]=\"resolvedTestId()\"\n [attr.tabindex]=\"isDisabled() ? -1 : 0\"\n [class.disabled]=\"isDisabled()\"\n [class.open]=\"isOpen()\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-haspopup]=\"'listbox'\"\n [attr.aria-controls]=\"isOpen() ? 'tn-select-dropdown-' + idNamespace() : null\"\n [attr.aria-label]=\"triggerAriaLabel()\"\n [attr.aria-labelledby]=\"fieldAria.labelledby()\"\n [attr.aria-describedby]=\"fieldAria.describedby()\"\n [attr.aria-invalid]=\"fieldAria.invalid()\"\n [attr.aria-required]=\"fieldAria.required()\"\n [attr.aria-disabled]=\"isDisabled()\"\n [attr.aria-activedescendant]=\"isOpen() ? focusedOptionId() : null\"\n (click)=\"toggleDropdown()\"\n (keydown)=\"onKeydown($event)\">\n\n <!-- Display Text -->\n <span\n class=\"tn-select-text\"\n [class.placeholder]=\"multiple() ? selectedValues().length === 0 : (selectedValue() === null || selectedValue() === undefined)\">\n {{ displayText() }}\n </span>\n\n <!-- Dropdown Arrow -->\n <div class=\"tn-select-arrow\" [class.open]=\"isOpen()\">\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <polyline points=\"6,9 12,15 18,9\" />\n </svg>\n </div>\n </div>\n</div>\n\n<!-- Dropdown panel \u2014 portaled via CDK Overlay so it escapes parent\n overflow/clipping. The trigger keeps DOM focus throughout (combobox\n pattern); options use aria-activedescendant for virtual focus. -->\n<ng-template #dropdownTemplate>\n <div\n class=\"tn-select-dropdown\"\n role=\"listbox\"\n [attr.aria-multiselectable]=\"multiple()\"\n [attr.id]=\"'tn-select-dropdown-' + idNamespace()\">\n\n <!-- Options List -->\n <div class=\"tn-select-options\">\n <!-- Select-all row (multiple mode with showSelectAll). Toggles every\n selectable option; its checkbox reflects all/some/none. -->\n @if (showSelectAllRow()) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events -->\n <div\n class=\"tn-select-option tn-select-select-all\"\n role=\"option\"\n tabindex=\"-1\"\n tnTestIdType=\"option\"\n [tnTestId]=\"selectAllTestIdParts()\"\n [attr.id]=\"selectAllId()\"\n [class.selected]=\"allSelected()\"\n [class.focused]=\"isSelectAllFocused()\"\n [attr.aria-selected]=\"allSelected()\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"toggleSelectAll()\">\n <tn-checkbox\n class=\"tn-select-check\"\n label=\"\"\n [checked]=\"allSelected()\"\n [indeterminate]=\"selectAllIndeterminate()\"\n [disabled]=\"true\"\n [hideLabel]=\"true\" />\n {{ resolvedSelectAllLabel() }}\n </div>\n <!-- Decorative only, and deliberately role-less: the panel above is a\n `role=\"listbox\"`, which owns `option` and `group` and nothing else,\n so a `role=\"separator\"` here invalidates the whole listbox\n (aria-required-children, #259). The rule is drawn by its class. -->\n <div class=\"tn-select-separator\"></div>\n }\n\n <!-- Regular Options (preceded by the synthetic empty option when allowEmpty is set) -->\n @for (option of displayOptions(); track $index) {\n <!-- Option activation lives on the trigger via aria-activedescendant; options have tabindex=\"-1\" and never receive DOM focus. -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events -->\n <div\n class=\"tn-select-option\"\n role=\"option\"\n tabindex=\"-1\"\n tnTestIdType=\"option\"\n [class.tn-select-empty-option]=\"isEmptyOption(option)\"\n [tnTestId]=\"optionTestIdParts(option)\"\n [attr.id]=\"optionId(option)\"\n [class.selected]=\"isOptionSelected(option)\"\n [class.focused]=\"isOptionFocused(option)\"\n [class.disabled]=\"option.disabled\"\n [attr.aria-selected]=\"isOptionSelected(option)\"\n [attr.aria-disabled]=\"option.disabled\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"onOptionClick(option)\">\n @if (multiple()) {\n <tn-checkbox\n class=\"tn-select-check\"\n label=\"\"\n [checked]=\"isOptionSelected(option)\"\n [disabled]=\"true\"\n [hideLabel]=\"true\" />\n }\n {{ option.label }}\n </div>\n }\n\n <!-- Option Groups -->\n @for (group of optionGroups(); track $index; let isFirst = $first) {\n <!-- Group Separator (not shown before first group if we have regular\n options). Role-less for the same reason as the one above the\n options \u2014 see there. -->\n @if (!isFirst || displayOptions().length > 0) {\n <div class=\"tn-select-separator\"></div>\n }\n\n <div role=\"group\" [attr.aria-labelledby]=\"'tn-select-group-' + idNamespace() + '-' + $index\">\n <!-- Group Label -->\n <div\n class=\"tn-select-group-label\"\n [attr.id]=\"'tn-select-group-' + idNamespace() + '-' + $index\"\n [class.disabled]=\"group.disabled\">\n {{ group.label }}\n </div>\n\n <!-- Group Options -->\n @for (option of group.options; track $index) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events -->\n <div\n class=\"tn-select-option\"\n role=\"option\"\n tabindex=\"-1\"\n tnTestIdType=\"option\"\n [tnTestId]=\"optionTestIdParts(option)\"\n [attr.id]=\"optionId(option)\"\n [class.selected]=\"isOptionSelected(option)\"\n [class.focused]=\"isOptionFocused(option)\"\n [class.disabled]=\"option.disabled || group.disabled\"\n [attr.aria-selected]=\"isOptionSelected(option)\"\n [attr.aria-disabled]=\"option.disabled || group.disabled\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"onOptionClick(option, !!group.disabled)\">\n @if (multiple()) {\n <tn-checkbox\n class=\"tn-select-check\"\n label=\"\"\n [checked]=\"isOptionSelected(option)\"\n [disabled]=\"true\"\n [hideLabel]=\"true\" />\n }\n {{ option.label }}\n </div>\n }\n </div>\n }\n\n <!-- No Options Message -->\n @if (!hasAnyOptions()) {\n <div class=\"tn-select-no-options\">\n {{ resolvedNoOptionsLabel() }}\n </div>\n }\n </div>\n </div>\n</ng-template>\n", styles: [".tn-select-container{position:relative;width:100%;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif}.tn-select-label{display:block;margin-bottom:.5rem;font-size:1rem;font-weight:500;color:var(--tn-fg1, #333);line-height:1.4}.tn-select-label.required .required-asterisk{color:var(--tn-error, var(--tn-red, #bd2626));margin-left:.25rem}.tn-select-trigger{position:relative;display:flex;align-items:center;min-height:2.5rem;padding:.5rem 2.5rem .5rem .75rem;background-color:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;cursor:pointer;transition:all .15s ease-in-out;outline:none;box-sizing:border-box}.tn-select-trigger:hover:not(.disabled){border-color:var(--tn-primary, #007bff)}.tn-select-trigger:focus-visible{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px color-mix(in srgb,var(--tn-primary, #007bff) 25%,transparent)}.tn-select-trigger.open{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px color-mix(in srgb,var(--tn-primary, #007bff) 25%,transparent)}.tn-select-trigger.error{border-color:var(--tn-error, var(--tn-red, #bd2626))}.tn-select-trigger.error:focus-visible,.tn-select-trigger.error.open{border-color:var(--tn-error, var(--tn-red, #bd2626));box-shadow:0 0 0 2px color-mix(in srgb,var(--tn-error, var(--tn-red, #bd2626)) 25%,transparent)}.tn-select-trigger.disabled{background-color:var(--tn-alt-bg1, #f8f9fa);color:var(--tn-fg2, #6c757d);cursor:not-allowed;opacity:.6}.tn-select-text{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--tn-fg1, #212529)}.tn-select-text.placeholder{color:var(--tn-alt-fg1, #999)}.tn-select-arrow{position:absolute;right:.75rem;top:50%;transform:translateY(-50%);color:var(--tn-fg2, #6c757d);transition:transform .15s ease-in-out;pointer-events:none}.tn-select-arrow.open{transform:translateY(-50%) rotate(180deg)}.tn-select-arrow svg{display:block}.tn-select-dropdown{--tn-select-dropdown-max-height: 350px;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-select-dropdown-max-height);overflow-y:auto}.tn-select-options{padding:.25rem 0}.tn-select-option{display:flex;align-items:center;padding:.5rem .75rem;overflow-wrap:anywhere;cursor:pointer;color:var(--tn-fg1, #212529);transition:background-color .15s ease-in-out;pointer-events:auto;position:relative;z-index:1001}.tn-select-option.selected{background-color:var(--tn-alt-bg1, #f8f9fa);color:var(--tn-fg1, #212529)}.tn-select-option:hover:not(.disabled):not(.focused){background-color:var(--tn-alt-bg2, #f8f9fa)}.tn-select-option.focused{background-color:var(--tn-alt-bg2, #e8f4fd);box-shadow:inset 2px 0 0 var(--tn-primary, #007bff)}.tn-select-option.disabled{color:var(--tn-fg2, #6c757d);cursor:not-allowed;opacity:.6}.tn-select-option.tn-select-empty-option{color:var(--tn-fg2, #6c757d)}.tn-select-select-all{font-weight:600}.tn-select-check{margin-right:.5rem;flex-shrink:0;pointer-events:none}.tn-select-separator{height:1px;background:var(--tn-lines, #e0e0e0);margin:.25rem 0}.tn-select-group-label{padding:.375rem .75rem;font-size:.75rem;font-weight:600;color:var(--tn-alt-fg1, #9ca3af);text-transform:uppercase;letter-spacing:.05em;cursor:default;-webkit-user-select:none;user-select:none}.tn-select-group-label.disabled{opacity:.6}.tn-select-no-options{padding:1rem .75rem;text-align:center;color:var(--tn-fg2, #6c757d);font-style:italic}.tn-select-error{margin-top:.25rem;font-size:.75rem;color:var(--tn-error, var(--tn-red, #bd2626))}@media(prefers-reduced-motion:reduce){.tn-select-trigger,.tn-select-option,.tn-select-arrow{transition:none}}@media(prefers-contrast:high){.tn-select-trigger{border-width:2px}.tn-select-option.selected{outline:2px solid var(--tn-fg1, #000);outline-offset:-2px}}\n"] }]
|
|
15675
|
+
], host: {
|
|
15676
|
+
'(keydown)': 'onHostKeydown($event)',
|
|
15677
|
+
}, changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"tn-select-container\">\n <!-- Select Trigger -->\n <div\n #triggerEl\n class=\"tn-select-trigger\"\n role=\"combobox\"\n tnTestIdType=\"select\"\n [tnTestId]=\"resolvedTestId()\"\n [attr.tabindex]=\"isDisabled() ? -1 : 0\"\n [class.disabled]=\"isDisabled()\"\n [class.open]=\"isOpen()\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-haspopup]=\"'listbox'\"\n [attr.aria-controls]=\"isOpen() ? 'tn-select-dropdown-' + idNamespace() : null\"\n [attr.aria-label]=\"triggerAriaLabel()\"\n [attr.aria-labelledby]=\"fieldAria.labelledby()\"\n [attr.aria-describedby]=\"fieldAria.describedby()\"\n [attr.aria-invalid]=\"fieldAria.invalid()\"\n [attr.aria-required]=\"fieldAria.required()\"\n [attr.aria-disabled]=\"isDisabled()\"\n [attr.aria-activedescendant]=\"isOpen() ? focusedOptionId() : null\"\n (click)=\"toggleDropdown()\"\n (keydown)=\"onKeydown($event)\">\n\n <!-- Display Text -->\n <span\n class=\"tn-select-text\"\n [class.placeholder]=\"multiple() ? selectedValues().length === 0 : (selectedValue() === null || selectedValue() === undefined)\">\n {{ displayText() }}\n </span>\n\n <!-- Dropdown Arrow -->\n <div class=\"tn-select-arrow\" [class.open]=\"isOpen()\">\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <polyline points=\"6,9 12,15 18,9\" />\n </svg>\n </div>\n </div>\n</div>\n\n<!-- Dropdown panel \u2014 portaled via CDK Overlay so it escapes parent\n overflow/clipping. The trigger keeps DOM focus throughout (combobox\n pattern); options use aria-activedescendant for virtual focus. -->\n<ng-template #dropdownTemplate>\n <div\n class=\"tn-select-dropdown\"\n role=\"listbox\"\n [attr.aria-multiselectable]=\"multiple()\"\n [attr.id]=\"'tn-select-dropdown-' + idNamespace()\">\n\n <!-- Options List -->\n <div class=\"tn-select-options\">\n <!-- Select-all row (multiple mode with showSelectAll). Toggles every\n selectable option; its checkbox reflects all/some/none. -->\n @if (showSelectAllRow()) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events -->\n <div\n class=\"tn-select-option tn-select-select-all\"\n role=\"option\"\n tabindex=\"-1\"\n tnTestIdType=\"option\"\n [tnTestId]=\"selectAllTestIdParts()\"\n [attr.id]=\"selectAllId()\"\n [class.selected]=\"allSelected()\"\n [class.focused]=\"isSelectAllFocused()\"\n [attr.aria-selected]=\"allSelected()\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"toggleSelectAll()\">\n <tn-checkbox\n class=\"tn-select-check\"\n label=\"\"\n [checked]=\"allSelected()\"\n [indeterminate]=\"selectAllIndeterminate()\"\n [disabled]=\"true\"\n [hideLabel]=\"true\" />\n {{ resolvedSelectAllLabel() }}\n </div>\n <!-- Decorative only, and deliberately role-less: the panel above is a\n `role=\"listbox\"`, which owns `option` and `group` and nothing else,\n so a `role=\"separator\"` here invalidates the whole listbox\n (aria-required-children, #259). The rule is drawn by its class. -->\n <div class=\"tn-select-separator\"></div>\n }\n\n <!-- Regular Options (preceded by the synthetic empty option when allowEmpty is set) -->\n @for (option of displayOptions(); track $index) {\n <!-- Option activation lives on the trigger via aria-activedescendant; options have tabindex=\"-1\" and never receive DOM focus. -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events -->\n <div\n class=\"tn-select-option\"\n role=\"option\"\n tabindex=\"-1\"\n tnTestIdType=\"option\"\n [class.tn-select-empty-option]=\"isEmptyOption(option)\"\n [tnTestId]=\"optionTestIdParts(option)\"\n [attr.id]=\"optionId(option)\"\n [class.selected]=\"isOptionSelected(option)\"\n [class.focused]=\"isOptionFocused(option)\"\n [class.disabled]=\"option.disabled\"\n [attr.aria-selected]=\"isOptionSelected(option)\"\n [attr.aria-disabled]=\"option.disabled\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"onOptionClick(option)\">\n @if (multiple()) {\n <tn-checkbox\n class=\"tn-select-check\"\n label=\"\"\n [checked]=\"isOptionSelected(option)\"\n [disabled]=\"true\"\n [hideLabel]=\"true\" />\n }\n {{ option.label }}\n </div>\n }\n\n <!-- Option Groups -->\n @for (group of optionGroups(); track $index; let isFirst = $first) {\n <!-- Group Separator (not shown before first group if we have regular\n options). Role-less for the same reason as the one above the\n options \u2014 see there. -->\n @if (!isFirst || displayOptions().length > 0) {\n <div class=\"tn-select-separator\"></div>\n }\n\n <div role=\"group\" [attr.aria-labelledby]=\"'tn-select-group-' + idNamespace() + '-' + $index\">\n <!-- Group Label -->\n <div\n class=\"tn-select-group-label\"\n [attr.id]=\"'tn-select-group-' + idNamespace() + '-' + $index\"\n [class.disabled]=\"group.disabled\">\n {{ group.label }}\n </div>\n\n <!-- Group Options -->\n @for (option of group.options; track $index) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events -->\n <div\n class=\"tn-select-option\"\n role=\"option\"\n tabindex=\"-1\"\n tnTestIdType=\"option\"\n [tnTestId]=\"optionTestIdParts(option)\"\n [attr.id]=\"optionId(option)\"\n [class.selected]=\"isOptionSelected(option)\"\n [class.focused]=\"isOptionFocused(option)\"\n [class.disabled]=\"option.disabled || group.disabled\"\n [attr.aria-selected]=\"isOptionSelected(option)\"\n [attr.aria-disabled]=\"option.disabled || group.disabled\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"onOptionClick(option, !!group.disabled)\">\n @if (multiple()) {\n <tn-checkbox\n class=\"tn-select-check\"\n label=\"\"\n [checked]=\"isOptionSelected(option)\"\n [disabled]=\"true\"\n [hideLabel]=\"true\" />\n }\n {{ option.label }}\n </div>\n }\n </div>\n }\n\n <!-- No Options Message -->\n @if (!hasAnyOptions()) {\n <div class=\"tn-select-no-options\">\n {{ resolvedNoOptionsLabel() }}\n </div>\n }\n </div>\n </div>\n</ng-template>\n", styles: [".tn-select-container{position:relative;width:100%;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif}.tn-select-label{display:block;margin-bottom:.5rem;font-size:1rem;font-weight:500;color:var(--tn-fg1, #333);line-height:1.4}.tn-select-label.required .required-asterisk{color:var(--tn-error, var(--tn-red, #bd2626));margin-left:.25rem}.tn-select-trigger{position:relative;display:flex;align-items:center;min-height:2.5rem;padding:.5rem 2.5rem .5rem .75rem;background-color:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;cursor:pointer;transition:all .15s ease-in-out;outline:none;box-sizing:border-box}.tn-select-trigger:hover:not(.disabled){border-color:var(--tn-primary, #007bff)}.tn-select-trigger:focus-visible{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px color-mix(in srgb,var(--tn-primary, #007bff) 25%,transparent)}.tn-select-trigger.open{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px color-mix(in srgb,var(--tn-primary, #007bff) 25%,transparent)}.tn-select-trigger.error{border-color:var(--tn-error, var(--tn-red, #bd2626))}.tn-select-trigger.error:focus-visible,.tn-select-trigger.error.open{border-color:var(--tn-error, var(--tn-red, #bd2626));box-shadow:0 0 0 2px color-mix(in srgb,var(--tn-error, var(--tn-red, #bd2626)) 25%,transparent)}.tn-select-trigger.disabled{background-color:var(--tn-alt-bg1, #f8f9fa);color:var(--tn-fg2, #6c757d);cursor:not-allowed;opacity:.6}.tn-select-text{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--tn-fg1, #212529)}.tn-select-text.placeholder{color:var(--tn-alt-fg1, #999)}.tn-select-arrow{position:absolute;right:.75rem;top:50%;transform:translateY(-50%);color:var(--tn-fg2, #6c757d);transition:transform .15s ease-in-out;pointer-events:none}.tn-select-arrow.open{transform:translateY(-50%) rotate(180deg)}.tn-select-arrow svg{display:block}.tn-select-dropdown{--tn-select-dropdown-max-height: 350px;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-select-dropdown-max-height);overflow-y:auto}.tn-select-options{padding:.25rem 0}.tn-select-option{display:flex;align-items:center;padding:.5rem .75rem;overflow-wrap:anywhere;cursor:pointer;color:var(--tn-fg1, #212529);transition:background-color .15s ease-in-out;pointer-events:auto;position:relative;z-index:1001}.tn-select-option.selected{background-color:var(--tn-alt-bg1, #f8f9fa);color:var(--tn-fg1, #212529)}.tn-select-option:hover:not(.disabled):not(.focused){background-color:var(--tn-alt-bg2, #f8f9fa)}.tn-select-option.focused{background-color:var(--tn-alt-bg2, #e8f4fd);box-shadow:inset 2px 0 0 var(--tn-primary, #007bff)}.tn-select-option.disabled{color:var(--tn-fg2, #6c757d);cursor:not-allowed;opacity:.6}.tn-select-option.tn-select-empty-option{color:var(--tn-fg2, #6c757d)}.tn-select-select-all{font-weight:600}.tn-select-check{margin-right:.5rem;flex-shrink:0;pointer-events:none}.tn-select-separator{height:1px;background:var(--tn-lines, #e0e0e0);margin:.25rem 0}.tn-select-group-label{padding:.375rem .75rem;font-size:.75rem;font-weight:600;color:var(--tn-alt-fg1, #9ca3af);text-transform:uppercase;letter-spacing:.05em;cursor:default;-webkit-user-select:none;user-select:none}.tn-select-group-label.disabled{opacity:.6}.tn-select-no-options{padding:1rem .75rem;text-align:center;color:var(--tn-fg2, #6c757d);font-style:italic}.tn-select-error{margin-top:.25rem;font-size:.75rem;color:var(--tn-error, var(--tn-red, #bd2626))}@media(prefers-reduced-motion:reduce){.tn-select-trigger,.tn-select-option,.tn-select-arrow{transition:none}}@media(prefers-contrast:high){.tn-select-trigger{border-width:2px}.tn-select-option.selected{outline:2px solid var(--tn-fg1, #000);outline-offset:-2px}}\n"] }]
|
|
15495
15678
|
}], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], optionGroups: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionGroups", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], noOptionsLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "noOptionsLabel", required: false }] }], allowEmpty: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowEmpty", required: false }] }], emptyLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyLabel", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], showSelectAll: [{ type: i0.Input, args: [{ isSignal: true, alias: "showSelectAll", required: false }] }], selectAllLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectAllLabel", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], optionTestIdKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionTestIdKey", required: false }] }], compareWith: [{ type: i0.Input, args: [{ isSignal: true, alias: "compareWith", required: false }] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], multiSelectionChange: [{ type: i0.Output, args: ["multiSelectionChange"] }], triggerEl: [{ type: i0.ViewChild, args: ['triggerEl', { isSignal: true }] }], dropdownTemplate: [{ type: i0.ViewChild, args: ['dropdownTemplate', { isSignal: true }] }] } });
|
|
15496
15679
|
|
|
15497
15680
|
/**
|
|
@@ -18334,7 +18517,9 @@ class TnTableComponent {
|
|
|
18334
18517
|
ngOnInit() {
|
|
18335
18518
|
this.initialized = true;
|
|
18336
18519
|
this.destroyRef.onDestroy(() => {
|
|
18337
|
-
|
|
18520
|
+
// Not `clearSelection()`: that emits, and `selectionChange` is already torn down
|
|
18521
|
+
// here, which warns NG0953. Nothing is left to receive it either way.
|
|
18522
|
+
this.resetSelectionState();
|
|
18338
18523
|
this.resizeObserver?.disconnect();
|
|
18339
18524
|
});
|
|
18340
18525
|
}
|
|
@@ -18897,10 +19082,19 @@ class TnTableComponent {
|
|
|
18897
19082
|
* the next reconcile.
|
|
18898
19083
|
*/
|
|
18899
19084
|
clearSelection() {
|
|
19085
|
+
this.resetSelectionState();
|
|
19086
|
+
this.emitSelectionIfChanged();
|
|
19087
|
+
}
|
|
19088
|
+
/**
|
|
19089
|
+
* Drops the selection state and tells nobody. Only teardown wants this: by the time
|
|
19090
|
+
* the `onDestroy` hook runs, `selectionChange` is an `OutputRef` Angular has already
|
|
19091
|
+
* destroyed, and emitting on it warns NG0953 in dev mode. Every live path wants
|
|
19092
|
+
* {@link clearSelection}, because a consumer mirroring the selection has to hear it.
|
|
19093
|
+
*/
|
|
19094
|
+
resetSelectionState() {
|
|
18900
19095
|
this.selection.clear();
|
|
18901
19096
|
this.selectedByKey.clear();
|
|
18902
19097
|
this.selectionCount.set(0);
|
|
18903
|
-
this.emitSelectionIfChanged();
|
|
18904
19098
|
}
|
|
18905
19099
|
/** Re-applies the retained selection to the rows currently on screen. */
|
|
18906
19100
|
syncSelectionToData(rows, selectionKey) {
|
|
@@ -24159,6 +24353,41 @@ class TnDateInputComponent {
|
|
|
24159
24353
|
}
|
|
24160
24354
|
// Month is the first field
|
|
24161
24355
|
}
|
|
24356
|
+
/**
|
|
24357
|
+
* Escape, which closes the calendar and CONSUMES the key.
|
|
24358
|
+
*
|
|
24359
|
+
* Opening the calendar does not move focus into it, so the keystroke a user
|
|
24360
|
+
* actually makes starts on a date segment — inside this host, not inside the
|
|
24361
|
+
* overlay. That is the path the `keydownEvents()` subscription in
|
|
24362
|
+
* `createOverlay` cannot serve: CDK's `OverlayKeyboardDispatcher` listens on
|
|
24363
|
+
* `<body>`, so any ancestor that stops the key first means it never runs.
|
|
24364
|
+
* `tn-drawer` in `over` mode is exactly such an ancestor — it calls
|
|
24365
|
+
* `stopPropagation()` on Escape from a handler on its own panel element — so
|
|
24366
|
+
* a calendar left waiting for the dispatcher stayed open while the drawer
|
|
24367
|
+
* closed underneath it (#324).
|
|
24368
|
+
*
|
|
24369
|
+
* Consuming it also keeps a `tn-side-panel` from acting on the same key:
|
|
24370
|
+
* since #322 the panel is a CDK overlay subscribed through that dispatcher,
|
|
24371
|
+
* and closing this calendar disposes its overlay mid-keystroke, leaving the
|
|
24372
|
+
* panel as the top-most subscriber for an Escape allowed to carry on.
|
|
24373
|
+
*
|
|
24374
|
+
* With the calendar closed the key is NOT consumed: there is nothing to
|
|
24375
|
+
* close and it belongs to whatever contains this field.
|
|
24376
|
+
*
|
|
24377
|
+
* Why the HOST rather than the segment inputs: `stopPropagation()` stops
|
|
24378
|
+
* ANCESTOR listeners, not other listeners on the same element. From here a
|
|
24379
|
+
* `tnTooltip` on this component still sees Escape and dismisses itself, and
|
|
24380
|
+
* only the containers ABOVE this field stop seeing it — the same placement
|
|
24381
|
+
* `tn-select` and `tn-autocomplete` use.
|
|
24382
|
+
*/
|
|
24383
|
+
onHostKeydown(event) {
|
|
24384
|
+
if (event.key !== 'Escape' || !this.isOpen()) {
|
|
24385
|
+
return;
|
|
24386
|
+
}
|
|
24387
|
+
event.preventDefault();
|
|
24388
|
+
event.stopPropagation();
|
|
24389
|
+
this.close();
|
|
24390
|
+
}
|
|
24162
24391
|
openDatepicker() {
|
|
24163
24392
|
if (this.isOpen()) {
|
|
24164
24393
|
return;
|
|
@@ -24235,11 +24464,27 @@ class TnDateInputComponent {
|
|
|
24235
24464
|
this.overlayRef.backdropClick().subscribe(() => {
|
|
24236
24465
|
this.close();
|
|
24237
24466
|
});
|
|
24467
|
+
// Escape arriving from INSIDE the calendar — a day button the user tabbed
|
|
24468
|
+
// or clicked to. That keydown starts in the overlay, which is not under
|
|
24469
|
+
// this component's host, so `onHostKeydown` never sees it; CDK's keyboard
|
|
24470
|
+
// dispatcher does, and hands the key to the top-most attached overlay that
|
|
24471
|
+
// HAS subscribers, stopping there. Subscribing is what makes this calendar
|
|
24472
|
+
// — rather than a `tn-side-panel` or dialog underneath it — the thing that
|
|
24473
|
+
// Escape closes (#324).
|
|
24474
|
+
//
|
|
24475
|
+
// Escape from the date segments is the other half and is handled on the
|
|
24476
|
+
// host, because an ancestor can stop the key before the dispatcher runs.
|
|
24477
|
+
this.overlayRef.keydownEvents().subscribe((event) => {
|
|
24478
|
+
if (event.key === 'Escape' && !event.altKey && !event.ctrlKey && !event.metaKey) {
|
|
24479
|
+
event.preventDefault();
|
|
24480
|
+
this.close();
|
|
24481
|
+
}
|
|
24482
|
+
});
|
|
24238
24483
|
this.portal = new TemplatePortal(this.calendarTemplate(), this.viewContainerRef);
|
|
24239
24484
|
this.overlayRef.attach(this.portal);
|
|
24240
24485
|
}
|
|
24241
24486
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnDateInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
24242
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.1.0", type: TnDateInputComponent, isStandalone: true, selector: "tn-date-input", inputs: { disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, dateFilter: { classPropertyName: "dateFilter", publicName: "dateFilter", isSignal: true, isRequired: false, transformFunction: null }, testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "tn-date-input" }, providers: [
|
|
24487
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.1.0", type: TnDateInputComponent, isStandalone: true, selector: "tn-date-input", inputs: { disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, dateFilter: { classPropertyName: "dateFilter", publicName: "dateFilter", isSignal: true, isRequired: false, transformFunction: null }, testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "keydown": "onHostKeydown($event)" }, classAttribute: "tn-date-input" }, providers: [
|
|
24243
24488
|
{
|
|
24244
24489
|
provide: NG_VALUE_ACCESSOR,
|
|
24245
24490
|
useExisting: forwardRef(() => TnDateInputComponent),
|
|
@@ -24256,7 +24501,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
24256
24501
|
multi: true
|
|
24257
24502
|
}
|
|
24258
24503
|
], host: {
|
|
24259
|
-
'class': 'tn-date-input'
|
|
24504
|
+
'class': 'tn-date-input',
|
|
24505
|
+
// Escape only, and deliberately HERE rather than beside the segments' own
|
|
24506
|
+
// keydown binding — see `onHostKeydown`.
|
|
24507
|
+
'(keydown)': 'onHostKeydown($event)'
|
|
24260
24508
|
}, template: "<div class=\"tn-date-input-container\" tnTestIdType=\"date-input\" [tnTestId]=\"testId()\">\n <div #wrapper ixInput class=\"tn-date-input-wrapper\" style=\"padding-right: 40px;\">\n <!-- Date segments MM/DD/YYYY -->\n <div class=\"tn-date-segment-group\">\n <input\n #monthInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-month\"\n placeholder=\"MM\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('month')\"\n (blur)=\"onSegmentBlur('month')\"\n (keydown)=\"onSegmentKeydown($event, 'month')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #dayInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-day\"\n placeholder=\"DD\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('day')\"\n (blur)=\"onSegmentBlur('day')\"\n (keydown)=\"onSegmentKeydown($event, 'day')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #yearInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-year\"\n placeholder=\"YYYY\"\n maxlength=\"4\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('year')\"\n (blur)=\"onSegmentBlur('year')\"\n (keydown)=\"onSegmentKeydown($event, 'year')\">\n </div>\n\n <button\n type=\"button\"\n class=\"tn-date-input-toggle\"\n aria-label=\"Open calendar\"\n [disabled]=\"isDisabled()\"\n (click)=\"openDatepicker()\">\n <span aria-hidden=\"true\">\uD83D\uDCC5</span>\n </button>\n </div>\n\n <ng-template #calendarTemplate>\n <tn-calendar\n class=\"tn-calendar\"\n [startView]=\"'month'\"\n [rangeMode]=\"false\"\n [selected]=\"value()\"\n (selectedChange)=\"onDateSelected($event)\" />\n </ng-template>\n</div>\n", styles: [":host{display:block;width:100%}.tn-date-input-container{position:relative;display:flex;align-items:center}.tn-date-input-wrapper{display:flex;align-items:center;width:100%;position:relative}.tn-date-segment-group{display:flex;align-items:center}.tn-date-segment{background:transparent;border:none;outline:none;font:inherit;color:inherit;padding:0;min-width:0;text-align:center;width:2.6ch}.tn-date-segment::placeholder{color:var(--tn-alt-fg1, #999);opacity:1}.tn-date-segment:focus{outline:none;background:var(--tn-bg2, rgba(0, 0, 0, .05));border-radius:2px}.tn-date-segment:focus::placeholder{opacity:0}.tn-date-segment.tn-date-segment-year{width:4ch}.tn-date-segment-separator{padding:0 2px;-webkit-user-select:none;user-select:none;color:var(--tn-alt-fg1, #999)}.tn-date-input-toggle{position:absolute;right:8px;z-index:2;pointer-events:auto;background:transparent;border:none;cursor:pointer;padding:4px;font-size:1rem}.tn-date-input-toggle:hover{background:var(--tn-bg2, #f0f0f0);border-radius:4px}.tn-date-input-toggle:disabled{cursor:not-allowed;opacity:.5}.tn-calendar{box-sizing:content-box;width:calc(7 * var(--calendar-cell-size));background:var(--tn-bg1, white);border:1px solid var(--tn-lines, #e0e0e0);border-radius:8px;box-shadow:0 4px 12px #00000026;padding:24px;--calendar-cell-size: 48px;--calendar-header-height: 44px;--calendar-cell-font-size: 1rem;--calendar-header-font-size: 14px}.tn-calendar ::ng-deep .tn-calendar-content{padding:0}\n"] }]
|
|
24261
24509
|
}], propDecorators: { disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], dateFilter: [{ type: i0.Input, args: [{ isSignal: true, alias: "dateFilter", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], monthRef: [{ type: i0.ViewChild, args: ['monthInput', { isSignal: true }] }], dayRef: [{ type: i0.ViewChild, args: ['dayInput', { isSignal: true }] }], yearRef: [{ type: i0.ViewChild, args: ['yearInput', { isSignal: true }] }], calendarTemplate: [{ type: i0.ViewChild, args: ['calendarTemplate', { isSignal: true }] }], wrapperEl: [{ type: i0.ViewChild, args: ['wrapper', { isSignal: true }] }] } });
|
|
24262
24510
|
|
|
@@ -24723,6 +24971,32 @@ class TnDateRangeInputComponent {
|
|
|
24723
24971
|
}
|
|
24724
24972
|
}
|
|
24725
24973
|
}
|
|
24974
|
+
/**
|
|
24975
|
+
* Escape, which closes the calendar and CONSUMES the key. Same reasoning as
|
|
24976
|
+
* `TnDateInputComponent.onHostKeydown`, which this mirrors: opening does not
|
|
24977
|
+
* move focus into the overlay, so the real keystroke starts on a date
|
|
24978
|
+
* segment inside this host, where an ancestor such as a `tn-drawer` in
|
|
24979
|
+
* `over` mode would stop it before CDK's keyboard dispatcher ever ran — and
|
|
24980
|
+
* a calendar waiting on the dispatcher stayed open while the drawer closed
|
|
24981
|
+
* underneath it (#324).
|
|
24982
|
+
*
|
|
24983
|
+
* Consuming it also keeps a `tn-side-panel` from acting on the same key: the
|
|
24984
|
+
* panel is a CDK overlay subscribed through that dispatcher (#322), and
|
|
24985
|
+
* closing this calendar disposes its overlay mid-keystroke, leaving the
|
|
24986
|
+
* panel as the top-most subscriber for an Escape allowed to carry on.
|
|
24987
|
+
*
|
|
24988
|
+
* Closed, the key is not consumed; it belongs to whatever contains this
|
|
24989
|
+
* field. On the HOST rather than the segments so that `stopPropagation()`
|
|
24990
|
+
* still leaves a `tnTooltip` on this component able to see the key.
|
|
24991
|
+
*/
|
|
24992
|
+
onHostKeydown(event) {
|
|
24993
|
+
if (event.key !== 'Escape' || !this.isOpen()) {
|
|
24994
|
+
return;
|
|
24995
|
+
}
|
|
24996
|
+
event.preventDefault();
|
|
24997
|
+
event.stopPropagation();
|
|
24998
|
+
this.close();
|
|
24999
|
+
}
|
|
24726
25000
|
openDatepicker() {
|
|
24727
25001
|
if (this.isOpen()) {
|
|
24728
25002
|
return;
|
|
@@ -24799,11 +25073,27 @@ class TnDateRangeInputComponent {
|
|
|
24799
25073
|
this.overlayRef.backdropClick().subscribe(() => {
|
|
24800
25074
|
this.close();
|
|
24801
25075
|
});
|
|
25076
|
+
// Escape arriving from INSIDE the calendar — a day button the user tabbed
|
|
25077
|
+
// or clicked to. That keydown starts in the overlay, which is not under
|
|
25078
|
+
// this component's host, so `onHostKeydown` never sees it; CDK's keyboard
|
|
25079
|
+
// dispatcher does, and hands the key to the top-most attached overlay that
|
|
25080
|
+
// HAS subscribers, stopping there. Subscribing is what makes this calendar
|
|
25081
|
+
// — rather than a `tn-side-panel` or dialog underneath it — the thing that
|
|
25082
|
+
// Escape closes (#324).
|
|
25083
|
+
//
|
|
25084
|
+
// Escape from the date segments is the other half and is handled on the
|
|
25085
|
+
// host, because an ancestor can stop the key before the dispatcher runs.
|
|
25086
|
+
this.overlayRef.keydownEvents().subscribe((event) => {
|
|
25087
|
+
if (event.key === 'Escape' && !event.altKey && !event.ctrlKey && !event.metaKey) {
|
|
25088
|
+
event.preventDefault();
|
|
25089
|
+
this.close();
|
|
25090
|
+
}
|
|
25091
|
+
});
|
|
24802
25092
|
this.portal = new TemplatePortal(this.calendarTemplate(), this.viewContainerRef);
|
|
24803
25093
|
this.overlayRef.attach(this.portal);
|
|
24804
25094
|
}
|
|
24805
25095
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnDateRangeInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
24806
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.1.0", type: TnDateRangeInputComponent, isStandalone: true, selector: "tn-date-range-input", inputs: { disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "tn-date-range-input" }, providers: [
|
|
25096
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.1.0", type: TnDateRangeInputComponent, isStandalone: true, selector: "tn-date-range-input", inputs: { disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "keydown": "onHostKeydown($event)" }, classAttribute: "tn-date-range-input" }, providers: [
|
|
24807
25097
|
{
|
|
24808
25098
|
provide: NG_VALUE_ACCESSOR,
|
|
24809
25099
|
useExisting: forwardRef(() => TnDateRangeInputComponent),
|
|
@@ -24820,7 +25110,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
24820
25110
|
multi: true
|
|
24821
25111
|
}
|
|
24822
25112
|
], host: {
|
|
24823
|
-
'class': 'tn-date-range-input'
|
|
25113
|
+
'class': 'tn-date-range-input',
|
|
25114
|
+
// Escape only, and deliberately HERE rather than beside the segments' own
|
|
25115
|
+
// keydown binding — see `onHostKeydown`.
|
|
25116
|
+
'(keydown)': 'onHostKeydown($event)'
|
|
24824
25117
|
}, template: "<div class=\"tn-date-range-container\" tnTestIdType=\"date-range\" [tnTestId]=\"testId()\">\n <div #wrapper ixInput class=\"tn-date-range-wrapper\" style=\"padding-right: 40px;\">\n <!-- Start date segments -->\n <div class=\"tn-date-segment-group\">\n <input\n #startMonthInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-month\"\n placeholder=\"MM\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('start', 'month')\"\n (blur)=\"onSegmentBlur('start', 'month')\"\n (keydown)=\"onSegmentKeydown($event, 'start', 'month')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #startDayInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-day\"\n placeholder=\"DD\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('start', 'day')\"\n (blur)=\"onSegmentBlur('start', 'day')\"\n (keydown)=\"onSegmentKeydown($event, 'start', 'day')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #startYearInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-year\"\n placeholder=\"YYYY\"\n maxlength=\"4\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('start', 'year')\"\n (blur)=\"onSegmentBlur('start', 'year')\"\n (keydown)=\"onSegmentKeydown($event, 'start', 'year')\">\n </div>\n\n <span class=\"tn-date-range-separator\">\u2013</span>\n\n <!-- End date segments -->\n <div class=\"tn-date-segment-group\">\n <input\n #endMonthInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-month\"\n placeholder=\"MM\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('end', 'month')\"\n (blur)=\"onSegmentBlur('end', 'month')\"\n (keydown)=\"onSegmentKeydown($event, 'end', 'month')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #endDayInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-day\"\n placeholder=\"DD\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('end', 'day')\"\n (blur)=\"onSegmentBlur('end', 'day')\"\n (keydown)=\"onSegmentKeydown($event, 'end', 'day')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #endYearInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-year\"\n placeholder=\"YYYY\"\n maxlength=\"4\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('end', 'year')\"\n (blur)=\"onSegmentBlur('end', 'year')\"\n (keydown)=\"onSegmentKeydown($event, 'end', 'year')\">\n </div>\n\n <button\n type=\"button\"\n class=\"tn-date-range-toggle\"\n aria-label=\"Open calendar\"\n [disabled]=\"isDisabled()\"\n (click)=\"openDatepicker()\">\n <span aria-hidden=\"true\">\uD83D\uDCC5</span>\n </button>\n </div>\n\n <ng-template #calendarTemplate>\n <tn-calendar\n class=\"tn-calendar\"\n [startView]=\"'month'\"\n [rangeMode]=\"true\"\n [selectedRange]=\"value()\"\n (selectedRangeChange)=\"onRangeSelected($event)\" />\n </ng-template>\n</div>\n", styles: [":host{display:block;width:100%}.tn-date-range-container{position:relative;display:flex;align-items:center}.tn-date-range-wrapper{display:flex;align-items:center;width:100%;position:relative}.tn-date-segment-group{display:flex;align-items:center}.tn-date-segment{background:transparent;border:none;outline:none;font:inherit;color:inherit;padding:0;min-width:0;text-align:center;width:2.6ch}.tn-date-segment::placeholder{color:var(--tn-alt-fg1, #999);opacity:1}.tn-date-segment:focus{outline:none;background:var(--tn-bg2, rgba(0, 0, 0, .05));border-radius:2px}.tn-date-segment:focus::placeholder{opacity:0}.tn-date-segment.tn-date-segment-year{width:4ch}.tn-date-segment-separator{padding:0 2px;-webkit-user-select:none;user-select:none;color:var(--tn-alt-fg1, #999)}.tn-date-range-separator{padding:0 .25em;-webkit-user-select:none;user-select:none;color:var(--tn-fg2, #666);flex-shrink:0}.tn-date-range-toggle{position:absolute;right:8px;z-index:2;pointer-events:auto;background:transparent;border:none;cursor:pointer;padding:4px;font-size:1rem}.tn-date-range-toggle:hover{background:var(--tn-bg2, #f0f0f0);border-radius:4px}.tn-date-range-toggle:disabled{cursor:not-allowed;opacity:.5}.tn-calendar{box-sizing:content-box;width:calc(7 * var(--calendar-cell-size));background:var(--tn-bg1, white);border:1px solid var(--tn-lines, #e0e0e0);border-radius:8px;box-shadow:0 4px 12px #00000026;padding:24px;--calendar-cell-size: 48px;--calendar-header-height: 44px;--calendar-cell-font-size: 1rem;--calendar-header-font-size: 14px}.tn-calendar ::ng-deep .tn-calendar-content{padding:0}\n"] }]
|
|
24825
25118
|
}], propDecorators: { disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], startMonthRef: [{ type: i0.ViewChild, args: ['startMonthInput', { isSignal: true }] }], startDayRef: [{ type: i0.ViewChild, args: ['startDayInput', { isSignal: true }] }], startYearRef: [{ type: i0.ViewChild, args: ['startYearInput', { isSignal: true }] }], endMonthRef: [{ type: i0.ViewChild, args: ['endMonthInput', { isSignal: true }] }], endDayRef: [{ type: i0.ViewChild, args: ['endDayInput', { isSignal: true }] }], endYearRef: [{ type: i0.ViewChild, args: ['endYearInput', { isSignal: true }] }], calendarTemplate: [{ type: i0.ViewChild, args: ['calendarTemplate', { isSignal: true }] }], wrapperEl: [{ type: i0.ViewChild, args: ['wrapper', { isSignal: true }] }] } });
|
|
24826
25119
|
|
|
@@ -28643,6 +28936,42 @@ class TnFilePickerComponent {
|
|
|
28643
28936
|
this.openFilePicker();
|
|
28644
28937
|
}
|
|
28645
28938
|
}
|
|
28939
|
+
/**
|
|
28940
|
+
* Escape, which closes the popup and CONSUMES the key.
|
|
28941
|
+
*
|
|
28942
|
+
* Opening the popup does not move focus into it, so the keystroke a user
|
|
28943
|
+
* actually makes starts on the path input — inside this host, not inside the
|
|
28944
|
+
* overlay. That is the path the `keydownEvents()` subscription in
|
|
28945
|
+
* `createOverlay` cannot serve: CDK's `OverlayKeyboardDispatcher` listens on
|
|
28946
|
+
* `<body>`, so any ancestor that stops the key first means it never runs.
|
|
28947
|
+
* `tn-drawer` in `over` mode is exactly such an ancestor — it calls
|
|
28948
|
+
* `stopPropagation()` on Escape from a handler on its own panel element — so
|
|
28949
|
+
* a picker left waiting for the dispatcher stayed open while the drawer
|
|
28950
|
+
* closed underneath it (#324).
|
|
28951
|
+
*
|
|
28952
|
+
* Consuming it also keeps a `tn-side-panel` from acting on the same key:
|
|
28953
|
+
* since #322 the panel is a CDK overlay subscribed through that dispatcher,
|
|
28954
|
+
* and closing this popup disposes its overlay mid-keystroke, leaving the
|
|
28955
|
+
* panel as the top-most subscriber for an Escape allowed to carry on.
|
|
28956
|
+
*
|
|
28957
|
+
* With the popup closed the key is NOT consumed: there is nothing to close
|
|
28958
|
+
* and it belongs to whatever contains this field. The inline-creation row is
|
|
28959
|
+
* unaffected either way — it lives in the popup, whose keydowns never reach
|
|
28960
|
+
* this host.
|
|
28961
|
+
*
|
|
28962
|
+
* Why the HOST rather than the input: `stopPropagation()` stops ANCESTOR
|
|
28963
|
+
* listeners, not other listeners on the same element. From here a `tnTooltip`
|
|
28964
|
+
* on this component still sees Escape and dismisses itself, and only the
|
|
28965
|
+
* containers ABOVE this field stop seeing it.
|
|
28966
|
+
*/
|
|
28967
|
+
onHostKeydown(event) {
|
|
28968
|
+
if (event.key !== 'Escape' || !this.isOpen()) {
|
|
28969
|
+
return;
|
|
28970
|
+
}
|
|
28971
|
+
event.preventDefault();
|
|
28972
|
+
event.stopPropagation();
|
|
28973
|
+
this.close();
|
|
28974
|
+
}
|
|
28646
28975
|
openFilePicker() {
|
|
28647
28976
|
if (this.isOpen() || this.isDisabled()) {
|
|
28648
28977
|
return;
|
|
@@ -28955,11 +29284,32 @@ class TnFilePickerComponent {
|
|
|
28955
29284
|
this.overlayRef.backdropClick().subscribe(() => {
|
|
28956
29285
|
this.close();
|
|
28957
29286
|
});
|
|
29287
|
+
// Escape arriving from INSIDE the popup — a folder row or footer button
|
|
29288
|
+
// the user tabbed or clicked to. That keydown starts in the overlay, which
|
|
29289
|
+
// is not under this component's host, so `onHostKeydown` never sees it;
|
|
29290
|
+
// CDK's keyboard dispatcher does, and hands the key to the top-most
|
|
29291
|
+
// attached overlay that HAS subscribers, stopping there. Subscribing is
|
|
29292
|
+
// what makes this popup — rather than a `tn-side-panel` or dialog
|
|
29293
|
+
// underneath it — the thing that Escape closes (#324).
|
|
29294
|
+
//
|
|
29295
|
+
// Escape from the path input is the other half and is handled on the host,
|
|
29296
|
+
// because an ancestor can stop the key before the dispatcher runs.
|
|
29297
|
+
//
|
|
29298
|
+
// The inline-creation row inside the popup stops Escape from propagating
|
|
29299
|
+
// while it is open (see `onInlineCreationKeyDown`), so the key never
|
|
29300
|
+
// reaches the dispatcher and cancelling that row still wins over closing
|
|
29301
|
+
// the popup.
|
|
29302
|
+
this.overlayRef.keydownEvents().subscribe((event) => {
|
|
29303
|
+
if (event.key === 'Escape' && !event.altKey && !event.ctrlKey && !event.metaKey) {
|
|
29304
|
+
event.preventDefault();
|
|
29305
|
+
this.close();
|
|
29306
|
+
}
|
|
29307
|
+
});
|
|
28958
29308
|
this.portal = new TemplatePortal(this.filePickerTemplate(), this.viewContainerRef);
|
|
28959
29309
|
this.overlayRef.attach(this.portal);
|
|
28960
29310
|
}
|
|
28961
29311
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnFilePickerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
28962
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.1.0", type: TnFilePickerComponent, isStandalone: true, selector: "tn-file-picker", inputs: { mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, multiSelect: { classPropertyName: "multiSelect", publicName: "multiSelect", isSignal: true, isRequired: false, transformFunction: null }, createActions: { classPropertyName: "createActions", publicName: "createActions", isSignal: true, isRequired: false, transformFunction: null }, allowManualInput: { classPropertyName: "allowManualInput", publicName: "allowManualInput", isSignal: true, isRequired: false, transformFunction: null }, openOnClick: { classPropertyName: "openOnClick", publicName: "openOnClick", 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 }, testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null }, startPath: { classPropertyName: "startPath", publicName: "startPath", isSignal: true, isRequired: false, transformFunction: null }, rootPath: { classPropertyName: "rootPath", publicName: "rootPath", isSignal: true, isRequired: false, transformFunction: null }, valueRoot: { classPropertyName: "valueRoot", publicName: "valueRoot", isSignal: true, isRequired: false, transformFunction: null }, fileExtensions: { classPropertyName: "fileExtensions", publicName: "fileExtensions", isSignal: true, isRequired: false, transformFunction: null }, callbacks: { classPropertyName: "callbacks", publicName: "callbacks", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectionChange: "selectionChange", pathChange: "pathChange", createAction: "createAction", error: "error" }, host: { properties: { "class.error": "hasError()" }, classAttribute: "tn-file-picker" }, providers: [
|
|
29312
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.1.0", type: TnFilePickerComponent, isStandalone: true, selector: "tn-file-picker", inputs: { mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, multiSelect: { classPropertyName: "multiSelect", publicName: "multiSelect", isSignal: true, isRequired: false, transformFunction: null }, createActions: { classPropertyName: "createActions", publicName: "createActions", isSignal: true, isRequired: false, transformFunction: null }, allowManualInput: { classPropertyName: "allowManualInput", publicName: "allowManualInput", isSignal: true, isRequired: false, transformFunction: null }, openOnClick: { classPropertyName: "openOnClick", publicName: "openOnClick", 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 }, testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null }, startPath: { classPropertyName: "startPath", publicName: "startPath", isSignal: true, isRequired: false, transformFunction: null }, rootPath: { classPropertyName: "rootPath", publicName: "rootPath", isSignal: true, isRequired: false, transformFunction: null }, valueRoot: { classPropertyName: "valueRoot", publicName: "valueRoot", isSignal: true, isRequired: false, transformFunction: null }, fileExtensions: { classPropertyName: "fileExtensions", publicName: "fileExtensions", isSignal: true, isRequired: false, transformFunction: null }, callbacks: { classPropertyName: "callbacks", publicName: "callbacks", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectionChange: "selectionChange", pathChange: "pathChange", createAction: "createAction", error: "error" }, host: { listeners: { "keydown": "onHostKeydown($event)" }, properties: { "class.error": "hasError()" }, classAttribute: "tn-file-picker" }, providers: [
|
|
28963
29313
|
{
|
|
28964
29314
|
provide: NG_VALUE_ACCESSOR,
|
|
28965
29315
|
useExisting: forwardRef(() => TnFilePickerComponent),
|
|
@@ -28985,7 +29335,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
28985
29335
|
}
|
|
28986
29336
|
], host: {
|
|
28987
29337
|
'class': 'tn-file-picker',
|
|
28988
|
-
'[class.error]': 'hasError()'
|
|
29338
|
+
'[class.error]': 'hasError()',
|
|
29339
|
+
// Escape only, and deliberately HERE rather than beside the input's own
|
|
29340
|
+
// bindings — see `onHostKeydown`.
|
|
29341
|
+
'(keydown)': 'onHostKeydown($event)'
|
|
28989
29342
|
}, template: "<div class=\"tn-file-picker-container\" tnTestIdType=\"file-picker\" [tnTestId]=\"resolvedTestId()\">\n <div #wrapper ixInput class=\"tn-file-picker-wrapper\" style=\"padding-right: 40px;\">\n <input\n type=\"text\"\n class=\"tn-file-picker-input\"\n tnTestIdType=\"input\"\n [tnTestId]=\"resolvedTestId()\"\n [class.error]=\"hasError()\"\n [value]=\"selectedPath()\"\n [placeholder]=\"placeholder()\"\n [readonly]=\"!allowManualInput()\"\n [disabled]=\"isDisabled()\"\n (click)=\"onInputClick()\"\n (change)=\"onPathCommit($event)\">\n\n <button\n type=\"button\"\n class=\"tn-file-picker-toggle\"\n aria-label=\"Open file picker\"\n tnTestIdType=\"button\"\n [tnTestId]=\"toggleTestId()\"\n [disabled]=\"isDisabled()\"\n (click)=\"openFilePicker()\">\n <tn-icon name=\"folder\" library=\"mdi\" />\n </button>\n </div>\n \n <ng-template #filePickerTemplate>\n <tn-file-picker-popup\n class=\"tn-file-picker-popup\"\n tnTestIdType=\"file-picker-popup\"\n [tnTestId]=\"resolvedTestId()\"\n [testIdBase]=\"resolvedTestId()\"\n [mode]=\"mode()\"\n [multiSelect]=\"multiSelect()\"\n [createActions]=\"createActions()\"\n [currentPath]=\"currentPath()\"\n [rootPath]=\"effectiveRootPath()\"\n [fileItems]=\"fileItems()\"\n [selectedItems]=\"selectedItems()\"\n [loading]=\"loading()\"\n [fileExtensions]=\"fileExtensions()\"\n (itemClick)=\"onItemClick($event)\"\n (itemDoubleClick)=\"onItemDoubleClick($event)\"\n (pathNavigate)=\"navigateToPath($event)\"\n (createAction)=\"createAction.emit($event)\"\n (created)=\"onCreated($event)\"\n (clearSelection)=\"onClearSelection()\"\n (submit)=\"onSubmit()\"\n (cancel)=\"onCancel()\"\n (close)=\"close()\" />\n </ng-template>\n</div>", styles: [":host{display:block;width:100%;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif}.tn-file-picker-container{position:relative;display:flex;align-items:center;width:100%}.tn-file-picker-wrapper{display:flex;align-items:center;width:100%;position:relative}.tn-file-picker-input{display:block;width:100%;min-height:2.5rem;padding:.5rem .75rem;font-size:1rem;line-height:1.5;color:var(--tn-fg1, #212529);background-color:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out;outline:none;box-sizing:border-box;font-family:inherit}.tn-file-picker-input::placeholder{color:var(--tn-alt-fg1, #999);opacity:1}.tn-file-picker-input:focus{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px #007bff40}.tn-file-picker-input:disabled{background-color:var(--tn-alt-bg1, #f8f9fa);color:var(--tn-fg2, #6c757d);cursor:not-allowed;opacity:.6}.tn-file-picker-input.error{border-color:var(--tn-error, var(--tn-red, #bd2626))}.tn-file-picker-input.error:focus{border-color:var(--tn-error, var(--tn-red, #bd2626));box-shadow:0 0 0 2px #dc354540}.tn-file-picker-toggle{position:absolute;right:8px;z-index:2;pointer-events:auto;background:transparent;border:none;cursor:pointer;padding:4px;color:var(--tn-fg1);border-radius:4px}.tn-file-picker-toggle:hover{background:var(--tn-bg2, #f0f0f0)}.tn-file-picker-toggle:focus{outline:2px solid var(--tn-primary);outline-offset:2px}.tn-file-picker-toggle:disabled{cursor:not-allowed;opacity:.5}.tn-file-picker-toggle tn-icon{font-size:var(--tn-icon-md, 20px)}:host:focus-within .tn-file-picker-input{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px #007bff40}:host.error .tn-file-picker-input{border-color:var(--tn-error, var(--tn-red, #bd2626))}:host.error .tn-file-picker-input:focus{border-color:var(--tn-error, var(--tn-red, #bd2626));box-shadow:0 0 0 2px #dc354540}@media(prefers-reduced-motion:reduce){.tn-file-picker-input,.tn-file-picker-toggle,.file-item,.breadcrumb-segment{transition:none}.tn-file-picker-loading tn-icon{animation:none}}@media(prefers-contrast:high){.tn-file-picker-input{border-width:2px}.file-item:hover,.file-item.selected{border:2px solid var(--tn-fg1)}.zfs-badge{border:1px solid var(--tn-fg1)}}@media(max-width:768px){:host ::ng-deep .tn-file-picker-overlay .tn-file-picker-dialog{min-width:300px;max-width:calc(100vw - 32px);max-height:calc(100vh - 64px)}.tn-file-picker-header{flex-direction:column;gap:12px;align-items:stretch}.tn-file-picker-breadcrumb{overflow-x:auto;scrollbar-width:none;-ms-overflow-style:none}.tn-file-picker-breadcrumb::-webkit-scrollbar{display:none}.file-item{padding:12px;min-height:56px}.file-info{font-size:1rem}}\n"] }]
|
|
28990
29343
|
}], propDecorators: { mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], multiSelect: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiSelect", required: false }] }], createActions: [{ type: i0.Input, args: [{ isSignal: true, alias: "createActions", required: false }] }], allowManualInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowManualInput", required: false }] }], openOnClick: [{ type: i0.Input, args: [{ isSignal: true, alias: "openOnClick", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], startPath: [{ type: i0.Input, args: [{ isSignal: true, alias: "startPath", required: false }] }], rootPath: [{ type: i0.Input, args: [{ isSignal: true, alias: "rootPath", required: false }] }], valueRoot: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueRoot", required: false }] }], fileExtensions: [{ type: i0.Input, args: [{ isSignal: true, alias: "fileExtensions", required: false }] }], callbacks: [{ type: i0.Input, args: [{ isSignal: true, alias: "callbacks", required: false }] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], pathChange: [{ type: i0.Output, args: ["pathChange"] }], createAction: [{ type: i0.Output, args: ["createAction"] }], error: [{ type: i0.Output, args: ["error"] }], wrapperEl: [{ type: i0.ViewChild, args: ['wrapper', { isSignal: true }] }], filePickerTemplate: [{ type: i0.ViewChild, args: ['filePickerTemplate', { isSignal: true }] }] } });
|
|
28991
29344
|
|