@skra/ui 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/consumer-guide/README.md +88 -85
- package/consumer-guide/custom-components.md +154 -0
- package/consumer-guide/wrapping-patterns.md +98 -90
- package/fesm2022/skra-ui-button.mjs +2 -2
- package/fesm2022/skra-ui-button.mjs.map +1 -1
- package/fesm2022/skra-ui-checkbox.mjs +2 -2
- package/fesm2022/skra-ui-checkbox.mjs.map +1 -1
- package/fesm2022/skra-ui-core.mjs +28 -3
- package/fesm2022/skra-ui-core.mjs.map +1 -1
- package/fesm2022/skra-ui-input.mjs +2 -2
- package/fesm2022/skra-ui-input.mjs.map +1 -1
- package/fesm2022/skra-ui-menu.mjs +38 -14
- package/fesm2022/skra-ui-menu.mjs.map +1 -1
- package/fesm2022/skra-ui-popover.mjs +25 -9
- package/fesm2022/skra-ui-popover.mjs.map +1 -1
- package/fesm2022/skra-ui-radio.mjs +2 -2
- package/fesm2022/skra-ui-radio.mjs.map +1 -1
- package/fesm2022/skra-ui-rating-display.mjs +2 -2
- package/fesm2022/skra-ui-rating-display.mjs.map +1 -1
- package/fesm2022/skra-ui-star-rating.mjs +2 -2
- package/fesm2022/skra-ui-star-rating.mjs.map +1 -1
- package/fesm2022/skra-ui-switch.mjs +2 -2
- package/fesm2022/skra-ui-switch.mjs.map +1 -1
- package/fesm2022/skra-ui-tooltip.mjs +7 -2
- package/fesm2022/skra-ui-tooltip.mjs.map +1 -1
- package/package.json +2 -2
- package/types/skra-ui-core.d.ts.map +1 -1
- package/types/skra-ui-menu.d.ts +18 -6
- package/types/skra-ui-menu.d.ts.map +1 -1
- package/types/skra-ui-popover.d.ts +13 -4
- package/types/skra-ui-popover.d.ts.map +1 -1
- package/types/skra-ui-tooltip.d.ts.map +1 -1
|
@@ -33,9 +33,10 @@ class SkraPopover {
|
|
|
33
33
|
...(ngDevMode ? [{ debugName: "placement" }] : /* istanbul ignore next */ []));
|
|
34
34
|
host = inject(ElementRef);
|
|
35
35
|
renderer = inject(Renderer2);
|
|
36
|
-
/** The consumer's `[popovertarget]` button
|
|
36
|
+
/** The consumer's `[popovertarget]` button; resolution is retried until it succeeds. */
|
|
37
37
|
trigger = null;
|
|
38
|
-
|
|
38
|
+
/** Latches the dev warning, NOT the resolution — see `ensureTrigger()`. */
|
|
39
|
+
warned = false;
|
|
39
40
|
positioner;
|
|
40
41
|
constructor() {
|
|
41
42
|
// All imperative DOM wiring lives in afterNextRender — a browser-only hook, so the native listener
|
|
@@ -80,16 +81,24 @@ class SkraPopover {
|
|
|
80
81
|
}
|
|
81
82
|
/**
|
|
82
83
|
* Resolve the `[popovertarget]` trigger and establish the disclosure ARIA on it (sanctioned a11y
|
|
83
|
-
* wiring, ADR 013-style — not style mutation).
|
|
84
|
-
*
|
|
84
|
+
* wiring, ADR 013-style — not style mutation). Returns the trigger (or null for an orphan
|
|
85
|
+
* popover, dev-warned once).
|
|
86
|
+
*
|
|
87
|
+
* Idempotent on SUCCESS only. It deliberately does NOT latch on failure: a trigger behind an
|
|
88
|
+
* `@if`, a lazy route or a `@defer` block is simply not in the DOM at the `afterNextRender` tick,
|
|
89
|
+
* and latching there would strand it forever — no disclosure ARIA and, because `onOpen()` bails
|
|
90
|
+
* on a null anchor, an unpositioned panel at viewport 0,0. Retrying on each open is cheap (one
|
|
91
|
+
* `querySelectorAll` per open, only while still unresolved) and makes the `onOpen()` call a real
|
|
92
|
+
* safety net rather than the dead code it was. The dev warning latches separately via `warned`
|
|
93
|
+
* so a late-rendered trigger does not spam the console once per open.
|
|
85
94
|
*/
|
|
86
95
|
ensureTrigger() {
|
|
87
|
-
if (this.
|
|
96
|
+
if (this.trigger)
|
|
88
97
|
return this.trigger;
|
|
89
|
-
this.resolved = true;
|
|
90
98
|
const id = this.host.nativeElement.id;
|
|
91
99
|
if (!id) {
|
|
92
|
-
if (typeof ngDevMode === 'undefined' || ngDevMode) {
|
|
100
|
+
if (!this.warned && (typeof ngDevMode === 'undefined' || ngDevMode)) {
|
|
101
|
+
this.warned = true;
|
|
93
102
|
console.warn('[skra-popover] has no `id` — it cannot be targeted by a `popovertarget` ' +
|
|
94
103
|
'trigger and the disclosure ARIA (aria-expanded/aria-controls) cannot be wired. ' +
|
|
95
104
|
'Give the popover an id and point the trigger at it: ' +
|
|
@@ -97,12 +106,19 @@ class SkraPopover {
|
|
|
97
106
|
}
|
|
98
107
|
return null;
|
|
99
108
|
}
|
|
109
|
+
// Scope the lookup to this node's own root, not `document`: under the ADR 012 §5 delivery
|
|
110
|
+
// contract both trigger and panel live inside a shadow root, where a document-rooted query
|
|
111
|
+
// finds nothing and the disclosure ARIA + positioning silently fail to wire. `getRootNode()`
|
|
112
|
+
// returns `document` in the light-DOM case, so this is strictly a widening.
|
|
113
|
+
//
|
|
100
114
|
// Match by attribute value rather than a built selector — no escaping needed, robust to ids
|
|
101
115
|
// with special characters (and `CSS.escape` is not universally available).
|
|
116
|
+
const root = this.host.nativeElement.getRootNode();
|
|
102
117
|
this.trigger =
|
|
103
|
-
Array.from(
|
|
118
|
+
Array.from(root.querySelectorAll('[popovertarget]')).find((el) => el.getAttribute('popovertarget') === id) ?? null;
|
|
104
119
|
if (!this.trigger) {
|
|
105
|
-
if (typeof ngDevMode === 'undefined' || ngDevMode) {
|
|
120
|
+
if (!this.warned && (typeof ngDevMode === 'undefined' || ngDevMode)) {
|
|
121
|
+
this.warned = true;
|
|
106
122
|
console.warn(`[skra-popover] no trigger found with [popovertarget="${id}"] — the popover ` +
|
|
107
123
|
'is unreachable and has no element to carry aria-expanded/aria-controls. ' +
|
|
108
124
|
'Add a focusable trigger button with the matching popovertarget.');
|
|
@@ -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; resolution is retried until it succeeds. */\r\n private trigger: HTMLElement | null = null;\r\n /** Latches the dev warning, NOT the resolution — see `ensureTrigger()`. */\r\n private warned = 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). Returns the trigger (or null for an orphan\r\n * popover, dev-warned once).\r\n *\r\n * Idempotent on SUCCESS only. It deliberately does NOT latch on failure: a trigger behind an\r\n * `@if`, a lazy route or a `@defer` block is simply not in the DOM at the `afterNextRender` tick,\r\n * and latching there would strand it forever — no disclosure ARIA and, because `onOpen()` bails\r\n * on a null anchor, an unpositioned panel at viewport 0,0. Retrying on each open is cheap (one\r\n * `querySelectorAll` per open, only while still unresolved) and makes the `onOpen()` call a real\r\n * safety net rather than the dead code it was. The dev warning latches separately via `warned`\r\n * so a late-rendered trigger does not spam the console once per open.\r\n */\r\n private ensureTrigger(): HTMLElement | null {\r\n if (this.trigger) return this.trigger;\r\n\r\n const id = this.host.nativeElement.id;\r\n if (!id) {\r\n if (!this.warned && (typeof ngDevMode === 'undefined' || ngDevMode)) {\r\n this.warned = true;\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 // Scope the lookup to this node's own root, not `document`: under the ADR 012 §5 delivery\r\n // contract both trigger and panel live inside a shadow root, where a document-rooted query\r\n // finds nothing and the disclosure ARIA + positioning silently fail to wire. `getRootNode()`\r\n // returns `document` in the light-DOM case, so this is strictly a widening.\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 const root = this.host.nativeElement.getRootNode() as Document | ShadowRoot;\r\n this.trigger =\r\n Array.from(root.querySelectorAll<HTMLElement>('[popovertarget]')).find(\r\n (el) => el.getAttribute('popovertarget') === id,\r\n ) ?? null;\r\n if (!this.trigger) {\r\n if (!this.warned && (typeof ngDevMode === 'undefined' || ngDevMode)) {\r\n this.warned = true;\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":"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"}
|
|
@@ -66,7 +66,7 @@ class SkraRadio {
|
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.1", ngImport: i0, type: SkraRadio, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
69
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.1.1", type: SkraRadio, isStandalone: true, selector: "input[type=\"radio\"][skraRadio]", host: { listeners: { "click": "blockWhenReadonly($event)", "keydown": "blockKeyWhenReadonly($event)" }, properties: { "attr.data-skra-disabled": "context?.isDisabled() ? \"\" : null", "attr.aria-invalid": "context?.hasError() ? \"true\" : null", "attr.aria-readonly": "context?.isReadonly() ? \"true\" : null", "attr.aria-describedby": "context?.describedByIds() ?? null" } }, ngImport: i0, template: '', isInline: true, styles: [":host{appearance:none;flex-shrink:0;cursor:pointer;outline:none;background-repeat:no-repeat;background-position:center;inline-size:var(--skra-comp-field-radio-size);block-size:var(--skra-comp-field-radio-size);border-width:var(--skra-comp-field-radio-border-width);border-style:solid;border-color:var(--skra-comp-field-radio-border-color);border-radius:50%;background-color:var(--skra-comp-field-radio-background);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),box-shadow var(--skra-comp-field-motion-duration) var(--skra-comp-field-motion-easing)}:host(:checked){background-color:var(--skra-comp-field-radio-checked-background);border-color:var(--skra-comp-field-radio-checked-background);background-image:radial-gradient(circle,var(--skra-comp-field-radio-dot-color) 0,var(--skra-comp-field-radio-dot-color) calc(var(--skra-comp-field-radio-dot-size) / 2),transparent calc(var(--skra-comp-field-radio-dot-size) / 2 + .5px))}@media(hover:hover)and (pointer:fine){:host(:hover:not(:disabled):not([aria-readonly=true])){border-color:var(--skra-comp-field-radio-border-color-hover)}:host(:checked:hover:not(:disabled):not([aria-readonly=true])){background-color:var(--skra-comp-field-radio-checked-background-hover);border-color:var(--skra-comp-field-radio-checked-background-hover)}}:host(:focus-visible){box-shadow:0 0 0 var(--skra-comp-field-focus-ring-width) var(--skra-comp-field-focus-ring-color)}:host(:disabled),:host([data-skra-disabled]){cursor:not-allowed;opacity:var(--skra-sys-opacity-disabled)}:host([aria-readonly=true]){cursor:default;background-color:var(--skra-comp-field-background-readonly);border-color:var(--skra-comp-field-border-color-readonly)}:host(:checked[aria-readonly=true]){background-color:var(--skra-comp-field-radio-checked-background-readonly);border-color:var(--skra-comp-field-radio-checked-background-readonly)}: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 });
|
|
69
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.1.1", type: SkraRadio, isStandalone: true, selector: "input[type=\"radio\"][skraRadio]", host: { listeners: { "click": "blockWhenReadonly($event)", "keydown": "blockKeyWhenReadonly($event)" }, properties: { "attr.data-skra-disabled": "context?.isDisabled() ? \"\" : null", "attr.aria-invalid": "context?.hasError() ? \"true\" : null", "attr.aria-readonly": "context?.isReadonly() ? \"true\" : null", "attr.aria-describedby": "context?.describedByIds() ?? null" } }, ngImport: i0, template: '', isInline: true, styles: [":host{appearance:none;flex-shrink:0;cursor:pointer;outline:none;background-repeat:no-repeat;background-position:center;inline-size:var(--skra-comp-field-radio-size);block-size:var(--skra-comp-field-radio-size);border-width:var(--skra-comp-field-radio-border-width);border-style:solid;border-color:var(--skra-comp-field-radio-border-color);border-radius:50%;background-color:var(--skra-comp-field-radio-background);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),box-shadow var(--skra-comp-field-motion-duration) var(--skra-comp-field-motion-easing)}:host(:checked){background-color:var(--skra-comp-field-radio-checked-background);border-color:var(--skra-comp-field-radio-checked-background);background-image:radial-gradient(circle,var(--skra-comp-field-radio-dot-color) 0,var(--skra-comp-field-radio-dot-color) calc(var(--skra-comp-field-radio-dot-size) / 2),transparent calc(var(--skra-comp-field-radio-dot-size) / 2 + .5px))}@media(hover:hover)and (pointer:fine){:host(:hover:not(:disabled):not([aria-readonly=true])){border-color:var(--skra-comp-field-radio-border-color-hover)}:host(:checked:hover:not(:disabled):not([aria-readonly=true])){background-color:var(--skra-comp-field-radio-checked-background-hover);border-color:var(--skra-comp-field-radio-checked-background-hover)}}:host(:focus-visible){box-shadow:0 0 0 var(--skra-comp-field-focus-ring-width) var(--skra-comp-field-focus-ring-color)}:host(:disabled),:host([data-skra-disabled]){cursor:not-allowed;opacity:var(--skra-sys-opacity-disabled)}:host([aria-readonly=true]){cursor:default;background-color:var(--skra-comp-field-background-readonly);border-color:var(--skra-comp-field-border-color-readonly)}:host(:checked[aria-readonly=true]){background-color:var(--skra-comp-field-radio-checked-background-readonly);border-color:var(--skra-comp-field-radio-checked-background-readonly);--skra-comp-field-radio-dot-color: var( --skra-comp-field-radio-dot-color-readonly )}: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 });
|
|
70
70
|
}
|
|
71
71
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.1", ngImport: i0, type: SkraRadio, decorators: [{
|
|
72
72
|
type: Component,
|
|
@@ -81,7 +81,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.1", ngImpor
|
|
|
81
81
|
'[attr.aria-describedby]': 'context?.describedByIds() ?? null',
|
|
82
82
|
'(click)': 'blockWhenReadonly($event)',
|
|
83
83
|
'(keydown)': 'blockKeyWhenReadonly($event)',
|
|
84
|
-
}, styles: [":host{appearance:none;flex-shrink:0;cursor:pointer;outline:none;background-repeat:no-repeat;background-position:center;inline-size:var(--skra-comp-field-radio-size);block-size:var(--skra-comp-field-radio-size);border-width:var(--skra-comp-field-radio-border-width);border-style:solid;border-color:var(--skra-comp-field-radio-border-color);border-radius:50%;background-color:var(--skra-comp-field-radio-background);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),box-shadow var(--skra-comp-field-motion-duration) var(--skra-comp-field-motion-easing)}:host(:checked){background-color:var(--skra-comp-field-radio-checked-background);border-color:var(--skra-comp-field-radio-checked-background);background-image:radial-gradient(circle,var(--skra-comp-field-radio-dot-color) 0,var(--skra-comp-field-radio-dot-color) calc(var(--skra-comp-field-radio-dot-size) / 2),transparent calc(var(--skra-comp-field-radio-dot-size) / 2 + .5px))}@media(hover:hover)and (pointer:fine){:host(:hover:not(:disabled):not([aria-readonly=true])){border-color:var(--skra-comp-field-radio-border-color-hover)}:host(:checked:hover:not(:disabled):not([aria-readonly=true])){background-color:var(--skra-comp-field-radio-checked-background-hover);border-color:var(--skra-comp-field-radio-checked-background-hover)}}:host(:focus-visible){box-shadow:0 0 0 var(--skra-comp-field-focus-ring-width) var(--skra-comp-field-focus-ring-color)}:host(:disabled),:host([data-skra-disabled]){cursor:not-allowed;opacity:var(--skra-sys-opacity-disabled)}:host([aria-readonly=true]){cursor:default;background-color:var(--skra-comp-field-background-readonly);border-color:var(--skra-comp-field-border-color-readonly)}:host(:checked[aria-readonly=true]){background-color:var(--skra-comp-field-radio-checked-background-readonly);border-color:var(--skra-comp-field-radio-checked-background-readonly)}: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"] }]
|
|
84
|
+
}, styles: [":host{appearance:none;flex-shrink:0;cursor:pointer;outline:none;background-repeat:no-repeat;background-position:center;inline-size:var(--skra-comp-field-radio-size);block-size:var(--skra-comp-field-radio-size);border-width:var(--skra-comp-field-radio-border-width);border-style:solid;border-color:var(--skra-comp-field-radio-border-color);border-radius:50%;background-color:var(--skra-comp-field-radio-background);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),box-shadow var(--skra-comp-field-motion-duration) var(--skra-comp-field-motion-easing)}:host(:checked){background-color:var(--skra-comp-field-radio-checked-background);border-color:var(--skra-comp-field-radio-checked-background);background-image:radial-gradient(circle,var(--skra-comp-field-radio-dot-color) 0,var(--skra-comp-field-radio-dot-color) calc(var(--skra-comp-field-radio-dot-size) / 2),transparent calc(var(--skra-comp-field-radio-dot-size) / 2 + .5px))}@media(hover:hover)and (pointer:fine){:host(:hover:not(:disabled):not([aria-readonly=true])){border-color:var(--skra-comp-field-radio-border-color-hover)}:host(:checked:hover:not(:disabled):not([aria-readonly=true])){background-color:var(--skra-comp-field-radio-checked-background-hover);border-color:var(--skra-comp-field-radio-checked-background-hover)}}:host(:focus-visible){box-shadow:0 0 0 var(--skra-comp-field-focus-ring-width) var(--skra-comp-field-focus-ring-color)}:host(:disabled),:host([data-skra-disabled]){cursor:not-allowed;opacity:var(--skra-sys-opacity-disabled)}:host([aria-readonly=true]){cursor:default;background-color:var(--skra-comp-field-background-readonly);border-color:var(--skra-comp-field-border-color-readonly)}:host(:checked[aria-readonly=true]){background-color:var(--skra-comp-field-radio-checked-background-readonly);border-color:var(--skra-comp-field-radio-checked-background-readonly);--skra-comp-field-radio-dot-color: var( --skra-comp-field-radio-dot-color-readonly )}: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"] }]
|
|
85
85
|
}], ctorParameters: () => [] });
|
|
86
86
|
|
|
87
87
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"skra-ui-radio.mjs","sources":["../../../../libs/ui/radio/src/lib/radio/radio.ts","../../../../libs/ui/radio/src/skra-ui-radio.ts"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n ElementRef,\n ViewEncapsulation,\n effect,\n inject,\n} from '@angular/core';\nimport { SkraFieldContext } from '@skra/ui/field';\n\n/**\n * Wires a native `<input type=\"radio\">` to SkraFieldContext when inside\n * `<skra-field>`, or stands alone with no cascade. Per ADR 039/040, Forge ships\n * no `<skra-radio>` / `<skra-radio-group>` wrappers — this is a Component-on-\n * the-native-host (`<input>` is void → `template: ''`, ADR 040 §2) carrying its\n * own `radio.scss` via Emulated encapsulation. Radios are\n * grouped via native `<fieldset skraFieldset>` + `<legend skraLegend>`,\n * coordination happens through the shared `name` attribute (HTML-native), and\n * one-of-N value selection is consumer-owned per ADR 037.\n *\n * <fieldset skraFieldset>\n * <legend skraLegend>Choose plan</legend>\n * <skra-field>\n * <input type=\"radio\" skraRadio name=\"plan\" value=\"free\"\n * [checked]=\"plan() === 'free'\" (change)=\"plan.set('free')\" />\n * <label skraLabelFor>Free</label>\n * </skra-field>\n * <skra-field>\n * <input type=\"radio\" skraRadio name=\"plan\" value=\"pro\"\n * [checked]=\"plan() === 'pro'\" (change)=\"plan.set('pro')\" />\n * <label skraLabelFor>Pro</label>\n * </skra-field>\n * </fieldset>\n *\n * Toggle state (`checked`) is NOT reflected as `data-skra-*` — slice CSS uses\n * the native `:checked` pseudo-class. Browser handles `aria-checked` natively.\n */\n@Component({\n selector: 'input[type=\"radio\"][skraRadio]',\n standalone: true,\n // Void host — no <ng-content>. See ADR 040 §2.\n template: '',\n styleUrl: './radio.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n encapsulation: ViewEncapsulation.Emulated,\n host: {\n '[attr.data-skra-disabled]': 'context?.isDisabled() ? \"\" : null',\n '[attr.aria-invalid]': 'context?.hasError() ? \"true\" : null',\n // Readonly is ARIA-only (native `readonly` doesn't apply to radios per HTML\n // spec). No `data-skra-readonly` — dead surface (styling keys off\n // `[aria-readonly]`), removed 2026-05-31 to match select's Q-3 ARIA-only\n // pattern (Batch 5 cleanroom review).\n '[attr.aria-readonly]': 'context?.isReadonly() ? \"true\" : null',\n '[attr.aria-describedby]': 'context?.describedByIds() ?? null',\n '(click)': 'blockWhenReadonly($event)',\n '(keydown)': 'blockKeyWhenReadonly($event)',\n },\n})\nexport class SkraRadio {\n private readonly el = inject<ElementRef<HTMLInputElement>>(ElementRef);\n readonly context = inject(SkraFieldContext, { optional: true });\n\n constructor() {\n if (this.context) {\n const ctx = this.context;\n const el = this.el.nativeElement;\n effect(() => {\n el.id = ctx.fieldId();\n });\n effect(() => {\n el.disabled = ctx.isDisabled();\n });\n }\n }\n\n /**\n * Readonly enforcement — native radio has no `[readonly]`, so without this the\n * `aria-readonly` we announce would be a lie (the user could still change the\n * selection). `preventDefault` on `click` blocks the selection change from both\n * pointer and Space/Enter (keyboard activation dispatches a click on form\n * controls). Mirrors the `<select skraSelect>` readonly guard.\n */\n protected blockWhenReadonly(event: Event): void {\n if (this.context?.isReadonly()) event.preventDefault();\n }\n\n /**\n * Arrow keys move selection between radios in a `name` group WITHOUT a click,\n * so they need a separate guard. Block every key except `Tab` (focus traversal)\n * when readonly.\n */\n protected blockKeyWhenReadonly(event: KeyboardEvent): void {\n if (this.context?.isReadonly() && event.key !== 'Tab') {\n event.preventDefault();\n }\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;;AAUA;;;;;;;;;;;;;;;;;;;;;;;;;;AA0BG;MAsBU,SAAS,CAAA;AACH,IAAA,EAAE,GAAG,MAAM,CAA+B,UAAU,CAAC;IAC7D,OAAO,GAAG,MAAM,CAAC,gBAAgB,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;AAE/D,IAAA,WAAA,GAAA;AACE,QAAA,IAAI,IAAI,CAAC,OAAO,EAAE;AAChB,YAAA,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO;AACxB,YAAA,MAAM,EAAE,GAAG,IAAI,CAAC,EAAE,CAAC,aAAa;YAChC,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;;;;;;AAMG;AACO,IAAA,iBAAiB,CAAC,KAAY,EAAA;AACtC,QAAA,IAAI,IAAI,CAAC,OAAO,EAAE,UAAU,EAAE;YAAE,KAAK,CAAC,cAAc,EAAE;IACxD;AAEA;;;;AAIG;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;uGArCW,SAAS,EAAA,IAAA,EAAA,EAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA;AAAT,IAAA,OAAA,IAAA,GAAA,EAAA,CAAA,oBAAA,CAAA,EAAA,UAAA,EAAA,QAAA,EAAA,OAAA,EAAA,QAAA,EAAA,IAAA,EAAA,SAAS,qdAjBV,EAAE,EAAA,QAAA,EAAA,IAAA,EAAA,MAAA,EAAA,CAAA,
|
|
1
|
+
{"version":3,"file":"skra-ui-radio.mjs","sources":["../../../../libs/ui/radio/src/lib/radio/radio.ts","../../../../libs/ui/radio/src/skra-ui-radio.ts"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n ElementRef,\n ViewEncapsulation,\n effect,\n inject,\n} from '@angular/core';\nimport { SkraFieldContext } from '@skra/ui/field';\n\n/**\n * Wires a native `<input type=\"radio\">` to SkraFieldContext when inside\n * `<skra-field>`, or stands alone with no cascade. Per ADR 039/040, Forge ships\n * no `<skra-radio>` / `<skra-radio-group>` wrappers — this is a Component-on-\n * the-native-host (`<input>` is void → `template: ''`, ADR 040 §2) carrying its\n * own `radio.scss` via Emulated encapsulation. Radios are\n * grouped via native `<fieldset skraFieldset>` + `<legend skraLegend>`,\n * coordination happens through the shared `name` attribute (HTML-native), and\n * one-of-N value selection is consumer-owned per ADR 037.\n *\n * <fieldset skraFieldset>\n * <legend skraLegend>Choose plan</legend>\n * <skra-field>\n * <input type=\"radio\" skraRadio name=\"plan\" value=\"free\"\n * [checked]=\"plan() === 'free'\" (change)=\"plan.set('free')\" />\n * <label skraLabelFor>Free</label>\n * </skra-field>\n * <skra-field>\n * <input type=\"radio\" skraRadio name=\"plan\" value=\"pro\"\n * [checked]=\"plan() === 'pro'\" (change)=\"plan.set('pro')\" />\n * <label skraLabelFor>Pro</label>\n * </skra-field>\n * </fieldset>\n *\n * Toggle state (`checked`) is NOT reflected as `data-skra-*` — slice CSS uses\n * the native `:checked` pseudo-class. Browser handles `aria-checked` natively.\n */\n@Component({\n selector: 'input[type=\"radio\"][skraRadio]',\n standalone: true,\n // Void host — no <ng-content>. See ADR 040 §2.\n template: '',\n styleUrl: './radio.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n encapsulation: ViewEncapsulation.Emulated,\n host: {\n '[attr.data-skra-disabled]': 'context?.isDisabled() ? \"\" : null',\n '[attr.aria-invalid]': 'context?.hasError() ? \"true\" : null',\n // Readonly is ARIA-only (native `readonly` doesn't apply to radios per HTML\n // spec). No `data-skra-readonly` — dead surface (styling keys off\n // `[aria-readonly]`), removed 2026-05-31 to match select's Q-3 ARIA-only\n // pattern (Batch 5 cleanroom review).\n '[attr.aria-readonly]': 'context?.isReadonly() ? \"true\" : null',\n '[attr.aria-describedby]': 'context?.describedByIds() ?? null',\n '(click)': 'blockWhenReadonly($event)',\n '(keydown)': 'blockKeyWhenReadonly($event)',\n },\n})\nexport class SkraRadio {\n private readonly el = inject<ElementRef<HTMLInputElement>>(ElementRef);\n readonly context = inject(SkraFieldContext, { optional: true });\n\n constructor() {\n if (this.context) {\n const ctx = this.context;\n const el = this.el.nativeElement;\n effect(() => {\n el.id = ctx.fieldId();\n });\n effect(() => {\n el.disabled = ctx.isDisabled();\n });\n }\n }\n\n /**\n * Readonly enforcement — native radio has no `[readonly]`, so without this the\n * `aria-readonly` we announce would be a lie (the user could still change the\n * selection). `preventDefault` on `click` blocks the selection change from both\n * pointer and Space/Enter (keyboard activation dispatches a click on form\n * controls). Mirrors the `<select skraSelect>` readonly guard.\n */\n protected blockWhenReadonly(event: Event): void {\n if (this.context?.isReadonly()) event.preventDefault();\n }\n\n /**\n * Arrow keys move selection between radios in a `name` group WITHOUT a click,\n * so they need a separate guard. Block every key except `Tab` (focus traversal)\n * when readonly.\n */\n protected blockKeyWhenReadonly(event: KeyboardEvent): void {\n if (this.context?.isReadonly() && event.key !== 'Tab') {\n event.preventDefault();\n }\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;;AAUA;;;;;;;;;;;;;;;;;;;;;;;;;;AA0BG;MAsBU,SAAS,CAAA;AACH,IAAA,EAAE,GAAG,MAAM,CAA+B,UAAU,CAAC;IAC7D,OAAO,GAAG,MAAM,CAAC,gBAAgB,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;AAE/D,IAAA,WAAA,GAAA;AACE,QAAA,IAAI,IAAI,CAAC,OAAO,EAAE;AAChB,YAAA,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO;AACxB,YAAA,MAAM,EAAE,GAAG,IAAI,CAAC,EAAE,CAAC,aAAa;YAChC,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;;;;;;AAMG;AACO,IAAA,iBAAiB,CAAC,KAAY,EAAA;AACtC,QAAA,IAAI,IAAI,CAAC,OAAO,EAAE,UAAU,EAAE;YAAE,KAAK,CAAC,cAAc,EAAE;IACxD;AAEA;;;;AAIG;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;uGArCW,SAAS,EAAA,IAAA,EAAA,EAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA;AAAT,IAAA,OAAA,IAAA,GAAA,EAAA,CAAA,oBAAA,CAAA,EAAA,UAAA,EAAA,QAAA,EAAA,OAAA,EAAA,QAAA,EAAA,IAAA,EAAA,SAAS,qdAjBV,EAAE,EAAA,QAAA,EAAA,IAAA,EAAA,MAAA,EAAA,CAAA,+yEAAA,CAAA,EAAA,eAAA,EAAA,EAAA,CAAA,uBAAA,CAAA,MAAA,EAAA,CAAA;;2FAiBD,SAAS,EAAA,UAAA,EAAA,CAAA;kBArBrB,SAAS;AACE,YAAA,IAAA,EAAA,CAAA,EAAA,QAAA,EAAA,gCAAgC,EAAA,UAAA,EAC9B,IAAI,EAAA,QAAA,EAEN,EAAE,EAAA,eAAA,EAEK,uBAAuB,CAAC,MAAM,EAAA,aAAA,EAChC,iBAAiB,CAAC,QAAQ,EAAA,IAAA,EACnC;AACJ,wBAAA,2BAA2B,EAAE,mCAAmC;AAChE,wBAAA,qBAAqB,EAAE,qCAAqC;;;;;AAK5D,wBAAA,sBAAsB,EAAE,uCAAuC;AAC/D,wBAAA,yBAAyB,EAAE,mCAAmC;AAC9D,wBAAA,SAAS,EAAE,2BAA2B;AACtC,wBAAA,WAAW,EAAE,8BAA8B;AAC5C,qBAAA,EAAA,MAAA,EAAA,CAAA,+yEAAA,CAAA,EAAA;;;ACxDH;;AAEG;;;;"}
|
|
@@ -63,7 +63,7 @@ class SkraRatingDisplay {
|
|
|
63
63
|
}, /* @ts-ignore */
|
|
64
64
|
...(ngDevMode ? [{ debugName: "fillBucket" }] : /* istanbul ignore next */ []));
|
|
65
65
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.1", ngImport: i0, type: SkraRatingDisplay, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
66
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.1", type: SkraRatingDisplay, isStandalone: true, selector: "skra-rating-display", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.data-skra-fill": "fillBucket()", "attr.data-skra-size": "size()" } }, ngImport: i0, template: "<!--\
|
|
66
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.1", type: SkraRatingDisplay, isStandalone: true, selector: "skra-rating-display", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.data-skra-fill": "fillBucket()", "attr.data-skra-size": "size()" } }, ngImport: i0, template: "<!--\n Two stacked glyph layers: an empty-stars track that defines the box, and a filled-stars\n overlay clipped to the quantized fill width (the SCSS maps data-skra-fill \u2192 inline-size).\n Both layers are decorative (aria-hidden); the accessible name, when any, is on the host\n (consumer-applied role=\"img\" + aria-label \u2014 ADR 033 Path 3).\n O-67 \u2014 the track draws OUTLINE stars and the fill draws SOLID ones, so filled vs empty is\n carried by shape (WCAG 1.4.1). The outline's outer edge is the solid star's, so a filled\n star covers the ring (bar a faint anti-aliased fringe at the edge \u2014 the pre-O-67 solid\n track did the same); a partial fill shows solid over outline.\n-->\n<div class=\"skra-rating-display-track\" aria-hidden=\"true\">\n @for (star of stars(); track star) {\n <svg\n class=\"skra-rating-display-glyph\"\n viewBox=\"0 0 24 24\"\n focusable=\"false\"\n aria-hidden=\"true\"\n >\n <path\n fill-rule=\"evenodd\"\n d=\"M12 17.27 18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21zM12 15.4l-3.76 2.27 1-4.28-3.32-2.88 4.38-.38L12 6.1l1.71 4.04 4.38.38-3.32 2.88 1 4.28z\"\n />\n </svg>\n }\n</div>\n<div class=\"skra-rating-display-fill\" aria-hidden=\"true\">\n @for (star of stars(); track star) {\n <svg\n class=\"skra-rating-display-glyph\"\n viewBox=\"0 0 24 24\"\n focusable=\"false\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M12 17.27 18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z\"\n />\n </svg>\n }\n</div>\n", styles: [":host{margin:0;position:relative;display:inline-flex;line-height:0;--_star-size: var(--skra-comp-star-rating-star-size-md)}:host([data-skra-size=sm]){--_star-size: var(--skra-comp-star-rating-star-size-sm)}:host([data-skra-size=lg]){--_star-size: var(--skra-comp-star-rating-star-size-lg)}.skra-rating-display-track,.skra-rating-display-fill{display:flex;gap:0;line-height:0}.skra-rating-display-fill{position:absolute;inset-block-start:0;inset-inline-start:0;block-size:100%;inline-size:0;overflow:hidden}.skra-rating-display-track .skra-rating-display-glyph{fill:var(--skra-comp-star-rating-star-color-empty)}.skra-rating-display-fill .skra-rating-display-glyph{fill:var(--skra-comp-star-rating-star-color-filled)}.skra-rating-display-glyph{flex:0 0 auto;inline-size:var(--_star-size);block-size:var(--_star-size)}:host([data-skra-fill=\"0\"]) .skra-rating-display-fill{inline-size:0%}:host([data-skra-fill=\"5\"]) .skra-rating-display-fill{inline-size:5%}:host([data-skra-fill=\"10\"]) .skra-rating-display-fill{inline-size:10%}:host([data-skra-fill=\"15\"]) .skra-rating-display-fill{inline-size:15%}:host([data-skra-fill=\"20\"]) .skra-rating-display-fill{inline-size:20%}:host([data-skra-fill=\"25\"]) .skra-rating-display-fill{inline-size:25%}:host([data-skra-fill=\"30\"]) .skra-rating-display-fill{inline-size:30%}:host([data-skra-fill=\"35\"]) .skra-rating-display-fill{inline-size:35%}:host([data-skra-fill=\"40\"]) .skra-rating-display-fill{inline-size:40%}:host([data-skra-fill=\"45\"]) .skra-rating-display-fill{inline-size:45%}:host([data-skra-fill=\"50\"]) .skra-rating-display-fill{inline-size:50%}:host([data-skra-fill=\"55\"]) .skra-rating-display-fill{inline-size:55%}:host([data-skra-fill=\"60\"]) .skra-rating-display-fill{inline-size:60%}:host([data-skra-fill=\"65\"]) .skra-rating-display-fill{inline-size:65%}:host([data-skra-fill=\"70\"]) .skra-rating-display-fill{inline-size:70%}:host([data-skra-fill=\"75\"]) .skra-rating-display-fill{inline-size:75%}:host([data-skra-fill=\"80\"]) .skra-rating-display-fill{inline-size:80%}:host([data-skra-fill=\"85\"]) .skra-rating-display-fill{inline-size:85%}:host([data-skra-fill=\"90\"]) .skra-rating-display-fill{inline-size:90%}:host([data-skra-fill=\"95\"]) .skra-rating-display-fill{inline-size:95%}:host([data-skra-fill=\"100\"]) .skra-rating-display-fill{inline-size:100%}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
67
67
|
}
|
|
68
68
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.1", ngImport: i0, type: SkraRatingDisplay, decorators: [{
|
|
69
69
|
type: Component,
|
|
@@ -72,7 +72,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.1", ngImpor
|
|
|
72
72
|
'[attr.data-skra-fill]': 'fillBucket()',
|
|
73
73
|
// Variant Surface size axis (ADR 035) — reuses the family star-size-* tokens.
|
|
74
74
|
'[attr.data-skra-size]': 'size()',
|
|
75
|
-
}, template: "<!--\
|
|
75
|
+
}, template: "<!--\n Two stacked glyph layers: an empty-stars track that defines the box, and a filled-stars\n overlay clipped to the quantized fill width (the SCSS maps data-skra-fill \u2192 inline-size).\n Both layers are decorative (aria-hidden); the accessible name, when any, is on the host\n (consumer-applied role=\"img\" + aria-label \u2014 ADR 033 Path 3).\n O-67 \u2014 the track draws OUTLINE stars and the fill draws SOLID ones, so filled vs empty is\n carried by shape (WCAG 1.4.1). The outline's outer edge is the solid star's, so a filled\n star covers the ring (bar a faint anti-aliased fringe at the edge \u2014 the pre-O-67 solid\n track did the same); a partial fill shows solid over outline.\n-->\n<div class=\"skra-rating-display-track\" aria-hidden=\"true\">\n @for (star of stars(); track star) {\n <svg\n class=\"skra-rating-display-glyph\"\n viewBox=\"0 0 24 24\"\n focusable=\"false\"\n aria-hidden=\"true\"\n >\n <path\n fill-rule=\"evenodd\"\n d=\"M12 17.27 18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21zM12 15.4l-3.76 2.27 1-4.28-3.32-2.88 4.38-.38L12 6.1l1.71 4.04 4.38.38-3.32 2.88 1 4.28z\"\n />\n </svg>\n }\n</div>\n<div class=\"skra-rating-display-fill\" aria-hidden=\"true\">\n @for (star of stars(); track star) {\n <svg\n class=\"skra-rating-display-glyph\"\n viewBox=\"0 0 24 24\"\n focusable=\"false\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M12 17.27 18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z\"\n />\n </svg>\n }\n</div>\n", styles: [":host{margin:0;position:relative;display:inline-flex;line-height:0;--_star-size: var(--skra-comp-star-rating-star-size-md)}:host([data-skra-size=sm]){--_star-size: var(--skra-comp-star-rating-star-size-sm)}:host([data-skra-size=lg]){--_star-size: var(--skra-comp-star-rating-star-size-lg)}.skra-rating-display-track,.skra-rating-display-fill{display:flex;gap:0;line-height:0}.skra-rating-display-fill{position:absolute;inset-block-start:0;inset-inline-start:0;block-size:100%;inline-size:0;overflow:hidden}.skra-rating-display-track .skra-rating-display-glyph{fill:var(--skra-comp-star-rating-star-color-empty)}.skra-rating-display-fill .skra-rating-display-glyph{fill:var(--skra-comp-star-rating-star-color-filled)}.skra-rating-display-glyph{flex:0 0 auto;inline-size:var(--_star-size);block-size:var(--_star-size)}:host([data-skra-fill=\"0\"]) .skra-rating-display-fill{inline-size:0%}:host([data-skra-fill=\"5\"]) .skra-rating-display-fill{inline-size:5%}:host([data-skra-fill=\"10\"]) .skra-rating-display-fill{inline-size:10%}:host([data-skra-fill=\"15\"]) .skra-rating-display-fill{inline-size:15%}:host([data-skra-fill=\"20\"]) .skra-rating-display-fill{inline-size:20%}:host([data-skra-fill=\"25\"]) .skra-rating-display-fill{inline-size:25%}:host([data-skra-fill=\"30\"]) .skra-rating-display-fill{inline-size:30%}:host([data-skra-fill=\"35\"]) .skra-rating-display-fill{inline-size:35%}:host([data-skra-fill=\"40\"]) .skra-rating-display-fill{inline-size:40%}:host([data-skra-fill=\"45\"]) .skra-rating-display-fill{inline-size:45%}:host([data-skra-fill=\"50\"]) .skra-rating-display-fill{inline-size:50%}:host([data-skra-fill=\"55\"]) .skra-rating-display-fill{inline-size:55%}:host([data-skra-fill=\"60\"]) .skra-rating-display-fill{inline-size:60%}:host([data-skra-fill=\"65\"]) .skra-rating-display-fill{inline-size:65%}:host([data-skra-fill=\"70\"]) .skra-rating-display-fill{inline-size:70%}:host([data-skra-fill=\"75\"]) .skra-rating-display-fill{inline-size:75%}:host([data-skra-fill=\"80\"]) .skra-rating-display-fill{inline-size:80%}:host([data-skra-fill=\"85\"]) .skra-rating-display-fill{inline-size:85%}:host([data-skra-fill=\"90\"]) .skra-rating-display-fill{inline-size:90%}:host([data-skra-fill=\"95\"]) .skra-rating-display-fill{inline-size:95%}:host([data-skra-fill=\"100\"]) .skra-rating-display-fill{inline-size:100%}\n"] }]
|
|
76
76
|
}], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }] } });
|
|
77
77
|
|
|
78
78
|
// Static fractional rating display — read-only counterpart to skra-star-rating
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"skra-ui-rating-display.mjs","sources":["../../../../libs/ui/rating-display/src/lib/rating-display/rating-display.ts","../../../../libs/ui/rating-display/src/lib/rating-display/rating-display.html","../../../../libs/ui/rating-display/src/index.ts","../../../../libs/ui/rating-display/src/skra-ui-rating-display.ts"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n ViewEncapsulation,\n computed,\n input,\n} from '@angular/core';\n\nexport type RatingDisplaySize = 'sm' | 'md' | 'lg';\n\n/**\n * `<skra-rating-display>` — a static, non-interactive **fractional** rating display\n * (\"4.3 out of 5 stars\"). The read-only counterpart to `skra-star-rating` (the\n * interactive whole-star INPUT in `libs/ui/forms`); a SEPARATE atom by atom-granularity\n * (opposite a11y contract `role=\"img\"` vs `radiogroup`; float vs int; no interaction).\n * Third resident of `libs/ui/data-display`, beside Avatar + StatusDot. See\n * `adr/specs/component-rating-display-spec.md`.\n *\n * **Element, not a decorated native (ADR 012):** there is no native rating element, like\n * StatusDot/Avatar. NOT a Web Component (ADR 000).\n *\n * **Value consumer-owned (ADR 037):** `value` is a plain `input<number>()` (float). The atom\n * owns no value, no `ControlValueAccessor`, no `@angular/forms`.\n *\n * **Fractional fill is CSP-clean (ADR 021):** the fill is QUANTIZED to a `data-skra-fill`\n * bucket reflected on the host (`{0,5,…,100}`); the SCSS maps each bucket to the fill-overlay\n * width. Zero `[style.--var]`, zero inline style — a bounded value is quantizable, so no\n * geometry exception is taken. State Surface all-N/A (non-focusable display; Badge/StatusDot\n * precedent).\n *\n * **Accessible name (ADR 033 Path 3 — the StatusDot/Avatar contract):** the host carries NO\n * default ARIA. The consumer declares semantics:\n * - decorative (Mode A, beside a visible value like \"4.3\") → `aria-hidden=\"true\"`;\n * - meaningful (Mode B, the stars are the sole carrier) → `role=\"img\"` + `aria-label`\n * TOGETHER (e.g. `aria-label=\"Rated 4.3 out of 5\"`).\n * The inner star glyphs are always decorative. NO `ariaLabel` input (boundary #8) — the label\n * is consumer-owned + localizable (the agnostic doctrine; Forge does not hard-code \"out of N\").\n *\n * **Look is skin-owned:** glyph colours + sizes come from the shared family surface\n * (`comp/star-rating.json`). NO `intent`/`emphasis`/`color`/`variant` look-inputs.\n */\n@Component({\n selector: 'skra-rating-display',\n standalone: true,\n templateUrl: './rating-display.html',\n styleUrl: './rating-display.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n encapsulation: ViewEncapsulation.Emulated,\n host: {\n // Quantized fractional fill (ADR 021 — data-attr, never [style.--var]).\n '[attr.data-skra-fill]': 'fillBucket()',\n // Variant Surface size axis (ADR 035) — reuses the family star-size-* tokens.\n '[attr.data-skra-size]': 'size()',\n },\n})\nexport class SkraRatingDisplay {\n /** The rating to display — FLOAT (e.g. 4.3). Consumer-owned (ADR 037); clamped to [0, max]. */\n readonly value = input<number>(0);\n /** Number of stars (semantic count). */\n readonly max = input<number>(5);\n /** ADR 035 size axis — the only variant axis. */\n readonly size = input<RatingDisplaySize>('md');\n\n /** Star slots [1..max] — both layers render the same count. */\n readonly stars = computed(() => {\n const n = Math.max(0, Math.floor(this.max()));\n return Array.from({ length: n }, (_, i) => i + 1);\n });\n\n /**\n * Fill percentage quantized to 5% buckets (`{0,5,…,100}`) — reflected as `data-skra-fill`,\n * mapped to the overlay width by the SCSS. 4.3/5 → 86% → bucket 85 (visually exact-enough).\n * A bounded, quantizable value is exactly what ADR 021 reflection is for — no style exception.\n */\n readonly fillBucket = computed(() => {\n const max = this.max();\n if (max <= 0) return 0;\n const ratio = this.value() / max;\n const clamped = Math.min(1, Math.max(0, ratio));\n return Math.round(clamped * 20) * 5;\n });\n}\n","<!--\
|
|
1
|
+
{"version":3,"file":"skra-ui-rating-display.mjs","sources":["../../../../libs/ui/rating-display/src/lib/rating-display/rating-display.ts","../../../../libs/ui/rating-display/src/lib/rating-display/rating-display.html","../../../../libs/ui/rating-display/src/index.ts","../../../../libs/ui/rating-display/src/skra-ui-rating-display.ts"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n ViewEncapsulation,\n computed,\n input,\n} from '@angular/core';\n\nexport type RatingDisplaySize = 'sm' | 'md' | 'lg';\n\n/**\n * `<skra-rating-display>` — a static, non-interactive **fractional** rating display\n * (\"4.3 out of 5 stars\"). The read-only counterpart to `skra-star-rating` (the\n * interactive whole-star INPUT in `libs/ui/forms`); a SEPARATE atom by atom-granularity\n * (opposite a11y contract `role=\"img\"` vs `radiogroup`; float vs int; no interaction).\n * Third resident of `libs/ui/data-display`, beside Avatar + StatusDot. See\n * `adr/specs/component-rating-display-spec.md`.\n *\n * **Element, not a decorated native (ADR 012):** there is no native rating element, like\n * StatusDot/Avatar. NOT a Web Component (ADR 000).\n *\n * **Value consumer-owned (ADR 037):** `value` is a plain `input<number>()` (float). The atom\n * owns no value, no `ControlValueAccessor`, no `@angular/forms`.\n *\n * **Fractional fill is CSP-clean (ADR 021):** the fill is QUANTIZED to a `data-skra-fill`\n * bucket reflected on the host (`{0,5,…,100}`); the SCSS maps each bucket to the fill-overlay\n * width. Zero `[style.--var]`, zero inline style — a bounded value is quantizable, so no\n * geometry exception is taken. State Surface all-N/A (non-focusable display; Badge/StatusDot\n * precedent).\n *\n * **Accessible name (ADR 033 Path 3 — the StatusDot/Avatar contract):** the host carries NO\n * default ARIA. The consumer declares semantics:\n * - decorative (Mode A, beside a visible value like \"4.3\") → `aria-hidden=\"true\"`;\n * - meaningful (Mode B, the stars are the sole carrier) → `role=\"img\"` + `aria-label`\n * TOGETHER (e.g. `aria-label=\"Rated 4.3 out of 5\"`).\n * The inner star glyphs are always decorative. NO `ariaLabel` input (boundary #8) — the label\n * is consumer-owned + localizable (the agnostic doctrine; Forge does not hard-code \"out of N\").\n *\n * **Look is skin-owned:** glyph colours + sizes come from the shared family surface\n * (`comp/star-rating.json`). NO `intent`/`emphasis`/`color`/`variant` look-inputs.\n */\n@Component({\n selector: 'skra-rating-display',\n standalone: true,\n templateUrl: './rating-display.html',\n styleUrl: './rating-display.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n encapsulation: ViewEncapsulation.Emulated,\n host: {\n // Quantized fractional fill (ADR 021 — data-attr, never [style.--var]).\n '[attr.data-skra-fill]': 'fillBucket()',\n // Variant Surface size axis (ADR 035) — reuses the family star-size-* tokens.\n '[attr.data-skra-size]': 'size()',\n },\n})\nexport class SkraRatingDisplay {\n /** The rating to display — FLOAT (e.g. 4.3). Consumer-owned (ADR 037); clamped to [0, max]. */\n readonly value = input<number>(0);\n /** Number of stars (semantic count). */\n readonly max = input<number>(5);\n /** ADR 035 size axis — the only variant axis. */\n readonly size = input<RatingDisplaySize>('md');\n\n /** Star slots [1..max] — both layers render the same count. */\n readonly stars = computed(() => {\n const n = Math.max(0, Math.floor(this.max()));\n return Array.from({ length: n }, (_, i) => i + 1);\n });\n\n /**\n * Fill percentage quantized to 5% buckets (`{0,5,…,100}`) — reflected as `data-skra-fill`,\n * mapped to the overlay width by the SCSS. 4.3/5 → 86% → bucket 85 (visually exact-enough).\n * A bounded, quantizable value is exactly what ADR 021 reflection is for — no style exception.\n */\n readonly fillBucket = computed(() => {\n const max = this.max();\n if (max <= 0) return 0;\n const ratio = this.value() / max;\n const clamped = Math.min(1, Math.max(0, ratio));\n return Math.round(clamped * 20) * 5;\n });\n}\n","<!--\n Two stacked glyph layers: an empty-stars track that defines the box, and a filled-stars\n overlay clipped to the quantized fill width (the SCSS maps data-skra-fill → inline-size).\n Both layers are decorative (aria-hidden); the accessible name, when any, is on the host\n (consumer-applied role=\"img\" + aria-label — ADR 033 Path 3).\n O-67 — the track draws OUTLINE stars and the fill draws SOLID ones, so filled vs empty is\n carried by shape (WCAG 1.4.1). The outline's outer edge is the solid star's, so a filled\n star covers the ring (bar a faint anti-aliased fringe at the edge — the pre-O-67 solid\n track did the same); a partial fill shows solid over outline.\n-->\n<div class=\"skra-rating-display-track\" aria-hidden=\"true\">\n @for (star of stars(); track star) {\n <svg\n class=\"skra-rating-display-glyph\"\n viewBox=\"0 0 24 24\"\n focusable=\"false\"\n aria-hidden=\"true\"\n >\n <path\n fill-rule=\"evenodd\"\n d=\"M12 17.27 18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21zM12 15.4l-3.76 2.27 1-4.28-3.32-2.88 4.38-.38L12 6.1l1.71 4.04 4.38.38-3.32 2.88 1 4.28z\"\n />\n </svg>\n }\n</div>\n<div class=\"skra-rating-display-fill\" aria-hidden=\"true\">\n @for (star of stars(); track star) {\n <svg\n class=\"skra-rating-display-glyph\"\n viewBox=\"0 0 24 24\"\n focusable=\"false\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M12 17.27 18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z\"\n />\n </svg>\n }\n</div>\n","// Static fractional rating display — read-only counterpart to skra-star-rating\n// (@skra/ui/star-rating); shares the family glyph surface (comp/star-rating.json).\nexport * from './lib/rating-display/rating-display';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;AAUA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BG;MAeU,iBAAiB,CAAA;;IAEnB,KAAK,GAAG,KAAK,CAAS,CAAC;8EAAC;;IAExB,GAAG,GAAG,KAAK,CAAS,CAAC;4EAAC;;IAEtB,IAAI,GAAG,KAAK,CAAoB,IAAI;6EAAC;;AAGrC,IAAA,KAAK,GAAG,QAAQ,CAAC,MAAK;AAC7B,QAAA,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;QAC7C,OAAO,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACnD,CAAC;8EAAC;AAEF;;;;AAIG;AACM,IAAA,UAAU,GAAG,QAAQ,CAAC,MAAK;AAClC,QAAA,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE;QACtB,IAAI,GAAG,IAAI,CAAC;AAAE,YAAA,OAAO,CAAC;QACtB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,EAAE,GAAG,GAAG;AAChC,QAAA,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;QAC/C,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,EAAE,CAAC,GAAG,CAAC;IACrC,CAAC;mFAAC;uGAzBS,iBAAiB,EAAA,IAAA,EAAA,EAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA;AAAjB,IAAA,OAAA,IAAA,GAAA,EAAA,CAAA,oBAAA,CAAA,EAAA,UAAA,EAAA,QAAA,EAAA,OAAA,EAAA,QAAA,EAAA,IAAA,EAAA,iBAAiB,4hBCvD9B,sjDAuCA,EAAA,MAAA,EAAA,CAAA,+yEAAA,CAAA,EAAA,eAAA,EAAA,EAAA,CAAA,uBAAA,CAAA,MAAA,EAAA,CAAA;;2FDgBa,iBAAiB,EAAA,UAAA,EAAA,CAAA;kBAd7B,SAAS;+BACE,qBAAqB,EAAA,UAAA,EACnB,IAAI,EAAA,eAAA,EAGC,uBAAuB,CAAC,MAAM,EAAA,aAAA,EAChC,iBAAiB,CAAC,QAAQ,EAAA,IAAA,EACnC;;AAEJ,wBAAA,uBAAuB,EAAE,cAAc;;AAEvC,wBAAA,uBAAuB,EAAE,QAAQ;AAClC,qBAAA,EAAA,QAAA,EAAA,sjDAAA,EAAA,MAAA,EAAA,CAAA,+yEAAA,CAAA,EAAA;;;AErDH;AACA;;ACDA;;AAEG;;;;"}
|
|
@@ -85,7 +85,7 @@ class SkraStarRating {
|
|
|
85
85
|
event.preventDefault();
|
|
86
86
|
}
|
|
87
87
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.1", ngImport: i0, type: SkraStarRating, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
88
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.1", type: SkraStarRating, isStandalone: true, selector: "skra-star-rating", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "valueChange" }, host: { attributes: { "role": "radiogroup" }, listeners: { "click": "blockWhenReadonly($event)", "keydown": "blockKeyWhenReadonly($event)" }, properties: { "attr.aria-labelledby": "context?.labelId() ?? null", "attr.aria-required": "context?.isRequired() ? \"true\" : null", "attr.aria-invalid": "context?.hasError() ? \"true\" : null", "attr.aria-readonly": "context?.isReadonly() ? \"true\" : null", "attr.aria-describedby": "context?.describedByIds() ?? null", "attr.data-skra-disabled": "isDisabled() ? \"\" : null", "attr.data-skra-size": "size()", "attr.id": "context?.fieldId() ?? null" } }, ngImport: i0, template: "@for (star of stars(); track star) {\
|
|
88
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.1", type: SkraStarRating, isStandalone: true, selector: "skra-star-rating", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "valueChange" }, host: { attributes: { "role": "radiogroup" }, listeners: { "click": "blockWhenReadonly($event)", "keydown": "blockKeyWhenReadonly($event)" }, properties: { "attr.aria-labelledby": "context?.labelId() ?? null", "attr.aria-required": "context?.isRequired() ? \"true\" : null", "attr.aria-invalid": "context?.hasError() ? \"true\" : null", "attr.aria-readonly": "context?.isReadonly() ? \"true\" : null", "attr.aria-describedby": "context?.describedByIds() ?? null", "attr.data-skra-disabled": "isDisabled() ? \"\" : null", "attr.data-skra-size": "size()", "attr.id": "context?.fieldId() ?? null" } }, ngImport: i0, template: "@for (star of stars(); track star) {\n<input\n type=\"radio\"\n class=\"skra-star-rating-input\"\n [name]=\"groupName()\"\n [id]=\"groupName() + '-' + star\"\n [value]=\"star\"\n [checked]=\"value() === star\"\n [disabled]=\"isDisabled()\"\n [attr.aria-label]=\"starLabel(star)\"\n (change)=\"onSelect(star)\"\n/>\n<label\n class=\"skra-star-rating-star\"\n [attr.for]=\"groupName() + '-' + star\"\n aria-hidden=\"true\"\n>\n <svg\n class=\"skra-star-rating-glyph\"\n viewBox=\"0 0 24 24\"\n focusable=\"false\"\n aria-hidden=\"true\"\n >\n <!-- O-67 \u2014 the empty/filled difference is carried by SHAPE (WCAG 1.4.1): an outline\n ring (same outer edge as the solid star, inner hole via evenodd) is always drawn;\n the solid star is painted over it only in the filled state. -->\n <path\n class=\"skra-star-rating-outline\"\n fill-rule=\"evenodd\"\n d=\"M12 17.27 18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21zM12 15.4l-3.76 2.27 1-4.28-3.32-2.88 4.38-.38L12 6.1l1.71 4.04 4.38.38-3.32 2.88 1 4.28z\"\n />\n <path\n class=\"skra-star-rating-solid\"\n d=\"M12 17.27 18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z\"\n />\n </svg>\n</label>\n}\n", styles: [":host{margin:0;display:inline-flex;flex-direction:row-reverse;align-items:center;gap:var(--skra-comp-star-rating-gap);--_star-size: var(--skra-comp-star-rating-star-size-md)}:host([data-skra-size=sm]){--_star-size: var(--skra-comp-star-rating-star-size-sm)}:host([data-skra-size=md]){--_star-size: var(--skra-comp-star-rating-star-size-md)}:host([data-skra-size=lg]){--_star-size: var(--skra-comp-star-rating-star-size-lg)}:host([data-skra-disabled]){opacity:var(--skra-sys-opacity-disabled);cursor:not-allowed}.skra-star-rating-input{position:absolute;inline-size:1px;block-size:1px;margin:0;padding:0;opacity:0;pointer-events:none}.skra-star-rating-star{--_outline: var(--skra-comp-star-rating-star-color-empty);--_fill: transparent;display:inline-flex;cursor:pointer;line-height:0;border-radius:.125rem}.skra-star-rating-glyph{inline-size:var(--_star-size);block-size:var(--_star-size)}.skra-star-rating-outline,.skra-star-rating-solid{transition:fill var(--skra-comp-star-rating-motion-duration) var(--skra-comp-star-rating-motion-easing)}.skra-star-rating-outline{fill:var(--_outline)}.skra-star-rating-solid{fill:var(--_fill)}.skra-star-rating-input:checked~.skra-star-rating-star{--_outline: var(--skra-comp-star-rating-star-color-filled);--_fill: var(--skra-comp-star-rating-star-color-filled)}@media(hover:hover)and (pointer:fine){:host(:hover:not([aria-readonly=true]):not([data-skra-disabled])) .skra-star-rating-star{--_outline: var(--skra-comp-star-rating-star-color-empty);--_fill: transparent}:host(:hover:not([aria-readonly=true]):not([data-skra-disabled])) .skra-star-rating-star:hover,:host(:hover:not([aria-readonly=true]):not([data-skra-disabled])) .skra-star-rating-star:hover~.skra-star-rating-star{--_outline: var(--skra-comp-star-rating-star-color-filled);--_fill: var(--skra-comp-star-rating-star-color-filled)}}:host([aria-readonly=true]) .skra-star-rating-star{cursor:default}.skra-star-rating-input:focus-visible+.skra-star-rating-star{outline:var(--skra-comp-star-rating-focus-ring-width) solid var(--skra-comp-star-rating-focus-ring-color);outline-offset:.125rem}@media(prefers-reduced-motion:reduce){.skra-star-rating-outline,.skra-star-rating-solid{transition:none}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
89
89
|
}
|
|
90
90
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.1", ngImport: i0, type: SkraStarRating, decorators: [{
|
|
91
91
|
type: Component,
|
|
@@ -104,7 +104,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.1", ngImpor
|
|
|
104
104
|
// Blocks the toggle from pointer (click) and keyboard (arrows fire change directly).
|
|
105
105
|
'(click)': 'blockWhenReadonly($event)',
|
|
106
106
|
'(keydown)': 'blockKeyWhenReadonly($event)',
|
|
107
|
-
}, template: "@for (star of stars(); track star) {\
|
|
107
|
+
}, template: "@for (star of stars(); track star) {\n<input\n type=\"radio\"\n class=\"skra-star-rating-input\"\n [name]=\"groupName()\"\n [id]=\"groupName() + '-' + star\"\n [value]=\"star\"\n [checked]=\"value() === star\"\n [disabled]=\"isDisabled()\"\n [attr.aria-label]=\"starLabel(star)\"\n (change)=\"onSelect(star)\"\n/>\n<label\n class=\"skra-star-rating-star\"\n [attr.for]=\"groupName() + '-' + star\"\n aria-hidden=\"true\"\n>\n <svg\n class=\"skra-star-rating-glyph\"\n viewBox=\"0 0 24 24\"\n focusable=\"false\"\n aria-hidden=\"true\"\n >\n <!-- O-67 \u2014 the empty/filled difference is carried by SHAPE (WCAG 1.4.1): an outline\n ring (same outer edge as the solid star, inner hole via evenodd) is always drawn;\n the solid star is painted over it only in the filled state. -->\n <path\n class=\"skra-star-rating-outline\"\n fill-rule=\"evenodd\"\n d=\"M12 17.27 18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21zM12 15.4l-3.76 2.27 1-4.28-3.32-2.88 4.38-.38L12 6.1l1.71 4.04 4.38.38-3.32 2.88 1 4.28z\"\n />\n <path\n class=\"skra-star-rating-solid\"\n d=\"M12 17.27 18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z\"\n />\n </svg>\n</label>\n}\n", styles: [":host{margin:0;display:inline-flex;flex-direction:row-reverse;align-items:center;gap:var(--skra-comp-star-rating-gap);--_star-size: var(--skra-comp-star-rating-star-size-md)}:host([data-skra-size=sm]){--_star-size: var(--skra-comp-star-rating-star-size-sm)}:host([data-skra-size=md]){--_star-size: var(--skra-comp-star-rating-star-size-md)}:host([data-skra-size=lg]){--_star-size: var(--skra-comp-star-rating-star-size-lg)}:host([data-skra-disabled]){opacity:var(--skra-sys-opacity-disabled);cursor:not-allowed}.skra-star-rating-input{position:absolute;inline-size:1px;block-size:1px;margin:0;padding:0;opacity:0;pointer-events:none}.skra-star-rating-star{--_outline: var(--skra-comp-star-rating-star-color-empty);--_fill: transparent;display:inline-flex;cursor:pointer;line-height:0;border-radius:.125rem}.skra-star-rating-glyph{inline-size:var(--_star-size);block-size:var(--_star-size)}.skra-star-rating-outline,.skra-star-rating-solid{transition:fill var(--skra-comp-star-rating-motion-duration) var(--skra-comp-star-rating-motion-easing)}.skra-star-rating-outline{fill:var(--_outline)}.skra-star-rating-solid{fill:var(--_fill)}.skra-star-rating-input:checked~.skra-star-rating-star{--_outline: var(--skra-comp-star-rating-star-color-filled);--_fill: var(--skra-comp-star-rating-star-color-filled)}@media(hover:hover)and (pointer:fine){:host(:hover:not([aria-readonly=true]):not([data-skra-disabled])) .skra-star-rating-star{--_outline: var(--skra-comp-star-rating-star-color-empty);--_fill: transparent}:host(:hover:not([aria-readonly=true]):not([data-skra-disabled])) .skra-star-rating-star:hover,:host(:hover:not([aria-readonly=true]):not([data-skra-disabled])) .skra-star-rating-star:hover~.skra-star-rating-star{--_outline: var(--skra-comp-star-rating-star-color-filled);--_fill: var(--skra-comp-star-rating-star-color-filled)}}:host([aria-readonly=true]) .skra-star-rating-star{cursor:default}.skra-star-rating-input:focus-visible+.skra-star-rating-star{outline:var(--skra-comp-star-rating-focus-ring-width) solid var(--skra-comp-star-rating-focus-ring-color);outline-offset:.125rem}@media(prefers-reduced-motion:reduce){.skra-star-rating-outline,.skra-star-rating-solid{transition:none}}\n"] }]
|
|
108
108
|
}], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], valueChange: [{ type: i0.Output, args: ["valueChange"] }] } });
|
|
109
109
|
|
|
110
110
|
// APG-radiogroup composite rendered as stars (ADR 039 §7).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"skra-ui-star-rating.mjs","sources":["../../../../libs/ui/star-rating/src/lib/star-rating/star-rating.ts","../../../../libs/ui/star-rating/src/lib/star-rating/star-rating.html","../../../../libs/ui/star-rating/src/index.ts","../../../../libs/ui/star-rating/src/skra-ui-star-rating.ts"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n ViewEncapsulation,\n computed,\n inject,\n input,\n output,\n} from '@angular/core';\nimport { SkraFieldContext } from '@skra/ui/field';\nimport { IdGeneratorService } from '@skra/ui/field';\n\nexport type StarRatingSize = 'sm' | 'md' | 'lg';\n\n/**\n * `<skra-star-rating>` — a whole-star rating **input**. An APG Rating composite\n * (`role=\"radiogroup\"`) rendered as stars: internally N `(input[type=radio] + label)`\n * pairs sharing one `name`, where the label IS the star glyph (inline SVG) and the\n * input is sr-only but focusable.\n *\n * Governed by the **ADR 039 §7 note (2026-06-24)**: a composite widget MAY render\n * internal native radios — this is NOT the §3.3-forbidden `<skra-radio-group>` —\n * because (1) value is consumer-owned (`[value]` / `(valueChange)`, controlled-only\n * per ADR 037, the Tabs `[selected]` precedent — no internal value-of-record signal),\n * (2) there is no `SkraRadioGroupContext`, (3) it is a distinct APG composite (like\n * `skra-select`), not a generic radio-group wrapper. Custom element justified per\n * ADR 012 — there is no native single host for a rating.\n *\n * <skra-field [hasError]=\"!form.rating()\">\n * <label skraLabelFor>Rate your experience</label>\n * <skra-star-rating [value]=\"form.rating()\" (valueChange)=\"form.rating.set($event)\" />\n * <skra-error>Please pick a rating</skra-error>\n * </skra-field>\n *\n * Keyboard is the **native radio group** (arrows move + select, wrap, single tab stop)\n * — ZERO CDK. The fill + hover-preview are **pure CSS** (`:checked`/`:hover` sibling\n * combinators) — CSP-clean (ADR 021), no `[style.--var]`, no TS for the preview. The\n * committed `aria-checked` is the only thing AT observes; hover-preview is visual-only.\n *\n * Optional `SkraFieldContext` (ADR 013): the radiogroup is named via `aria-labelledby`\n * → the field label (`SkraFieldContext.labelId`, since a radiogroup is not labelable by\n * `for`/`id`); `disabled`/`required`/`readonly`/`invalid`/`describedby` cascade. Value is\n * NEVER in the context (ADR 037). The `readonly` state shows a committed whole-star rating\n * without allowing change (\"show my 4★\").\n *\n * Whole-star only (half-step input deferred). Fractional aggregate display (\"4.3★\") is the\n * separate `skra-rating-display` atom (`libs/ui/data-display`).\n */\n@Component({\n selector: 'skra-star-rating',\n standalone: true,\n templateUrl: './star-rating.html',\n styleUrl: './star-rating.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n encapsulation: ViewEncapsulation.Emulated,\n host: {\n role: 'radiogroup',\n // Named by the field label (Path 1) — radiogroups are not labelable by for/id.\n '[attr.aria-labelledby]': 'context?.labelId() ?? null',\n '[attr.aria-required]': 'context?.isRequired() ? \"true\" : null',\n '[attr.aria-invalid]': 'context?.hasError() ? \"true\" : null',\n '[attr.aria-readonly]': 'context?.isReadonly() ? \"true\" : null',\n '[attr.aria-describedby]': 'context?.describedByIds() ?? null',\n '[attr.data-skra-disabled]': 'isDisabled() ? \"\" : null',\n '[attr.data-skra-size]': 'size()',\n '[attr.id]': 'context?.fieldId() ?? null',\n // Readonly enforcement — without it the announced aria-readonly would be a lie.\n // Blocks the toggle from pointer (click) and keyboard (arrows fire change directly).\n '(click)': 'blockWhenReadonly($event)',\n '(keydown)': 'blockKeyWhenReadonly($event)',\n },\n})\nexport class SkraStarRating {\n private readonly idGen = inject(IdGeneratorService);\n readonly context = inject(SkraFieldContext, { optional: true });\n\n /** Committed rating; 0 (or out of [1,max]) = unrated. Consumer-owned, controlled (ADR 037). */\n readonly value = input<number>(0);\n /** Number of stars. Semantic count, not a look-input. */\n readonly max = input<number>(5);\n /** ADR 035 size axis — the only variant axis. */\n readonly size = input<StarRatingSize>('md');\n /** Fires on USER selection only (never on programmatic [value] changes). */\n readonly valueChange = output<number>();\n\n private readonly fallbackName = this.idGen.nextId('rating');\n /** Unique radio-group name so multiple ratings on a page don't merge. */\n readonly groupName = computed(\n () => this.context?.fieldId() || this.fallbackName,\n );\n\n readonly isDisabled = computed(() => this.context?.isDisabled() ?? false);\n readonly isReadonly = computed(() => this.context?.isReadonly() ?? false);\n\n /**\n * Star values HIGH→LOW (`[max, …, 1]`) so the CSS sibling combinator\n * `input:checked ~ label` fills the checked star and every lower one. The visual\n * order is restored left-to-right via `flex-direction: row-reverse` in the SCSS.\n */\n readonly stars = computed(() => {\n const n = Math.max(0, Math.floor(this.max()));\n return Array.from({ length: n }, (_, i) => n - i);\n });\n\n protected starLabel(star: number): string {\n return star === 1 ? '1 star' : `${star} stars`;\n }\n\n protected onSelect(star: number): void {\n // Reached only when not readonly (click/keydown are blocked below when readonly),\n // and never on programmatic [value] changes — so this is user-intent only.\n this.valueChange.emit(star);\n }\n\n protected blockWhenReadonly(event: Event): void {\n if (this.isReadonly()) event.preventDefault();\n }\n\n protected blockKeyWhenReadonly(event: KeyboardEvent): void {\n if (this.isReadonly() && event.key !== 'Tab') event.preventDefault();\n }\n}\n","@for (star of stars(); track star) {\
|
|
1
|
+
{"version":3,"file":"skra-ui-star-rating.mjs","sources":["../../../../libs/ui/star-rating/src/lib/star-rating/star-rating.ts","../../../../libs/ui/star-rating/src/lib/star-rating/star-rating.html","../../../../libs/ui/star-rating/src/index.ts","../../../../libs/ui/star-rating/src/skra-ui-star-rating.ts"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n ViewEncapsulation,\n computed,\n inject,\n input,\n output,\n} from '@angular/core';\nimport { SkraFieldContext } from '@skra/ui/field';\nimport { IdGeneratorService } from '@skra/ui/field';\n\nexport type StarRatingSize = 'sm' | 'md' | 'lg';\n\n/**\n * `<skra-star-rating>` — a whole-star rating **input**. An APG Rating composite\n * (`role=\"radiogroup\"`) rendered as stars: internally N `(input[type=radio] + label)`\n * pairs sharing one `name`, where the label IS the star glyph (inline SVG) and the\n * input is sr-only but focusable.\n *\n * Governed by the **ADR 039 §7 note (2026-06-24)**: a composite widget MAY render\n * internal native radios — this is NOT the §3.3-forbidden `<skra-radio-group>` —\n * because (1) value is consumer-owned (`[value]` / `(valueChange)`, controlled-only\n * per ADR 037, the Tabs `[selected]` precedent — no internal value-of-record signal),\n * (2) there is no `SkraRadioGroupContext`, (3) it is a distinct APG composite (like\n * `skra-select`), not a generic radio-group wrapper. Custom element justified per\n * ADR 012 — there is no native single host for a rating.\n *\n * <skra-field [hasError]=\"!form.rating()\">\n * <label skraLabelFor>Rate your experience</label>\n * <skra-star-rating [value]=\"form.rating()\" (valueChange)=\"form.rating.set($event)\" />\n * <skra-error>Please pick a rating</skra-error>\n * </skra-field>\n *\n * Keyboard is the **native radio group** (arrows move + select, wrap, single tab stop)\n * — ZERO CDK. The fill + hover-preview are **pure CSS** (`:checked`/`:hover` sibling\n * combinators) — CSP-clean (ADR 021), no `[style.--var]`, no TS for the preview. The\n * committed `aria-checked` is the only thing AT observes; hover-preview is visual-only.\n *\n * Optional `SkraFieldContext` (ADR 013): the radiogroup is named via `aria-labelledby`\n * → the field label (`SkraFieldContext.labelId`, since a radiogroup is not labelable by\n * `for`/`id`); `disabled`/`required`/`readonly`/`invalid`/`describedby` cascade. Value is\n * NEVER in the context (ADR 037). The `readonly` state shows a committed whole-star rating\n * without allowing change (\"show my 4★\").\n *\n * Whole-star only (half-step input deferred). Fractional aggregate display (\"4.3★\") is the\n * separate `skra-rating-display` atom (`libs/ui/data-display`).\n */\n@Component({\n selector: 'skra-star-rating',\n standalone: true,\n templateUrl: './star-rating.html',\n styleUrl: './star-rating.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n encapsulation: ViewEncapsulation.Emulated,\n host: {\n role: 'radiogroup',\n // Named by the field label (Path 1) — radiogroups are not labelable by for/id.\n '[attr.aria-labelledby]': 'context?.labelId() ?? null',\n '[attr.aria-required]': 'context?.isRequired() ? \"true\" : null',\n '[attr.aria-invalid]': 'context?.hasError() ? \"true\" : null',\n '[attr.aria-readonly]': 'context?.isReadonly() ? \"true\" : null',\n '[attr.aria-describedby]': 'context?.describedByIds() ?? null',\n '[attr.data-skra-disabled]': 'isDisabled() ? \"\" : null',\n '[attr.data-skra-size]': 'size()',\n '[attr.id]': 'context?.fieldId() ?? null',\n // Readonly enforcement — without it the announced aria-readonly would be a lie.\n // Blocks the toggle from pointer (click) and keyboard (arrows fire change directly).\n '(click)': 'blockWhenReadonly($event)',\n '(keydown)': 'blockKeyWhenReadonly($event)',\n },\n})\nexport class SkraStarRating {\n private readonly idGen = inject(IdGeneratorService);\n readonly context = inject(SkraFieldContext, { optional: true });\n\n /** Committed rating; 0 (or out of [1,max]) = unrated. Consumer-owned, controlled (ADR 037). */\n readonly value = input<number>(0);\n /** Number of stars. Semantic count, not a look-input. */\n readonly max = input<number>(5);\n /** ADR 035 size axis — the only variant axis. */\n readonly size = input<StarRatingSize>('md');\n /** Fires on USER selection only (never on programmatic [value] changes). */\n readonly valueChange = output<number>();\n\n private readonly fallbackName = this.idGen.nextId('rating');\n /** Unique radio-group name so multiple ratings on a page don't merge. */\n readonly groupName = computed(\n () => this.context?.fieldId() || this.fallbackName,\n );\n\n readonly isDisabled = computed(() => this.context?.isDisabled() ?? false);\n readonly isReadonly = computed(() => this.context?.isReadonly() ?? false);\n\n /**\n * Star values HIGH→LOW (`[max, …, 1]`) so the CSS sibling combinator\n * `input:checked ~ label` fills the checked star and every lower one. The visual\n * order is restored left-to-right via `flex-direction: row-reverse` in the SCSS.\n */\n readonly stars = computed(() => {\n const n = Math.max(0, Math.floor(this.max()));\n return Array.from({ length: n }, (_, i) => n - i);\n });\n\n protected starLabel(star: number): string {\n return star === 1 ? '1 star' : `${star} stars`;\n }\n\n protected onSelect(star: number): void {\n // Reached only when not readonly (click/keydown are blocked below when readonly),\n // and never on programmatic [value] changes — so this is user-intent only.\n this.valueChange.emit(star);\n }\n\n protected blockWhenReadonly(event: Event): void {\n if (this.isReadonly()) event.preventDefault();\n }\n\n protected blockKeyWhenReadonly(event: KeyboardEvent): void {\n if (this.isReadonly() && event.key !== 'Tab') event.preventDefault();\n }\n}\n","@for (star of stars(); track star) {\n<input\n type=\"radio\"\n class=\"skra-star-rating-input\"\n [name]=\"groupName()\"\n [id]=\"groupName() + '-' + star\"\n [value]=\"star\"\n [checked]=\"value() === star\"\n [disabled]=\"isDisabled()\"\n [attr.aria-label]=\"starLabel(star)\"\n (change)=\"onSelect(star)\"\n/>\n<label\n class=\"skra-star-rating-star\"\n [attr.for]=\"groupName() + '-' + star\"\n aria-hidden=\"true\"\n>\n <svg\n class=\"skra-star-rating-glyph\"\n viewBox=\"0 0 24 24\"\n focusable=\"false\"\n aria-hidden=\"true\"\n >\n <!-- O-67 — the empty/filled difference is carried by SHAPE (WCAG 1.4.1): an outline\n ring (same outer edge as the solid star, inner hole via evenodd) is always drawn;\n the solid star is painted over it only in the filled state. -->\n <path\n class=\"skra-star-rating-outline\"\n fill-rule=\"evenodd\"\n d=\"M12 17.27 18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21zM12 15.4l-3.76 2.27 1-4.28-3.32-2.88 4.38-.38L12 6.1l1.71 4.04 4.38.38-3.32 2.88 1 4.28z\"\n />\n <path\n class=\"skra-star-rating-solid\"\n d=\"M12 17.27 18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z\"\n />\n </svg>\n</label>\n}\n","// APG-radiogroup composite rendered as stars (ADR 039 §7).\nexport * from './lib/star-rating/star-rating';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
|
|
@@ -68,7 +68,7 @@ class SkraSwitch {
|
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.1", ngImport: i0, type: SkraSwitch, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
71
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.1.1", type: SkraSwitch, isStandalone: true, selector: "input[type=\"checkbox\"][skraSwitch]", host: { attributes: { "role": "switch" }, listeners: { "click": "blockWhenReadonly($event)", "keydown": "blockKeyWhenReadonly($event)" }, properties: { "attr.data-skra-disabled": "context?.isDisabled() ? \"\" : null", "attr.aria-invalid": "context?.hasError() ? \"true\" : null", "attr.aria-readonly": "context?.isReadonly() ? \"true\" : null", "attr.aria-describedby": "context?.describedByIds() ?? null" } }, ngImport: i0, template: '', isInline: true, styles: [":host{appearance:none;box-sizing:border-box;flex-shrink:0;cursor:pointer;outline:none;inline-size:var(--skra-comp-field-switch-track-inline-size);block-size:var(--skra-comp-field-switch-track-block-size);border-width:var(--skra-comp-field-switch-border-width);border-style:solid;border-color:var(--skra-comp-field-switch-border-color);border-radius:var(--skra-comp-field-switch-track-radius);background-color:var(--skra-comp-field-switch-background);--_switch-thumb: var(--skra-comp-field-switch-thumb-color);background-repeat:no-repeat;background-size:var(--skra-comp-field-switch-thumb-size) var(--skra-comp-field-switch-thumb-size);background-position:left var(--skra-comp-field-switch-thumb-inset) center;background-image:radial-gradient(circle,var(--_switch-thumb) 49%,transparent 50%);transition:background-color var(--skra-comp-field-motion-duration) var(--skra-comp-field-motion-easing),background-position 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),box-shadow var(--skra-comp-field-motion-duration) var(--skra-comp-field-motion-easing)}:host(:checked){--_switch-thumb: var(--skra-comp-field-switch-thumb-color-checked);background-color:var(--skra-comp-field-switch-checked-background);border-color:var(--skra-comp-field-switch-checked-background);background-position:right var(--skra-comp-field-switch-thumb-inset) center}@media(hover:hover)and (pointer:fine){:host(:hover:not(:disabled):not([aria-readonly=true])){background-color:var(--skra-comp-field-switch-background-hover);border-color:var(--skra-comp-field-switch-border-color-hover)}:host(:checked:hover:not(:disabled):not([aria-readonly=true])){background-color:var(--skra-comp-field-switch-checked-background-hover);border-color:var(--skra-comp-field-switch-checked-background-hover)}}:host(:focus-visible){box-shadow:0 0 0 var(--skra-comp-field-focus-ring-width) var(--skra-comp-field-focus-ring-color)}:host(:disabled),:host([data-skra-disabled]){cursor:not-allowed;opacity:var(--skra-sys-opacity-disabled)}:host([aria-readonly=true]){cursor:default;background-color:var(--skra-comp-field-background-readonly);border-color:var(--skra-comp-field-border-color-readonly)}:host(:checked[aria-readonly=true]){background-color:var(--skra-comp-field-switch-checked-background-readonly);border-color:var(--skra-comp-field-switch-checked-background-readonly)}: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 });
|
|
71
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.1.1", type: SkraSwitch, isStandalone: true, selector: "input[type=\"checkbox\"][skraSwitch]", host: { attributes: { "role": "switch" }, listeners: { "click": "blockWhenReadonly($event)", "keydown": "blockKeyWhenReadonly($event)" }, properties: { "attr.data-skra-disabled": "context?.isDisabled() ? \"\" : null", "attr.aria-invalid": "context?.hasError() ? \"true\" : null", "attr.aria-readonly": "context?.isReadonly() ? \"true\" : null", "attr.aria-describedby": "context?.describedByIds() ?? null" } }, ngImport: i0, template: '', isInline: true, styles: [":host{appearance:none;box-sizing:border-box;flex-shrink:0;cursor:pointer;outline:none;inline-size:var(--skra-comp-field-switch-track-inline-size);block-size:var(--skra-comp-field-switch-track-block-size);border-width:var(--skra-comp-field-switch-border-width);border-style:solid;border-color:var(--skra-comp-field-switch-border-color);border-radius:var(--skra-comp-field-switch-track-radius);background-color:var(--skra-comp-field-switch-background);--_switch-thumb: var(--skra-comp-field-switch-thumb-color);background-repeat:no-repeat;background-size:var(--skra-comp-field-switch-thumb-size) var(--skra-comp-field-switch-thumb-size);background-position:left var(--skra-comp-field-switch-thumb-inset) center;background-image:radial-gradient(circle,var(--_switch-thumb) 49%,transparent 50%);transition:background-color var(--skra-comp-field-motion-duration) var(--skra-comp-field-motion-easing),background-position 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),box-shadow var(--skra-comp-field-motion-duration) var(--skra-comp-field-motion-easing)}:host(:checked){--_switch-thumb: var(--skra-comp-field-switch-thumb-color-checked);background-color:var(--skra-comp-field-switch-checked-background);border-color:var(--skra-comp-field-switch-checked-background);background-position:right var(--skra-comp-field-switch-thumb-inset) center}@media(hover:hover)and (pointer:fine){:host(:hover:not(:disabled):not([aria-readonly=true])){background-color:var(--skra-comp-field-switch-background-hover);border-color:var(--skra-comp-field-switch-border-color-hover)}:host(:checked:hover:not(:disabled):not([aria-readonly=true])){background-color:var(--skra-comp-field-switch-checked-background-hover);border-color:var(--skra-comp-field-switch-checked-background-hover)}}:host(:focus-visible){box-shadow:0 0 0 var(--skra-comp-field-focus-ring-width) var(--skra-comp-field-focus-ring-color)}:host(:disabled),:host([data-skra-disabled]){cursor:not-allowed;opacity:var(--skra-sys-opacity-disabled)}:host([aria-readonly=true]){cursor:default;background-color:var(--skra-comp-field-switch-background-readonly);border-color:var(--skra-comp-field-border-color-readonly);--_switch-thumb: var(--skra-comp-field-switch-thumb-color-readonly)}:host(:checked[aria-readonly=true]){background-color:var(--skra-comp-field-switch-checked-background-readonly);border-color:var(--skra-comp-field-switch-checked-background-readonly);--_switch-thumb: var(--skra-comp-field-switch-thumb-color-checked-readonly)}: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 });
|
|
72
72
|
}
|
|
73
73
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.1", ngImport: i0, type: SkraSwitch, decorators: [{
|
|
74
74
|
type: Component,
|
|
@@ -83,7 +83,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.1", ngImpor
|
|
|
83
83
|
'[attr.aria-describedby]': 'context?.describedByIds() ?? null',
|
|
84
84
|
'(click)': 'blockWhenReadonly($event)',
|
|
85
85
|
'(keydown)': 'blockKeyWhenReadonly($event)',
|
|
86
|
-
}, styles: [":host{appearance:none;box-sizing:border-box;flex-shrink:0;cursor:pointer;outline:none;inline-size:var(--skra-comp-field-switch-track-inline-size);block-size:var(--skra-comp-field-switch-track-block-size);border-width:var(--skra-comp-field-switch-border-width);border-style:solid;border-color:var(--skra-comp-field-switch-border-color);border-radius:var(--skra-comp-field-switch-track-radius);background-color:var(--skra-comp-field-switch-background);--_switch-thumb: var(--skra-comp-field-switch-thumb-color);background-repeat:no-repeat;background-size:var(--skra-comp-field-switch-thumb-size) var(--skra-comp-field-switch-thumb-size);background-position:left var(--skra-comp-field-switch-thumb-inset) center;background-image:radial-gradient(circle,var(--_switch-thumb) 49%,transparent 50%);transition:background-color var(--skra-comp-field-motion-duration) var(--skra-comp-field-motion-easing),background-position 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),box-shadow var(--skra-comp-field-motion-duration) var(--skra-comp-field-motion-easing)}:host(:checked){--_switch-thumb: var(--skra-comp-field-switch-thumb-color-checked);background-color:var(--skra-comp-field-switch-checked-background);border-color:var(--skra-comp-field-switch-checked-background);background-position:right var(--skra-comp-field-switch-thumb-inset) center}@media(hover:hover)and (pointer:fine){:host(:hover:not(:disabled):not([aria-readonly=true])){background-color:var(--skra-comp-field-switch-background-hover);border-color:var(--skra-comp-field-switch-border-color-hover)}:host(:checked:hover:not(:disabled):not([aria-readonly=true])){background-color:var(--skra-comp-field-switch-checked-background-hover);border-color:var(--skra-comp-field-switch-checked-background-hover)}}:host(:focus-visible){box-shadow:0 0 0 var(--skra-comp-field-focus-ring-width) var(--skra-comp-field-focus-ring-color)}:host(:disabled),:host([data-skra-disabled]){cursor:not-allowed;opacity:var(--skra-sys-opacity-disabled)}:host([aria-readonly=true]){cursor:default;background-color:var(--skra-comp-field-background-readonly);border-color:var(--skra-comp-field-border-color-readonly)}:host(:checked[aria-readonly=true]){background-color:var(--skra-comp-field-switch-checked-background-readonly);border-color:var(--skra-comp-field-switch-checked-background-readonly)}: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"] }]
|
|
86
|
+
}, styles: [":host{appearance:none;box-sizing:border-box;flex-shrink:0;cursor:pointer;outline:none;inline-size:var(--skra-comp-field-switch-track-inline-size);block-size:var(--skra-comp-field-switch-track-block-size);border-width:var(--skra-comp-field-switch-border-width);border-style:solid;border-color:var(--skra-comp-field-switch-border-color);border-radius:var(--skra-comp-field-switch-track-radius);background-color:var(--skra-comp-field-switch-background);--_switch-thumb: var(--skra-comp-field-switch-thumb-color);background-repeat:no-repeat;background-size:var(--skra-comp-field-switch-thumb-size) var(--skra-comp-field-switch-thumb-size);background-position:left var(--skra-comp-field-switch-thumb-inset) center;background-image:radial-gradient(circle,var(--_switch-thumb) 49%,transparent 50%);transition:background-color var(--skra-comp-field-motion-duration) var(--skra-comp-field-motion-easing),background-position 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),box-shadow var(--skra-comp-field-motion-duration) var(--skra-comp-field-motion-easing)}:host(:checked){--_switch-thumb: var(--skra-comp-field-switch-thumb-color-checked);background-color:var(--skra-comp-field-switch-checked-background);border-color:var(--skra-comp-field-switch-checked-background);background-position:right var(--skra-comp-field-switch-thumb-inset) center}@media(hover:hover)and (pointer:fine){:host(:hover:not(:disabled):not([aria-readonly=true])){background-color:var(--skra-comp-field-switch-background-hover);border-color:var(--skra-comp-field-switch-border-color-hover)}:host(:checked:hover:not(:disabled):not([aria-readonly=true])){background-color:var(--skra-comp-field-switch-checked-background-hover);border-color:var(--skra-comp-field-switch-checked-background-hover)}}:host(:focus-visible){box-shadow:0 0 0 var(--skra-comp-field-focus-ring-width) var(--skra-comp-field-focus-ring-color)}:host(:disabled),:host([data-skra-disabled]){cursor:not-allowed;opacity:var(--skra-sys-opacity-disabled)}:host([aria-readonly=true]){cursor:default;background-color:var(--skra-comp-field-switch-background-readonly);border-color:var(--skra-comp-field-border-color-readonly);--_switch-thumb: var(--skra-comp-field-switch-thumb-color-readonly)}:host(:checked[aria-readonly=true]){background-color:var(--skra-comp-field-switch-checked-background-readonly);border-color:var(--skra-comp-field-switch-checked-background-readonly);--_switch-thumb: var(--skra-comp-field-switch-thumb-color-checked-readonly)}: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"] }]
|
|
87
87
|
}], ctorParameters: () => [] });
|
|
88
88
|
|
|
89
89
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"skra-ui-switch.mjs","sources":["../../../../libs/ui/switch/src/lib/switch/switch.ts","../../../../libs/ui/switch/src/skra-ui-switch.ts"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n ElementRef,\n ViewEncapsulation,\n effect,\n inject,\n} from '@angular/core';\nimport { SkraFieldContext } from '@skra/ui/field';\n\n/**\n * Wires a native `<input type=\"checkbox\">` to the **switch** role + skin when inside\n * `<skra-field>` (ADR 013), or standalone with no cascade. Per ADR 039/040, Forge ships\n * no `<skra-switch>` wrapper — this is a Component-on-the-native-host (`<input>` is void →\n * `template: ''`, ADR 040 §2) carrying its own `switch.scss` via Emulated encapsulation. The\n * ADR 039 revision history named this exact pattern as the future switch primitive.\n *\n * **The directive SETS `role=\"switch\"`** (it is not part of the selector) — the consumer must\n * not be asked to repeat an obvious, required ARIA role. No native HTML element produces switch\n * semantics, so `role=\"switch\"` is the legitimate ARIA mechanism (native-attr-first rule 2). The\n * browser maps the native checked state to `aria-checked` (on/off) under this role.\n *\n * <!-- inside Field -->\n * <skra-field>\n * <input type=\"checkbox\" skraSwitch [checked]=\"x()\" (change)=\"x.set($any($event.target).checked)\" />\n * <label skraLabelFor>Enable notifications</label>\n * </skra-field>\n *\n * <!-- standalone (label wraps input — no id/for needed) -->\n * <label>\n * <input type=\"checkbox\" skraSwitch [checked]=\"x()\" (change)=\"x.set($any($event.target).checked)\" />\n * Dark mode\n * </label>\n *\n * Value channel stays consumer-owned per ADR 037 (native `[checked]` + `(change)`, Reactive\n * Forms `formControlName`, Signal Forms `[formField]`, or `[(ngModel)]` — all orthogonal). Toggle\n * state is NOT reflected as `data-skra-*` (that would require Forge to observe native state,\n * violating ADR 037); the skin keys off the native `:checked` pseudo-class.\n *\n * Distinct from `skraCheckbox` (the atom-granularity split): a switch is strictly binary —\n * `role=\"switch\"` forbids `aria-checked=\"mixed\"`, so there is **no `indeterminate` input**.\n */\n@Component({\n selector: 'input[type=\"checkbox\"][skraSwitch]',\n standalone: true,\n // Void host — no <ng-content>. See ADR 040 §2.\n template: '',\n styleUrl: './switch.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n encapsulation: ViewEncapsulation.Emulated,\n host: {\n // The directive owns the switch semantic (resolved in scope, refines the ADR 039 sketch).\n role: 'switch',\n '[attr.data-skra-disabled]': 'context?.isDisabled() ? \"\" : null',\n '[attr.aria-invalid]': 'context?.hasError() ? \"true\" : null',\n // Readonly is ARIA-only (native `readonly` doesn't apply to checkboxes per HTML spec);\n // styling keys off `[aria-readonly]`. Mirrors skraCheckbox / skraRadio.\n '[attr.aria-readonly]': 'context?.isReadonly() ? \"true\" : null',\n '[attr.aria-describedby]': 'context?.describedByIds() ?? null',\n '(click)': 'blockWhenReadonly($event)',\n '(keydown)': 'blockKeyWhenReadonly($event)',\n },\n})\nexport class SkraSwitch {\n private readonly el = inject<ElementRef<HTMLInputElement>>(ElementRef);\n readonly context = inject(SkraFieldContext, { optional: true });\n\n constructor() {\n // Field-cascaded native properties — only bind when context is present so\n // standalone consumers retain full control of their static attributes.\n if (this.context) {\n const ctx = this.context;\n const el = this.el.nativeElement;\n effect(() => {\n el.id = ctx.fieldId();\n });\n effect(() => {\n el.disabled = ctx.isDisabled();\n });\n }\n }\n\n /**\n * Readonly enforcement — native checkbox has no `[readonly]`, so without this the\n * `aria-readonly` we announce would be a lie (the user could still toggle it).\n * `preventDefault` on `click` blocks the toggle from both pointer and Space (keyboard\n * activation dispatches a click on form controls). Mirrors the checkbox/radio guards.\n */\n protected blockWhenReadonly(event: Event): void {\n if (this.context?.isReadonly()) event.preventDefault();\n }\n\n /** Block keyboard activation (Space) when readonly; let `Tab` through. */\n protected blockKeyWhenReadonly(event: KeyboardEvent): void {\n if (this.context?.isReadonly() && event.key !== 'Tab') {\n event.preventDefault();\n }\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;;AAUA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BG;MAsBU,UAAU,CAAA;AACJ,IAAA,EAAE,GAAG,MAAM,CAA+B,UAAU,CAAC;IAC7D,OAAO,GAAG,MAAM,CAAC,gBAAgB,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;AAE/D,IAAA,WAAA,GAAA;;;AAGE,QAAA,IAAI,IAAI,CAAC,OAAO,EAAE;AAChB,YAAA,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO;AACxB,YAAA,MAAM,EAAE,GAAG,IAAI,CAAC,EAAE,CAAC,aAAa;YAChC,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;;AAGU,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;uGAlCW,UAAU,EAAA,IAAA,EAAA,EAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA;AAAV,IAAA,OAAA,IAAA,GAAA,EAAA,CAAA,oBAAA,CAAA,EAAA,UAAA,EAAA,QAAA,EAAA,OAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAU,2fAjBX,EAAE,EAAA,QAAA,EAAA,IAAA,EAAA,MAAA,EAAA,CAAA,
|
|
1
|
+
{"version":3,"file":"skra-ui-switch.mjs","sources":["../../../../libs/ui/switch/src/lib/switch/switch.ts","../../../../libs/ui/switch/src/skra-ui-switch.ts"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n ElementRef,\n ViewEncapsulation,\n effect,\n inject,\n} from '@angular/core';\nimport { SkraFieldContext } from '@skra/ui/field';\n\n/**\n * Wires a native `<input type=\"checkbox\">` to the **switch** role + skin when inside\n * `<skra-field>` (ADR 013), or standalone with no cascade. Per ADR 039/040, Forge ships\n * no `<skra-switch>` wrapper — this is a Component-on-the-native-host (`<input>` is void →\n * `template: ''`, ADR 040 §2) carrying its own `switch.scss` via Emulated encapsulation. The\n * ADR 039 revision history named this exact pattern as the future switch primitive.\n *\n * **The directive SETS `role=\"switch\"`** (it is not part of the selector) — the consumer must\n * not be asked to repeat an obvious, required ARIA role. No native HTML element produces switch\n * semantics, so `role=\"switch\"` is the legitimate ARIA mechanism (native-attr-first rule 2). The\n * browser maps the native checked state to `aria-checked` (on/off) under this role.\n *\n * <!-- inside Field -->\n * <skra-field>\n * <input type=\"checkbox\" skraSwitch [checked]=\"x()\" (change)=\"x.set($any($event.target).checked)\" />\n * <label skraLabelFor>Enable notifications</label>\n * </skra-field>\n *\n * <!-- standalone (label wraps input — no id/for needed) -->\n * <label>\n * <input type=\"checkbox\" skraSwitch [checked]=\"x()\" (change)=\"x.set($any($event.target).checked)\" />\n * Dark mode\n * </label>\n *\n * Value channel stays consumer-owned per ADR 037 (native `[checked]` + `(change)`, Reactive\n * Forms `formControlName`, Signal Forms `[formField]`, or `[(ngModel)]` — all orthogonal). Toggle\n * state is NOT reflected as `data-skra-*` (that would require Forge to observe native state,\n * violating ADR 037); the skin keys off the native `:checked` pseudo-class.\n *\n * Distinct from `skraCheckbox` (the atom-granularity split): a switch is strictly binary —\n * `role=\"switch\"` forbids `aria-checked=\"mixed\"`, so there is **no `indeterminate` input**.\n */\n@Component({\n selector: 'input[type=\"checkbox\"][skraSwitch]',\n standalone: true,\n // Void host — no <ng-content>. See ADR 040 §2.\n template: '',\n styleUrl: './switch.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n encapsulation: ViewEncapsulation.Emulated,\n host: {\n // The directive owns the switch semantic (resolved in scope, refines the ADR 039 sketch).\n role: 'switch',\n '[attr.data-skra-disabled]': 'context?.isDisabled() ? \"\" : null',\n '[attr.aria-invalid]': 'context?.hasError() ? \"true\" : null',\n // Readonly is ARIA-only (native `readonly` doesn't apply to checkboxes per HTML spec);\n // styling keys off `[aria-readonly]`. Mirrors skraCheckbox / skraRadio.\n '[attr.aria-readonly]': 'context?.isReadonly() ? \"true\" : null',\n '[attr.aria-describedby]': 'context?.describedByIds() ?? null',\n '(click)': 'blockWhenReadonly($event)',\n '(keydown)': 'blockKeyWhenReadonly($event)',\n },\n})\nexport class SkraSwitch {\n private readonly el = inject<ElementRef<HTMLInputElement>>(ElementRef);\n readonly context = inject(SkraFieldContext, { optional: true });\n\n constructor() {\n // Field-cascaded native properties — only bind when context is present so\n // standalone consumers retain full control of their static attributes.\n if (this.context) {\n const ctx = this.context;\n const el = this.el.nativeElement;\n effect(() => {\n el.id = ctx.fieldId();\n });\n effect(() => {\n el.disabled = ctx.isDisabled();\n });\n }\n }\n\n /**\n * Readonly enforcement — native checkbox has no `[readonly]`, so without this the\n * `aria-readonly` we announce would be a lie (the user could still toggle it).\n * `preventDefault` on `click` blocks the toggle from both pointer and Space (keyboard\n * activation dispatches a click on form controls). Mirrors the checkbox/radio guards.\n */\n protected blockWhenReadonly(event: Event): void {\n if (this.context?.isReadonly()) event.preventDefault();\n }\n\n /** Block keyboard activation (Space) when readonly; let `Tab` through. */\n protected blockKeyWhenReadonly(event: KeyboardEvent): void {\n if (this.context?.isReadonly() && event.key !== 'Tab') {\n event.preventDefault();\n }\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;;AAUA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BG;MAsBU,UAAU,CAAA;AACJ,IAAA,EAAE,GAAG,MAAM,CAA+B,UAAU,CAAC;IAC7D,OAAO,GAAG,MAAM,CAAC,gBAAgB,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;AAE/D,IAAA,WAAA,GAAA;;;AAGE,QAAA,IAAI,IAAI,CAAC,OAAO,EAAE;AAChB,YAAA,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO;AACxB,YAAA,MAAM,EAAE,GAAG,IAAI,CAAC,EAAE,CAAC,aAAa;YAChC,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;;AAGU,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;uGAlCW,UAAU,EAAA,IAAA,EAAA,EAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA;AAAV,IAAA,OAAA,IAAA,GAAA,EAAA,CAAA,oBAAA,CAAA,EAAA,UAAA,EAAA,QAAA,EAAA,OAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAU,2fAjBX,EAAE,EAAA,QAAA,EAAA,IAAA,EAAA,MAAA,EAAA,CAAA,mzFAAA,CAAA,EAAA,eAAA,EAAA,EAAA,CAAA,uBAAA,CAAA,MAAA,EAAA,CAAA;;2FAiBD,UAAU,EAAA,UAAA,EAAA,CAAA;kBArBtB,SAAS;AACE,YAAA,IAAA,EAAA,CAAA,EAAA,QAAA,EAAA,oCAAoC,EAAA,UAAA,EAClC,IAAI,EAAA,QAAA,EAEN,EAAE,EAAA,eAAA,EAEK,uBAAuB,CAAC,MAAM,EAAA,aAAA,EAChC,iBAAiB,CAAC,QAAQ,EAAA,IAAA,EACnC;;AAEJ,wBAAA,IAAI,EAAE,QAAQ;AACd,wBAAA,2BAA2B,EAAE,mCAAmC;AAChE,wBAAA,qBAAqB,EAAE,qCAAqC;;;AAG5D,wBAAA,sBAAsB,EAAE,uCAAuC;AAC/D,wBAAA,yBAAyB,EAAE,mCAAmC;AAC9D,wBAAA,SAAS,EAAE,2BAA2B;AACtC,wBAAA,WAAW,EAAE,8BAA8B;AAC5C,qBAAA,EAAA,MAAA,EAAA,CAAA,mzFAAA,CAAA,EAAA;;;AC7DH;;AAEG;;;;"}
|
|
@@ -37,8 +37,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.1", ngImpor
|
|
|
37
37
|
}], propDecorators: { text: [{ type: i0.Input, args: [{ isSignal: true, alias: "text", required: false }] }] } });
|
|
38
38
|
|
|
39
39
|
/** A focusable trigger is required (WCAG 2.1.1); used only for the dev-mode nudge. */
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
// Keyboard-reachable only: a `disabled` control is not focusable (and in most engines fires no
|
|
41
|
+
// pointer events either), and `tabindex="-1"` is programmatically focusable but not tab-reachable.
|
|
42
|
+
// Both were false negatives before — and a tooltip on a `<button disabled>` is THE canonical
|
|
43
|
+
// tooltip a11y trap, i.e. exactly what this dev-mode guard exists to catch.
|
|
44
|
+
const FOCUSABLE_SELECTOR = 'a[href],area[href],button:not([disabled]),input:not([disabled]),' +
|
|
45
|
+
'select:not([disabled]),textarea:not([disabled]),summary,' +
|
|
46
|
+
'[tabindex]:not([tabindex^="-"]),[contenteditable]:not([contenteditable="false"])';
|
|
42
47
|
/**
|
|
43
48
|
* SkraTooltip — a supplementary, non-interactive text description on a focusable trigger, shown on
|
|
44
49
|
* hover AND keyboard focus (ADR 0044 native top layer via the Popover API). Applied to the consumer's
|