@skra/ui 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -201
- package/NOTICE +7 -7
- package/README.md +39 -39
- package/consumer-guide/README.md +84 -84
- package/consumer-guide/forms-and-binding.md +19 -3
- package/consumer-guide/layout.md +19 -0
- package/consumer-guide/overlays.md +100 -100
- package/consumer-guide/setup-and-gotchas.md +165 -103
- package/consumer-guide/theming-and-encapsulation.md +129 -115
- package/fesm2022/skra-ui-accordion.mjs +61 -43
- package/fesm2022/skra-ui-accordion.mjs.map +1 -1
- package/fesm2022/skra-ui-alert.mjs +26 -18
- package/fesm2022/skra-ui-alert.mjs.map +1 -1
- package/fesm2022/skra-ui-avatar.mjs +12 -8
- package/fesm2022/skra-ui-avatar.mjs.map +1 -1
- package/fesm2022/skra-ui-badge.mjs +19 -13
- package/fesm2022/skra-ui-badge.mjs.map +1 -1
- package/fesm2022/skra-ui-button.mjs +28 -19
- package/fesm2022/skra-ui-button.mjs.map +1 -1
- package/fesm2022/skra-ui-checkbox.mjs +3 -3
- package/fesm2022/skra-ui-checkbox.mjs.map +1 -1
- package/fesm2022/skra-ui-chip.mjs +15 -11
- package/fesm2022/skra-ui-chip.mjs.map +1 -1
- package/fesm2022/skra-ui-combobox.mjs +135 -130
- package/fesm2022/skra-ui-combobox.mjs.map +1 -1
- package/fesm2022/skra-ui-core.mjs +95 -73
- package/fesm2022/skra-ui-core.mjs.map +1 -1
- package/fesm2022/skra-ui-counter.mjs +9 -6
- package/fesm2022/skra-ui-counter.mjs.map +1 -1
- package/fesm2022/skra-ui-datepicker.mjs +334 -307
- package/fesm2022/skra-ui-datepicker.mjs.map +1 -1
- package/fesm2022/skra-ui-dialog.mjs +5 -4
- package/fesm2022/skra-ui-dialog.mjs.map +1 -1
- package/fesm2022/skra-ui-field.mjs +65 -43
- package/fesm2022/skra-ui-field.mjs.map +1 -1
- package/fesm2022/skra-ui-icon.mjs +5 -4
- package/fesm2022/skra-ui-icon.mjs.map +1 -1
- package/fesm2022/skra-ui-input.mjs +53 -12
- package/fesm2022/skra-ui-input.mjs.map +1 -1
- package/fesm2022/skra-ui-kbd.mjs +3 -3
- package/fesm2022/skra-ui-kbd.mjs.map +1 -1
- package/fesm2022/skra-ui-layout.mjs +33 -21
- package/fesm2022/skra-ui-layout.mjs.map +1 -1
- package/fesm2022/skra-ui-link.mjs +3 -3
- package/fesm2022/skra-ui-link.mjs.map +1 -1
- package/fesm2022/skra-ui-loading-indicator.mjs +3 -3
- package/fesm2022/skra-ui-loading-indicator.mjs.map +1 -1
- package/fesm2022/skra-ui-menu.mjs +8 -7
- package/fesm2022/skra-ui-menu.mjs.map +1 -1
- package/fesm2022/skra-ui-multi-select.mjs +102 -98
- package/fesm2022/skra-ui-multi-select.mjs.map +1 -1
- package/fesm2022/skra-ui-native-select.mjs +35 -17
- package/fesm2022/skra-ui-native-select.mjs.map +1 -1
- package/fesm2022/skra-ui-popover.mjs +5 -4
- package/fesm2022/skra-ui-popover.mjs.map +1 -1
- package/fesm2022/skra-ui-progress.mjs +7 -5
- package/fesm2022/skra-ui-progress.mjs.map +1 -1
- package/fesm2022/skra-ui-radio.mjs +3 -3
- package/fesm2022/skra-ui-radio.mjs.map +1 -1
- package/fesm2022/skra-ui-rating-display.mjs +14 -9
- package/fesm2022/skra-ui-rating-display.mjs.map +1 -1
- package/fesm2022/skra-ui-select.mjs +98 -95
- package/fesm2022/skra-ui-select.mjs.map +1 -1
- package/fesm2022/skra-ui-skeleton.mjs +16 -13
- package/fesm2022/skra-ui-skeleton.mjs.map +1 -1
- package/fesm2022/skra-ui-star-rating.mjs +18 -11
- package/fesm2022/skra-ui-star-rating.mjs.map +1 -1
- package/fesm2022/skra-ui-status-dot.mjs +7 -5
- package/fesm2022/skra-ui-status-dot.mjs.map +1 -1
- package/fesm2022/skra-ui-switch.mjs +3 -3
- package/fesm2022/skra-ui-switch.mjs.map +1 -1
- package/fesm2022/skra-ui-tabs.mjs +43 -29
- package/fesm2022/skra-ui-tabs.mjs.map +1 -1
- package/fesm2022/skra-ui-textarea.mjs +41 -15
- package/fesm2022/skra-ui-textarea.mjs.map +1 -1
- package/fesm2022/skra-ui-toast.mjs +35 -23
- package/fesm2022/skra-ui-toast.mjs.map +1 -1
- package/fesm2022/skra-ui-tooltip.mjs +12 -9
- package/fesm2022/skra-ui-tooltip.mjs.map +1 -1
- package/fesm2022/skra-ui-typography.mjs +9 -6
- package/fesm2022/skra-ui-typography.mjs.map +1 -1
- package/package.json +6 -6
- package/types/skra-ui-field.d.ts +3 -3
- package/types/skra-ui-field.d.ts.map +1 -1
- package/types/skra-ui-input.d.ts +24 -1
- package/types/skra-ui-input.d.ts.map +1 -1
- package/types/skra-ui-native-select.d.ts +9 -1
- package/types/skra-ui-native-select.d.ts.map +1 -1
- package/types/skra-ui-textarea.d.ts +10 -1
- package/types/skra-ui-textarea.d.ts.map +1 -1
|
@@ -23,21 +23,25 @@ export { SkraOption, SkraOptionDetail, SkraOptionLeading, SkraSelectTrigger } fr
|
|
|
23
23
|
*/
|
|
24
24
|
class SkraMultiSelect extends _SkraSelectBase {
|
|
25
25
|
/** Consumer-owned ARRAY value channel (ADR 037) — default `[]` (never undefined). */
|
|
26
|
-
value = input([],
|
|
26
|
+
value = input([], /* @ts-ignore */
|
|
27
|
+
...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
27
28
|
/** Fires ONLY on user toggle — emits a NEW array; never on a programmatic `value()` change (ADR 037). */
|
|
28
29
|
valueChange = output();
|
|
29
30
|
/** Cardinality hint (ADR 0046) — multi-select (drives the option checkbox indicator + aria-multiselectable). */
|
|
30
31
|
multiple = true;
|
|
31
32
|
/** The options whose value is a member of `value()` — feeds the trigger summary + seed. */
|
|
32
|
-
selectedOptions = computed(() => this.options().filter((o) => this.isSelected(o.value())),
|
|
33
|
+
selectedOptions = computed(() => this.options().filter((o) => this.isSelected(o.value())), /* @ts-ignore */
|
|
34
|
+
...(ngDevMode ? [{ debugName: "selectedOptions" }] : /* istanbul ignore next */ []));
|
|
33
35
|
/** Comma-joined identity labels of the selected options (value display, ADR 0045 — language-neutral). */
|
|
34
36
|
summary = computed(() => this.selectedOptions()
|
|
35
37
|
.map((o) => o.getLabel())
|
|
36
|
-
.join(', '),
|
|
38
|
+
.join(', '), /* @ts-ignore */
|
|
39
|
+
...(ngDevMode ? [{ debugName: "summary" }] : /* istanbul ignore next */ []));
|
|
37
40
|
triggerContext = computed(() => ({
|
|
38
41
|
$implicit: this.value(),
|
|
39
42
|
options: this.selectedOptions(),
|
|
40
|
-
}),
|
|
43
|
+
}), /* @ts-ignore */
|
|
44
|
+
...(ngDevMode ? [{ debugName: "triggerContext" }] : /* istanbul ignore next */ []));
|
|
41
45
|
constructor() {
|
|
42
46
|
super();
|
|
43
47
|
if (typeof ngDevMode === 'undefined' || ngDevMode) {
|
|
@@ -71,103 +75,103 @@ class SkraMultiSelect extends _SkraSelectBase {
|
|
|
71
75
|
activeSeedItem() {
|
|
72
76
|
return this.selectedOptions()[0] ?? null;
|
|
73
77
|
}
|
|
74
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "
|
|
75
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "
|
|
76
|
-
<button
|
|
77
|
-
#trigger
|
|
78
|
-
class="skra-select-trigger"
|
|
79
|
-
type="button"
|
|
80
|
-
role="combobox"
|
|
81
|
-
aria-haspopup="listbox"
|
|
82
|
-
[attr.id]="triggerId()"
|
|
83
|
-
[attr.popovertarget]="panelId"
|
|
84
|
-
[attr.aria-controls]="panelId"
|
|
85
|
-
[attr.aria-expanded]="isOpen()"
|
|
86
|
-
[attr.aria-activedescendant]="activeOptionId()"
|
|
87
|
-
[attr.aria-describedby]="describedBy()"
|
|
88
|
-
[attr.aria-invalid]="hasError() ? 'true' : null"
|
|
89
|
-
[attr.aria-required]="isRequired() ? 'true' : null"
|
|
90
|
-
[attr.aria-readonly]="isReadonly() ? 'true' : null"
|
|
91
|
-
[disabled]="isDisabled()"
|
|
92
|
-
(click)="onTriggerClick($event)"
|
|
93
|
-
(keydown)="onTriggerKeydown($event)"
|
|
94
|
-
>
|
|
95
|
-
<span class="skra-select-trigger-value">
|
|
96
|
-
@if (triggerTemplate(); as tpl) {
|
|
97
|
-
<ng-container
|
|
98
|
-
[ngTemplateOutlet]="tpl"
|
|
99
|
-
[ngTemplateOutletContext]="triggerContext()"
|
|
100
|
-
></ng-container>
|
|
101
|
-
} @else if (selectedOptions().length) {
|
|
102
|
-
{{ summary() }}
|
|
103
|
-
} @else {
|
|
104
|
-
<span class="skra-select-placeholder">{{ placeholder() }}</span>
|
|
105
|
-
}
|
|
106
|
-
</span>
|
|
107
|
-
<span class="skra-select-arrow" aria-hidden="true"></span>
|
|
108
|
-
</button>
|
|
109
|
-
|
|
110
|
-
<div
|
|
111
|
-
#panel
|
|
112
|
-
class="skra-select-listbox"
|
|
113
|
-
[id]="panelId"
|
|
114
|
-
role="listbox"
|
|
115
|
-
aria-multiselectable="true"
|
|
116
|
-
popover="auto"
|
|
117
|
-
(toggle)="onPanelToggle($event)"
|
|
118
|
-
>
|
|
119
|
-
<ng-content></ng-content>
|
|
120
|
-
</div>
|
|
78
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.1", ngImport: i0, type: SkraMultiSelect, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
79
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.1", type: SkraMultiSelect, isStandalone: true, selector: "skra-multi-select", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "valueChange" }, providers: [{ provide: SKRA_LISTBOX_PANEL, useExisting: SkraMultiSelect }], exportAs: ["skraMultiSelect"], usesInheritance: true, ngImport: i0, template: `
|
|
80
|
+
<button
|
|
81
|
+
#trigger
|
|
82
|
+
class="skra-select-trigger"
|
|
83
|
+
type="button"
|
|
84
|
+
role="combobox"
|
|
85
|
+
aria-haspopup="listbox"
|
|
86
|
+
[attr.id]="triggerId()"
|
|
87
|
+
[attr.popovertarget]="panelId"
|
|
88
|
+
[attr.aria-controls]="panelId"
|
|
89
|
+
[attr.aria-expanded]="isOpen()"
|
|
90
|
+
[attr.aria-activedescendant]="activeOptionId()"
|
|
91
|
+
[attr.aria-describedby]="describedBy()"
|
|
92
|
+
[attr.aria-invalid]="hasError() ? 'true' : null"
|
|
93
|
+
[attr.aria-required]="isRequired() ? 'true' : null"
|
|
94
|
+
[attr.aria-readonly]="isReadonly() ? 'true' : null"
|
|
95
|
+
[disabled]="isDisabled()"
|
|
96
|
+
(click)="onTriggerClick($event)"
|
|
97
|
+
(keydown)="onTriggerKeydown($event)"
|
|
98
|
+
>
|
|
99
|
+
<span class="skra-select-trigger-value">
|
|
100
|
+
@if (triggerTemplate(); as tpl) {
|
|
101
|
+
<ng-container
|
|
102
|
+
[ngTemplateOutlet]="tpl"
|
|
103
|
+
[ngTemplateOutletContext]="triggerContext()"
|
|
104
|
+
></ng-container>
|
|
105
|
+
} @else if (selectedOptions().length) {
|
|
106
|
+
{{ summary() }}
|
|
107
|
+
} @else {
|
|
108
|
+
<span class="skra-select-placeholder">{{ placeholder() }}</span>
|
|
109
|
+
}
|
|
110
|
+
</span>
|
|
111
|
+
<span class="skra-select-arrow" aria-hidden="true"></span>
|
|
112
|
+
</button>
|
|
113
|
+
|
|
114
|
+
<div
|
|
115
|
+
#panel
|
|
116
|
+
class="skra-select-listbox"
|
|
117
|
+
[id]="panelId"
|
|
118
|
+
role="listbox"
|
|
119
|
+
aria-multiselectable="true"
|
|
120
|
+
popover="auto"
|
|
121
|
+
(toggle)="onPanelToggle($event)"
|
|
122
|
+
>
|
|
123
|
+
<ng-content></ng-content>
|
|
124
|
+
</div>
|
|
121
125
|
`, isInline: true, styles: ["@charset \"UTF-8\";:host{display:block;box-sizing:border-box;margin:0}.skra-select-trigger{display:flex;align-items:center;gap:var(--skra-comp-select-gap);inline-size:100%;min-block-size:var(--skra-comp-select-min-block-size);box-sizing:border-box;margin:0;padding-block:var(--skra-comp-select-padding-block);padding-inline:var(--skra-comp-select-padding-inline);text-align:start;cursor:pointer;background:var(--skra-comp-select-background);color:var(--skra-comp-select-foreground);border-color:var(--skra-comp-select-border-color);border-width:var(--skra-comp-select-border-width);border-style:var(--skra-comp-select-border-style);border-radius:var(--skra-comp-select-radius);box-shadow:var(--skra-comp-select-shadow);font-family:var(--skra-sys-typography-role-body-medium-font-family);font-size:var(--skra-sys-typography-role-body-medium-font-size);font-weight:var(--skra-sys-typography-role-body-medium-font-weight);line-height:var(--skra-sys-typography-role-body-medium-line-height);letter-spacing:var(--skra-sys-typography-role-body-medium-letter-spacing);transition:background-color var(--skra-comp-select-motion-duration) var(--skra-comp-select-motion-easing),border-color var(--skra-comp-select-motion-duration) var(--skra-comp-select-motion-easing)}.skra-select-trigger:hover:not(:disabled):not([aria-readonly=true]){background:var(--skra-comp-select-background-hover)}.skra-select-trigger:focus-visible{outline:var(--skra-comp-select-focus-ring-width) solid var(--skra-comp-select-focus-ring-color);outline-offset:.125rem}.skra-select-trigger:disabled{background:var(--skra-comp-select-background-disabled);color:var(--skra-comp-select-foreground-disabled);cursor:not-allowed}.skra-select-trigger[aria-readonly=true]{background:var(--skra-comp-select-background-readonly);cursor:default}.skra-select-trigger[aria-invalid=true]{border-color:var(--skra-comp-select-border-color-validation-invalid)}.skra-select-trigger-value{flex:1 1 auto;min-inline-size:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;--skra-comp-icon-size: var(--skra-comp-select-icon-size)}.skra-select-placeholder{color:var(--skra-comp-select-foreground-disabled)}.skra-select-arrow{flex:none;inline-size:var(--skra-comp-select-arrow-icon-size);block-size:var(--skra-comp-select-arrow-icon-size);background-color:var(--skra-comp-select-arrow-color);mask:url(\"data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3E%3Cpath%20d='M6%209l6%206%206-6'%20fill='none'%20stroke='%23000'%20stroke-width='2'/%3E%3C/svg%3E\") no-repeat center/contain;-webkit-mask:url(\"data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3E%3Cpath%20d='M6%209l6%206%206-6'%20fill='none'%20stroke='%23000'%20stroke-width='2'/%3E%3C/svg%3E\") no-repeat center/contain;transition:transform var(--skra-comp-select-motion-duration) var(--skra-comp-select-motion-easing)}.skra-select-trigger[aria-expanded=true] .skra-select-arrow{transform:rotate(180deg)}.skra-select-listbox{position:fixed;margin:0;inset:auto;box-sizing:border-box;padding-block:var(--skra-comp-select-panel-padding-block);padding-inline:var(--skra-comp-select-panel-padding-inline);background:var(--skra-comp-select-panel-background);color:var(--skra-comp-select-panel-foreground);border:var(--skra-comp-select-border-width) solid var(--skra-comp-select-panel-border-color);border-radius:var(--skra-comp-select-panel-radius);box-shadow:var(--skra-comp-select-panel-shadow);inline-size:max-content;max-block-size:var(--skra-comp-select-panel-max-block-size);overflow-y:auto;flex-direction:column;opacity:0;transition:opacity var(--skra-comp-select-motion-duration) var(--skra-comp-select-motion-easing),display var(--skra-comp-select-motion-duration) allow-discrete,overlay var(--skra-comp-select-motion-duration) allow-discrete}.skra-select-listbox:popover-open{display:flex;opacity:1}@starting-style{.skra-select-listbox:popover-open{opacity:0}}@media(prefers-reduced-motion:reduce){.skra-select-trigger,.skra-select-arrow,.skra-select-listbox{transition-duration:0ms}}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
122
126
|
}
|
|
123
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "
|
|
127
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.1", ngImport: i0, type: SkraMultiSelect, decorators: [{
|
|
124
128
|
type: Component,
|
|
125
|
-
args: [{ selector: 'skra-multi-select', standalone: true, exportAs: 'skraMultiSelect', imports: [NgTemplateOutlet], template: `
|
|
126
|
-
<button
|
|
127
|
-
#trigger
|
|
128
|
-
class="skra-select-trigger"
|
|
129
|
-
type="button"
|
|
130
|
-
role="combobox"
|
|
131
|
-
aria-haspopup="listbox"
|
|
132
|
-
[attr.id]="triggerId()"
|
|
133
|
-
[attr.popovertarget]="panelId"
|
|
134
|
-
[attr.aria-controls]="panelId"
|
|
135
|
-
[attr.aria-expanded]="isOpen()"
|
|
136
|
-
[attr.aria-activedescendant]="activeOptionId()"
|
|
137
|
-
[attr.aria-describedby]="describedBy()"
|
|
138
|
-
[attr.aria-invalid]="hasError() ? 'true' : null"
|
|
139
|
-
[attr.aria-required]="isRequired() ? 'true' : null"
|
|
140
|
-
[attr.aria-readonly]="isReadonly() ? 'true' : null"
|
|
141
|
-
[disabled]="isDisabled()"
|
|
142
|
-
(click)="onTriggerClick($event)"
|
|
143
|
-
(keydown)="onTriggerKeydown($event)"
|
|
144
|
-
>
|
|
145
|
-
<span class="skra-select-trigger-value">
|
|
146
|
-
@if (triggerTemplate(); as tpl) {
|
|
147
|
-
<ng-container
|
|
148
|
-
[ngTemplateOutlet]="tpl"
|
|
149
|
-
[ngTemplateOutletContext]="triggerContext()"
|
|
150
|
-
></ng-container>
|
|
151
|
-
} @else if (selectedOptions().length) {
|
|
152
|
-
{{ summary() }}
|
|
153
|
-
} @else {
|
|
154
|
-
<span class="skra-select-placeholder">{{ placeholder() }}</span>
|
|
155
|
-
}
|
|
156
|
-
</span>
|
|
157
|
-
<span class="skra-select-arrow" aria-hidden="true"></span>
|
|
158
|
-
</button>
|
|
159
|
-
|
|
160
|
-
<div
|
|
161
|
-
#panel
|
|
162
|
-
class="skra-select-listbox"
|
|
163
|
-
[id]="panelId"
|
|
164
|
-
role="listbox"
|
|
165
|
-
aria-multiselectable="true"
|
|
166
|
-
popover="auto"
|
|
167
|
-
(toggle)="onPanelToggle($event)"
|
|
168
|
-
>
|
|
169
|
-
<ng-content></ng-content>
|
|
170
|
-
</div>
|
|
129
|
+
args: [{ selector: 'skra-multi-select', standalone: true, exportAs: 'skraMultiSelect', imports: [NgTemplateOutlet], template: `
|
|
130
|
+
<button
|
|
131
|
+
#trigger
|
|
132
|
+
class="skra-select-trigger"
|
|
133
|
+
type="button"
|
|
134
|
+
role="combobox"
|
|
135
|
+
aria-haspopup="listbox"
|
|
136
|
+
[attr.id]="triggerId()"
|
|
137
|
+
[attr.popovertarget]="panelId"
|
|
138
|
+
[attr.aria-controls]="panelId"
|
|
139
|
+
[attr.aria-expanded]="isOpen()"
|
|
140
|
+
[attr.aria-activedescendant]="activeOptionId()"
|
|
141
|
+
[attr.aria-describedby]="describedBy()"
|
|
142
|
+
[attr.aria-invalid]="hasError() ? 'true' : null"
|
|
143
|
+
[attr.aria-required]="isRequired() ? 'true' : null"
|
|
144
|
+
[attr.aria-readonly]="isReadonly() ? 'true' : null"
|
|
145
|
+
[disabled]="isDisabled()"
|
|
146
|
+
(click)="onTriggerClick($event)"
|
|
147
|
+
(keydown)="onTriggerKeydown($event)"
|
|
148
|
+
>
|
|
149
|
+
<span class="skra-select-trigger-value">
|
|
150
|
+
@if (triggerTemplate(); as tpl) {
|
|
151
|
+
<ng-container
|
|
152
|
+
[ngTemplateOutlet]="tpl"
|
|
153
|
+
[ngTemplateOutletContext]="triggerContext()"
|
|
154
|
+
></ng-container>
|
|
155
|
+
} @else if (selectedOptions().length) {
|
|
156
|
+
{{ summary() }}
|
|
157
|
+
} @else {
|
|
158
|
+
<span class="skra-select-placeholder">{{ placeholder() }}</span>
|
|
159
|
+
}
|
|
160
|
+
</span>
|
|
161
|
+
<span class="skra-select-arrow" aria-hidden="true"></span>
|
|
162
|
+
</button>
|
|
163
|
+
|
|
164
|
+
<div
|
|
165
|
+
#panel
|
|
166
|
+
class="skra-select-listbox"
|
|
167
|
+
[id]="panelId"
|
|
168
|
+
role="listbox"
|
|
169
|
+
aria-multiselectable="true"
|
|
170
|
+
popover="auto"
|
|
171
|
+
(toggle)="onPanelToggle($event)"
|
|
172
|
+
>
|
|
173
|
+
<ng-content></ng-content>
|
|
174
|
+
</div>
|
|
171
175
|
`, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.Emulated, providers: [{ provide: SKRA_LISTBOX_PANEL, useExisting: SkraMultiSelect }], styles: ["@charset \"UTF-8\";:host{display:block;box-sizing:border-box;margin:0}.skra-select-trigger{display:flex;align-items:center;gap:var(--skra-comp-select-gap);inline-size:100%;min-block-size:var(--skra-comp-select-min-block-size);box-sizing:border-box;margin:0;padding-block:var(--skra-comp-select-padding-block);padding-inline:var(--skra-comp-select-padding-inline);text-align:start;cursor:pointer;background:var(--skra-comp-select-background);color:var(--skra-comp-select-foreground);border-color:var(--skra-comp-select-border-color);border-width:var(--skra-comp-select-border-width);border-style:var(--skra-comp-select-border-style);border-radius:var(--skra-comp-select-radius);box-shadow:var(--skra-comp-select-shadow);font-family:var(--skra-sys-typography-role-body-medium-font-family);font-size:var(--skra-sys-typography-role-body-medium-font-size);font-weight:var(--skra-sys-typography-role-body-medium-font-weight);line-height:var(--skra-sys-typography-role-body-medium-line-height);letter-spacing:var(--skra-sys-typography-role-body-medium-letter-spacing);transition:background-color var(--skra-comp-select-motion-duration) var(--skra-comp-select-motion-easing),border-color var(--skra-comp-select-motion-duration) var(--skra-comp-select-motion-easing)}.skra-select-trigger:hover:not(:disabled):not([aria-readonly=true]){background:var(--skra-comp-select-background-hover)}.skra-select-trigger:focus-visible{outline:var(--skra-comp-select-focus-ring-width) solid var(--skra-comp-select-focus-ring-color);outline-offset:.125rem}.skra-select-trigger:disabled{background:var(--skra-comp-select-background-disabled);color:var(--skra-comp-select-foreground-disabled);cursor:not-allowed}.skra-select-trigger[aria-readonly=true]{background:var(--skra-comp-select-background-readonly);cursor:default}.skra-select-trigger[aria-invalid=true]{border-color:var(--skra-comp-select-border-color-validation-invalid)}.skra-select-trigger-value{flex:1 1 auto;min-inline-size:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;--skra-comp-icon-size: var(--skra-comp-select-icon-size)}.skra-select-placeholder{color:var(--skra-comp-select-foreground-disabled)}.skra-select-arrow{flex:none;inline-size:var(--skra-comp-select-arrow-icon-size);block-size:var(--skra-comp-select-arrow-icon-size);background-color:var(--skra-comp-select-arrow-color);mask:url(\"data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3E%3Cpath%20d='M6%209l6%206%206-6'%20fill='none'%20stroke='%23000'%20stroke-width='2'/%3E%3C/svg%3E\") no-repeat center/contain;-webkit-mask:url(\"data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3E%3Cpath%20d='M6%209l6%206%206-6'%20fill='none'%20stroke='%23000'%20stroke-width='2'/%3E%3C/svg%3E\") no-repeat center/contain;transition:transform var(--skra-comp-select-motion-duration) var(--skra-comp-select-motion-easing)}.skra-select-trigger[aria-expanded=true] .skra-select-arrow{transform:rotate(180deg)}.skra-select-listbox{position:fixed;margin:0;inset:auto;box-sizing:border-box;padding-block:var(--skra-comp-select-panel-padding-block);padding-inline:var(--skra-comp-select-panel-padding-inline);background:var(--skra-comp-select-panel-background);color:var(--skra-comp-select-panel-foreground);border:var(--skra-comp-select-border-width) solid var(--skra-comp-select-panel-border-color);border-radius:var(--skra-comp-select-panel-radius);box-shadow:var(--skra-comp-select-panel-shadow);inline-size:max-content;max-block-size:var(--skra-comp-select-panel-max-block-size);overflow-y:auto;flex-direction:column;opacity:0;transition:opacity var(--skra-comp-select-motion-duration) var(--skra-comp-select-motion-easing),display var(--skra-comp-select-motion-duration) allow-discrete,overlay var(--skra-comp-select-motion-duration) allow-discrete}.skra-select-listbox:popover-open{display:flex;opacity:1}@starting-style{.skra-select-listbox:popover-open{opacity:0}}@media(prefers-reduced-motion:reduce){.skra-select-trigger,.skra-select-arrow,.skra-select-listbox{transition-duration:0ms}}\n"] }]
|
|
172
176
|
}], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], valueChange: [{ type: i0.Output, args: ["valueChange"] }] } });
|
|
173
177
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"skra-ui-multi-select.mjs","sources":["../../../../libs/ui/multi-select/src/lib/multi-select/multi-select.ts","../../../../libs/ui/multi-select/src/skra-ui-multi-select.ts"],"sourcesContent":["import { NgTemplateOutlet } from '@angular/common';\nimport {\n ChangeDetectionStrategy,\n Component,\n ViewEncapsulation,\n afterRenderEffect,\n computed,\n input,\n output,\n} from '@angular/core';\n\nimport { _SkraSelectBase } from '@skra/ui/core';\nimport {\n SKRA_LISTBOX_PANEL,\n type SkraListboxKeyboardItem,\n} from '@skra/ui/core';\n\ndeclare const ngDevMode: boolean | undefined;\n\n/**\n * SkraMultiSelect — a custom MULTI-select listbox: the WAI-ARIA APG multi-selectable listbox combobox.\n * The multi-cardinality sibling of `skra-select` over the shared `_SkraSelectBase` (**ADR 0046** —\n * cardinality split: a separate component with an HONEST `readonly T[]` value contract, NOT a `multiple`\n * flag). The trigger (`role=\"combobox\"`) opens a top-layer listbox panel (`popover=\"auto\"`, ADR 0044,\n * `aria-multiselectable=\"true\"`) of the SAME projected `skra-option` children — reused verbatim; the\n * option swaps its indicator to a checkbox via the `SkraListboxPanel.multiple` hint.\n *\n * - **Value (ADR 037):** `value = input<readonly T[]>([])` + `valueChange = output<readonly T[]>()` —\n * consumer-owned, no `@angular/forms`. Selection is membership (`compareWith` per element).\n * - **Toggle (ADR 0046):** picking an option toggles its membership and KEEPS THE PANEL OPEN (the one\n * behavioural divergence from single-select, which closes).\n * - **Trigger summary (ADR 0045 / 033):** comma-joined `getLabel()` of selected options — the VALUE\n * display, never the accessible name. Count-style / chips → the opt-in `[skraSelectTrigger]` recipe.\n *\n * Full contract: adr/specs/component-multi-select-spec.md.\n */\n@Component({\n selector: 'skra-multi-select',\n standalone: true,\n exportAs: 'skraMultiSelect',\n imports: [NgTemplateOutlet],\n template: `\n <button\n #trigger\n class=\"skra-select-trigger\"\n type=\"button\"\n role=\"combobox\"\n aria-haspopup=\"listbox\"\n [attr.id]=\"triggerId()\"\n [attr.popovertarget]=\"panelId\"\n [attr.aria-controls]=\"panelId\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-activedescendant]=\"activeOptionId()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-invalid]=\"hasError() ? 'true' : null\"\n [attr.aria-required]=\"isRequired() ? 'true' : null\"\n [attr.aria-readonly]=\"isReadonly() ? 'true' : null\"\n [disabled]=\"isDisabled()\"\n (click)=\"onTriggerClick($event)\"\n (keydown)=\"onTriggerKeydown($event)\"\n >\n <span class=\"skra-select-trigger-value\">\n @if (triggerTemplate(); as tpl) {\n <ng-container\n [ngTemplateOutlet]=\"tpl\"\n [ngTemplateOutletContext]=\"triggerContext()\"\n ></ng-container>\n } @else if (selectedOptions().length) {\n {{ summary() }}\n } @else {\n <span class=\"skra-select-placeholder\">{{ placeholder() }}</span>\n }\n </span>\n <span class=\"skra-select-arrow\" aria-hidden=\"true\"></span>\n </button>\n\n <div\n #panel\n class=\"skra-select-listbox\"\n [id]=\"panelId\"\n role=\"listbox\"\n aria-multiselectable=\"true\"\n popover=\"auto\"\n (toggle)=\"onPanelToggle($event)\"\n >\n <ng-content></ng-content>\n </div>\n `,\n styleUrl: './multi-select.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n encapsulation: ViewEncapsulation.Emulated,\n providers: [{ provide: SKRA_LISTBOX_PANEL, useExisting: SkraMultiSelect }],\n})\nexport class SkraMultiSelect<T = unknown> extends _SkraSelectBase<T> {\n /** Consumer-owned ARRAY value channel (ADR 037) — default `[]` (never undefined). */\n readonly value = input<readonly T[]>([]);\n /** Fires ONLY on user toggle — emits a NEW array; never on a programmatic `value()` change (ADR 037). */\n readonly valueChange = output<readonly T[]>();\n /** Cardinality hint (ADR 0046) — multi-select (drives the option checkbox indicator + aria-multiselectable). */\n readonly multiple = true;\n\n /** The options whose value is a member of `value()` — feeds the trigger summary + seed. */\n protected readonly selectedOptions = computed(() =>\n this.options().filter((o) => this.isSelected(o.value())),\n );\n\n /** Comma-joined identity labels of the selected options (value display, ADR 0045 — language-neutral). */\n protected readonly summary = computed(() =>\n this.selectedOptions()\n .map((o) => o.getLabel())\n .join(', '),\n );\n\n protected readonly triggerContext = computed(() => ({\n $implicit: this.value(),\n options: this.selectedOptions(),\n }));\n\n constructor() {\n super();\n if (typeof ngDevMode === 'undefined' || ngDevMode) {\n // Dev-only consumer warning. Uses afterRenderEffect (NOT effect) so the projected\n // options' required `value` inputs are read only AFTER each render applies their\n // bindings — an eager read during the initial CD (SSR / first render) OR a runtime\n // re-projection would throw NG0950. afterRender hooks never run on the server, so\n // this stays SSR-safe. Covers F-13 (initial render) + F-16 (runtime re-projection).\n afterRenderEffect(() => {\n const opts = this.options();\n const unmatched = this.value().filter(\n (v) => !opts.some((o) => this.compareWith()(o.value() as T, v)),\n );\n if (unmatched.length) {\n console.warn(\n '[skra-multi-select] value contains entries with no matching <skra-option> — they are ' +\n 'dropped from the trigger summary (likely a compareWith mismatch for object values).',\n );\n }\n });\n }\n }\n\n // ── SkraListboxPanel seam ──\n isSelected(value: unknown): boolean {\n return this.value().some((x) => this.compareWith()(x, value as T));\n }\n\n selectValue(value: unknown): void {\n const present = this.isSelected(value);\n const next = present\n ? this.value().filter((x) => !this.compareWith()(x, value as T))\n : [...this.value(), value as T];\n this.valueChange.emit(next);\n // NO close() — multi-select keeps the panel open for further toggling (ADR 0046).\n }\n\n protected activeSeedItem(): SkraListboxKeyboardItem | null {\n return this.selectedOptions()[0] ?? null;\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;;;;AAmBA;;;;;;;;;;;;;;;;AAgBG;AA0DG,MAAO,eAA6B,SAAQ,eAAkB,CAAA;;
|
|
1
|
+
{"version":3,"file":"skra-ui-multi-select.mjs","sources":["../../../../libs/ui/multi-select/src/lib/multi-select/multi-select.ts","../../../../libs/ui/multi-select/src/skra-ui-multi-select.ts"],"sourcesContent":["import { NgTemplateOutlet } from '@angular/common';\r\nimport {\r\n ChangeDetectionStrategy,\r\n Component,\r\n ViewEncapsulation,\r\n afterRenderEffect,\r\n computed,\r\n input,\r\n output,\r\n} from '@angular/core';\r\n\r\nimport { _SkraSelectBase } from '@skra/ui/core';\r\nimport {\r\n SKRA_LISTBOX_PANEL,\r\n type SkraListboxKeyboardItem,\r\n} from '@skra/ui/core';\r\n\r\ndeclare const ngDevMode: boolean | undefined;\r\n\r\n/**\r\n * SkraMultiSelect — a custom MULTI-select listbox: the WAI-ARIA APG multi-selectable listbox combobox.\r\n * The multi-cardinality sibling of `skra-select` over the shared `_SkraSelectBase` (**ADR 0046** —\r\n * cardinality split: a separate component with an HONEST `readonly T[]` value contract, NOT a `multiple`\r\n * flag). The trigger (`role=\"combobox\"`) opens a top-layer listbox panel (`popover=\"auto\"`, ADR 0044,\r\n * `aria-multiselectable=\"true\"`) of the SAME projected `skra-option` children — reused verbatim; the\r\n * option swaps its indicator to a checkbox via the `SkraListboxPanel.multiple` hint.\r\n *\r\n * - **Value (ADR 037):** `value = input<readonly T[]>([])` + `valueChange = output<readonly T[]>()` —\r\n * consumer-owned, no `@angular/forms`. Selection is membership (`compareWith` per element).\r\n * - **Toggle (ADR 0046):** picking an option toggles its membership and KEEPS THE PANEL OPEN (the one\r\n * behavioural divergence from single-select, which closes).\r\n * - **Trigger summary (ADR 0045 / 033):** comma-joined `getLabel()` of selected options — the VALUE\r\n * display, never the accessible name. Count-style / chips → the opt-in `[skraSelectTrigger]` recipe.\r\n *\r\n * Full contract: adr/specs/component-multi-select-spec.md.\r\n */\r\n@Component({\r\n selector: 'skra-multi-select',\r\n standalone: true,\r\n exportAs: 'skraMultiSelect',\r\n imports: [NgTemplateOutlet],\r\n template: `\r\n <button\r\n #trigger\r\n class=\"skra-select-trigger\"\r\n type=\"button\"\r\n role=\"combobox\"\r\n aria-haspopup=\"listbox\"\r\n [attr.id]=\"triggerId()\"\r\n [attr.popovertarget]=\"panelId\"\r\n [attr.aria-controls]=\"panelId\"\r\n [attr.aria-expanded]=\"isOpen()\"\r\n [attr.aria-activedescendant]=\"activeOptionId()\"\r\n [attr.aria-describedby]=\"describedBy()\"\r\n [attr.aria-invalid]=\"hasError() ? 'true' : null\"\r\n [attr.aria-required]=\"isRequired() ? 'true' : null\"\r\n [attr.aria-readonly]=\"isReadonly() ? 'true' : null\"\r\n [disabled]=\"isDisabled()\"\r\n (click)=\"onTriggerClick($event)\"\r\n (keydown)=\"onTriggerKeydown($event)\"\r\n >\r\n <span class=\"skra-select-trigger-value\">\r\n @if (triggerTemplate(); as tpl) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"tpl\"\r\n [ngTemplateOutletContext]=\"triggerContext()\"\r\n ></ng-container>\r\n } @else if (selectedOptions().length) {\r\n {{ summary() }}\r\n } @else {\r\n <span class=\"skra-select-placeholder\">{{ placeholder() }}</span>\r\n }\r\n </span>\r\n <span class=\"skra-select-arrow\" aria-hidden=\"true\"></span>\r\n </button>\r\n\r\n <div\r\n #panel\r\n class=\"skra-select-listbox\"\r\n [id]=\"panelId\"\r\n role=\"listbox\"\r\n aria-multiselectable=\"true\"\r\n popover=\"auto\"\r\n (toggle)=\"onPanelToggle($event)\"\r\n >\r\n <ng-content></ng-content>\r\n </div>\r\n `,\r\n styleUrl: './multi-select.scss',\r\n changeDetection: ChangeDetectionStrategy.OnPush,\r\n encapsulation: ViewEncapsulation.Emulated,\r\n providers: [{ provide: SKRA_LISTBOX_PANEL, useExisting: SkraMultiSelect }],\r\n})\r\nexport class SkraMultiSelect<T = unknown> extends _SkraSelectBase<T> {\r\n /** Consumer-owned ARRAY value channel (ADR 037) — default `[]` (never undefined). */\r\n readonly value = input<readonly T[]>([]);\r\n /** Fires ONLY on user toggle — emits a NEW array; never on a programmatic `value()` change (ADR 037). */\r\n readonly valueChange = output<readonly T[]>();\r\n /** Cardinality hint (ADR 0046) — multi-select (drives the option checkbox indicator + aria-multiselectable). */\r\n readonly multiple = true;\r\n\r\n /** The options whose value is a member of `value()` — feeds the trigger summary + seed. */\r\n protected readonly selectedOptions = computed(() =>\r\n this.options().filter((o) => this.isSelected(o.value())),\r\n );\r\n\r\n /** Comma-joined identity labels of the selected options (value display, ADR 0045 — language-neutral). */\r\n protected readonly summary = computed(() =>\r\n this.selectedOptions()\r\n .map((o) => o.getLabel())\r\n .join(', '),\r\n );\r\n\r\n protected readonly triggerContext = computed(() => ({\r\n $implicit: this.value(),\r\n options: this.selectedOptions(),\r\n }));\r\n\r\n constructor() {\r\n super();\r\n if (typeof ngDevMode === 'undefined' || ngDevMode) {\r\n // Dev-only consumer warning. Uses afterRenderEffect (NOT effect) so the projected\r\n // options' required `value` inputs are read only AFTER each render applies their\r\n // bindings — an eager read during the initial CD (SSR / first render) OR a runtime\r\n // re-projection would throw NG0950. afterRender hooks never run on the server, so\r\n // this stays SSR-safe. Covers F-13 (initial render) + F-16 (runtime re-projection).\r\n afterRenderEffect(() => {\r\n const opts = this.options();\r\n const unmatched = this.value().filter(\r\n (v) => !opts.some((o) => this.compareWith()(o.value() as T, v)),\r\n );\r\n if (unmatched.length) {\r\n console.warn(\r\n '[skra-multi-select] value contains entries with no matching <skra-option> — they are ' +\r\n 'dropped from the trigger summary (likely a compareWith mismatch for object values).',\r\n );\r\n }\r\n });\r\n }\r\n }\r\n\r\n // ── SkraListboxPanel seam ──\r\n isSelected(value: unknown): boolean {\r\n return this.value().some((x) => this.compareWith()(x, value as T));\r\n }\r\n\r\n selectValue(value: unknown): void {\r\n const present = this.isSelected(value);\r\n const next = present\r\n ? this.value().filter((x) => !this.compareWith()(x, value as T))\r\n : [...this.value(), value as T];\r\n this.valueChange.emit(next);\r\n // NO close() — multi-select keeps the panel open for further toggling (ADR 0046).\r\n }\r\n\r\n protected activeSeedItem(): SkraListboxKeyboardItem | null {\r\n return this.selectedOptions()[0] ?? null;\r\n }\r\n}\r\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
|
|
@@ -25,6 +25,14 @@ import { SkraFieldContext } from '@skra/ui/field';
|
|
|
25
25
|
* </select>
|
|
26
26
|
* </skra-field>
|
|
27
27
|
*
|
|
28
|
+
* <!-- standalone: native implicit label (wrapping) — no id/for needed -->
|
|
29
|
+
* <label>Country <select skraSelect><option value="pl">Poland</option></select></label>
|
|
30
|
+
*
|
|
31
|
+
* `SkraFieldContext` is injected `{ optional: true }` (ADR 013 §3.7) — same
|
|
32
|
+
* posture as SkraInput / SkraCheckbox. Standalone, the atom renders the skin
|
|
33
|
+
* only, the readonly guards are inert, and the consumer owns every attribute
|
|
34
|
+
* including the accessible name.
|
|
35
|
+
*
|
|
28
36
|
* Native `<select>` is the only web-platform widget whose dropdown renders in
|
|
29
37
|
* the OS / browser top-layer, above modal `<dialog>`, above Shadow DOM
|
|
30
38
|
* boundaries, above z-index stacking contexts. By staying native at Tier 1 we
|
|
@@ -50,12 +58,14 @@ import { SkraFieldContext } from '@skra/ui/field';
|
|
|
50
58
|
*/
|
|
51
59
|
class SkraNativeSelect {
|
|
52
60
|
el = inject(ElementRef);
|
|
53
|
-
context = inject(SkraFieldContext);
|
|
61
|
+
context = inject(SkraFieldContext, { optional: true });
|
|
54
62
|
constructor() {
|
|
55
63
|
const el = this.el.nativeElement;
|
|
56
|
-
// Reject native multi-select loudly in dev
|
|
57
|
-
//
|
|
58
|
-
//
|
|
64
|
+
// Reject native multi-select loudly in dev — Forge fails on contract
|
|
65
|
+
// violations rather than degrading silently. This throw is unrelated to the
|
|
66
|
+
// (now optional, ADR 013 §3.7) field context: `[multiple]` is a capability
|
|
67
|
+
// this atom genuinely cannot honour, whereas a missing field is a supported
|
|
68
|
+
// standalone mode. Dev-only: never crashes a production bundle. A `<select
|
|
59
69
|
// multiple>` that truly wants native UX can drop `skraSelect` and stay raw
|
|
60
70
|
// HTML; for a real multi-select, use the Tier 2 <skra-select>.
|
|
61
71
|
if (isDevMode() && el.multiple) {
|
|
@@ -67,9 +77,18 @@ class SkraNativeSelect {
|
|
|
67
77
|
'Tier 2 <skra-select> (CDK listbox) for real multi-select. See ' +
|
|
68
78
|
'adr/specs/0039-select-directive-spec.md §4.1.');
|
|
69
79
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
80
|
+
// Field-cascaded native properties — only bind when context is present so
|
|
81
|
+
// standalone consumers retain full control of their static attributes.
|
|
82
|
+
// Mirrors SkraInput / SkraCheckbox.
|
|
83
|
+
if (this.context) {
|
|
84
|
+
const ctx = this.context;
|
|
85
|
+
effect(() => {
|
|
86
|
+
el.id = ctx.fieldId();
|
|
87
|
+
});
|
|
88
|
+
effect(() => {
|
|
89
|
+
el.disabled = ctx.isDisabled();
|
|
90
|
+
});
|
|
91
|
+
}
|
|
73
92
|
}
|
|
74
93
|
/**
|
|
75
94
|
* Readonly: suppress the pointer affordance that opens the native listbox.
|
|
@@ -78,7 +97,7 @@ class SkraNativeSelect {
|
|
|
78
97
|
* trade-off for a value-display control).
|
|
79
98
|
*/
|
|
80
99
|
blockWhenReadonly(event) {
|
|
81
|
-
if (this.context
|
|
100
|
+
if (this.context?.isReadonly())
|
|
82
101
|
event.preventDefault();
|
|
83
102
|
}
|
|
84
103
|
/**
|
|
@@ -89,21 +108,20 @@ class SkraNativeSelect {
|
|
|
89
108
|
* select, so all are blocked in readonly.
|
|
90
109
|
*/
|
|
91
110
|
blockKeyWhenReadonly(event) {
|
|
92
|
-
if (this.context
|
|
111
|
+
if (this.context?.isReadonly() && event.key !== 'Tab') {
|
|
93
112
|
event.preventDefault();
|
|
94
113
|
}
|
|
95
114
|
}
|
|
96
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "
|
|
97
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "
|
|
115
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.1", ngImport: i0, type: SkraNativeSelect, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
116
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.1.1", type: SkraNativeSelect, isStandalone: true, selector: "select[skraSelect]", host: { listeners: { "mousedown": "blockWhenReadonly($event)", "keydown": "blockKeyWhenReadonly($event)" }, properties: { "attr.aria-describedby": "context?.describedByIds() ?? null", "attr.aria-invalid": "context?.hasError() ? \"true\" : null", "attr.aria-required": "context?.isRequired() ? \"true\" : null", "attr.aria-readonly": "context?.isReadonly() ? \"true\" : null" } }, ngImport: i0, template: '<ng-content></ng-content>', isInline: true, styles: [":host{background-color:var(--skra-comp-field-background);color:var(--skra-comp-field-foreground);border-color:var(--skra-comp-field-border-color);border-width:var(--skra-comp-field-border-width);border-style:var(--skra-comp-field-border-style);border-radius:var(--skra-comp-field-radius);padding-block:var(--skra-comp-field-padding-block);padding-inline:var(--skra-comp-field-padding-inline);box-shadow:var(--skra-comp-field-shadow);font:inherit;outline:none;cursor:pointer;transition:background-color var(--skra-comp-field-motion-duration) var(--skra-comp-field-motion-easing),border-color var(--skra-comp-field-motion-duration) var(--skra-comp-field-motion-easing),color var(--skra-comp-field-motion-duration) var(--skra-comp-field-motion-easing),box-shadow var(--skra-comp-field-motion-duration) var(--skra-comp-field-motion-easing)}:host(:not([size])){appearance:none;padding-inline-end:var(--skra-comp-field-select-arrow-padding-inline-end);background-image:var(--skra-comp-field-select-chevron-glyph);background-repeat:no-repeat;background-position:right var(--skra-comp-field-padding-inline) center;background-size:var(--skra-comp-field-select-arrow-icon-size)}:host([size]){background-image:none;cursor:default}:host(:not([size]):hover:not(:disabled):not([aria-readonly=true])){background-color:var(--skra-comp-field-background-hover);border-color:var(--skra-comp-field-border-color-hover)}:host(:focus-visible){border-color:var(--skra-comp-field-focus-ring-color);box-shadow:0 0 0 var(--skra-comp-field-focus-ring-width) var(--skra-comp-field-focus-ring-color)}:host(:disabled){cursor:not-allowed;opacity:var(--skra-sys-opacity-disabled)}:host([aria-readonly=true]){cursor:default}:host([aria-invalid=true]){border-color:var(--skra-comp-field-error-color)}:host([aria-invalid=true]:focus-visible){box-shadow:0 0 0 var(--skra-comp-field-focus-ring-width) var(--skra-comp-field-error-color)}@media(prefers-reduced-motion:reduce){:host{transition:none}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
98
117
|
}
|
|
99
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "
|
|
118
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.1", ngImport: i0, type: SkraNativeSelect, decorators: [{
|
|
100
119
|
type: Component,
|
|
101
120
|
args: [{ selector: 'select[skraSelect]', standalone: true, template: '<ng-content></ng-content>', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.Emulated, host: {
|
|
102
|
-
'[
|
|
103
|
-
'[attr.aria-
|
|
104
|
-
'[attr.aria-
|
|
105
|
-
'[attr.aria-
|
|
106
|
-
'[attr.aria-readonly]': 'context.isReadonly() ? "true" : null',
|
|
121
|
+
'[attr.aria-describedby]': 'context?.describedByIds() ?? null',
|
|
122
|
+
'[attr.aria-invalid]': 'context?.hasError() ? "true" : null',
|
|
123
|
+
'[attr.aria-required]': 'context?.isRequired() ? "true" : null',
|
|
124
|
+
'[attr.aria-readonly]': 'context?.isReadonly() ? "true" : null',
|
|
107
125
|
// No `data-skra-readonly` — slice consumes `[aria-readonly="true"]`
|
|
108
126
|
// directly; emitting a parallel data-attr would be dead surface
|
|
109
127
|
// (no slice selector reads it). Matches checkbox/radio readonly pattern
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"skra-ui-native-select.mjs","sources":["../../../../libs/ui/native-select/src/lib/select/select.ts","../../../../libs/ui/native-select/src/index.ts","../../../../libs/ui/native-select/src/skra-ui-native-select.ts"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n ElementRef,\n ViewEncapsulation,\n effect,\n inject,\n isDevMode,\n} from '@angular/core';\nimport { SkraFieldContext } from '@skra/ui/field';\n\n/**\n * Wires a native `<select>` inside `<skra-field>` to SkraFieldContext (ADR 013).\n * Tier 1 Foundations primitive — **single-select only**: a dropdown, or a\n * single-select scrolling listbox via the native `[size]` attribute.\n *\n * Native `[multiple]` is deliberately NOT supported (the directive rejects it,\n * see below). Native `<select multiple>` UX is not production-grade and cannot\n * be fixed at the native-`<select>` tier: selection requires modifier keys or\n * drag, the plain-click-toggle affordance exists only for a bare `<select\n * multiple>` in some engines (and breaks with an explicit `[size]`), and the\n * option markers are UA-drawn / unstylable cross-browser. Multi-select — chip\n * display, search, custom option templates, and a styleable per-row checkbox —\n * is wholly a Tier 2 `skra-select` (CDK listbox) concern per\n * `adr/specs/0039-select-directive-spec.md` §1.1 / §4.1.\n *\n * <skra-field>\n * <label skraLabelFor>Country</label>\n * <select skraSelect [value]=\"country()\" (change)=\"country.set($any($event.target).value)\">\n * <option value=\"pl\">Poland</option>\n * <option value=\"de\">Germany</option>\n * </select>\n * </skra-field>\n *\n * Native `<select>` is the only web-platform widget whose dropdown renders in\n * the OS / browser top-layer, above modal `<dialog>`, above Shadow DOM\n * boundaries, above z-index stacking contexts. By staying native at Tier 1 we\n * inherit that for free — Tier 2 custom listbox will solve the same problem\n * via CDK overlay + Top Layer API.\n *\n * Value channel stays consumer-owned per ADR 037: bind via native `[value]` +\n * `(change)`, Reactive Forms `formControlName`, Signal Forms `[formField]`,\n * or `[(ngModel)]` — all work orthogonally.\n *\n * `readonly` is ARIA-only because HTML spec does not apply native `[readonly]`\n * to `<select>` (same gotcha as `<input type=\"checkbox|radio\">`). Unlike a text\n * input, though, a native `<select>` is an *uncontrolled* widget: if the listbox\n * opens and the user picks an option, the displayed value changes regardless of\n * the consumer's binding — so \"consumer prevents change\" cannot actually hold a\n * native select readonly. The directive therefore enforces readonly behaviourally\n * (blocks the picker from opening / the value from changing — see\n * `blockWhenReadonly` / `blockKeyWhenReadonly`), mirroring the imperative\n * `el.disabled` enforcement below. Both states are made non-interactive; only\n * readonly stays focusable + un-greyed and presents as the bare value. This is\n * interaction integrity for the control's own state — NOT a value channel, so the\n * ADR 037 no-`@angular/forms` / no-CVA boundary is untouched.\n */\n@Component({\n selector: 'select[skraSelect]',\n standalone: true,\n // Content-bearing host: <ng-content> projects the consumer's\n // <option>/<optgroup> children through. `template: ''` (as used for the void\n // <input> host) would discard them — see ADR 040 §4.\n template: '<ng-content></ng-content>',\n styleUrl: './select.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n encapsulation: ViewEncapsulation.Emulated,\n host: {\n '[
|
|
1
|
+
{"version":3,"file":"skra-ui-native-select.mjs","sources":["../../../../libs/ui/native-select/src/lib/select/select.ts","../../../../libs/ui/native-select/src/index.ts","../../../../libs/ui/native-select/src/skra-ui-native-select.ts"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n ElementRef,\n ViewEncapsulation,\n effect,\n inject,\n isDevMode,\n} from '@angular/core';\nimport { SkraFieldContext } from '@skra/ui/field';\n\n/**\n * Wires a native `<select>` inside `<skra-field>` to SkraFieldContext (ADR 013).\n * Tier 1 Foundations primitive — **single-select only**: a dropdown, or a\n * single-select scrolling listbox via the native `[size]` attribute.\n *\n * Native `[multiple]` is deliberately NOT supported (the directive rejects it,\n * see below). Native `<select multiple>` UX is not production-grade and cannot\n * be fixed at the native-`<select>` tier: selection requires modifier keys or\n * drag, the plain-click-toggle affordance exists only for a bare `<select\n * multiple>` in some engines (and breaks with an explicit `[size]`), and the\n * option markers are UA-drawn / unstylable cross-browser. Multi-select — chip\n * display, search, custom option templates, and a styleable per-row checkbox —\n * is wholly a Tier 2 `skra-select` (CDK listbox) concern per\n * `adr/specs/0039-select-directive-spec.md` §1.1 / §4.1.\n *\n * <skra-field>\n * <label skraLabelFor>Country</label>\n * <select skraSelect [value]=\"country()\" (change)=\"country.set($any($event.target).value)\">\n * <option value=\"pl\">Poland</option>\n * <option value=\"de\">Germany</option>\n * </select>\n * </skra-field>\n *\n * <!-- standalone: native implicit label (wrapping) — no id/for needed -->\n * <label>Country <select skraSelect><option value=\"pl\">Poland</option></select></label>\n *\n * `SkraFieldContext` is injected `{ optional: true }` (ADR 013 §3.7) — same\n * posture as SkraInput / SkraCheckbox. Standalone, the atom renders the skin\n * only, the readonly guards are inert, and the consumer owns every attribute\n * including the accessible name.\n *\n * Native `<select>` is the only web-platform widget whose dropdown renders in\n * the OS / browser top-layer, above modal `<dialog>`, above Shadow DOM\n * boundaries, above z-index stacking contexts. By staying native at Tier 1 we\n * inherit that for free — Tier 2 custom listbox will solve the same problem\n * via CDK overlay + Top Layer API.\n *\n * Value channel stays consumer-owned per ADR 037: bind via native `[value]` +\n * `(change)`, Reactive Forms `formControlName`, Signal Forms `[formField]`,\n * or `[(ngModel)]` — all work orthogonally.\n *\n * `readonly` is ARIA-only because HTML spec does not apply native `[readonly]`\n * to `<select>` (same gotcha as `<input type=\"checkbox|radio\">`). Unlike a text\n * input, though, a native `<select>` is an *uncontrolled* widget: if the listbox\n * opens and the user picks an option, the displayed value changes regardless of\n * the consumer's binding — so \"consumer prevents change\" cannot actually hold a\n * native select readonly. The directive therefore enforces readonly behaviourally\n * (blocks the picker from opening / the value from changing — see\n * `blockWhenReadonly` / `blockKeyWhenReadonly`), mirroring the imperative\n * `el.disabled` enforcement below. Both states are made non-interactive; only\n * readonly stays focusable + un-greyed and presents as the bare value. This is\n * interaction integrity for the control's own state — NOT a value channel, so the\n * ADR 037 no-`@angular/forms` / no-CVA boundary is untouched.\n */\n@Component({\n selector: 'select[skraSelect]',\n standalone: true,\n // Content-bearing host: <ng-content> projects the consumer's\n // <option>/<optgroup> children through. `template: ''` (as used for the void\n // <input> host) would discard them — see ADR 040 §4.\n template: '<ng-content></ng-content>',\n styleUrl: './select.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n encapsulation: ViewEncapsulation.Emulated,\n host: {\n '[attr.aria-describedby]': 'context?.describedByIds() ?? null',\n '[attr.aria-invalid]': 'context?.hasError() ? \"true\" : null',\n '[attr.aria-required]': 'context?.isRequired() ? \"true\" : null',\n '[attr.aria-readonly]': 'context?.isReadonly() ? \"true\" : null',\n // No `data-skra-readonly` — slice consumes `[aria-readonly=\"true\"]`\n // directly; emitting a parallel data-attr would be dead surface\n // (no slice selector reads it). Matches checkbox/radio readonly pattern\n // in `_form-controls.scss` (ARIA-only branch, no data-attr companion).\n '(mousedown)': 'blockWhenReadonly($event)',\n '(keydown)': 'blockKeyWhenReadonly($event)',\n },\n})\nexport class SkraNativeSelect {\n private readonly el = inject<ElementRef<HTMLSelectElement>>(ElementRef);\n readonly context = inject(SkraFieldContext, { optional: true });\n\n constructor() {\n const el = this.el.nativeElement;\n\n // Reject native multi-select loudly in dev — Forge fails on contract\n // violations rather than degrading silently. This throw is unrelated to the\n // (now optional, ADR 013 §3.7) field context: `[multiple]` is a capability\n // this atom genuinely cannot honour, whereas a missing field is a supported\n // standalone mode. Dev-only: never crashes a production bundle. A `<select\n // multiple>` that truly wants native UX can drop `skraSelect` and stay raw\n // HTML; for a real multi-select, use the Tier 2 <skra-select>.\n if (isDevMode() && el.multiple) {\n throw new Error(\n '[skraSelect] does not support the native [multiple] attribute at ' +\n 'Tier 1. Native multi-select UX is not production-grade (modifier-key/' +\n 'drag selection, UA-drawn unstylable markers, browser-fragile) and ' +\n 'cannot be fixed at the native <select> tier. Use a single-select ' +\n '<select skraSelect> (optionally with [size] for a listbox), or the ' +\n 'Tier 2 <skra-select> (CDK listbox) for real multi-select. See ' +\n 'adr/specs/0039-select-directive-spec.md §4.1.',\n );\n }\n\n // Field-cascaded native properties — only bind when context is present so\n // standalone consumers retain full control of their static attributes.\n // Mirrors SkraInput / SkraCheckbox.\n if (this.context) {\n const ctx = this.context;\n effect(() => {\n el.id = ctx.fieldId();\n });\n effect(() => {\n el.disabled = ctx.isDisabled();\n });\n }\n }\n\n /**\n * Readonly: suppress the pointer affordance that opens the native listbox.\n * `preventDefault` on `mousedown` stops the picker from opening (and, on a\n * native select, focus-on-click — Tab focus still works, which is the right\n * trade-off for a value-display control).\n */\n protected blockWhenReadonly(event: Event): void {\n if (this.context?.isReadonly()) event.preventDefault();\n }\n\n /**\n * Readonly: suppress every keyboard affordance that opens or mutates the\n * select, while letting Tab through for focus traversal. Space / Enter /\n * Alt+ArrowDown open the listbox; bare Arrows + first-letter typeahead change\n * the value inline without opening it — all are value-changing on a native\n * select, so all are blocked in readonly.\n */\n protected blockKeyWhenReadonly(event: KeyboardEvent): void {\n if (this.context?.isReadonly() && event.key !== 'Tab') {\n event.preventDefault();\n }\n }\n}\n","// select[skraSelect] native form-control directive (SkraNativeSelect — ADR 0054 §4.3).\r\nexport * from './lib/select/select';\r\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;;AAWA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqDG;MAwBU,gBAAgB,CAAA;AACV,IAAA,EAAE,GAAG,MAAM,CAAgC,UAAU,CAAC;IAC9D,OAAO,GAAG,MAAM,CAAC,gBAAgB,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;AAE/D,IAAA,WAAA,GAAA;AACE,QAAA,MAAM,EAAE,GAAG,IAAI,CAAC,EAAE,CAAC,aAAa;;;;;;;;AAShC,QAAA,IAAI,SAAS,EAAE,IAAI,EAAE,CAAC,QAAQ,EAAE;YAC9B,MAAM,IAAI,KAAK,CACb,mEAAmE;gBACjE,uEAAuE;gBACvE,oEAAoE;gBACpE,mEAAmE;gBACnE,qEAAqE;gBACrE,gEAAgE;AAChE,gBAAA,+CAA+C,CAClD;QACH;;;;AAKA,QAAA,IAAI,IAAI,CAAC,OAAO,EAAE;AAChB,YAAA,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO;YACxB,MAAM,CAAC,MAAK;AACV,gBAAA,EAAE,CAAC,EAAE,GAAG,GAAG,CAAC,OAAO,EAAE;AACvB,YAAA,CAAC,CAAC;YACF,MAAM,CAAC,MAAK;AACV,gBAAA,EAAE,CAAC,QAAQ,GAAG,GAAG,CAAC,UAAU,EAAE;AAChC,YAAA,CAAC,CAAC;QACJ;IACF;AAEA;;;;;AAKG;AACO,IAAA,iBAAiB,CAAC,KAAY,EAAA;AACtC,QAAA,IAAI,IAAI,CAAC,OAAO,EAAE,UAAU,EAAE;YAAE,KAAK,CAAC,cAAc,EAAE;IACxD;AAEA;;;;;;AAMG;AACO,IAAA,oBAAoB,CAAC,KAAoB,EAAA;AACjD,QAAA,IAAI,IAAI,CAAC,OAAO,EAAE,UAAU,EAAE,IAAI,KAAK,CAAC,GAAG,KAAK,KAAK,EAAE;YACrD,KAAK,CAAC,cAAc,EAAE;QACxB;IACF;uGA7DW,gBAAgB,EAAA,IAAA,EAAA,EAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA;AAAhB,IAAA,OAAA,IAAA,GAAA,EAAA,CAAA,oBAAA,CAAA,EAAA,UAAA,EAAA,QAAA,EAAA,OAAA,EAAA,QAAA,EAAA,IAAA,EAAA,gBAAgB,0cAjBjB,2BAA2B,EAAA,QAAA,EAAA,IAAA,EAAA,MAAA,EAAA,CAAA,26DAAA,CAAA,EAAA,eAAA,EAAA,EAAA,CAAA,uBAAA,CAAA,MAAA,EAAA,CAAA;;2FAiB1B,gBAAgB,EAAA,UAAA,EAAA,CAAA;kBAvB5B,SAAS;AACE,YAAA,IAAA,EAAA,CAAA,EAAA,QAAA,EAAA,oBAAoB,EAAA,UAAA,EAClB,IAAI,EAAA,QAAA,EAIN,2BAA2B,EAAA,eAAA,EAEpB,uBAAuB,CAAC,MAAM,EAAA,aAAA,EAChC,iBAAiB,CAAC,QAAQ,EAAA,IAAA,EACnC;AACJ,wBAAA,yBAAyB,EAAE,mCAAmC;AAC9D,wBAAA,qBAAqB,EAAE,qCAAqC;AAC5D,wBAAA,sBAAsB,EAAE,uCAAuC;AAC/D,wBAAA,sBAAsB,EAAE,uCAAuC;;;;;AAK/D,wBAAA,aAAa,EAAE,2BAA2B;AAC1C,wBAAA,WAAW,EAAE,8BAA8B;AAC5C,qBAAA,EAAA,MAAA,EAAA,CAAA,26DAAA,CAAA,EAAA;;;ACtFH;;ACAA;;AAEG;;;;"}
|
|
@@ -29,7 +29,8 @@ import { _attachPositioner } from '@skra/ui/core';
|
|
|
29
29
|
*/
|
|
30
30
|
class SkraPopover {
|
|
31
31
|
/** Placement intent; `flip`/`shift` adjust at runtime. */
|
|
32
|
-
placement = input('bottom',
|
|
32
|
+
placement = input('bottom', /* @ts-ignore */
|
|
33
|
+
...(ngDevMode ? [{ debugName: "placement" }] : /* istanbul ignore next */ []));
|
|
33
34
|
host = inject(ElementRef);
|
|
34
35
|
renderer = inject(Renderer2);
|
|
35
36
|
/** The consumer's `[popovertarget]` button, resolved once (lazily-idempotent via `resolved`). */
|
|
@@ -134,10 +135,10 @@ class SkraPopover {
|
|
|
134
135
|
this.renderer.removeAttribute(this.trigger, 'aria-controls');
|
|
135
136
|
}
|
|
136
137
|
}
|
|
137
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "
|
|
138
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "
|
|
138
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.1", ngImport: i0, type: SkraPopover, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
139
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.1", type: SkraPopover, isStandalone: true, selector: "skra-popover", inputs: { placement: { classPropertyName: "placement", publicName: "placement", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "popover": "auto" } }, exportAs: ["skraPopover"], ngImport: i0, template: '<ng-content></ng-content>', isInline: true, styles: ["@charset \"UTF-8\";:host{position:fixed;margin:0;inset:auto;box-sizing:border-box;background:var(--skra-comp-popover-background);color:var(--skra-comp-popover-foreground);border-color:var(--skra-comp-popover-border-color);border-width:var(--skra-comp-popover-border-width);border-style:var(--skra-comp-popover-border-style);border-radius:var(--skra-comp-popover-radius);padding-block:var(--skra-comp-popover-padding-block);padding-inline:var(--skra-comp-popover-padding-inline);box-shadow:var(--skra-comp-popover-shadow);inline-size:max-content;max-inline-size:var(--skra-comp-popover-max-inline-size);font-family:var(--skra-sys-typography-role-body-medium-font-family);font-size:var(--skra-sys-typography-role-body-medium-font-size);font-weight:var(--skra-sys-typography-role-body-medium-font-weight);line-height:var(--skra-sys-typography-role-body-medium-line-height);letter-spacing:var(--skra-sys-typography-role-body-medium-letter-spacing);opacity:0;transition:opacity var(--skra-comp-popover-motion-duration) var(--skra-comp-popover-motion-easing),display var(--skra-comp-popover-motion-duration) allow-discrete,overlay var(--skra-comp-popover-motion-duration) allow-discrete}:host(:popover-open){display:block;opacity:1}@starting-style{:host(:popover-open){opacity:0}}@media(prefers-reduced-motion:reduce){:host{transition-duration:0ms}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
139
140
|
}
|
|
140
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "
|
|
141
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.1", ngImport: i0, type: SkraPopover, decorators: [{
|
|
141
142
|
type: Component,
|
|
142
143
|
args: [{ selector: 'skra-popover', standalone: true, exportAs: 'skraPopover', template: '<ng-content></ng-content>', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.Emulated, host: {
|
|
143
144
|
// Native top layer + platform-owned toggle / light-dismiss / Escape / stacking (ADR 0044).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"skra-ui-popover.mjs","sources":["../../../../libs/ui/popover/src/lib/popover/popover.ts","../../../../libs/ui/popover/src/skra-ui-popover.ts"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n DestroyRef,\n ElementRef,\n Renderer2,\n ViewEncapsulation,\n afterNextRender,\n inject,\n input,\n} from '@angular/core';\n\nimport {\n _attachPositioner,\n type SkraPlacement,\n type _PositionerHandle,\n} from '@skra/ui/core';\n\ndeclare const ngDevMode: boolean | undefined;\n\n/**\n * SkraPopover — a NON-MODAL, anchored panel of arbitrary (interactive) content, rendered in the\n * browser TOP LAYER via the native Popover API (`popover=\"auto\"`), per ADR 0044. It is the middle\n * ground between Tooltip (supplementary, non-interactive, hover/focus) and the page-blocking modal\n * Dialog.\n *\n * SHAPE (scope 2026-06-06): a Component on the PANEL with projected content — NOT a directive on the\n * trigger + `TemplateRef`. The trigger is the consumer's own button linked by the native\n * `popovertarget` attribute (zero Forge trigger API); toggle, light-dismiss (pointer-outside),\n * Escape and invocation-order stacking are ALL platform-owned (contrast Tooltip's `popover=\"manual\"`,\n * which hand-wires Escape). The consumer MUST give `<skra-popover>` an `id` so `popovertarget` can\n * reference it.\n *\n * <button skraButton popovertarget=\"user-menu\">Account</button>\n * <skra-popover id=\"user-menu\"> …interactive content… </skra-popover>\n *\n * Forge owns only: the token-driven surface (see `popover.scss`), positioning via `SkraPositioner`\n * (geometry-only inline; ADR 0044 §3 rules 4–5), and the disclosure ARIA on the trigger\n * (`aria-expanded` / `aria-controls`). Focus is NOT trapped and NOT force-moved (non-modal — ADR 0044\n * §3 rule 8); reachability + focus-return are native to the Popover API. Panel naming is the\n * consumer's `aria-labelledby` → projected heading (ADR 033 container carve-out — no `ariaLabel`\n * input). A right-click context menu is NOT a Popover concern (→ `skra-menu` + recipe; SRP + WCAG).\n *\n * Full contract: adr/specs/component-popover-spec.md.\n */\n@Component({\n selector: 'skra-popover',\n standalone: true,\n exportAs: 'skraPopover',\n template: '<ng-content></ng-content>',\n styleUrl: './popover.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n encapsulation: ViewEncapsulation.Emulated,\n host: {\n // Native top layer + platform-owned toggle / light-dismiss / Escape / stacking (ADR 0044).\n popover: 'auto',\n // NOTE: `toggle` is wired IMPERATIVELY (addEventListener) in the constructor, NOT as a host listener.\n // A consumer template binding `(toggle)` on <skra-popover> (a documented side-effect path) SHADOWS a\n // host-metadata `(toggle)` listener for the same event — which would silently break positioning +\n // aria-expanded. A raw `addEventListener` fires alongside the consumer's binding. (Found via the\n // skra-menu browser probe, 2026-06-07; same fix applied there.)\n },\n})\nexport class SkraPopover {\n /** Placement intent; `flip`/`shift` adjust at runtime. */\n readonly placement = input<SkraPlacement>('bottom');\n\n private readonly host = inject<ElementRef<HTMLElement>>(ElementRef);\n private readonly renderer = inject(Renderer2);\n\n /** The consumer's `[popovertarget]` button, resolved once (lazily-idempotent via `resolved`). */\n private trigger: HTMLElement | null = null;\n private resolved = false;\n private positioner?: _PositionerHandle;\n\n constructor() {\n // All imperative DOM wiring lives in afterNextRender — a browser-only hook, so the native listener\n // and the trigger resolution never run on the server (SSR-safe by construction; O-18).\n afterNextRender(() => {\n // Imperative (NOT host-metadata) so a consumer `(toggle)` binding does not shadow it — see host {}.\n this.host.nativeElement.addEventListener('toggle', this.onToggleNative);\n // Resolve + wire the trigger after the view exists (the trigger may be a later sibling), so the\n // disclosure relationship (aria-expanded=\"false\" + aria-controls) is present before first open.\n this.ensureTrigger();\n });\n inject(DestroyRef).onDestroy(() => this.teardown());\n }\n\n // ── Toggle (imperative native listener — see host {} note) ────────────────────\n private readonly onToggleNative = (event: Event): void => {\n if ((event as ToggleEvent).newState === 'open') this.onOpen();\n else this.onClose();\n };\n\n private onOpen(): void {\n const anchor = this.ensureTrigger(); // safety net if afterNextRender has not yet run\n if (anchor) {\n this.renderer.setAttribute(anchor, 'aria-expanded', 'true');\n } else {\n return; // orphan popover (dev-warned at init); nothing to anchor to.\n }\n\n this.positioner?.destroy();\n this.positioner = _attachPositioner(\n anchor,\n this.host.nativeElement,\n () => ({\n placement: this.placement(),\n offset: this.cssLenPx('--skra-comp-popover-offset'),\n viewportPadding: this.cssLenPx('--skra-comp-popover-viewport-padding'),\n }),\n );\n }\n\n private onClose(): void {\n if (this.trigger) {\n this.renderer.setAttribute(this.trigger, 'aria-expanded', 'false');\n }\n this.positioner?.destroy();\n this.positioner = undefined;\n }\n\n /**\n * Resolve the `[popovertarget]` trigger and establish the disclosure ARIA on it (sanctioned a11y\n * wiring, ADR 013-style — not style mutation). Idempotent: runs at most once. Returns the trigger\n * (or null for an orphan popover, dev-warned once).\n */\n private ensureTrigger(): HTMLElement | null {\n if (this.resolved) return this.trigger;\n this.resolved = true;\n\n const id = this.host.nativeElement.id;\n if (!id) {\n if (typeof ngDevMode === 'undefined' || ngDevMode) {\n console.warn(\n '[skra-popover] has no `id` — it cannot be targeted by a `popovertarget` ' +\n 'trigger and the disclosure ARIA (aria-expanded/aria-controls) cannot be wired. ' +\n 'Give the popover an id and point the trigger at it: ' +\n '<button popovertarget=\"my-id\">…</button> <skra-popover id=\"my-id\">…',\n );\n }\n return null;\n }\n\n // Match by attribute value rather than a built selector — no escaping needed, robust to ids\n // with special characters (and `CSS.escape` is not universally available).\n this.trigger =\n Array.from(\n document.querySelectorAll<HTMLElement>('[popovertarget]'),\n ).find((el) => el.getAttribute('popovertarget') === id) ?? null;\n if (!this.trigger) {\n if (typeof ngDevMode === 'undefined' || ngDevMode) {\n console.warn(\n `[skra-popover] no trigger found with [popovertarget=\"${id}\"] — the popover ` +\n 'is unreachable and has no element to carry aria-expanded/aria-controls. ' +\n 'Add a focusable trigger button with the matching popovertarget.',\n );\n }\n return null;\n }\n\n // Establish the disclosure relationship before the first open (APG Disclosure).\n this.renderer.setAttribute(this.trigger, 'aria-controls', id);\n this.renderer.setAttribute(this.trigger, 'aria-expanded', 'false');\n return this.trigger;\n }\n\n /** Read a length comp token (px/rem) off the host computed style for the positioner. */\n private cssLenPx(prop: string): number {\n const raw = getComputedStyle(this.host.nativeElement)\n .getPropertyValue(prop)\n .trim();\n if (raw.endsWith('px')) return parseFloat(raw);\n if (raw.endsWith('rem')) {\n const rootFontSize =\n parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;\n return parseFloat(raw) * rootFontSize;\n }\n return parseFloat(raw) || 0;\n }\n\n private teardown(): void {\n this.host.nativeElement.removeEventListener('toggle', this.onToggleNative);\n this.positioner?.destroy();\n if (this.trigger) {\n this.renderer.removeAttribute(this.trigger, 'aria-expanded');\n this.renderer.removeAttribute(this.trigger, 'aria-controls');\n }\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"skra-ui-popover.mjs","sources":["../../../../libs/ui/popover/src/lib/popover/popover.ts","../../../../libs/ui/popover/src/skra-ui-popover.ts"],"sourcesContent":["import {\r\n ChangeDetectionStrategy,\r\n Component,\r\n DestroyRef,\r\n ElementRef,\r\n Renderer2,\r\n ViewEncapsulation,\r\n afterNextRender,\r\n inject,\r\n input,\r\n} from '@angular/core';\r\n\r\nimport {\r\n _attachPositioner,\r\n type SkraPlacement,\r\n type _PositionerHandle,\r\n} from '@skra/ui/core';\r\n\r\ndeclare const ngDevMode: boolean | undefined;\r\n\r\n/**\r\n * SkraPopover — a NON-MODAL, anchored panel of arbitrary (interactive) content, rendered in the\r\n * browser TOP LAYER via the native Popover API (`popover=\"auto\"`), per ADR 0044. It is the middle\r\n * ground between Tooltip (supplementary, non-interactive, hover/focus) and the page-blocking modal\r\n * Dialog.\r\n *\r\n * SHAPE (scope 2026-06-06): a Component on the PANEL with projected content — NOT a directive on the\r\n * trigger + `TemplateRef`. The trigger is the consumer's own button linked by the native\r\n * `popovertarget` attribute (zero Forge trigger API); toggle, light-dismiss (pointer-outside),\r\n * Escape and invocation-order stacking are ALL platform-owned (contrast Tooltip's `popover=\"manual\"`,\r\n * which hand-wires Escape). The consumer MUST give `<skra-popover>` an `id` so `popovertarget` can\r\n * reference it.\r\n *\r\n * <button skraButton popovertarget=\"user-menu\">Account</button>\r\n * <skra-popover id=\"user-menu\"> …interactive content… </skra-popover>\r\n *\r\n * Forge owns only: the token-driven surface (see `popover.scss`), positioning via `SkraPositioner`\r\n * (geometry-only inline; ADR 0044 §3 rules 4–5), and the disclosure ARIA on the trigger\r\n * (`aria-expanded` / `aria-controls`). Focus is NOT trapped and NOT force-moved (non-modal — ADR 0044\r\n * §3 rule 8); reachability + focus-return are native to the Popover API. Panel naming is the\r\n * consumer's `aria-labelledby` → projected heading (ADR 033 container carve-out — no `ariaLabel`\r\n * input). A right-click context menu is NOT a Popover concern (→ `skra-menu` + recipe; SRP + WCAG).\r\n *\r\n * Full contract: adr/specs/component-popover-spec.md.\r\n */\r\n@Component({\r\n selector: 'skra-popover',\r\n standalone: true,\r\n exportAs: 'skraPopover',\r\n template: '<ng-content></ng-content>',\r\n styleUrl: './popover.scss',\r\n changeDetection: ChangeDetectionStrategy.OnPush,\r\n encapsulation: ViewEncapsulation.Emulated,\r\n host: {\r\n // Native top layer + platform-owned toggle / light-dismiss / Escape / stacking (ADR 0044).\r\n popover: 'auto',\r\n // NOTE: `toggle` is wired IMPERATIVELY (addEventListener) in the constructor, NOT as a host listener.\r\n // A consumer template binding `(toggle)` on <skra-popover> (a documented side-effect path) SHADOWS a\r\n // host-metadata `(toggle)` listener for the same event — which would silently break positioning +\r\n // aria-expanded. A raw `addEventListener` fires alongside the consumer's binding. (Found via the\r\n // skra-menu browser probe, 2026-06-07; same fix applied there.)\r\n },\r\n})\r\nexport class SkraPopover {\r\n /** Placement intent; `flip`/`shift` adjust at runtime. */\r\n readonly placement = input<SkraPlacement>('bottom');\r\n\r\n private readonly host = inject<ElementRef<HTMLElement>>(ElementRef);\r\n private readonly renderer = inject(Renderer2);\r\n\r\n /** The consumer's `[popovertarget]` button, resolved once (lazily-idempotent via `resolved`). */\r\n private trigger: HTMLElement | null = null;\r\n private resolved = false;\r\n private positioner?: _PositionerHandle;\r\n\r\n constructor() {\r\n // All imperative DOM wiring lives in afterNextRender — a browser-only hook, so the native listener\r\n // and the trigger resolution never run on the server (SSR-safe by construction; O-18).\r\n afterNextRender(() => {\r\n // Imperative (NOT host-metadata) so a consumer `(toggle)` binding does not shadow it — see host {}.\r\n this.host.nativeElement.addEventListener('toggle', this.onToggleNative);\r\n // Resolve + wire the trigger after the view exists (the trigger may be a later sibling), so the\r\n // disclosure relationship (aria-expanded=\"false\" + aria-controls) is present before first open.\r\n this.ensureTrigger();\r\n });\r\n inject(DestroyRef).onDestroy(() => this.teardown());\r\n }\r\n\r\n // ── Toggle (imperative native listener — see host {} note) ────────────────────\r\n private readonly onToggleNative = (event: Event): void => {\r\n if ((event as ToggleEvent).newState === 'open') this.onOpen();\r\n else this.onClose();\r\n };\r\n\r\n private onOpen(): void {\r\n const anchor = this.ensureTrigger(); // safety net if afterNextRender has not yet run\r\n if (anchor) {\r\n this.renderer.setAttribute(anchor, 'aria-expanded', 'true');\r\n } else {\r\n return; // orphan popover (dev-warned at init); nothing to anchor to.\r\n }\r\n\r\n this.positioner?.destroy();\r\n this.positioner = _attachPositioner(\r\n anchor,\r\n this.host.nativeElement,\r\n () => ({\r\n placement: this.placement(),\r\n offset: this.cssLenPx('--skra-comp-popover-offset'),\r\n viewportPadding: this.cssLenPx('--skra-comp-popover-viewport-padding'),\r\n }),\r\n );\r\n }\r\n\r\n private onClose(): void {\r\n if (this.trigger) {\r\n this.renderer.setAttribute(this.trigger, 'aria-expanded', 'false');\r\n }\r\n this.positioner?.destroy();\r\n this.positioner = undefined;\r\n }\r\n\r\n /**\r\n * Resolve the `[popovertarget]` trigger and establish the disclosure ARIA on it (sanctioned a11y\r\n * wiring, ADR 013-style — not style mutation). Idempotent: runs at most once. Returns the trigger\r\n * (or null for an orphan popover, dev-warned once).\r\n */\r\n private ensureTrigger(): HTMLElement | null {\r\n if (this.resolved) return this.trigger;\r\n this.resolved = true;\r\n\r\n const id = this.host.nativeElement.id;\r\n if (!id) {\r\n if (typeof ngDevMode === 'undefined' || ngDevMode) {\r\n console.warn(\r\n '[skra-popover] has no `id` — it cannot be targeted by a `popovertarget` ' +\r\n 'trigger and the disclosure ARIA (aria-expanded/aria-controls) cannot be wired. ' +\r\n 'Give the popover an id and point the trigger at it: ' +\r\n '<button popovertarget=\"my-id\">…</button> <skra-popover id=\"my-id\">…',\r\n );\r\n }\r\n return null;\r\n }\r\n\r\n // Match by attribute value rather than a built selector — no escaping needed, robust to ids\r\n // with special characters (and `CSS.escape` is not universally available).\r\n this.trigger =\r\n Array.from(\r\n document.querySelectorAll<HTMLElement>('[popovertarget]'),\r\n ).find((el) => el.getAttribute('popovertarget') === id) ?? null;\r\n if (!this.trigger) {\r\n if (typeof ngDevMode === 'undefined' || ngDevMode) {\r\n console.warn(\r\n `[skra-popover] no trigger found with [popovertarget=\"${id}\"] — the popover ` +\r\n 'is unreachable and has no element to carry aria-expanded/aria-controls. ' +\r\n 'Add a focusable trigger button with the matching popovertarget.',\r\n );\r\n }\r\n return null;\r\n }\r\n\r\n // Establish the disclosure relationship before the first open (APG Disclosure).\r\n this.renderer.setAttribute(this.trigger, 'aria-controls', id);\r\n this.renderer.setAttribute(this.trigger, 'aria-expanded', 'false');\r\n return this.trigger;\r\n }\r\n\r\n /** Read a length comp token (px/rem) off the host computed style for the positioner. */\r\n private cssLenPx(prop: string): number {\r\n const raw = getComputedStyle(this.host.nativeElement)\r\n .getPropertyValue(prop)\r\n .trim();\r\n if (raw.endsWith('px')) return parseFloat(raw);\r\n if (raw.endsWith('rem')) {\r\n const rootFontSize =\r\n parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;\r\n return parseFloat(raw) * rootFontSize;\r\n }\r\n return parseFloat(raw) || 0;\r\n }\r\n\r\n private teardown(): void {\r\n this.host.nativeElement.removeEventListener('toggle', this.onToggleNative);\r\n this.positioner?.destroy();\r\n if (this.trigger) {\r\n this.renderer.removeAttribute(this.trigger, 'aria-expanded');\r\n this.renderer.removeAttribute(this.trigger, 'aria-controls');\r\n }\r\n }\r\n}\r\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;;AAoBA;;;;;;;;;;;;;;;;;;;;;;;;AAwBG;MAmBU,WAAW,CAAA;;IAEb,SAAS,GAAG,KAAK,CAAgB,QAAQ;kFAAC;AAElC,IAAA,IAAI,GAAG,MAAM,CAA0B,UAAU,CAAC;AAClD,IAAA,QAAQ,GAAG,MAAM,CAAC,SAAS,CAAC;;IAGrC,OAAO,GAAuB,IAAI;IAClC,QAAQ,GAAG,KAAK;AAChB,IAAA,UAAU;AAElB,IAAA,WAAA,GAAA;;;QAGE,eAAe,CAAC,MAAK;;AAEnB,YAAA,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,gBAAgB,CAAC,QAAQ,EAAE,IAAI,CAAC,cAAc,CAAC;;;YAGvE,IAAI,CAAC,aAAa,EAAE;AACtB,QAAA,CAAC,CAAC;AACF,QAAA,MAAM,CAAC,UAAU,CAAC,CAAC,SAAS,CAAC,MAAM,IAAI,CAAC,QAAQ,EAAE,CAAC;IACrD;;AAGiB,IAAA,cAAc,GAAG,CAAC,KAAY,KAAU;AACvD,QAAA,IAAK,KAAqB,CAAC,QAAQ,KAAK,MAAM;YAAE,IAAI,CAAC,MAAM,EAAE;;YACxD,IAAI,CAAC,OAAO,EAAE;AACrB,IAAA,CAAC;IAEO,MAAM,GAAA;QACZ,MAAM,MAAM,GAAG,IAAI,CAAC,aAAa,EAAE,CAAC;QACpC,IAAI,MAAM,EAAE;YACV,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC,MAAM,EAAE,eAAe,EAAE,MAAM,CAAC;QAC7D;aAAO;AACL,YAAA,OAAO;QACT;AAEA,QAAA,IAAI,CAAC,UAAU,EAAE,OAAO,EAAE;AAC1B,QAAA,IAAI,CAAC,UAAU,GAAG,iBAAiB,CACjC,MAAM,EACN,IAAI,CAAC,IAAI,CAAC,aAAa,EACvB,OAAO;AACL,YAAA,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE;AAC3B,YAAA,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC,4BAA4B,CAAC;AACnD,YAAA,eAAe,EAAE,IAAI,CAAC,QAAQ,CAAC,sCAAsC,CAAC;AACvE,SAAA,CAAC,CACH;IACH;IAEQ,OAAO,GAAA;AACb,QAAA,IAAI,IAAI,CAAC,OAAO,EAAE;AAChB,YAAA,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC,IAAI,CAAC,OAAO,EAAE,eAAe,EAAE,OAAO,CAAC;QACpE;AACA,QAAA,IAAI,CAAC,UAAU,EAAE,OAAO,EAAE;AAC1B,QAAA,IAAI,CAAC,UAAU,GAAG,SAAS;IAC7B;AAEA;;;;AAIG;IACK,aAAa,GAAA;QACnB,IAAI,IAAI,CAAC,QAAQ;YAAE,OAAO,IAAI,CAAC,OAAO;AACtC,QAAA,IAAI,CAAC,QAAQ,GAAG,IAAI;QAEpB,MAAM,EAAE,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,EAAE;QACrC,IAAI,CAAC,EAAE,EAAE;AACP,YAAA,IAAI,OAAO,SAAS,KAAK,WAAW,IAAI,SAAS,EAAE;gBACjD,OAAO,CAAC,IAAI,CACV,0EAA0E;oBACxE,iFAAiF;oBACjF,sDAAsD;AACtD,oBAAA,qEAAqE,CACxE;YACH;AACA,YAAA,OAAO,IAAI;QACb;;;AAIA,QAAA,IAAI,CAAC,OAAO;AACV,YAAA,KAAK,CAAC,IAAI,CACR,QAAQ,CAAC,gBAAgB,CAAc,iBAAiB,CAAC,CAC1D,CAAC,IAAI,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,YAAY,CAAC,eAAe,CAAC,KAAK,EAAE,CAAC,IAAI,IAAI;AACjE,QAAA,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE;AACjB,YAAA,IAAI,OAAO,SAAS,KAAK,WAAW,IAAI,SAAS,EAAE;AACjD,gBAAA,OAAO,CAAC,IAAI,CACV,CAAA,qDAAA,EAAwD,EAAE,CAAA,iBAAA,CAAmB;oBAC3E,0EAA0E;AAC1E,oBAAA,iEAAiE,CACpE;YACH;AACA,YAAA,OAAO,IAAI;QACb;;AAGA,QAAA,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC,IAAI,CAAC,OAAO,EAAE,eAAe,EAAE,EAAE,CAAC;AAC7D,QAAA,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC,IAAI,CAAC,OAAO,EAAE,eAAe,EAAE,OAAO,CAAC;QAClE,OAAO,IAAI,CAAC,OAAO;IACrB;;AAGQ,IAAA,QAAQ,CAAC,IAAY,EAAA;QAC3B,MAAM,GAAG,GAAG,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,aAAa;aACjD,gBAAgB,CAAC,IAAI;AACrB,aAAA,IAAI,EAAE;AACT,QAAA,IAAI,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC;AAAE,YAAA,OAAO,UAAU,CAAC,GAAG,CAAC;AAC9C,QAAA,IAAI,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE;AACvB,YAAA,MAAM,YAAY,GAChB,UAAU,CAAC,gBAAgB,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAC,QAAQ,CAAC,IAAI,EAAE;AACvE,YAAA,OAAO,UAAU,CAAC,GAAG,CAAC,GAAG,YAAY;QACvC;AACA,QAAA,OAAO,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC;IAC7B;IAEQ,QAAQ,GAAA;AACd,QAAA,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,mBAAmB,CAAC,QAAQ,EAAE,IAAI,CAAC,cAAc,CAAC;AAC1E,QAAA,IAAI,CAAC,UAAU,EAAE,OAAO,EAAE;AAC1B,QAAA,IAAI,IAAI,CAAC,OAAO,EAAE;YAChB,IAAI,CAAC,QAAQ,CAAC,eAAe,CAAC,IAAI,CAAC,OAAO,EAAE,eAAe,CAAC;YAC5D,IAAI,CAAC,QAAQ,CAAC,eAAe,CAAC,IAAI,CAAC,OAAO,EAAE,eAAe,CAAC;QAC9D;IACF;uGA7HW,WAAW,EAAA,IAAA,EAAA,EAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA;AAAX,IAAA,OAAA,IAAA,GAAA,EAAA,CAAA,oBAAA,CAAA,EAAA,UAAA,EAAA,QAAA,EAAA,OAAA,EAAA,QAAA,EAAA,IAAA,EAAA,WAAW,gSAdZ,2BAA2B,EAAA,QAAA,EAAA,IAAA,EAAA,MAAA,EAAA,CAAA,m0CAAA,CAAA,EAAA,eAAA,EAAA,EAAA,CAAA,uBAAA,CAAA,MAAA,EAAA,CAAA;;2FAc1B,WAAW,EAAA,UAAA,EAAA,CAAA;kBAlBvB,SAAS;AACE,YAAA,IAAA,EAAA,CAAA,EAAA,QAAA,EAAA,cAAc,EAAA,UAAA,EACZ,IAAI,EAAA,QAAA,EACN,aAAa,YACb,2BAA2B,EAAA,eAAA,EAEpB,uBAAuB,CAAC,MAAM,EAAA,aAAA,EAChC,iBAAiB,CAAC,QAAQ,EAAA,IAAA,EACnC;;AAEJ,wBAAA,OAAO,EAAE,MAAM;;;;;;AAMhB,qBAAA,EAAA,MAAA,EAAA,CAAA,m0CAAA,CAAA,EAAA;;;AC7DH;;AAEG;;;;"}
|
|
@@ -31,13 +31,15 @@ import { input, ViewEncapsulation, ChangeDetectionStrategy, Component } from '@a
|
|
|
31
31
|
*/
|
|
32
32
|
class SkraProgress {
|
|
33
33
|
/** Current completion, reflected to the native `value` attribute (native clamps to [0, max]). */
|
|
34
|
-
value = input(0,
|
|
34
|
+
value = input(0, /* @ts-ignore */
|
|
35
|
+
...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
35
36
|
/** Upper bound, reflected to the native `max` attribute. Drives native `aria-valuemax`. */
|
|
36
|
-
max = input(100,
|
|
37
|
-
|
|
38
|
-
static
|
|
37
|
+
max = input(100, /* @ts-ignore */
|
|
38
|
+
...(ngDevMode ? [{ debugName: "max" }] : /* istanbul ignore next */ []));
|
|
39
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.1", ngImport: i0, type: SkraProgress, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
40
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.1", type: SkraProgress, isStandalone: true, selector: "progress[skraProgress]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.value": "value()", "attr.max": "max()" } }, ngImport: i0, template: '', isInline: true, styles: [":host{appearance:none;-webkit-appearance:none;box-sizing:border-box;display:block;inline-size:100%;block-size:var(--skra-comp-progress-block-size);border-width:var(--skra-comp-progress-border-width);border-style:var(--skra-comp-progress-border-style);border-color:var(--skra-comp-progress-border-color);border-radius:var(--skra-comp-progress-radius);box-shadow:var(--skra-comp-progress-shadow);background-color:var(--skra-comp-progress-background);overflow:hidden;margin:0}:host::-webkit-progress-bar{background-color:var(--skra-comp-progress-background);border-radius:var(--skra-comp-progress-radius)}:host::-webkit-progress-value{background-color:var(--skra-comp-progress-fill-color);transition:inline-size var(--skra-comp-progress-motion-duration) var(--skra-comp-progress-motion-easing)}:host::-moz-progress-bar{background-color:var(--skra-comp-progress-fill-color)}@media(prefers-reduced-motion:reduce){:host::-webkit-progress-value{transition:none}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
39
41
|
}
|
|
40
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "
|
|
42
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.1", ngImport: i0, type: SkraProgress, decorators: [{
|
|
41
43
|
type: Component,
|
|
42
44
|
args: [{ selector: 'progress[skraProgress]', standalone: true, template: '', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.Emulated, host: {
|
|
43
45
|
// The data contract — reflected to the NATIVE attributes. The browser derives
|