@skra/ui 0.2.0 → 0.3.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.
Files changed (88) hide show
  1. package/LICENSE +201 -201
  2. package/NOTICE +7 -7
  3. package/README.md +39 -39
  4. package/consumer-guide/README.md +84 -84
  5. package/consumer-guide/forms-and-binding.md +19 -3
  6. package/consumer-guide/layout.md +19 -0
  7. package/consumer-guide/overlays.md +100 -100
  8. package/consumer-guide/setup-and-gotchas.md +165 -103
  9. package/consumer-guide/theming-and-encapsulation.md +129 -115
  10. package/fesm2022/skra-ui-accordion.mjs +61 -43
  11. package/fesm2022/skra-ui-accordion.mjs.map +1 -1
  12. package/fesm2022/skra-ui-alert.mjs +26 -18
  13. package/fesm2022/skra-ui-alert.mjs.map +1 -1
  14. package/fesm2022/skra-ui-avatar.mjs +12 -8
  15. package/fesm2022/skra-ui-avatar.mjs.map +1 -1
  16. package/fesm2022/skra-ui-badge.mjs +19 -13
  17. package/fesm2022/skra-ui-badge.mjs.map +1 -1
  18. package/fesm2022/skra-ui-button.mjs +28 -19
  19. package/fesm2022/skra-ui-button.mjs.map +1 -1
  20. package/fesm2022/skra-ui-checkbox.mjs +3 -3
  21. package/fesm2022/skra-ui-checkbox.mjs.map +1 -1
  22. package/fesm2022/skra-ui-chip.mjs +15 -11
  23. package/fesm2022/skra-ui-chip.mjs.map +1 -1
  24. package/fesm2022/skra-ui-combobox.mjs +135 -130
  25. package/fesm2022/skra-ui-combobox.mjs.map +1 -1
  26. package/fesm2022/skra-ui-core.mjs +95 -73
  27. package/fesm2022/skra-ui-core.mjs.map +1 -1
  28. package/fesm2022/skra-ui-counter.mjs +9 -6
  29. package/fesm2022/skra-ui-counter.mjs.map +1 -1
  30. package/fesm2022/skra-ui-datepicker.mjs +334 -307
  31. package/fesm2022/skra-ui-datepicker.mjs.map +1 -1
  32. package/fesm2022/skra-ui-dialog.mjs +5 -4
  33. package/fesm2022/skra-ui-dialog.mjs.map +1 -1
  34. package/fesm2022/skra-ui-field.mjs +44 -34
  35. package/fesm2022/skra-ui-field.mjs.map +1 -1
  36. package/fesm2022/skra-ui-icon.mjs +5 -4
  37. package/fesm2022/skra-ui-icon.mjs.map +1 -1
  38. package/fesm2022/skra-ui-input.mjs +53 -12
  39. package/fesm2022/skra-ui-input.mjs.map +1 -1
  40. package/fesm2022/skra-ui-kbd.mjs +3 -3
  41. package/fesm2022/skra-ui-kbd.mjs.map +1 -1
  42. package/fesm2022/skra-ui-layout.mjs +33 -21
  43. package/fesm2022/skra-ui-layout.mjs.map +1 -1
  44. package/fesm2022/skra-ui-link.mjs +3 -3
  45. package/fesm2022/skra-ui-link.mjs.map +1 -1
  46. package/fesm2022/skra-ui-loading-indicator.mjs +3 -3
  47. package/fesm2022/skra-ui-loading-indicator.mjs.map +1 -1
  48. package/fesm2022/skra-ui-menu.mjs +8 -7
  49. package/fesm2022/skra-ui-menu.mjs.map +1 -1
  50. package/fesm2022/skra-ui-multi-select.mjs +102 -98
  51. package/fesm2022/skra-ui-multi-select.mjs.map +1 -1
  52. package/fesm2022/skra-ui-native-select.mjs +35 -17
  53. package/fesm2022/skra-ui-native-select.mjs.map +1 -1
  54. package/fesm2022/skra-ui-popover.mjs +5 -4
  55. package/fesm2022/skra-ui-popover.mjs.map +1 -1
  56. package/fesm2022/skra-ui-progress.mjs +7 -5
  57. package/fesm2022/skra-ui-progress.mjs.map +1 -1
  58. package/fesm2022/skra-ui-radio.mjs +3 -3
  59. package/fesm2022/skra-ui-radio.mjs.map +1 -1
  60. package/fesm2022/skra-ui-rating-display.mjs +14 -9
  61. package/fesm2022/skra-ui-rating-display.mjs.map +1 -1
  62. package/fesm2022/skra-ui-select.mjs +98 -95
  63. package/fesm2022/skra-ui-select.mjs.map +1 -1
  64. package/fesm2022/skra-ui-skeleton.mjs +16 -13
  65. package/fesm2022/skra-ui-skeleton.mjs.map +1 -1
  66. package/fesm2022/skra-ui-star-rating.mjs +18 -11
  67. package/fesm2022/skra-ui-star-rating.mjs.map +1 -1
  68. package/fesm2022/skra-ui-status-dot.mjs +7 -5
  69. package/fesm2022/skra-ui-status-dot.mjs.map +1 -1
  70. package/fesm2022/skra-ui-switch.mjs +3 -3
  71. package/fesm2022/skra-ui-switch.mjs.map +1 -1
  72. package/fesm2022/skra-ui-tabs.mjs +43 -29
  73. package/fesm2022/skra-ui-tabs.mjs.map +1 -1
  74. package/fesm2022/skra-ui-textarea.mjs +41 -15
  75. package/fesm2022/skra-ui-textarea.mjs.map +1 -1
  76. package/fesm2022/skra-ui-toast.mjs +35 -23
  77. package/fesm2022/skra-ui-toast.mjs.map +1 -1
  78. package/fesm2022/skra-ui-tooltip.mjs +12 -9
  79. package/fesm2022/skra-ui-tooltip.mjs.map +1 -1
  80. package/fesm2022/skra-ui-typography.mjs +9 -6
  81. package/fesm2022/skra-ui-typography.mjs.map +1 -1
  82. package/package.json +5 -5
  83. package/types/skra-ui-input.d.ts +24 -1
  84. package/types/skra-ui-input.d.ts.map +1 -1
  85. package/types/skra-ui-native-select.d.ts +9 -1
  86. package/types/skra-ui-native-select.d.ts.map +1 -1
  87. package/types/skra-ui-textarea.d.ts +10 -1
  88. package/types/skra-ui-textarea.d.ts.map +1 -1
@@ -116,10 +116,10 @@ class SkraMenuItem {
116
116
  // Activating a command closes the menu (APG). The consumer's (click) handler runs independently.
117
117
  this.menu?.requestClose();
118
118
  }
119
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: SkraMenuItem, deps: [], target: i0.ɵɵFactoryTarget.Component });
120
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.7", type: SkraMenuItem, isStandalone: true, selector: "button[skraMenuItem]", host: { attributes: { "role": "menuitem", "type": "button", "tabindex": "-1" }, listeners: { "click": "onActivate($event)" } }, exportAs: ["skraMenuItem"], ngImport: i0, template: '<ng-content></ng-content>', isInline: true, styles: ["@charset \"UTF-8\";:host{display:flex;align-items:center;gap:var(--skra-comp-menu-item-gap);inline-size:100%;min-block-size:var(--skra-comp-menu-item-min-block-size);margin:0;box-sizing:border-box;padding-block:var(--skra-comp-menu-item-padding-block);padding-inline:var(--skra-comp-menu-item-padding-inline);appearance:none;background:transparent;border:0;color:var(--skra-comp-menu-item-foreground);text-align:start;cursor:pointer;font-family:var(--skra-sys-typography-role-body-medium-font-family);font-size:var(--skra-sys-typography-role-body-medium-font-size);font-weight:var(--skra-sys-typography-role-body-medium-font-weight);line-height:var(--skra-sys-typography-role-body-medium-line-height);letter-spacing:var(--skra-sys-typography-role-body-medium-letter-spacing);transition:background-color var(--skra-comp-menu-motion-duration) var(--skra-comp-menu-motion-easing)}:host(:hover:not([aria-disabled=true])),:host(:focus-visible:not([aria-disabled=true])){background-color:var(--skra-comp-menu-item-background-hover)}:host(:focus-visible){outline:var(--skra-comp-menu-focus-ring-width) solid var(--skra-comp-menu-focus-ring-color);outline-offset:calc(-1 * var(--skra-comp-menu-focus-ring-width))}:host([aria-disabled=true]){color:var(--skra-comp-menu-item-foreground-disabled);cursor:default}@media(prefers-reduced-motion:reduce){:host{transition-duration:0ms}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
119
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.1", ngImport: i0, type: SkraMenuItem, deps: [], target: i0.ɵɵFactoryTarget.Component });
120
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.1.1", type: SkraMenuItem, isStandalone: true, selector: "button[skraMenuItem]", host: { attributes: { "role": "menuitem", "type": "button", "tabindex": "-1" }, listeners: { "click": "onActivate($event)" } }, exportAs: ["skraMenuItem"], ngImport: i0, template: '<ng-content></ng-content>', isInline: true, styles: ["@charset \"UTF-8\";:host{display:flex;align-items:center;gap:var(--skra-comp-menu-item-gap);inline-size:100%;min-block-size:var(--skra-comp-menu-item-min-block-size);margin:0;box-sizing:border-box;padding-block:var(--skra-comp-menu-item-padding-block);padding-inline:var(--skra-comp-menu-item-padding-inline);appearance:none;background:transparent;border:0;color:var(--skra-comp-menu-item-foreground);text-align:start;cursor:pointer;font-family:var(--skra-sys-typography-role-body-medium-font-family);font-size:var(--skra-sys-typography-role-body-medium-font-size);font-weight:var(--skra-sys-typography-role-body-medium-font-weight);line-height:var(--skra-sys-typography-role-body-medium-line-height);letter-spacing:var(--skra-sys-typography-role-body-medium-letter-spacing);transition:background-color var(--skra-comp-menu-motion-duration) var(--skra-comp-menu-motion-easing)}:host(:hover:not([aria-disabled=true])),:host(:focus-visible:not([aria-disabled=true])){background-color:var(--skra-comp-menu-item-background-hover)}:host(:focus-visible){outline:var(--skra-comp-menu-focus-ring-width) solid var(--skra-comp-menu-focus-ring-color);outline-offset:calc(-1 * var(--skra-comp-menu-focus-ring-width))}:host([aria-disabled=true]){color:var(--skra-comp-menu-item-foreground-disabled);cursor:default}@media(prefers-reduced-motion:reduce){:host{transition-duration:0ms}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
121
121
  }
122
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: SkraMenuItem, decorators: [{
122
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.1", ngImport: i0, type: SkraMenuItem, decorators: [{
123
123
  type: Component,
124
124
  args: [{ selector: 'button[skraMenuItem]', standalone: true, exportAs: 'skraMenuItem', template: '<ng-content></ng-content>', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.Emulated, host: {
125
125
  role: 'menuitem',
@@ -158,7 +158,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.7", ngImpor
158
158
  */
159
159
  class SkraMenu {
160
160
  /** Placement intent; `flip`/`shift` adjust at runtime. */
161
- placement = input('bottom', ...(ngDevMode ? [{ debugName: "placement" }] : /* istanbul ignore next */ []));
161
+ placement = input('bottom', /* @ts-ignore */
162
+ ...(ngDevMode ? [{ debugName: "placement" }] : /* istanbul ignore next */ []));
162
163
  menuItems = contentChildren(SkraMenuItem, { ...(ngDevMode ? { debugName: "menuItems" } : /* istanbul ignore next */ {}), descendants: true });
163
164
  host = inject(ElementRef);
164
165
  renderer = inject(Renderer2);
@@ -307,10 +308,10 @@ class SkraMenu {
307
308
  this.renderer.removeAttribute(this.trigger, 'aria-controls');
308
309
  }
309
310
  }
310
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: SkraMenu, deps: [], target: i0.ɵɵFactoryTarget.Component });
311
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.2.7", type: SkraMenu, isStandalone: true, selector: "skra-menu", inputs: { placement: { classPropertyName: "placement", publicName: "placement", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "menu", "popover": "auto" } }, providers: [{ provide: SKRA_MENU, useExisting: SkraMenu }], queries: [{ propertyName: "menuItems", predicate: SkraMenuItem, descendants: true, isSignal: true }], exportAs: ["skraMenu"], ngImport: i0, template: '<ng-content></ng-content>', isInline: true, styles: ["@charset \"UTF-8\";:host{position:fixed;margin:0;inset:auto;box-sizing:border-box;background:var(--skra-comp-menu-background);color:var(--skra-comp-menu-foreground);border-color:var(--skra-comp-menu-border-color);border-width:var(--skra-comp-menu-border-width);border-style:var(--skra-comp-menu-border-style);border-radius:var(--skra-comp-menu-radius);padding-block:var(--skra-comp-menu-padding-block);padding-inline:var(--skra-comp-menu-padding-inline);box-shadow:var(--skra-comp-menu-shadow);inline-size:max-content;max-inline-size:var(--skra-comp-menu-max-inline-size);flex-direction:column;opacity:0;transition:opacity var(--skra-comp-menu-motion-duration) var(--skra-comp-menu-motion-easing),display var(--skra-comp-menu-motion-duration) allow-discrete,overlay var(--skra-comp-menu-motion-duration) allow-discrete}:host(:popover-open){display:flex;opacity:1}@starting-style{:host(:popover-open){opacity:0}}@media(prefers-reduced-motion:reduce){:host{transition-duration:0ms}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
311
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.1", ngImport: i0, type: SkraMenu, deps: [], target: i0.ɵɵFactoryTarget.Component });
312
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "22.1.1", type: SkraMenu, isStandalone: true, selector: "skra-menu", inputs: { placement: { classPropertyName: "placement", publicName: "placement", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "menu", "popover": "auto" } }, providers: [{ provide: SKRA_MENU, useExisting: SkraMenu }], queries: [{ propertyName: "menuItems", predicate: SkraMenuItem, descendants: true, isSignal: true }], exportAs: ["skraMenu"], ngImport: i0, template: '<ng-content></ng-content>', isInline: true, styles: ["@charset \"UTF-8\";:host{position:fixed;margin:0;inset:auto;box-sizing:border-box;background:var(--skra-comp-menu-background);color:var(--skra-comp-menu-foreground);border-color:var(--skra-comp-menu-border-color);border-width:var(--skra-comp-menu-border-width);border-style:var(--skra-comp-menu-border-style);border-radius:var(--skra-comp-menu-radius);padding-block:var(--skra-comp-menu-padding-block);padding-inline:var(--skra-comp-menu-padding-inline);box-shadow:var(--skra-comp-menu-shadow);inline-size:max-content;max-inline-size:var(--skra-comp-menu-max-inline-size);flex-direction:column;opacity:0;transition:opacity var(--skra-comp-menu-motion-duration) var(--skra-comp-menu-motion-easing),display var(--skra-comp-menu-motion-duration) allow-discrete,overlay var(--skra-comp-menu-motion-duration) allow-discrete}:host(:popover-open){display:flex;opacity:1}@starting-style{:host(:popover-open){opacity:0}}@media(prefers-reduced-motion:reduce){:host{transition-duration:0ms}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
312
313
  }
313
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: SkraMenu, decorators: [{
314
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.1", ngImport: i0, type: SkraMenu, decorators: [{
314
315
  type: Component,
315
316
  args: [{ selector: 'skra-menu', standalone: true, exportAs: 'skraMenu', template: '<ng-content></ng-content>', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.Emulated, providers: [{ provide: SKRA_MENU, useExisting: SkraMenu }], host: {
316
317
  role: 'menu',
@@ -1 +1 @@
1
- {"version":3,"file":"skra-ui-menu.mjs","sources":["../../../../libs/ui/menu/src/lib/menu/menu-keyboard.ts","../../../../libs/ui/menu/src/lib/menu/menu-types.ts","../../../../libs/ui/menu/src/lib/menu/menu-item.ts","../../../../libs/ui/menu/src/lib/menu/menu.ts","../../../../libs/ui/menu/src/skra-ui-menu.ts"],"sourcesContent":["import { FocusKeyManager } from '@angular/cdk/a11y';\n\nimport type { SkraMenuKeyboardItem } from './menu-types';\n\n/**\n * SkraMenuKeyboard — the WAI-ARIA APG Menu keyboard model, confined to ONE file.\n *\n * This is the **only** module in Forge that imports `@angular/cdk`. It wraps CDK `FocusKeyManager`\n * (roving REAL focus — the APG-canonical menu model, NOT `aria-activedescendant`) so the engine is an\n * implementation detail: swapping it (e.g. to a hand-rolled controller) is a one-file change with zero\n * impact on `SkraMenu` / `SkraMenuItem` — exactly as `SkraPositioner` wraps `@floating-ui/dom`\n * (ADR 0044 §3 rule 5; recorded in §7, 2026-06-07).\n *\n * Owns: Down/Up roving (wrap), Home/End, type-ahead, and Tab→dismiss. Enter/Space (native button\n * activation) and Escape + light-dismiss (`popover=\"auto\"`) stay platform-owned — the controller never\n * touches them.\n */\nexport class SkraMenuKeyboard {\n private manager?: FocusKeyManager<SkraMenuKeyboardItem>;\n private items: SkraMenuKeyboardItem[] = [];\n private tabOutHandler?: () => void;\n\n /** Register the APG \"Tab dismisses the menu\" callback (fired by `FocusKeyManager.tabOut`). */\n onTabOut(handler: () => void): void {\n this.tabOutHandler = handler;\n }\n\n /**\n * (Re)build the manager from the current projected item set. Called from an `effect` on the menu's\n * signal `contentChildren` — a passed array is a static snapshot (it does not auto-track like a live\n * `QueryList`), so the manager is rebuilt whenever the item set changes (the documented signal↔CDK seam).\n */\n setItems(items: readonly SkraMenuKeyboardItem[]): void {\n this.items = [...items];\n this.manager?.destroy();\n this.manager = new FocusKeyManager<SkraMenuKeyboardItem>(this.items)\n .withWrap()\n .withTypeAhead()\n .withHomeAndEnd()\n // Disabled items stay in the roving order (APG focusable-but-inert — discoverable); activation is\n // blocked on the item itself. `() => false` also stops `FocusKeyManager` from reading `item.disabled`\n // (which on our signal-input component would be a truthy function).\n .skipPredicate(() => false);\n this.manager.tabOut.subscribe(() => this.tabOutHandler?.());\n }\n\n /** Delegate Up/Down/Home/End/type-ahead. Enter/Space/Escape are intentionally left to the platform. */\n onKeydown(event: KeyboardEvent): void {\n this.manager?.onKeydown(event);\n }\n\n /** Move roving focus to the first ENABLED item (APG: focus enters the menu on open). */\n focusFirst(): void {\n if (!this.manager || this.items.length === 0) return;\n const firstEnabled = this.items.findIndex((item) => !item.isDisabled());\n this.manager.setActiveItem(firstEnabled >= 0 ? firstEnabled : 0);\n }\n\n destroy(): void {\n this.manager?.destroy();\n this.manager = undefined;\n }\n}\n","import { InjectionToken } from '@angular/core';\n\n/**\n * The item shape the menu keyboard controller drives. Structurally a CDK `FocusableOption`\n * (`focus()`), plus the menu's own `getLabel()` (type-ahead) and `isDisabled()` (focusable-but-inert\n * roving). Declared HERE — cdk-free — so the only `@angular/cdk` importer stays `menu-keyboard.ts`.\n */\nexport interface SkraMenuKeyboardItem {\n focus(): void;\n getLabel(): string;\n isDisabled(): boolean;\n}\n\n/** What a `skraMenuItem` needs from its `skra-menu` — narrow contract that breaks the menu↔item cycle. */\nexport interface SkraMenuPanel {\n /** Dismiss the menu after an item activates (native `hidePopover()` → focus returns to the trigger). */\n requestClose(): void;\n}\n\n/**\n * DI token a `skra-menu` provides (`useExisting`) and a `button[skraMenuItem]` injects. A token\n * (not the `SkraMenu` class) so the item file never imports the menu file — no runtime import cycle.\n */\nexport const SKRA_MENU = new InjectionToken<SkraMenuPanel>('SKRA_MENU');\n","import {\n ChangeDetectionStrategy,\n Component,\n ElementRef,\n ViewEncapsulation,\n inject,\n} from '@angular/core';\n\nimport { SKRA_MENU, type SkraMenuKeyboardItem } from './menu-types';\n\ndeclare const ngDevMode: boolean | undefined;\n\n/**\n * SkraMenuItem — a single command in a `skra-menu`, a Component on the native command host\n * `button[skraMenuItem]` (ADR 012/040: a `<button>` is the right semantic — Enter/Space→click +\n * focusability for free; its implicit role is overridden to `role=\"menuitem\"` per WAI-ARIA APG).\n *\n * Roving focus: `tabindex=\"-1\"` (the keyboard controller moves real focus). Implements\n * `SkraMenuKeyboardItem` (`focus`/`getLabel`/`isDisabled`) so `SkraMenuKeyboard` (CDK `FocusKeyManager`)\n * can drive it. On activation it asks the menu to close (native `hidePopover()` → focus returns to the\n * trigger).\n *\n * **Disabled is native-attr-first (boundary #9, ADR 039 precedent): the consumer sets\n * `aria-disabled=\"true\"` directly — Forge OBSERVES it, exposing NO `disabled` input.** This (a) keeps a\n * disabled item **focusable/discoverable** in the roving order (APG) — native `disabled` would remove it\n * from focus, and (b) avoids a Forge input shadowing the native `disabled` attribute. Styling keys off\n * `:host([aria-disabled=\"true\"])`; activation is blocked here.\n *\n * Full contract: adr/specs/component-menu-spec.md.\n */\n@Component({\n selector: 'button[skraMenuItem]',\n standalone: true,\n exportAs: 'skraMenuItem',\n template: '<ng-content></ng-content>',\n styleUrl: './menu-item.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n encapsulation: ViewEncapsulation.Emulated,\n host: {\n role: 'menuitem',\n type: 'button', // never an implicit submit if the menu sits inside a <form>\n tabindex: '-1', // roving — focus is moved programmatically by SkraMenuKeyboard\n '(click)': 'onActivate($event)',\n },\n})\nexport class SkraMenuItem implements SkraMenuKeyboardItem {\n private readonly host = inject<ElementRef<HTMLElement>>(ElementRef);\n private readonly menu = inject(SKRA_MENU, { optional: true });\n\n constructor() {\n if (typeof ngDevMode === 'undefined' || ngDevMode) {\n if (!this.menu) {\n console.warn(\n '[skraMenuItem] has no <skra-menu> ancestor — it is not registered with any keyboard ' +\n 'controller and cannot close a menu on activation. Project it inside a <skra-menu>.',\n );\n }\n }\n }\n\n // ── SkraMenuKeyboardItem (driven by SkraMenuKeyboard / CDK FocusKeyManager) ──\n focus(): void {\n this.host.nativeElement.focus();\n }\n getLabel(): string {\n return (this.host.nativeElement.textContent ?? '').trim();\n }\n /** Soft-disabled = the consumer's `aria-disabled=\"true\"` (native-attr-first); read live (no input). */\n isDisabled(): boolean {\n return this.host.nativeElement.getAttribute('aria-disabled') === 'true';\n }\n\n protected onActivate(event: MouseEvent): void {\n if (this.isDisabled()) {\n // Soft-disabled: block the native default AND the consumer's own (click) on this same host\n // (the host listener runs first — Material's MatMenuItem precedent).\n event.preventDefault();\n event.stopImmediatePropagation();\n return;\n }\n // Activating a command closes the menu (APG). The consumer's (click) handler runs independently.\n this.menu?.requestClose();\n }\n}\n","import {\n ChangeDetectionStrategy,\n Component,\n DestroyRef,\n ElementRef,\n Renderer2,\n ViewEncapsulation,\n afterNextRender,\n contentChildren,\n effect,\n inject,\n input,\n} from '@angular/core';\n\nimport {\n _attachPositioner,\n type SkraPlacement,\n type _PositionerHandle,\n} from '@skra/ui/core';\nimport { SkraMenuKeyboard } from './menu-keyboard';\nimport { SkraMenuItem } from './menu-item';\nimport { SKRA_MENU, type SkraMenuPanel } from './menu-types';\n\ndeclare const ngDevMode: boolean | undefined;\n\n/**\n * SkraMenu — a WAI-ARIA APG **Menu Button** menu: a homogeneous set of commands anchored to a\n * trigger, navigated as one composite widget with arrow keys, where activating a command closes it.\n * Rendered in the browser TOP LAYER via the native Popover API (`popover=\"auto\"`), per ADR 0044.\n *\n * SHAPE (scope 2026-06-07): a Component on the PANEL with projected `button[skraMenuItem]` children\n * (the Popover model, ADR 040) — NOT a directive on the trigger, NOT a data-driven `[items]` array.\n * The trigger is the consumer's own button linked by the native `popovertarget` attribute; toggle,\n * light-dismiss and Escape are platform-owned. The consumer MUST give `<skra-menu>` an `id`.\n *\n * <button skraButton popovertarget=\"actions\" aria-haspopup=\"menu\">Actions</button>\n * <skra-menu id=\"actions\">\n * <button skraMenuItem (click)=\"edit()\">Edit</button>\n * <button skraMenuItem disabled (click)=\"archive()\">Archive</button>\n * <button skraMenuItem (click)=\"remove()\">Delete</button>\n * </skra-menu>\n *\n * Forge adds, on top of the Popover substrate: the `role=\"menu\"` keyboard model via `SkraMenuKeyboard`\n * (CDK `FocusKeyManager` — Down/Up roving + wrap, Home/End, type-ahead, Tab-dismiss), **focus moving\n * INTO the menu on open** (a composite widget — unlike non-modal Popover; the per-component ADR 0044\n * §3 rule-8 decision), and the trigger's disclosure ARIA (`aria-haspopup=\"menu\"` / `aria-controls` /\n * `aria-expanded`). Side effects on open/close: bind the native `(toggle)` — there is no Forge output\n * (native-first; contrast Dialog, whose `<dialog>` has no open event). Context menus (right-click) are\n * a recipe with a visible button, never a trigger mode (SRP + WCAG 2.1.1).\n *\n * Full contract: adr/specs/component-menu-spec.md.\n */\n@Component({\n selector: 'skra-menu',\n standalone: true,\n exportAs: 'skraMenu',\n template: '<ng-content></ng-content>',\n styleUrl: './menu.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n encapsulation: ViewEncapsulation.Emulated,\n providers: [{ provide: SKRA_MENU, useExisting: SkraMenu }],\n host: {\n role: 'menu',\n // Native top layer + platform-owned toggle / light-dismiss / Escape / stacking (ADR 0044).\n popover: 'auto',\n // NOTE: `toggle` and `keydown` are wired IMPERATIVELY in the constructor, NOT as host listeners.\n // A consumer template binding `(toggle)` on <skra-menu> (the 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` is independent of Angular's host/template listener slot, so\n // it always fires alongside the consumer's binding.\n },\n})\nexport class SkraMenu implements SkraMenuPanel {\n /** Placement intent; `flip`/`shift` adjust at runtime. */\n readonly placement = input<SkraPlacement>('bottom');\n\n private readonly menuItems = contentChildren(SkraMenuItem, {\n descendants: true,\n });\n\n private readonly host = inject<ElementRef<HTMLElement>>(ElementRef);\n private readonly renderer = inject(Renderer2);\n private readonly keyboard = new SkraMenuKeyboard();\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 // Feed the controller whenever the projected item set changes (signal → array snapshot rebuild).\n effect(() => this.keyboard.setItems(this.menuItems()));\n // APG: Tab/Shift+Tab dismisses the menu; native Tab then proceeds from the trigger.\n this.keyboard.onTabOut(() => this.host.nativeElement.hidePopover?.());\n\n // All imperative DOM wiring lives in afterNextRender — a browser-only hook, so the native listeners\n // and the trigger resolution never run on the server (SSR-safe by construction; O-18). The listeners\n // attach right after first paint, long before any user interaction can fire them.\n afterNextRender(() => {\n const el = this.host.nativeElement;\n // Gate disabled items at the CAPTURE phase: a component host listener registers AFTER the parent\n // template's `(click)` on the same button, so a bubble-phase guard cannot stop the consumer's own\n // handler. A capture listener on the menu host fires before the target's listeners — preventing both\n // the consumer's `(click)` and the item's `onActivate` for an `aria-disabled` item.\n el.addEventListener('click', this.onCaptureClick, true);\n // Imperative (NOT host-metadata) so a consumer `(toggle)` binding does not shadow them — see host {}.\n el.addEventListener('toggle', this.onToggleNative);\n el.addEventListener('keydown', this.onKeydownNative);\n\n // Resolve + wire the trigger after the view exists (it may be a later sibling), so the disclosure\n // relationship (aria-haspopup + aria-controls + aria-expanded=\"false\") is present before first open.\n this.ensureTrigger();\n if (typeof ngDevMode === 'undefined' || ngDevMode) this.warnIfEmpty();\n });\n inject(DestroyRef).onDestroy(() => this.teardown());\n }\n\n /** SKRA_MENU contract — close on item activation (native focus-return to the trigger). */\n requestClose(): void {\n this.host.nativeElement.hidePopover?.();\n }\n\n /** Capture-phase guard: block clicks on an `aria-disabled` item before they reach its listeners. */\n private readonly onCaptureClick = (event: Event): void => {\n const item = (event.target as HTMLElement | null)?.closest?.(\n '[skraMenuItem]',\n );\n if (item?.getAttribute('aria-disabled') === 'true') {\n event.preventDefault();\n event.stopPropagation();\n }\n };\n\n // ── Toggle / keydown (imperative native listeners — 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 readonly onKeydownNative = (event: Event): void => {\n this.keyboard.onKeydown(event as KeyboardEvent);\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 this.positioner?.destroy();\n this.positioner = _attachPositioner(\n anchor,\n this.host.nativeElement,\n () => ({\n placement: this.placement(),\n offset: this.cssLenPx('--skra-comp-menu-offset'),\n viewportPadding: this.cssLenPx('--skra-comp-menu-viewport-padding'),\n }),\n );\n }\n // Focus enters the menu (APG Menu Button) regardless of how it opened — unlike non-modal Popover.\n this.keyboard.focusFirst();\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 menu disclosure ARIA on it (sanctioned a11y\n * wiring, ADR 013-style — not style mutation). Idempotent. Returns the trigger (or null for an orphan\n * menu, 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-menu] has no `id` — it cannot be targeted by a `popovertarget` trigger and the ' +\n 'disclosure ARIA (aria-haspopup/aria-controls/aria-expanded) cannot be wired. Give the ' +\n 'menu an id and point a button at it: ' +\n '<button popovertarget=\"my-id\" aria-haspopup=\"menu\">…</button> <skra-menu id=\"my-id\">…',\n );\n }\n return null;\n }\n\n // Match by attribute value (no escaping; robust to special chars; `CSS.escape` is not universal).\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-menu] no trigger found with [popovertarget=\"${id}\"] — the menu is unreachable and ` +\n 'has no element to carry aria-haspopup/aria-controls/aria-expanded. Add a focusable trigger ' +\n 'button with the matching popovertarget.',\n );\n }\n return null;\n }\n\n // Establish the Menu Button relationship before the first open (APG; aria-haspopup distinguishes a\n // menu from Popover's generic disclosure).\n this.renderer.setAttribute(this.trigger, 'aria-haspopup', 'menu');\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 /** Dev-only nudge: a role=\"menu\" with no menuitems is invalid — a link-list/content panel is a Popover. */\n private warnIfEmpty(): void {\n if (this.menuItems().length === 0) {\n console.warn(\n '[skra-menu] has no `button[skraMenuItem]` children — a `role=\"menu\"` with no menu items is ' +\n 'invalid. If this is a panel of arbitrary content or a list of navigation links, use ' +\n '<skra-popover> (disclosure) instead — see the Menu-vs-Popover boundary in the spec.',\n );\n }\n }\n\n private teardown(): void {\n const el = this.host.nativeElement;\n el.removeEventListener('click', this.onCaptureClick, true);\n el.removeEventListener('toggle', this.onToggleNative);\n el.removeEventListener('keydown', this.onKeydownNative);\n this.keyboard.destroy();\n this.positioner?.destroy();\n if (this.trigger) {\n this.renderer.removeAttribute(this.trigger, 'aria-haspopup');\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-menu.mjs","sources":["../../../../libs/ui/menu/src/lib/menu/menu-keyboard.ts","../../../../libs/ui/menu/src/lib/menu/menu-types.ts","../../../../libs/ui/menu/src/lib/menu/menu-item.ts","../../../../libs/ui/menu/src/lib/menu/menu.ts","../../../../libs/ui/menu/src/skra-ui-menu.ts"],"sourcesContent":["import { FocusKeyManager } from '@angular/cdk/a11y';\r\n\r\nimport type { SkraMenuKeyboardItem } from './menu-types';\r\n\r\n/**\r\n * SkraMenuKeyboard — the WAI-ARIA APG Menu keyboard model, confined to ONE file.\r\n *\r\n * This is the **only** module in Forge that imports `@angular/cdk`. It wraps CDK `FocusKeyManager`\r\n * (roving REAL focus — the APG-canonical menu model, NOT `aria-activedescendant`) so the engine is an\r\n * implementation detail: swapping it (e.g. to a hand-rolled controller) is a one-file change with zero\r\n * impact on `SkraMenu` / `SkraMenuItem` — exactly as `SkraPositioner` wraps `@floating-ui/dom`\r\n * (ADR 0044 §3 rule 5; recorded in §7, 2026-06-07).\r\n *\r\n * Owns: Down/Up roving (wrap), Home/End, type-ahead, and Tab→dismiss. Enter/Space (native button\r\n * activation) and Escape + light-dismiss (`popover=\"auto\"`) stay platform-owned — the controller never\r\n * touches them.\r\n */\r\nexport class SkraMenuKeyboard {\r\n private manager?: FocusKeyManager<SkraMenuKeyboardItem>;\r\n private items: SkraMenuKeyboardItem[] = [];\r\n private tabOutHandler?: () => void;\r\n\r\n /** Register the APG \"Tab dismisses the menu\" callback (fired by `FocusKeyManager.tabOut`). */\r\n onTabOut(handler: () => void): void {\r\n this.tabOutHandler = handler;\r\n }\r\n\r\n /**\r\n * (Re)build the manager from the current projected item set. Called from an `effect` on the menu's\r\n * signal `contentChildren` — a passed array is a static snapshot (it does not auto-track like a live\r\n * `QueryList`), so the manager is rebuilt whenever the item set changes (the documented signal↔CDK seam).\r\n */\r\n setItems(items: readonly SkraMenuKeyboardItem[]): void {\r\n this.items = [...items];\r\n this.manager?.destroy();\r\n this.manager = new FocusKeyManager<SkraMenuKeyboardItem>(this.items)\r\n .withWrap()\r\n .withTypeAhead()\r\n .withHomeAndEnd()\r\n // Disabled items stay in the roving order (APG focusable-but-inert — discoverable); activation is\r\n // blocked on the item itself. `() => false` also stops `FocusKeyManager` from reading `item.disabled`\r\n // (which on our signal-input component would be a truthy function).\r\n .skipPredicate(() => false);\r\n this.manager.tabOut.subscribe(() => this.tabOutHandler?.());\r\n }\r\n\r\n /** Delegate Up/Down/Home/End/type-ahead. Enter/Space/Escape are intentionally left to the platform. */\r\n onKeydown(event: KeyboardEvent): void {\r\n this.manager?.onKeydown(event);\r\n }\r\n\r\n /** Move roving focus to the first ENABLED item (APG: focus enters the menu on open). */\r\n focusFirst(): void {\r\n if (!this.manager || this.items.length === 0) return;\r\n const firstEnabled = this.items.findIndex((item) => !item.isDisabled());\r\n this.manager.setActiveItem(firstEnabled >= 0 ? firstEnabled : 0);\r\n }\r\n\r\n destroy(): void {\r\n this.manager?.destroy();\r\n this.manager = undefined;\r\n }\r\n}\r\n","import { InjectionToken } from '@angular/core';\r\n\r\n/**\r\n * The item shape the menu keyboard controller drives. Structurally a CDK `FocusableOption`\r\n * (`focus()`), plus the menu's own `getLabel()` (type-ahead) and `isDisabled()` (focusable-but-inert\r\n * roving). Declared HERE — cdk-free — so the only `@angular/cdk` importer stays `menu-keyboard.ts`.\r\n */\r\nexport interface SkraMenuKeyboardItem {\r\n focus(): void;\r\n getLabel(): string;\r\n isDisabled(): boolean;\r\n}\r\n\r\n/** What a `skraMenuItem` needs from its `skra-menu` — narrow contract that breaks the menu↔item cycle. */\r\nexport interface SkraMenuPanel {\r\n /** Dismiss the menu after an item activates (native `hidePopover()` → focus returns to the trigger). */\r\n requestClose(): void;\r\n}\r\n\r\n/**\r\n * DI token a `skra-menu` provides (`useExisting`) and a `button[skraMenuItem]` injects. A token\r\n * (not the `SkraMenu` class) so the item file never imports the menu file — no runtime import cycle.\r\n */\r\nexport const SKRA_MENU = new InjectionToken<SkraMenuPanel>('SKRA_MENU');\r\n","import {\r\n ChangeDetectionStrategy,\r\n Component,\r\n ElementRef,\r\n ViewEncapsulation,\r\n inject,\r\n} from '@angular/core';\r\n\r\nimport { SKRA_MENU, type SkraMenuKeyboardItem } from './menu-types';\r\n\r\ndeclare const ngDevMode: boolean | undefined;\r\n\r\n/**\r\n * SkraMenuItem — a single command in a `skra-menu`, a Component on the native command host\r\n * `button[skraMenuItem]` (ADR 012/040: a `<button>` is the right semantic — Enter/Space→click +\r\n * focusability for free; its implicit role is overridden to `role=\"menuitem\"` per WAI-ARIA APG).\r\n *\r\n * Roving focus: `tabindex=\"-1\"` (the keyboard controller moves real focus). Implements\r\n * `SkraMenuKeyboardItem` (`focus`/`getLabel`/`isDisabled`) so `SkraMenuKeyboard` (CDK `FocusKeyManager`)\r\n * can drive it. On activation it asks the menu to close (native `hidePopover()` → focus returns to the\r\n * trigger).\r\n *\r\n * **Disabled is native-attr-first (boundary #9, ADR 039 precedent): the consumer sets\r\n * `aria-disabled=\"true\"` directly — Forge OBSERVES it, exposing NO `disabled` input.** This (a) keeps a\r\n * disabled item **focusable/discoverable** in the roving order (APG) — native `disabled` would remove it\r\n * from focus, and (b) avoids a Forge input shadowing the native `disabled` attribute. Styling keys off\r\n * `:host([aria-disabled=\"true\"])`; activation is blocked here.\r\n *\r\n * Full contract: adr/specs/component-menu-spec.md.\r\n */\r\n@Component({\r\n selector: 'button[skraMenuItem]',\r\n standalone: true,\r\n exportAs: 'skraMenuItem',\r\n template: '<ng-content></ng-content>',\r\n styleUrl: './menu-item.scss',\r\n changeDetection: ChangeDetectionStrategy.OnPush,\r\n encapsulation: ViewEncapsulation.Emulated,\r\n host: {\r\n role: 'menuitem',\r\n type: 'button', // never an implicit submit if the menu sits inside a <form>\r\n tabindex: '-1', // roving — focus is moved programmatically by SkraMenuKeyboard\r\n '(click)': 'onActivate($event)',\r\n },\r\n})\r\nexport class SkraMenuItem implements SkraMenuKeyboardItem {\r\n private readonly host = inject<ElementRef<HTMLElement>>(ElementRef);\r\n private readonly menu = inject(SKRA_MENU, { optional: true });\r\n\r\n constructor() {\r\n if (typeof ngDevMode === 'undefined' || ngDevMode) {\r\n if (!this.menu) {\r\n console.warn(\r\n '[skraMenuItem] has no <skra-menu> ancestor — it is not registered with any keyboard ' +\r\n 'controller and cannot close a menu on activation. Project it inside a <skra-menu>.',\r\n );\r\n }\r\n }\r\n }\r\n\r\n // ── SkraMenuKeyboardItem (driven by SkraMenuKeyboard / CDK FocusKeyManager) ──\r\n focus(): void {\r\n this.host.nativeElement.focus();\r\n }\r\n getLabel(): string {\r\n return (this.host.nativeElement.textContent ?? '').trim();\r\n }\r\n /** Soft-disabled = the consumer's `aria-disabled=\"true\"` (native-attr-first); read live (no input). */\r\n isDisabled(): boolean {\r\n return this.host.nativeElement.getAttribute('aria-disabled') === 'true';\r\n }\r\n\r\n protected onActivate(event: MouseEvent): void {\r\n if (this.isDisabled()) {\r\n // Soft-disabled: block the native default AND the consumer's own (click) on this same host\r\n // (the host listener runs first — Material's MatMenuItem precedent).\r\n event.preventDefault();\r\n event.stopImmediatePropagation();\r\n return;\r\n }\r\n // Activating a command closes the menu (APG). The consumer's (click) handler runs independently.\r\n this.menu?.requestClose();\r\n }\r\n}\r\n","import {\r\n ChangeDetectionStrategy,\r\n Component,\r\n DestroyRef,\r\n ElementRef,\r\n Renderer2,\r\n ViewEncapsulation,\r\n afterNextRender,\r\n contentChildren,\r\n effect,\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\nimport { SkraMenuKeyboard } from './menu-keyboard';\r\nimport { SkraMenuItem } from './menu-item';\r\nimport { SKRA_MENU, type SkraMenuPanel } from './menu-types';\r\n\r\ndeclare const ngDevMode: boolean | undefined;\r\n\r\n/**\r\n * SkraMenu — a WAI-ARIA APG **Menu Button** menu: a homogeneous set of commands anchored to a\r\n * trigger, navigated as one composite widget with arrow keys, where activating a command closes it.\r\n * Rendered in the browser TOP LAYER via the native Popover API (`popover=\"auto\"`), per ADR 0044.\r\n *\r\n * SHAPE (scope 2026-06-07): a Component on the PANEL with projected `button[skraMenuItem]` children\r\n * (the Popover model, ADR 040) — NOT a directive on the trigger, NOT a data-driven `[items]` array.\r\n * The trigger is the consumer's own button linked by the native `popovertarget` attribute; toggle,\r\n * light-dismiss and Escape are platform-owned. The consumer MUST give `<skra-menu>` an `id`.\r\n *\r\n * <button skraButton popovertarget=\"actions\" aria-haspopup=\"menu\">Actions</button>\r\n * <skra-menu id=\"actions\">\r\n * <button skraMenuItem (click)=\"edit()\">Edit</button>\r\n * <button skraMenuItem disabled (click)=\"archive()\">Archive</button>\r\n * <button skraMenuItem (click)=\"remove()\">Delete</button>\r\n * </skra-menu>\r\n *\r\n * Forge adds, on top of the Popover substrate: the `role=\"menu\"` keyboard model via `SkraMenuKeyboard`\r\n * (CDK `FocusKeyManager` — Down/Up roving + wrap, Home/End, type-ahead, Tab-dismiss), **focus moving\r\n * INTO the menu on open** (a composite widget — unlike non-modal Popover; the per-component ADR 0044\r\n * §3 rule-8 decision), and the trigger's disclosure ARIA (`aria-haspopup=\"menu\"` / `aria-controls` /\r\n * `aria-expanded`). Side effects on open/close: bind the native `(toggle)` — there is no Forge output\r\n * (native-first; contrast Dialog, whose `<dialog>` has no open event). Context menus (right-click) are\r\n * a recipe with a visible button, never a trigger mode (SRP + WCAG 2.1.1).\r\n *\r\n * Full contract: adr/specs/component-menu-spec.md.\r\n */\r\n@Component({\r\n selector: 'skra-menu',\r\n standalone: true,\r\n exportAs: 'skraMenu',\r\n template: '<ng-content></ng-content>',\r\n styleUrl: './menu.scss',\r\n changeDetection: ChangeDetectionStrategy.OnPush,\r\n encapsulation: ViewEncapsulation.Emulated,\r\n providers: [{ provide: SKRA_MENU, useExisting: SkraMenu }],\r\n host: {\r\n role: 'menu',\r\n // Native top layer + platform-owned toggle / light-dismiss / Escape / stacking (ADR 0044).\r\n popover: 'auto',\r\n // NOTE: `toggle` and `keydown` are wired IMPERATIVELY in the constructor, NOT as host listeners.\r\n // A consumer template binding `(toggle)` on <skra-menu> (the 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` is independent of Angular's host/template listener slot, so\r\n // it always fires alongside the consumer's binding.\r\n },\r\n})\r\nexport class SkraMenu implements SkraMenuPanel {\r\n /** Placement intent; `flip`/`shift` adjust at runtime. */\r\n readonly placement = input<SkraPlacement>('bottom');\r\n\r\n private readonly menuItems = contentChildren(SkraMenuItem, {\r\n descendants: true,\r\n });\r\n\r\n private readonly host = inject<ElementRef<HTMLElement>>(ElementRef);\r\n private readonly renderer = inject(Renderer2);\r\n private readonly keyboard = new SkraMenuKeyboard();\r\n\r\n /** The consumer's `[popovertarget]` button, resolved once (lazily-idempotent via `resolved`). */\r\n private trigger: HTMLElement | null = null;\r\n private resolved = false;\r\n private positioner?: _PositionerHandle;\r\n\r\n constructor() {\r\n // Feed the controller whenever the projected item set changes (signal → array snapshot rebuild).\r\n effect(() => this.keyboard.setItems(this.menuItems()));\r\n // APG: Tab/Shift+Tab dismisses the menu; native Tab then proceeds from the trigger.\r\n this.keyboard.onTabOut(() => this.host.nativeElement.hidePopover?.());\r\n\r\n // All imperative DOM wiring lives in afterNextRender — a browser-only hook, so the native listeners\r\n // and the trigger resolution never run on the server (SSR-safe by construction; O-18). The listeners\r\n // attach right after first paint, long before any user interaction can fire them.\r\n afterNextRender(() => {\r\n const el = this.host.nativeElement;\r\n // Gate disabled items at the CAPTURE phase: a component host listener registers AFTER the parent\r\n // template's `(click)` on the same button, so a bubble-phase guard cannot stop the consumer's own\r\n // handler. A capture listener on the menu host fires before the target's listeners — preventing both\r\n // the consumer's `(click)` and the item's `onActivate` for an `aria-disabled` item.\r\n el.addEventListener('click', this.onCaptureClick, true);\r\n // Imperative (NOT host-metadata) so a consumer `(toggle)` binding does not shadow them — see host {}.\r\n el.addEventListener('toggle', this.onToggleNative);\r\n el.addEventListener('keydown', this.onKeydownNative);\r\n\r\n // Resolve + wire the trigger after the view exists (it may be a later sibling), so the disclosure\r\n // relationship (aria-haspopup + aria-controls + aria-expanded=\"false\") is present before first open.\r\n this.ensureTrigger();\r\n if (typeof ngDevMode === 'undefined' || ngDevMode) this.warnIfEmpty();\r\n });\r\n inject(DestroyRef).onDestroy(() => this.teardown());\r\n }\r\n\r\n /** SKRA_MENU contract — close on item activation (native focus-return to the trigger). */\r\n requestClose(): void {\r\n this.host.nativeElement.hidePopover?.();\r\n }\r\n\r\n /** Capture-phase guard: block clicks on an `aria-disabled` item before they reach its listeners. */\r\n private readonly onCaptureClick = (event: Event): void => {\r\n const item = (event.target as HTMLElement | null)?.closest?.(\r\n '[skraMenuItem]',\r\n );\r\n if (item?.getAttribute('aria-disabled') === 'true') {\r\n event.preventDefault();\r\n event.stopPropagation();\r\n }\r\n };\r\n\r\n // ── Toggle / keydown (imperative native listeners — 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 readonly onKeydownNative = (event: Event): void => {\r\n this.keyboard.onKeydown(event as KeyboardEvent);\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 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-menu-offset'),\r\n viewportPadding: this.cssLenPx('--skra-comp-menu-viewport-padding'),\r\n }),\r\n );\r\n }\r\n // Focus enters the menu (APG Menu Button) regardless of how it opened — unlike non-modal Popover.\r\n this.keyboard.focusFirst();\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 menu disclosure ARIA on it (sanctioned a11y\r\n * wiring, ADR 013-style — not style mutation). Idempotent. Returns the trigger (or null for an orphan\r\n * menu, dev-warned once).\r\n */\r\n private ensureTrigger(): HTMLElement | null {\r\n if (this.resolved) return this.trigger;\r\n this.resolved = true;\r\n\r\n const id = this.host.nativeElement.id;\r\n if (!id) {\r\n if (typeof ngDevMode === 'undefined' || ngDevMode) {\r\n console.warn(\r\n '[skra-menu] has no `id` — it cannot be targeted by a `popovertarget` trigger and the ' +\r\n 'disclosure ARIA (aria-haspopup/aria-controls/aria-expanded) cannot be wired. Give the ' +\r\n 'menu an id and point a button at it: ' +\r\n '<button popovertarget=\"my-id\" aria-haspopup=\"menu\">…</button> <skra-menu id=\"my-id\">…',\r\n );\r\n }\r\n return null;\r\n }\r\n\r\n // Match by attribute value (no escaping; robust to special chars; `CSS.escape` is not universal).\r\n this.trigger =\r\n Array.from(\r\n document.querySelectorAll<HTMLElement>('[popovertarget]'),\r\n ).find((el) => el.getAttribute('popovertarget') === id) ?? null;\r\n if (!this.trigger) {\r\n if (typeof ngDevMode === 'undefined' || ngDevMode) {\r\n console.warn(\r\n `[skra-menu] no trigger found with [popovertarget=\"${id}\"] — the menu is unreachable and ` +\r\n 'has no element to carry aria-haspopup/aria-controls/aria-expanded. Add a focusable trigger ' +\r\n 'button with the matching popovertarget.',\r\n );\r\n }\r\n return null;\r\n }\r\n\r\n // Establish the Menu Button relationship before the first open (APG; aria-haspopup distinguishes a\r\n // menu from Popover's generic disclosure).\r\n this.renderer.setAttribute(this.trigger, 'aria-haspopup', 'menu');\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 /** Dev-only nudge: a role=\"menu\" with no menuitems is invalid — a link-list/content panel is a Popover. */\r\n private warnIfEmpty(): void {\r\n if (this.menuItems().length === 0) {\r\n console.warn(\r\n '[skra-menu] has no `button[skraMenuItem]` children — a `role=\"menu\"` with no menu items is ' +\r\n 'invalid. If this is a panel of arbitrary content or a list of navigation links, use ' +\r\n '<skra-popover> (disclosure) instead — see the Menu-vs-Popover boundary in the spec.',\r\n );\r\n }\r\n }\r\n\r\n private teardown(): void {\r\n const el = this.host.nativeElement;\r\n el.removeEventListener('click', this.onCaptureClick, true);\r\n el.removeEventListener('toggle', this.onToggleNative);\r\n el.removeEventListener('keydown', this.onKeydownNative);\r\n this.keyboard.destroy();\r\n this.positioner?.destroy();\r\n if (this.trigger) {\r\n this.renderer.removeAttribute(this.trigger, 'aria-haspopup');\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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"}
@@ -23,21 +23,25 @@ export { SkraOption, SkraOptionDetail, SkraOptionLeading, SkraSelectTrigger } fr
23
23
  */
24
24
  class SkraMultiSelect extends _SkraSelectBase {
25
25
  /** Consumer-owned ARRAY value channel (ADR 037) — default `[]` (never undefined). */
26
- value = input([], ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
26
+ value = input([], /* @ts-ignore */
27
+ ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
27
28
  /** Fires ONLY on user toggle — emits a NEW array; never on a programmatic `value()` change (ADR 037). */
28
29
  valueChange = output();
29
30
  /** Cardinality hint (ADR 0046) — multi-select (drives the option checkbox indicator + aria-multiselectable). */
30
31
  multiple = true;
31
32
  /** The options whose value is a member of `value()` — feeds the trigger summary + seed. */
32
- selectedOptions = computed(() => this.options().filter((o) => this.isSelected(o.value())), ...(ngDevMode ? [{ debugName: "selectedOptions" }] : /* istanbul ignore next */ []));
33
+ selectedOptions = computed(() => this.options().filter((o) => this.isSelected(o.value())), /* @ts-ignore */
34
+ ...(ngDevMode ? [{ debugName: "selectedOptions" }] : /* istanbul ignore next */ []));
33
35
  /** Comma-joined identity labels of the selected options (value display, ADR 0045 — language-neutral). */
34
36
  summary = computed(() => this.selectedOptions()
35
37
  .map((o) => o.getLabel())
36
- .join(', '), ...(ngDevMode ? [{ debugName: "summary" }] : /* istanbul ignore next */ []));
38
+ .join(', '), /* @ts-ignore */
39
+ ...(ngDevMode ? [{ debugName: "summary" }] : /* istanbul ignore next */ []));
37
40
  triggerContext = computed(() => ({
38
41
  $implicit: this.value(),
39
42
  options: this.selectedOptions(),
40
- }), ...(ngDevMode ? [{ debugName: "triggerContext" }] : /* istanbul ignore next */ []));
43
+ }), /* @ts-ignore */
44
+ ...(ngDevMode ? [{ debugName: "triggerContext" }] : /* istanbul ignore next */ []));
41
45
  constructor() {
42
46
  super();
43
47
  if (typeof ngDevMode === 'undefined' || ngDevMode) {
@@ -71,103 +75,103 @@ class SkraMultiSelect extends _SkraSelectBase {
71
75
  activeSeedItem() {
72
76
  return this.selectedOptions()[0] ?? null;
73
77
  }
74
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: SkraMultiSelect, deps: [], target: i0.ɵɵFactoryTarget.Component });
75
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.7", type: SkraMultiSelect, isStandalone: true, selector: "skra-multi-select", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "valueChange" }, providers: [{ provide: SKRA_LISTBOX_PANEL, useExisting: SkraMultiSelect }], exportAs: ["skraMultiSelect"], usesInheritance: true, ngImport: i0, template: `
76
- <button
77
- #trigger
78
- class="skra-select-trigger"
79
- type="button"
80
- role="combobox"
81
- aria-haspopup="listbox"
82
- [attr.id]="triggerId()"
83
- [attr.popovertarget]="panelId"
84
- [attr.aria-controls]="panelId"
85
- [attr.aria-expanded]="isOpen()"
86
- [attr.aria-activedescendant]="activeOptionId()"
87
- [attr.aria-describedby]="describedBy()"
88
- [attr.aria-invalid]="hasError() ? 'true' : null"
89
- [attr.aria-required]="isRequired() ? 'true' : null"
90
- [attr.aria-readonly]="isReadonly() ? 'true' : null"
91
- [disabled]="isDisabled()"
92
- (click)="onTriggerClick($event)"
93
- (keydown)="onTriggerKeydown($event)"
94
- >
95
- <span class="skra-select-trigger-value">
96
- @if (triggerTemplate(); as tpl) {
97
- <ng-container
98
- [ngTemplateOutlet]="tpl"
99
- [ngTemplateOutletContext]="triggerContext()"
100
- ></ng-container>
101
- } @else if (selectedOptions().length) {
102
- {{ summary() }}
103
- } @else {
104
- <span class="skra-select-placeholder">{{ placeholder() }}</span>
105
- }
106
- </span>
107
- <span class="skra-select-arrow" aria-hidden="true"></span>
108
- </button>
109
-
110
- <div
111
- #panel
112
- class="skra-select-listbox"
113
- [id]="panelId"
114
- role="listbox"
115
- aria-multiselectable="true"
116
- popover="auto"
117
- (toggle)="onPanelToggle($event)"
118
- >
119
- <ng-content></ng-content>
120
- </div>
78
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.1", ngImport: i0, type: SkraMultiSelect, deps: [], target: i0.ɵɵFactoryTarget.Component });
79
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.1", type: SkraMultiSelect, isStandalone: true, selector: "skra-multi-select", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "valueChange" }, providers: [{ provide: SKRA_LISTBOX_PANEL, useExisting: SkraMultiSelect }], exportAs: ["skraMultiSelect"], usesInheritance: true, ngImport: i0, template: `
80
+ <button
81
+ #trigger
82
+ class="skra-select-trigger"
83
+ type="button"
84
+ role="combobox"
85
+ aria-haspopup="listbox"
86
+ [attr.id]="triggerId()"
87
+ [attr.popovertarget]="panelId"
88
+ [attr.aria-controls]="panelId"
89
+ [attr.aria-expanded]="isOpen()"
90
+ [attr.aria-activedescendant]="activeOptionId()"
91
+ [attr.aria-describedby]="describedBy()"
92
+ [attr.aria-invalid]="hasError() ? 'true' : null"
93
+ [attr.aria-required]="isRequired() ? 'true' : null"
94
+ [attr.aria-readonly]="isReadonly() ? 'true' : null"
95
+ [disabled]="isDisabled()"
96
+ (click)="onTriggerClick($event)"
97
+ (keydown)="onTriggerKeydown($event)"
98
+ >
99
+ <span class="skra-select-trigger-value">
100
+ @if (triggerTemplate(); as tpl) {
101
+ <ng-container
102
+ [ngTemplateOutlet]="tpl"
103
+ [ngTemplateOutletContext]="triggerContext()"
104
+ ></ng-container>
105
+ } @else if (selectedOptions().length) {
106
+ {{ summary() }}
107
+ } @else {
108
+ <span class="skra-select-placeholder">{{ placeholder() }}</span>
109
+ }
110
+ </span>
111
+ <span class="skra-select-arrow" aria-hidden="true"></span>
112
+ </button>
113
+
114
+ <div
115
+ #panel
116
+ class="skra-select-listbox"
117
+ [id]="panelId"
118
+ role="listbox"
119
+ aria-multiselectable="true"
120
+ popover="auto"
121
+ (toggle)="onPanelToggle($event)"
122
+ >
123
+ <ng-content></ng-content>
124
+ </div>
121
125
  `, isInline: true, styles: ["@charset \"UTF-8\";:host{display:block;box-sizing:border-box;margin:0}.skra-select-trigger{display:flex;align-items:center;gap:var(--skra-comp-select-gap);inline-size:100%;min-block-size:var(--skra-comp-select-min-block-size);box-sizing:border-box;margin:0;padding-block:var(--skra-comp-select-padding-block);padding-inline:var(--skra-comp-select-padding-inline);text-align:start;cursor:pointer;background:var(--skra-comp-select-background);color:var(--skra-comp-select-foreground);border-color:var(--skra-comp-select-border-color);border-width:var(--skra-comp-select-border-width);border-style:var(--skra-comp-select-border-style);border-radius:var(--skra-comp-select-radius);box-shadow:var(--skra-comp-select-shadow);font-family:var(--skra-sys-typography-role-body-medium-font-family);font-size:var(--skra-sys-typography-role-body-medium-font-size);font-weight:var(--skra-sys-typography-role-body-medium-font-weight);line-height:var(--skra-sys-typography-role-body-medium-line-height);letter-spacing:var(--skra-sys-typography-role-body-medium-letter-spacing);transition:background-color var(--skra-comp-select-motion-duration) var(--skra-comp-select-motion-easing),border-color var(--skra-comp-select-motion-duration) var(--skra-comp-select-motion-easing)}.skra-select-trigger:hover:not(:disabled):not([aria-readonly=true]){background:var(--skra-comp-select-background-hover)}.skra-select-trigger:focus-visible{outline:var(--skra-comp-select-focus-ring-width) solid var(--skra-comp-select-focus-ring-color);outline-offset:.125rem}.skra-select-trigger:disabled{background:var(--skra-comp-select-background-disabled);color:var(--skra-comp-select-foreground-disabled);cursor:not-allowed}.skra-select-trigger[aria-readonly=true]{background:var(--skra-comp-select-background-readonly);cursor:default}.skra-select-trigger[aria-invalid=true]{border-color:var(--skra-comp-select-border-color-validation-invalid)}.skra-select-trigger-value{flex:1 1 auto;min-inline-size:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;--skra-comp-icon-size: var(--skra-comp-select-icon-size)}.skra-select-placeholder{color:var(--skra-comp-select-foreground-disabled)}.skra-select-arrow{flex:none;inline-size:var(--skra-comp-select-arrow-icon-size);block-size:var(--skra-comp-select-arrow-icon-size);background-color:var(--skra-comp-select-arrow-color);mask:url(\"data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3E%3Cpath%20d='M6%209l6%206%206-6'%20fill='none'%20stroke='%23000'%20stroke-width='2'/%3E%3C/svg%3E\") no-repeat center/contain;-webkit-mask:url(\"data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3E%3Cpath%20d='M6%209l6%206%206-6'%20fill='none'%20stroke='%23000'%20stroke-width='2'/%3E%3C/svg%3E\") no-repeat center/contain;transition:transform var(--skra-comp-select-motion-duration) var(--skra-comp-select-motion-easing)}.skra-select-trigger[aria-expanded=true] .skra-select-arrow{transform:rotate(180deg)}.skra-select-listbox{position:fixed;margin:0;inset:auto;box-sizing:border-box;padding-block:var(--skra-comp-select-panel-padding-block);padding-inline:var(--skra-comp-select-panel-padding-inline);background:var(--skra-comp-select-panel-background);color:var(--skra-comp-select-panel-foreground);border:var(--skra-comp-select-border-width) solid var(--skra-comp-select-panel-border-color);border-radius:var(--skra-comp-select-panel-radius);box-shadow:var(--skra-comp-select-panel-shadow);inline-size:max-content;max-block-size:var(--skra-comp-select-panel-max-block-size);overflow-y:auto;flex-direction:column;opacity:0;transition:opacity var(--skra-comp-select-motion-duration) var(--skra-comp-select-motion-easing),display var(--skra-comp-select-motion-duration) allow-discrete,overlay var(--skra-comp-select-motion-duration) allow-discrete}.skra-select-listbox:popover-open{display:flex;opacity:1}@starting-style{.skra-select-listbox:popover-open{opacity:0}}@media(prefers-reduced-motion:reduce){.skra-select-trigger,.skra-select-arrow,.skra-select-listbox{transition-duration:0ms}}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
122
126
  }
123
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: SkraMultiSelect, decorators: [{
127
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.1", ngImport: i0, type: SkraMultiSelect, decorators: [{
124
128
  type: Component,
125
- args: [{ selector: 'skra-multi-select', standalone: true, exportAs: 'skraMultiSelect', imports: [NgTemplateOutlet], template: `
126
- <button
127
- #trigger
128
- class="skra-select-trigger"
129
- type="button"
130
- role="combobox"
131
- aria-haspopup="listbox"
132
- [attr.id]="triggerId()"
133
- [attr.popovertarget]="panelId"
134
- [attr.aria-controls]="panelId"
135
- [attr.aria-expanded]="isOpen()"
136
- [attr.aria-activedescendant]="activeOptionId()"
137
- [attr.aria-describedby]="describedBy()"
138
- [attr.aria-invalid]="hasError() ? 'true' : null"
139
- [attr.aria-required]="isRequired() ? 'true' : null"
140
- [attr.aria-readonly]="isReadonly() ? 'true' : null"
141
- [disabled]="isDisabled()"
142
- (click)="onTriggerClick($event)"
143
- (keydown)="onTriggerKeydown($event)"
144
- >
145
- <span class="skra-select-trigger-value">
146
- @if (triggerTemplate(); as tpl) {
147
- <ng-container
148
- [ngTemplateOutlet]="tpl"
149
- [ngTemplateOutletContext]="triggerContext()"
150
- ></ng-container>
151
- } @else if (selectedOptions().length) {
152
- {{ summary() }}
153
- } @else {
154
- <span class="skra-select-placeholder">{{ placeholder() }}</span>
155
- }
156
- </span>
157
- <span class="skra-select-arrow" aria-hidden="true"></span>
158
- </button>
159
-
160
- <div
161
- #panel
162
- class="skra-select-listbox"
163
- [id]="panelId"
164
- role="listbox"
165
- aria-multiselectable="true"
166
- popover="auto"
167
- (toggle)="onPanelToggle($event)"
168
- >
169
- <ng-content></ng-content>
170
- </div>
129
+ args: [{ selector: 'skra-multi-select', standalone: true, exportAs: 'skraMultiSelect', imports: [NgTemplateOutlet], template: `
130
+ <button
131
+ #trigger
132
+ class="skra-select-trigger"
133
+ type="button"
134
+ role="combobox"
135
+ aria-haspopup="listbox"
136
+ [attr.id]="triggerId()"
137
+ [attr.popovertarget]="panelId"
138
+ [attr.aria-controls]="panelId"
139
+ [attr.aria-expanded]="isOpen()"
140
+ [attr.aria-activedescendant]="activeOptionId()"
141
+ [attr.aria-describedby]="describedBy()"
142
+ [attr.aria-invalid]="hasError() ? 'true' : null"
143
+ [attr.aria-required]="isRequired() ? 'true' : null"
144
+ [attr.aria-readonly]="isReadonly() ? 'true' : null"
145
+ [disabled]="isDisabled()"
146
+ (click)="onTriggerClick($event)"
147
+ (keydown)="onTriggerKeydown($event)"
148
+ >
149
+ <span class="skra-select-trigger-value">
150
+ @if (triggerTemplate(); as tpl) {
151
+ <ng-container
152
+ [ngTemplateOutlet]="tpl"
153
+ [ngTemplateOutletContext]="triggerContext()"
154
+ ></ng-container>
155
+ } @else if (selectedOptions().length) {
156
+ {{ summary() }}
157
+ } @else {
158
+ <span class="skra-select-placeholder">{{ placeholder() }}</span>
159
+ }
160
+ </span>
161
+ <span class="skra-select-arrow" aria-hidden="true"></span>
162
+ </button>
163
+
164
+ <div
165
+ #panel
166
+ class="skra-select-listbox"
167
+ [id]="panelId"
168
+ role="listbox"
169
+ aria-multiselectable="true"
170
+ popover="auto"
171
+ (toggle)="onPanelToggle($event)"
172
+ >
173
+ <ng-content></ng-content>
174
+ </div>
171
175
  `, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.Emulated, providers: [{ provide: SKRA_LISTBOX_PANEL, useExisting: SkraMultiSelect }], styles: ["@charset \"UTF-8\";:host{display:block;box-sizing:border-box;margin:0}.skra-select-trigger{display:flex;align-items:center;gap:var(--skra-comp-select-gap);inline-size:100%;min-block-size:var(--skra-comp-select-min-block-size);box-sizing:border-box;margin:0;padding-block:var(--skra-comp-select-padding-block);padding-inline:var(--skra-comp-select-padding-inline);text-align:start;cursor:pointer;background:var(--skra-comp-select-background);color:var(--skra-comp-select-foreground);border-color:var(--skra-comp-select-border-color);border-width:var(--skra-comp-select-border-width);border-style:var(--skra-comp-select-border-style);border-radius:var(--skra-comp-select-radius);box-shadow:var(--skra-comp-select-shadow);font-family:var(--skra-sys-typography-role-body-medium-font-family);font-size:var(--skra-sys-typography-role-body-medium-font-size);font-weight:var(--skra-sys-typography-role-body-medium-font-weight);line-height:var(--skra-sys-typography-role-body-medium-line-height);letter-spacing:var(--skra-sys-typography-role-body-medium-letter-spacing);transition:background-color var(--skra-comp-select-motion-duration) var(--skra-comp-select-motion-easing),border-color var(--skra-comp-select-motion-duration) var(--skra-comp-select-motion-easing)}.skra-select-trigger:hover:not(:disabled):not([aria-readonly=true]){background:var(--skra-comp-select-background-hover)}.skra-select-trigger:focus-visible{outline:var(--skra-comp-select-focus-ring-width) solid var(--skra-comp-select-focus-ring-color);outline-offset:.125rem}.skra-select-trigger:disabled{background:var(--skra-comp-select-background-disabled);color:var(--skra-comp-select-foreground-disabled);cursor:not-allowed}.skra-select-trigger[aria-readonly=true]{background:var(--skra-comp-select-background-readonly);cursor:default}.skra-select-trigger[aria-invalid=true]{border-color:var(--skra-comp-select-border-color-validation-invalid)}.skra-select-trigger-value{flex:1 1 auto;min-inline-size:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;--skra-comp-icon-size: var(--skra-comp-select-icon-size)}.skra-select-placeholder{color:var(--skra-comp-select-foreground-disabled)}.skra-select-arrow{flex:none;inline-size:var(--skra-comp-select-arrow-icon-size);block-size:var(--skra-comp-select-arrow-icon-size);background-color:var(--skra-comp-select-arrow-color);mask:url(\"data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3E%3Cpath%20d='M6%209l6%206%206-6'%20fill='none'%20stroke='%23000'%20stroke-width='2'/%3E%3C/svg%3E\") no-repeat center/contain;-webkit-mask:url(\"data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3E%3Cpath%20d='M6%209l6%206%206-6'%20fill='none'%20stroke='%23000'%20stroke-width='2'/%3E%3C/svg%3E\") no-repeat center/contain;transition:transform var(--skra-comp-select-motion-duration) var(--skra-comp-select-motion-easing)}.skra-select-trigger[aria-expanded=true] .skra-select-arrow{transform:rotate(180deg)}.skra-select-listbox{position:fixed;margin:0;inset:auto;box-sizing:border-box;padding-block:var(--skra-comp-select-panel-padding-block);padding-inline:var(--skra-comp-select-panel-padding-inline);background:var(--skra-comp-select-panel-background);color:var(--skra-comp-select-panel-foreground);border:var(--skra-comp-select-border-width) solid var(--skra-comp-select-panel-border-color);border-radius:var(--skra-comp-select-panel-radius);box-shadow:var(--skra-comp-select-panel-shadow);inline-size:max-content;max-block-size:var(--skra-comp-select-panel-max-block-size);overflow-y:auto;flex-direction:column;opacity:0;transition:opacity var(--skra-comp-select-motion-duration) var(--skra-comp-select-motion-easing),display var(--skra-comp-select-motion-duration) allow-discrete,overlay var(--skra-comp-select-motion-duration) allow-discrete}.skra-select-listbox:popover-open{display:flex;opacity:1}@starting-style{.skra-select-listbox:popover-open{opacity:0}}@media(prefers-reduced-motion:reduce){.skra-select-trigger,.skra-select-arrow,.skra-select-listbox{transition-duration:0ms}}\n"] }]
172
176
  }], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], valueChange: [{ type: i0.Output, args: ["valueChange"] }] } });
173
177
 
@@ -1 +1 @@
1
- {"version":3,"file":"skra-ui-multi-select.mjs","sources":["../../../../libs/ui/multi-select/src/lib/multi-select/multi-select.ts","../../../../libs/ui/multi-select/src/skra-ui-multi-select.ts"],"sourcesContent":["import { NgTemplateOutlet } from '@angular/common';\nimport {\n ChangeDetectionStrategy,\n Component,\n ViewEncapsulation,\n afterRenderEffect,\n computed,\n input,\n output,\n} from '@angular/core';\n\nimport { _SkraSelectBase } from '@skra/ui/core';\nimport {\n SKRA_LISTBOX_PANEL,\n type SkraListboxKeyboardItem,\n} from '@skra/ui/core';\n\ndeclare const ngDevMode: boolean | undefined;\n\n/**\n * SkraMultiSelect — a custom MULTI-select listbox: the WAI-ARIA APG multi-selectable listbox combobox.\n * The multi-cardinality sibling of `skra-select` over the shared `_SkraSelectBase` (**ADR 0046** —\n * cardinality split: a separate component with an HONEST `readonly T[]` value contract, NOT a `multiple`\n * flag). The trigger (`role=\"combobox\"`) opens a top-layer listbox panel (`popover=\"auto\"`, ADR 0044,\n * `aria-multiselectable=\"true\"`) of the SAME projected `skra-option` children — reused verbatim; the\n * option swaps its indicator to a checkbox via the `SkraListboxPanel.multiple` hint.\n *\n * - **Value (ADR 037):** `value = input<readonly T[]>([])` + `valueChange = output<readonly T[]>()` —\n * consumer-owned, no `@angular/forms`. Selection is membership (`compareWith` per element).\n * - **Toggle (ADR 0046):** picking an option toggles its membership and KEEPS THE PANEL OPEN (the one\n * behavioural divergence from single-select, which closes).\n * - **Trigger summary (ADR 0045 / 033):** comma-joined `getLabel()` of selected options — the VALUE\n * display, never the accessible name. Count-style / chips → the opt-in `[skraSelectTrigger]` recipe.\n *\n * Full contract: adr/specs/component-multi-select-spec.md.\n */\n@Component({\n selector: 'skra-multi-select',\n standalone: true,\n exportAs: 'skraMultiSelect',\n imports: [NgTemplateOutlet],\n template: `\n <button\n #trigger\n class=\"skra-select-trigger\"\n type=\"button\"\n role=\"combobox\"\n aria-haspopup=\"listbox\"\n [attr.id]=\"triggerId()\"\n [attr.popovertarget]=\"panelId\"\n [attr.aria-controls]=\"panelId\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-activedescendant]=\"activeOptionId()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-invalid]=\"hasError() ? 'true' : null\"\n [attr.aria-required]=\"isRequired() ? 'true' : null\"\n [attr.aria-readonly]=\"isReadonly() ? 'true' : null\"\n [disabled]=\"isDisabled()\"\n (click)=\"onTriggerClick($event)\"\n (keydown)=\"onTriggerKeydown($event)\"\n >\n <span class=\"skra-select-trigger-value\">\n @if (triggerTemplate(); as tpl) {\n <ng-container\n [ngTemplateOutlet]=\"tpl\"\n [ngTemplateOutletContext]=\"triggerContext()\"\n ></ng-container>\n } @else if (selectedOptions().length) {\n {{ summary() }}\n } @else {\n <span class=\"skra-select-placeholder\">{{ placeholder() }}</span>\n }\n </span>\n <span class=\"skra-select-arrow\" aria-hidden=\"true\"></span>\n </button>\n\n <div\n #panel\n class=\"skra-select-listbox\"\n [id]=\"panelId\"\n role=\"listbox\"\n aria-multiselectable=\"true\"\n popover=\"auto\"\n (toggle)=\"onPanelToggle($event)\"\n >\n <ng-content></ng-content>\n </div>\n `,\n styleUrl: './multi-select.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n encapsulation: ViewEncapsulation.Emulated,\n providers: [{ provide: SKRA_LISTBOX_PANEL, useExisting: SkraMultiSelect }],\n})\nexport class SkraMultiSelect<T = unknown> extends _SkraSelectBase<T> {\n /** Consumer-owned ARRAY value channel (ADR 037) — default `[]` (never undefined). */\n readonly value = input<readonly T[]>([]);\n /** Fires ONLY on user toggle — emits a NEW array; never on a programmatic `value()` change (ADR 037). */\n readonly valueChange = output<readonly T[]>();\n /** Cardinality hint (ADR 0046) — multi-select (drives the option checkbox indicator + aria-multiselectable). */\n readonly multiple = true;\n\n /** The options whose value is a member of `value()` — feeds the trigger summary + seed. */\n protected readonly selectedOptions = computed(() =>\n this.options().filter((o) => this.isSelected(o.value())),\n );\n\n /** Comma-joined identity labels of the selected options (value display, ADR 0045 — language-neutral). */\n protected readonly summary = computed(() =>\n this.selectedOptions()\n .map((o) => o.getLabel())\n .join(', '),\n );\n\n protected readonly triggerContext = computed(() => ({\n $implicit: this.value(),\n options: this.selectedOptions(),\n }));\n\n constructor() {\n super();\n if (typeof ngDevMode === 'undefined' || ngDevMode) {\n // Dev-only consumer warning. Uses afterRenderEffect (NOT effect) so the projected\n // options' required `value` inputs are read only AFTER each render applies their\n // bindings — an eager read during the initial CD (SSR / first render) OR a runtime\n // re-projection would throw NG0950. afterRender hooks never run on the server, so\n // this stays SSR-safe. Covers F-13 (initial render) + F-16 (runtime re-projection).\n afterRenderEffect(() => {\n const opts = this.options();\n const unmatched = this.value().filter(\n (v) => !opts.some((o) => this.compareWith()(o.value() as T, v)),\n );\n if (unmatched.length) {\n console.warn(\n '[skra-multi-select] value contains entries with no matching <skra-option> — they are ' +\n 'dropped from the trigger summary (likely a compareWith mismatch for object values).',\n );\n }\n });\n }\n }\n\n // ── SkraListboxPanel seam ──\n isSelected(value: unknown): boolean {\n return this.value().some((x) => this.compareWith()(x, value as T));\n }\n\n selectValue(value: unknown): void {\n const present = this.isSelected(value);\n const next = present\n ? this.value().filter((x) => !this.compareWith()(x, value as T))\n : [...this.value(), value as T];\n this.valueChange.emit(next);\n // NO close() — multi-select keeps the panel open for further toggling (ADR 0046).\n }\n\n protected activeSeedItem(): SkraListboxKeyboardItem | null {\n return this.selectedOptions()[0] ?? null;\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;;;;AAmBA;;;;;;;;;;;;;;;;AAgBG;AA0DG,MAAO,eAA6B,SAAQ,eAAkB,CAAA;;AAEzD,IAAA,KAAK,GAAG,KAAK,CAAe,EAAE,4EAAC;;IAE/B,WAAW,GAAG,MAAM,EAAgB;;IAEpC,QAAQ,GAAG,IAAI;;AAGL,IAAA,eAAe,GAAG,QAAQ,CAAC,MAC5C,IAAI,CAAC,OAAO,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,sFACzD;;IAGkB,OAAO,GAAG,QAAQ,CAAC,MACpC,IAAI,CAAC,eAAe;SACjB,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,QAAQ,EAAE;AACvB,SAAA,IAAI,CAAC,IAAI,CAAC,EAAA,IAAA,SAAA,GAAA,CAAA,EAAA,SAAA,EAAA,SAAA,EAAA,CAAA,8BAAA,EAAA,CAAA,CACd;AAEkB,IAAA,cAAc,GAAG,QAAQ,CAAC,OAAO;AAClD,QAAA,SAAS,EAAE,IAAI,CAAC,KAAK,EAAE;AACvB,QAAA,OAAO,EAAE,IAAI,CAAC,eAAe,EAAE;AAChC,KAAA,CAAC,qFAAC;AAEH,IAAA,WAAA,GAAA;AACE,QAAA,KAAK,EAAE;AACP,QAAA,IAAI,OAAO,SAAS,KAAK,WAAW,IAAI,SAAS,EAAE;;;;;;YAMjD,iBAAiB,CAAC,MAAK;AACrB,gBAAA,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,EAAE;AAC3B,gBAAA,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC,MAAM,CACnC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,KAAK,EAAO,EAAE,CAAC,CAAC,CAAC,CAChE;AACD,gBAAA,IAAI,SAAS,CAAC,MAAM,EAAE;oBACpB,OAAO,CAAC,IAAI,CACV,uFAAuF;AACrF,wBAAA,qFAAqF,CACxF;gBACH;AACF,YAAA,CAAC,CAAC;QACJ;IACF;;AAGA,IAAA,UAAU,CAAC,KAAc,EAAA;QACvB,OAAO,IAAI,CAAC,KAAK,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,EAAE,KAAU,CAAC,CAAC;IACpE;AAEA,IAAA,WAAW,CAAC,KAAc,EAAA;QACxB,MAAM,OAAO,GAAG,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC;QACtC,MAAM,IAAI,GAAG;cACT,IAAI,CAAC,KAAK,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,EAAE,KAAU,CAAC;cAC7D,CAAC,GAAG,IAAI,CAAC,KAAK,EAAE,EAAE,KAAU,CAAC;AACjC,QAAA,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC;;IAE7B;IAEU,cAAc,GAAA;QACtB,OAAO,IAAI,CAAC,eAAe,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI;IAC1C;uGAhEW,eAAe,EAAA,IAAA,EAAA,EAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA;AAAf,IAAA,OAAA,IAAA,GAAA,EAAA,CAAA,oBAAA,CAAA,EAAA,UAAA,EAAA,QAAA,EAAA,OAAA,EAAA,QAAA,EAAA,IAAA,EAAA,eAAe,EAAA,YAAA,EAAA,IAAA,EAAA,QAAA,EAAA,mBAAA,EAAA,MAAA,EAAA,EAAA,KAAA,EAAA,EAAA,iBAAA,EAAA,OAAA,EAAA,UAAA,EAAA,OAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,KAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,EAAA,OAAA,EAAA,EAAA,WAAA,EAAA,aAAA,EAAA,EAAA,SAAA,EAFf,CAAC,EAAE,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,eAAe,EAAE,CAAC,EAAA,QAAA,EAAA,CAAA,iBAAA,CAAA,EAAA,eAAA,EAAA,IAAA,EAAA,QAAA,EAAA,EAAA,EAAA,QAAA,EAlDhE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8CT,EAAA,CAAA,EAAA,QAAA,EAAA,IAAA,EAAA,MAAA,EAAA,CAAA,g/HAAA,CAAA,EAAA,YAAA,EAAA,CAAA,EAAA,IAAA,EAAA,WAAA,EAAA,IAAA,EA/CS,gBAAgB,EAAA,QAAA,EAAA,oBAAA,EAAA,MAAA,EAAA,CAAA,yBAAA,EAAA,kBAAA,EAAA,0BAAA,CAAA,EAAA,CAAA,EAAA,eAAA,EAAA,EAAA,CAAA,uBAAA,CAAA,MAAA,EAAA,CAAA;;2FAqDf,eAAe,EAAA,UAAA,EAAA,CAAA;kBAzD3B,SAAS;+BACE,mBAAmB,EAAA,UAAA,EACjB,IAAI,EAAA,QAAA,EACN,iBAAiB,WAClB,CAAC,gBAAgB,CAAC,EAAA,QAAA,EACjB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8CT,EAAA,CAAA,EAAA,eAAA,EAEgB,uBAAuB,CAAC,MAAM,EAAA,aAAA,EAChC,iBAAiB,CAAC,QAAQ,EAAA,SAAA,EAC9B,CAAC,EAAE,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAA,eAAiB,EAAE,CAAC,EAAA,MAAA,EAAA,CAAA,g/HAAA,CAAA,EAAA;;;AC3F5E;;AAEG;;;;"}
1
+ {"version":3,"file":"skra-ui-multi-select.mjs","sources":["../../../../libs/ui/multi-select/src/lib/multi-select/multi-select.ts","../../../../libs/ui/multi-select/src/skra-ui-multi-select.ts"],"sourcesContent":["import { NgTemplateOutlet } from '@angular/common';\r\nimport {\r\n ChangeDetectionStrategy,\r\n Component,\r\n ViewEncapsulation,\r\n afterRenderEffect,\r\n computed,\r\n input,\r\n output,\r\n} from '@angular/core';\r\n\r\nimport { _SkraSelectBase } from '@skra/ui/core';\r\nimport {\r\n SKRA_LISTBOX_PANEL,\r\n type SkraListboxKeyboardItem,\r\n} from '@skra/ui/core';\r\n\r\ndeclare const ngDevMode: boolean | undefined;\r\n\r\n/**\r\n * SkraMultiSelect — a custom MULTI-select listbox: the WAI-ARIA APG multi-selectable listbox combobox.\r\n * The multi-cardinality sibling of `skra-select` over the shared `_SkraSelectBase` (**ADR 0046** —\r\n * cardinality split: a separate component with an HONEST `readonly T[]` value contract, NOT a `multiple`\r\n * flag). The trigger (`role=\"combobox\"`) opens a top-layer listbox panel (`popover=\"auto\"`, ADR 0044,\r\n * `aria-multiselectable=\"true\"`) of the SAME projected `skra-option` children — reused verbatim; the\r\n * option swaps its indicator to a checkbox via the `SkraListboxPanel.multiple` hint.\r\n *\r\n * - **Value (ADR 037):** `value = input<readonly T[]>([])` + `valueChange = output<readonly T[]>()` —\r\n * consumer-owned, no `@angular/forms`. Selection is membership (`compareWith` per element).\r\n * - **Toggle (ADR 0046):** picking an option toggles its membership and KEEPS THE PANEL OPEN (the one\r\n * behavioural divergence from single-select, which closes).\r\n * - **Trigger summary (ADR 0045 / 033):** comma-joined `getLabel()` of selected options — the VALUE\r\n * display, never the accessible name. Count-style / chips → the opt-in `[skraSelectTrigger]` recipe.\r\n *\r\n * Full contract: adr/specs/component-multi-select-spec.md.\r\n */\r\n@Component({\r\n selector: 'skra-multi-select',\r\n standalone: true,\r\n exportAs: 'skraMultiSelect',\r\n imports: [NgTemplateOutlet],\r\n template: `\r\n <button\r\n #trigger\r\n class=\"skra-select-trigger\"\r\n type=\"button\"\r\n role=\"combobox\"\r\n aria-haspopup=\"listbox\"\r\n [attr.id]=\"triggerId()\"\r\n [attr.popovertarget]=\"panelId\"\r\n [attr.aria-controls]=\"panelId\"\r\n [attr.aria-expanded]=\"isOpen()\"\r\n [attr.aria-activedescendant]=\"activeOptionId()\"\r\n [attr.aria-describedby]=\"describedBy()\"\r\n [attr.aria-invalid]=\"hasError() ? 'true' : null\"\r\n [attr.aria-required]=\"isRequired() ? 'true' : null\"\r\n [attr.aria-readonly]=\"isReadonly() ? 'true' : null\"\r\n [disabled]=\"isDisabled()\"\r\n (click)=\"onTriggerClick($event)\"\r\n (keydown)=\"onTriggerKeydown($event)\"\r\n >\r\n <span class=\"skra-select-trigger-value\">\r\n @if (triggerTemplate(); as tpl) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"tpl\"\r\n [ngTemplateOutletContext]=\"triggerContext()\"\r\n ></ng-container>\r\n } @else if (selectedOptions().length) {\r\n {{ summary() }}\r\n } @else {\r\n <span class=\"skra-select-placeholder\">{{ placeholder() }}</span>\r\n }\r\n </span>\r\n <span class=\"skra-select-arrow\" aria-hidden=\"true\"></span>\r\n </button>\r\n\r\n <div\r\n #panel\r\n class=\"skra-select-listbox\"\r\n [id]=\"panelId\"\r\n role=\"listbox\"\r\n aria-multiselectable=\"true\"\r\n popover=\"auto\"\r\n (toggle)=\"onPanelToggle($event)\"\r\n >\r\n <ng-content></ng-content>\r\n </div>\r\n `,\r\n styleUrl: './multi-select.scss',\r\n changeDetection: ChangeDetectionStrategy.OnPush,\r\n encapsulation: ViewEncapsulation.Emulated,\r\n providers: [{ provide: SKRA_LISTBOX_PANEL, useExisting: SkraMultiSelect }],\r\n})\r\nexport class SkraMultiSelect<T = unknown> extends _SkraSelectBase<T> {\r\n /** Consumer-owned ARRAY value channel (ADR 037) — default `[]` (never undefined). */\r\n readonly value = input<readonly T[]>([]);\r\n /** Fires ONLY on user toggle — emits a NEW array; never on a programmatic `value()` change (ADR 037). */\r\n readonly valueChange = output<readonly T[]>();\r\n /** Cardinality hint (ADR 0046) — multi-select (drives the option checkbox indicator + aria-multiselectable). */\r\n readonly multiple = true;\r\n\r\n /** The options whose value is a member of `value()` — feeds the trigger summary + seed. */\r\n protected readonly selectedOptions = computed(() =>\r\n this.options().filter((o) => this.isSelected(o.value())),\r\n );\r\n\r\n /** Comma-joined identity labels of the selected options (value display, ADR 0045 — language-neutral). */\r\n protected readonly summary = computed(() =>\r\n this.selectedOptions()\r\n .map((o) => o.getLabel())\r\n .join(', '),\r\n );\r\n\r\n protected readonly triggerContext = computed(() => ({\r\n $implicit: this.value(),\r\n options: this.selectedOptions(),\r\n }));\r\n\r\n constructor() {\r\n super();\r\n if (typeof ngDevMode === 'undefined' || ngDevMode) {\r\n // Dev-only consumer warning. Uses afterRenderEffect (NOT effect) so the projected\r\n // options' required `value` inputs are read only AFTER each render applies their\r\n // bindings — an eager read during the initial CD (SSR / first render) OR a runtime\r\n // re-projection would throw NG0950. afterRender hooks never run on the server, so\r\n // this stays SSR-safe. Covers F-13 (initial render) + F-16 (runtime re-projection).\r\n afterRenderEffect(() => {\r\n const opts = this.options();\r\n const unmatched = this.value().filter(\r\n (v) => !opts.some((o) => this.compareWith()(o.value() as T, v)),\r\n );\r\n if (unmatched.length) {\r\n console.warn(\r\n '[skra-multi-select] value contains entries with no matching <skra-option> — they are ' +\r\n 'dropped from the trigger summary (likely a compareWith mismatch for object values).',\r\n );\r\n }\r\n });\r\n }\r\n }\r\n\r\n // ── SkraListboxPanel seam ──\r\n isSelected(value: unknown): boolean {\r\n return this.value().some((x) => this.compareWith()(x, value as T));\r\n }\r\n\r\n selectValue(value: unknown): void {\r\n const present = this.isSelected(value);\r\n const next = present\r\n ? this.value().filter((x) => !this.compareWith()(x, value as T))\r\n : [...this.value(), value as T];\r\n this.valueChange.emit(next);\r\n // NO close() — multi-select keeps the panel open for further toggling (ADR 0046).\r\n }\r\n\r\n protected activeSeedItem(): SkraListboxKeyboardItem | null {\r\n return this.selectedOptions()[0] ?? null;\r\n }\r\n}\r\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
@@ -25,6 +25,14 @@ import { SkraFieldContext } from '@skra/ui/field';
25
25
  * </select>
26
26
  * </skra-field>
27
27
  *
28
+ * <!-- standalone: native implicit label (wrapping) — no id/for needed -->
29
+ * <label>Country <select skraSelect><option value="pl">Poland</option></select></label>
30
+ *
31
+ * `SkraFieldContext` is injected `{ optional: true }` (ADR 013 §3.7) — same
32
+ * posture as SkraInput / SkraCheckbox. Standalone, the atom renders the skin
33
+ * only, the readonly guards are inert, and the consumer owns every attribute
34
+ * including the accessible name.
35
+ *
28
36
  * Native `<select>` is the only web-platform widget whose dropdown renders in
29
37
  * the OS / browser top-layer, above modal `<dialog>`, above Shadow DOM
30
38
  * boundaries, above z-index stacking contexts. By staying native at Tier 1 we
@@ -50,12 +58,14 @@ import { SkraFieldContext } from '@skra/ui/field';
50
58
  */
51
59
  class SkraNativeSelect {
52
60
  el = inject(ElementRef);
53
- context = inject(SkraFieldContext);
61
+ context = inject(SkraFieldContext, { optional: true });
54
62
  constructor() {
55
63
  const el = this.el.nativeElement;
56
- // Reject native multi-select loudly in dev (mirrors the mandatory-context
57
- // throw — Forge fails on contract violations rather than degrading
58
- // silently). Dev-only: never crashes a production bundle. A `<select
64
+ // Reject native multi-select loudly in dev — Forge fails on contract
65
+ // violations rather than degrading silently. This throw is unrelated to the
66
+ // (now optional, ADR 013 §3.7) field context: `[multiple]` is a capability
67
+ // this atom genuinely cannot honour, whereas a missing field is a supported
68
+ // standalone mode. Dev-only: never crashes a production bundle. A `<select
59
69
  // multiple>` that truly wants native UX can drop `skraSelect` and stay raw
60
70
  // HTML; for a real multi-select, use the Tier 2 <skra-select>.
61
71
  if (isDevMode() && el.multiple) {
@@ -67,9 +77,18 @@ class SkraNativeSelect {
67
77
  'Tier 2 <skra-select> (CDK listbox) for real multi-select. See ' +
68
78
  'adr/specs/0039-select-directive-spec.md §4.1.');
69
79
  }
70
- effect(() => {
71
- el.disabled = this.context.isDisabled();
72
- });
80
+ // Field-cascaded native properties — only bind when context is present so
81
+ // standalone consumers retain full control of their static attributes.
82
+ // Mirrors SkraInput / SkraCheckbox.
83
+ if (this.context) {
84
+ const ctx = this.context;
85
+ effect(() => {
86
+ el.id = ctx.fieldId();
87
+ });
88
+ effect(() => {
89
+ el.disabled = ctx.isDisabled();
90
+ });
91
+ }
73
92
  }
74
93
  /**
75
94
  * Readonly: suppress the pointer affordance that opens the native listbox.
@@ -78,7 +97,7 @@ class SkraNativeSelect {
78
97
  * trade-off for a value-display control).
79
98
  */
80
99
  blockWhenReadonly(event) {
81
- if (this.context.isReadonly())
100
+ if (this.context?.isReadonly())
82
101
  event.preventDefault();
83
102
  }
84
103
  /**
@@ -89,21 +108,20 @@ class SkraNativeSelect {
89
108
  * select, so all are blocked in readonly.
90
109
  */
91
110
  blockKeyWhenReadonly(event) {
92
- if (this.context.isReadonly() && event.key !== 'Tab') {
111
+ if (this.context?.isReadonly() && event.key !== 'Tab') {
93
112
  event.preventDefault();
94
113
  }
95
114
  }
96
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: SkraNativeSelect, deps: [], target: i0.ɵɵFactoryTarget.Component });
97
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.7", type: SkraNativeSelect, isStandalone: true, selector: "select[skraSelect]", host: { listeners: { "mousedown": "blockWhenReadonly($event)", "keydown": "blockKeyWhenReadonly($event)" }, properties: { "id": "context.fieldId()", "attr.aria-describedby": "context.describedByIds()", "attr.aria-invalid": "context.hasError() ? \"true\" : null", "attr.aria-required": "context.isRequired() ? \"true\" : null", "attr.aria-readonly": "context.isReadonly() ? \"true\" : null" } }, ngImport: i0, template: '<ng-content></ng-content>', isInline: true, styles: [":host{background-color:var(--skra-comp-field-background);color:var(--skra-comp-field-foreground);border-color:var(--skra-comp-field-border-color);border-width:var(--skra-comp-field-border-width);border-style:var(--skra-comp-field-border-style);border-radius:var(--skra-comp-field-radius);padding-block:var(--skra-comp-field-padding-block);padding-inline:var(--skra-comp-field-padding-inline);box-shadow:var(--skra-comp-field-shadow);font:inherit;outline:none;cursor:pointer;transition:background-color var(--skra-comp-field-motion-duration) var(--skra-comp-field-motion-easing),border-color var(--skra-comp-field-motion-duration) var(--skra-comp-field-motion-easing),color var(--skra-comp-field-motion-duration) var(--skra-comp-field-motion-easing),box-shadow var(--skra-comp-field-motion-duration) var(--skra-comp-field-motion-easing)}:host(:not([size])){appearance:none;padding-inline-end:var(--skra-comp-field-select-arrow-padding-inline-end);background-image:var(--skra-comp-field-select-chevron-glyph);background-repeat:no-repeat;background-position:right var(--skra-comp-field-padding-inline) center;background-size:var(--skra-comp-field-select-arrow-icon-size)}:host([size]){background-image:none;cursor:default}:host(:not([size]):hover:not(:disabled):not([aria-readonly=true])){background-color:var(--skra-comp-field-background-hover);border-color:var(--skra-comp-field-border-color-hover)}:host(:focus-visible){border-color:var(--skra-comp-field-focus-ring-color);box-shadow:0 0 0 var(--skra-comp-field-focus-ring-width) var(--skra-comp-field-focus-ring-color)}:host(:disabled){cursor:not-allowed;opacity:var(--skra-sys-opacity-disabled)}:host([aria-readonly=true]){cursor:default}:host([aria-invalid=true]){border-color:var(--skra-comp-field-error-color)}:host([aria-invalid=true]:focus-visible){box-shadow:0 0 0 var(--skra-comp-field-focus-ring-width) var(--skra-comp-field-error-color)}@media(prefers-reduced-motion:reduce){:host{transition:none}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
115
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.1", ngImport: i0, type: SkraNativeSelect, deps: [], target: i0.ɵɵFactoryTarget.Component });
116
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.1.1", type: SkraNativeSelect, isStandalone: true, selector: "select[skraSelect]", host: { listeners: { "mousedown": "blockWhenReadonly($event)", "keydown": "blockKeyWhenReadonly($event)" }, properties: { "attr.aria-describedby": "context?.describedByIds() ?? null", "attr.aria-invalid": "context?.hasError() ? \"true\" : null", "attr.aria-required": "context?.isRequired() ? \"true\" : null", "attr.aria-readonly": "context?.isReadonly() ? \"true\" : null" } }, ngImport: i0, template: '<ng-content></ng-content>', isInline: true, styles: [":host{background-color:var(--skra-comp-field-background);color:var(--skra-comp-field-foreground);border-color:var(--skra-comp-field-border-color);border-width:var(--skra-comp-field-border-width);border-style:var(--skra-comp-field-border-style);border-radius:var(--skra-comp-field-radius);padding-block:var(--skra-comp-field-padding-block);padding-inline:var(--skra-comp-field-padding-inline);box-shadow:var(--skra-comp-field-shadow);font:inherit;outline:none;cursor:pointer;transition:background-color var(--skra-comp-field-motion-duration) var(--skra-comp-field-motion-easing),border-color var(--skra-comp-field-motion-duration) var(--skra-comp-field-motion-easing),color var(--skra-comp-field-motion-duration) var(--skra-comp-field-motion-easing),box-shadow var(--skra-comp-field-motion-duration) var(--skra-comp-field-motion-easing)}:host(:not([size])){appearance:none;padding-inline-end:var(--skra-comp-field-select-arrow-padding-inline-end);background-image:var(--skra-comp-field-select-chevron-glyph);background-repeat:no-repeat;background-position:right var(--skra-comp-field-padding-inline) center;background-size:var(--skra-comp-field-select-arrow-icon-size)}:host([size]){background-image:none;cursor:default}:host(:not([size]):hover:not(:disabled):not([aria-readonly=true])){background-color:var(--skra-comp-field-background-hover);border-color:var(--skra-comp-field-border-color-hover)}:host(:focus-visible){border-color:var(--skra-comp-field-focus-ring-color);box-shadow:0 0 0 var(--skra-comp-field-focus-ring-width) var(--skra-comp-field-focus-ring-color)}:host(:disabled){cursor:not-allowed;opacity:var(--skra-sys-opacity-disabled)}:host([aria-readonly=true]){cursor:default}:host([aria-invalid=true]){border-color:var(--skra-comp-field-error-color)}:host([aria-invalid=true]:focus-visible){box-shadow:0 0 0 var(--skra-comp-field-focus-ring-width) var(--skra-comp-field-error-color)}@media(prefers-reduced-motion:reduce){:host{transition:none}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
98
117
  }
99
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: SkraNativeSelect, decorators: [{
118
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.1", ngImport: i0, type: SkraNativeSelect, decorators: [{
100
119
  type: Component,
101
120
  args: [{ selector: 'select[skraSelect]', standalone: true, template: '<ng-content></ng-content>', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.Emulated, host: {
102
- '[id]': 'context.fieldId()',
103
- '[attr.aria-describedby]': 'context.describedByIds()',
104
- '[attr.aria-invalid]': 'context.hasError() ? "true" : null',
105
- '[attr.aria-required]': 'context.isRequired() ? "true" : null',
106
- '[attr.aria-readonly]': 'context.isReadonly() ? "true" : null',
121
+ '[attr.aria-describedby]': 'context?.describedByIds() ?? null',
122
+ '[attr.aria-invalid]': 'context?.hasError() ? "true" : null',
123
+ '[attr.aria-required]': 'context?.isRequired() ? "true" : null',
124
+ '[attr.aria-readonly]': 'context?.isReadonly() ? "true" : null',
107
125
  // No `data-skra-readonly` — slice consumes `[aria-readonly="true"]`
108
126
  // directly; emitting a parallel data-attr would be dead surface
109
127
  // (no slice selector reads it). Matches checkbox/radio readonly pattern