forty-cdk 0.22.0 → 0.24.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 (148) hide show
  1. package/README.md +8 -8
  2. package/breakpoints/README.md +22 -1
  3. package/combobox/README.md +1 -1
  4. package/context-menu/README.md +2 -2
  5. package/date-picker/README.md +2 -2
  6. package/dialog/README.md +10 -0
  7. package/drawer/README.md +10 -0
  8. package/dropdown-menu/README.md +3 -3
  9. package/fesm2022/forty-cdk-accordion.mjs +140 -140
  10. package/fesm2022/forty-cdk-aspect-ratio.mjs +43 -43
  11. package/fesm2022/forty-cdk-avatar.mjs +128 -128
  12. package/fesm2022/forty-cdk-avatar.mjs.map +1 -1
  13. package/fesm2022/forty-cdk-breadcrumbs.mjs +70 -70
  14. package/fesm2022/forty-cdk-breakpoints.mjs +59 -58
  15. package/fesm2022/forty-cdk-breakpoints.mjs.map +1 -1
  16. package/fesm2022/forty-cdk-button.mjs +152 -152
  17. package/fesm2022/forty-cdk-calendar.mjs +587 -587
  18. package/fesm2022/forty-cdk-carousel.mjs +336 -336
  19. package/fesm2022/forty-cdk-carousel.mjs.map +1 -1
  20. package/fesm2022/forty-cdk-checkbox.mjs +110 -110
  21. package/fesm2022/forty-cdk-combobox.mjs +1292 -1358
  22. package/fesm2022/forty-cdk-combobox.mjs.map +1 -1
  23. package/fesm2022/forty-cdk-context-menu.mjs +264 -326
  24. package/fesm2022/forty-cdk-context-menu.mjs.map +1 -1
  25. package/fesm2022/forty-cdk-core-overlay.mjs +4732 -0
  26. package/fesm2022/forty-cdk-core-overlay.mjs.map +1 -0
  27. package/fesm2022/forty-cdk-core.mjs +3436 -7743
  28. package/fesm2022/forty-cdk-core.mjs.map +1 -1
  29. package/fesm2022/forty-cdk-date-field.mjs +477 -477
  30. package/fesm2022/forty-cdk-date-field.mjs.map +1 -1
  31. package/fesm2022/forty-cdk-date-picker.mjs +585 -618
  32. package/fesm2022/forty-cdk-date-picker.mjs.map +1 -1
  33. package/fesm2022/forty-cdk-dialog.mjs +265 -264
  34. package/fesm2022/forty-cdk-dialog.mjs.map +1 -1
  35. package/fesm2022/forty-cdk-disclosure.mjs +66 -66
  36. package/fesm2022/forty-cdk-drag-drop.mjs +329 -329
  37. package/fesm2022/forty-cdk-drag-drop.mjs.map +1 -1
  38. package/fesm2022/forty-cdk-drawer.mjs +873 -872
  39. package/fesm2022/forty-cdk-drawer.mjs.map +1 -1
  40. package/fesm2022/forty-cdk-dropdown-menu.mjs +225 -273
  41. package/fesm2022/forty-cdk-dropdown-menu.mjs.map +1 -1
  42. package/fesm2022/forty-cdk-field.mjs +216 -216
  43. package/fesm2022/forty-cdk-fieldset.mjs +105 -105
  44. package/fesm2022/forty-cdk-file-upload.mjs +88 -88
  45. package/fesm2022/forty-cdk-hover-card.mjs +205 -194
  46. package/fesm2022/forty-cdk-hover-card.mjs.map +1 -1
  47. package/fesm2022/forty-cdk-input.mjs +129 -129
  48. package/fesm2022/forty-cdk-internationalized-date.mjs +90 -90
  49. package/fesm2022/forty-cdk-listbox.mjs +463 -401
  50. package/fesm2022/forty-cdk-listbox.mjs.map +1 -1
  51. package/fesm2022/forty-cdk-menu.mjs +717 -788
  52. package/fesm2022/forty-cdk-menu.mjs.map +1 -1
  53. package/fesm2022/forty-cdk-menubar.mjs +454 -446
  54. package/fesm2022/forty-cdk-menubar.mjs.map +1 -1
  55. package/fesm2022/forty-cdk-meter.mjs +66 -66
  56. package/fesm2022/forty-cdk-meter.mjs.map +1 -1
  57. package/fesm2022/forty-cdk-navigation-menu.mjs +493 -495
  58. package/fesm2022/forty-cdk-navigation-menu.mjs.map +1 -1
  59. package/fesm2022/forty-cdk-number-input.mjs +350 -350
  60. package/fesm2022/forty-cdk-number-input.mjs.map +1 -1
  61. package/fesm2022/forty-cdk-otp-input.mjs +178 -178
  62. package/fesm2022/forty-cdk-pagination.mjs +142 -142
  63. package/fesm2022/forty-cdk-pane-resizer.mjs +140 -140
  64. package/fesm2022/forty-cdk-popover.mjs +265 -260
  65. package/fesm2022/forty-cdk-popover.mjs.map +1 -1
  66. package/fesm2022/forty-cdk-progress.mjs +94 -94
  67. package/fesm2022/forty-cdk-radio-group.mjs +159 -159
  68. package/fesm2022/forty-cdk-radio-group.mjs.map +1 -1
  69. package/fesm2022/forty-cdk-scroll-area.mjs +294 -294
  70. package/fesm2022/forty-cdk-scroll-area.mjs.map +1 -1
  71. package/fesm2022/forty-cdk-search.mjs +178 -178
  72. package/fesm2022/forty-cdk-select.mjs +762 -786
  73. package/fesm2022/forty-cdk-select.mjs.map +1 -1
  74. package/fesm2022/forty-cdk-separator.mjs +33 -33
  75. package/fesm2022/forty-cdk-shared.mjs +5 -4
  76. package/fesm2022/forty-cdk-shared.mjs.map +1 -1
  77. package/fesm2022/forty-cdk-slider.mjs +253 -253
  78. package/fesm2022/forty-cdk-stepper.mjs +315 -315
  79. package/fesm2022/forty-cdk-switch.mjs +65 -65
  80. package/fesm2022/forty-cdk-table-virtualization.mjs +119 -119
  81. package/fesm2022/forty-cdk-table.mjs +1633 -1640
  82. package/fesm2022/forty-cdk-table.mjs.map +1 -1
  83. package/fesm2022/forty-cdk-tabs.mjs +140 -141
  84. package/fesm2022/forty-cdk-tabs.mjs.map +1 -1
  85. package/fesm2022/forty-cdk-time-field.mjs +505 -505
  86. package/fesm2022/forty-cdk-time-field.mjs.map +1 -1
  87. package/fesm2022/forty-cdk-time-picker.mjs +388 -359
  88. package/fesm2022/forty-cdk-time-picker.mjs.map +1 -1
  89. package/fesm2022/forty-cdk-toast.mjs +579 -580
  90. package/fesm2022/forty-cdk-toast.mjs.map +1 -1
  91. package/fesm2022/forty-cdk-toggle.mjs +224 -224
  92. package/fesm2022/forty-cdk-toolbar.mjs +140 -140
  93. package/fesm2022/forty-cdk-tooltip.mjs +268 -258
  94. package/fesm2022/forty-cdk-tooltip.mjs.map +1 -1
  95. package/fesm2022/forty-cdk-tree.mjs +567 -571
  96. package/fesm2022/forty-cdk-tree.mjs.map +1 -1
  97. package/fesm2022/forty-cdk-virtual-reorder.mjs +67 -67
  98. package/fesm2022/forty-cdk-virtualization.mjs +156 -156
  99. package/fesm2022/forty-cdk-visually-hidden.mjs +4 -4
  100. package/fesm2022/forty-cdk.mjs +3 -3
  101. package/hover-card/README.md +2 -0
  102. package/listbox/README.md +19 -7
  103. package/menu/README.md +5 -5
  104. package/menubar/README.md +7 -7
  105. package/navigation-menu/README.md +1 -1
  106. package/package.json +5 -1
  107. package/popover/README.md +2 -0
  108. package/select/README.md +48 -5
  109. package/shared/README.md +62 -1
  110. package/stepper/README.md +12 -0
  111. package/table/README.md +15 -3
  112. package/tabs/README.md +12 -1
  113. package/time-picker/README.md +41 -1
  114. package/tooltip/README.md +2 -0
  115. package/types/forty-cdk-avatar.d.ts +2 -1
  116. package/types/forty-cdk-breakpoints.d.ts +1 -0
  117. package/types/forty-cdk-carousel.d.ts +1 -1
  118. package/types/forty-cdk-combobox.d.ts +70 -83
  119. package/types/forty-cdk-context-menu.d.ts +47 -94
  120. package/types/forty-cdk-core-overlay.d.ts +3578 -0
  121. package/types/forty-cdk-core.d.ts +464 -3415
  122. package/types/forty-cdk-date-field.d.ts +8 -8
  123. package/types/forty-cdk-date-picker.d.ts +56 -75
  124. package/types/forty-cdk-dialog.d.ts +2 -1
  125. package/types/forty-cdk-drag-drop.d.ts +3 -3
  126. package/types/forty-cdk-drawer.d.ts +3 -2
  127. package/types/forty-cdk-dropdown-menu.d.ts +24 -58
  128. package/types/forty-cdk-hover-card.d.ts +28 -7
  129. package/types/forty-cdk-listbox.d.ts +40 -14
  130. package/types/forty-cdk-menu.d.ts +55 -125
  131. package/types/forty-cdk-menubar.d.ts +41 -56
  132. package/types/forty-cdk-meter.d.ts +1 -0
  133. package/types/forty-cdk-navigation-menu.d.ts +7 -9
  134. package/types/forty-cdk-number-input.d.ts +1 -1
  135. package/types/forty-cdk-popover.d.ts +29 -19
  136. package/types/forty-cdk-radio-group.d.ts +1 -1
  137. package/types/forty-cdk-scroll-area.d.ts +2 -1
  138. package/types/forty-cdk-select.d.ts +89 -86
  139. package/types/forty-cdk-shared.d.ts +2 -1
  140. package/types/forty-cdk-table.d.ts +159 -171
  141. package/types/forty-cdk-tabs.d.ts +12 -13
  142. package/types/forty-cdk-time-field.d.ts +4 -4
  143. package/types/forty-cdk-time-picker.d.ts +96 -48
  144. package/types/forty-cdk-toast.d.ts +4 -5
  145. package/types/forty-cdk-tooltip.d.ts +24 -8
  146. package/types/forty-cdk-tree.d.ts +9 -14
  147. package/types/forty-cdk-visually-hidden.d.ts +1 -1
  148. package/visually-hidden/README.md +47 -0
package/README.md CHANGED
@@ -43,13 +43,13 @@ Two packages are regular dependencies, installed automatically and never declare
43
43
 
44
44
  **The package name itself exports nothing.** `import { … } from 'forty-cdk'` resolves to no symbol, and your editor will not auto-import anything under the bare package name — by design, so that every symbol has exactly one import path. There are three specifiers you do import from:
45
45
 
46
- | Specifier | What it exports |
47
- | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
48
- | `forty-cdk/<primitive>` | The primitive's directives, components, context tokens and defaults provider — `ForDialog` from `forty-cdk/dialog`, `ForAccordion` from `forty-cdk/accordion`, and so on for every entry in the tables below. |
49
- | [`forty-cdk/shared`](shared) | The cross-primitive contract types a primitive's public API references — `WritingDirection`, `VetoableEvent`, `DateAdapter`, `FloatingSide`, … — declared once and published once. Six of them ship from their own primitive instead; see its README. |
50
- | `forty-cdk/internationalized-date` | The `@internationalized/date` adapters, kept apart so that optional peer stays genuinely optional. |
46
+ | Specifier | What it exports |
47
+ | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
48
+ | `forty-cdk/<primitive>` | The primitive's directives, components, context tokens and defaults provider — `ForDialog` from `forty-cdk/dialog`, `ForAccordion` from `forty-cdk/accordion`, and so on for every entry in the tables below. |
49
+ | [`forty-cdk/shared`](shared) | The cross-primitive contract types a primitive's public API references — `WritingDirection`, `VetoableEvent`, `DateAdapter`, `FloatingSide`, … — declared once and published once. Eight ship from their own primitive instead; that README names them. |
50
+ | `forty-cdk/internationalized-date` | The `@internationalized/date` adapters, kept apart so that optional peer stays genuinely optional. |
51
51
 
52
- `forty-cdk/core` resolves too, but it is **not** public: it holds the engines and DI singletons the library refactors freely, and it exists so every primitive resolves that shared implementation to one compiled module. If a symbol you need is not exported by the three specifiers above, it is internal by design — [open an issue](https://github.com/tutkli/forty-cdk/issues) rather than importing from `core`.
52
+ `forty-cdk/core` and `forty-cdk/core-overlay` resolve too, but neither is **public**: together they hold the engines and DI singletons the library refactors freely, and they exist so every primitive resolves that shared implementation to one compiled module. They are two rather than one for a bundling reason you get for free: a published module is a bundler's chunk-splitting unit, so keeping the positioning engine (`@floating-ui/dom` and the overlay shells) in its own module means a lazy route that renders no overlay does not load it. Measured on a seven-lazy-route app, that is **41.7 kB raw / 12.1 kB transfer** a non-overlay route no longer pays. If a symbol you need is not exported by the three specifiers above, it is internal by design — [open an issue](https://github.com/tutkli/forty-cdk/issues) rather than importing from either.
53
53
 
54
54
  ## Errors
55
55
 
@@ -156,7 +156,7 @@ The tables below group the primitives by purpose. The link on each name opens th
156
156
  | [Separator](separator) | A static, optionally semantic divider between groups of content, horizontal or vertical. |
157
157
  | [Aspect Ratio](aspect-ratio) | A container that keeps its content at a fixed width-to-height ratio. |
158
158
  | [Avatar](avatar) | A user image with a graceful fallback across its loading lifecycle. |
159
- | [Visually Hidden](visually-hidden) | Hides content visually while keeping it in the accessibility tree — screen-reader-only labels and announcements. |
159
+ | [Visually Hidden](visually-hidden) | Hides content visually while keeping it in the accessibility tree — screen-reader-only labels, plus the injectable `LiveAnnouncer`. |
160
160
 
161
161
  ### Feedback
162
162
 
@@ -171,7 +171,7 @@ Headless — no DOM or ARIA of their own; an `inject*` / provider API that other
171
171
 
172
172
  | Utility | What it is |
173
173
  | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
174
- | [Breakpoints](breakpoints) | A signal-first, zoneless, SSR-safe viewport breakpoint observer (`injectBreakpoints`). |
174
+ | [Breakpoints](breakpoints) | A signal-first, zoneless, SSR-safe viewport breakpoint observer (`injectBreakpoints`), plus the `prefers-reduced-motion` detector. |
175
175
  | [Drag & Drop](drag-drop) | Headless, accessible drag-and-drop for sortable lists and cross-list transfers, keyboard and pointer driven. |
176
176
  | [Virtualization](virtualization) | A headless windowing core (`injectVirtualizer`) plus a `[forVirtualViewport]` layer that renders only the visible slice of huge lists. |
177
177
  | [Table Virtualization](table-virtualization) | `[forTableVirtualized]`, the adapter that windows a `[forTable]` grid — its own entry point because it composes both the table and the windowing core. |
@@ -82,6 +82,27 @@ Now `injectBreakpoints()` autocompletes `'mobile' | 'tablet' | 'laptop' | 'deskt
82
82
  | `active` | the largest breakpoint whose `min-width` matches, or `null` below the smallest |
83
83
  | `matches(query)` | escape hatch for an arbitrary media query (orientation, `prefers-*`, …) |
84
84
 
85
+ ### `injectPrefersReducedMotion`
86
+
87
+ The same shape for a different query: `injectPrefersReducedMotion()` returns a `Signal<boolean>` that is `true` while the user has asked their OS to suppress animation, and flips if they change the setting mid-session. Call it from an injection context, like `injectBreakpoints()`.
88
+
89
+ ```ts
90
+ import { computed } from '@angular/core';
91
+ import { injectPrefersReducedMotion } from 'forty-cdk/breakpoints';
92
+
93
+ export class Panel {
94
+ private readonly reducedMotion = injectPrefersReducedMotion();
95
+
96
+ protected readonly transition = computed(() =>
97
+ this.reducedMotion() ? 'none' : 'transform 200ms ease-out',
98
+ );
99
+ }
100
+ ```
101
+
102
+ It is published here because forty-cdk ships no styles: the animation on a `data-state` change is yours, so honouring the preference is yours too — and a signal is what a `computed()` or a `[style]` binding can branch on, which a CSS `@media` block cannot. Treat `true` as "skip the animated path entirely", not "shorten the duration": the setting asks for no motion, not less of it.
103
+
104
+ `bp.matches('(prefers-reduced-motion: reduce)')` resolves to the same thing. Prefer the named helper — it is the one the library's own motion-bearing primitives (drag gestures, carousel, drawer) read, so the query string stays spelled in one place.
105
+
85
106
  ## SSR
86
107
 
87
- On the server (or where `matchMedia` is unavailable) every query signal reads `false` and `active` reads `null`. No `matchMedia` access happens server-side, so the helper is safe under Angular Universal.
108
+ On the server (or where `matchMedia` is unavailable) every query signal reads `false` and `active` reads `null`. No `matchMedia` access happens server-side, so the helper is safe under Angular Universal. `injectPrefersReducedMotion()` reads `false` there for the same reason: the server render takes the animated branch, and the client applies the real preference on its first observation.
@@ -582,7 +582,7 @@ When `[totalCount]` is omitted, the directive falls back to `options().length` a
582
582
  `[forCombobox]` exposes a `dir: 'ltr' | 'rtl'` input (default `'ltr'`). It drives:
583
583
 
584
584
  - **Chip keyboard navigation** — ArrowLeft / ArrowRight roles swap so they follow the visual order of the chip cluster, not the DOM order. See _Chip keyboard_ above.
585
- - **Default popover placement** — `align` defaults to `'start'` in LTR and `'end'` in RTL so the listbox anchors to the visually-leading edge of the input (`side` defaults to `'bottom'` in both). A consumer-provided `[align]` is honoured as-is — no automatic flip — so advanced layouts can pin an alignment regardless of writing direction.
585
+ - **Default popover placement** — `align` defaults to `'start'` in LTR and `'end'` in RTL so the listbox anchors to the visually-leading edge of the input (`side` defaults to `'bottom'` in both). A consumer-provided `[align]` is honoured as-is — no automatic flip — so advanced layouts can pin an alignment regardless of writing direction. `provideForComboboxDefaults({ align })` pins it for a whole scope the same way; its default is `null`, which is what "follow the writing direction" is spelled as there. `side` is scope-defaultable through the same provider, with the plain `'bottom'` fallback — writing direction does not enter into it.
586
586
 
587
587
  The native `<input>` handles caret movement and BiDi from the document's CSS `direction` already, so there's nothing extra to do for the typed text itself.
588
588
 
@@ -120,8 +120,8 @@ Both triggers carry `[menuPositioning]`, a partial `{ side, align, sideOffset, a
120
120
  | Property | Type | Description |
121
121
  | --------------------------- | --------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
122
122
  | `open` | `model<boolean>` | Two-way bindable. Whether the menu is shown.<br>**Default:** `false` |
123
- | `side` | `input<string>` | Anchor side relative to the pointer.<br>**Default:** `'bottom'` |
124
- | `align` | `input<string>` | Alignment along `side` (`'start'` / `'center'` / `'end'`).<br>**Default:** `'start'` |
123
+ | `side` | `input<string>` | Anchor side relative to the pointer. The default is read from `provideForContextMenuDefaults` for the surrounding scope.<br>**Default:** `'bottom'` |
124
+ | `align` | `input<string>` | Alignment along `side` (`'start'` / `'center'` / `'end'`). The default is read from `provideForContextMenuDefaults` for the surrounding scope.<br>**Default:** `'start'` |
125
125
  | `sideOffset` | `input<number>` | Gap (px) between the pointer and the menu along the main axis.<br>**Default:** `0` |
126
126
  | `alignOffset` | `input<number>` | Gap (px) along the cross axis (parallel to `side`).<br>**Default:** `0` |
127
127
  | `fallbackAxisSideDirection` | `input<'none' \| 'start' \| 'end'>` | When both sides of the preferred axis overflow, lets `flip` drop the menu to a perpendicular side instead of clipping. `'none'` keeps only the opposite same-axis placement. The default is read from `provideForContextMenuDefaults` for the surrounding scope — set it once for the whole app rather than per call site.<br>**Default:** `'none'` |
@@ -157,7 +157,7 @@ The library is styleless: presence in the DOM is the consumer's job (`@if (open(
157
157
  | `formatOptions` | `input<Intl.DateTimeFormatOptions>` | Options for the text rendered by `[forDatePickerValue]`.<br>**Default:** `{ year: 'numeric', month: 'long', day: 'numeric' }` |
158
158
  | `locale` | `input<string \| null>` | BCP 47 locale for the text rendered by `[forDatePickerValue]`. Not forwarded to the projected calendar — bind its `[locale]` too.<br>**Default:** `null` → runtime locale |
159
159
  | `placeholder` | `input<string>` | Fallback text for `[forDatePickerValue]` when empty.<br>**Default:** `''` |
160
- | `side` / `align` | `input` | Anchored placement (popover mode only).<br>**Default:** `'bottom'` / `'start'` |
160
+ | `side` / `align` | `input` | Anchored placement (popover mode only). Defaults from `provideForDatePickerDefaults` / `provideForDateRangePickerDefaults`.<br>**Default:** `'bottom'` / `'start'` |
161
161
  | `dir` | `input<'ltr' \| 'rtl' \| null>` | Writing direction.<br>**Default:** `null` resolves the ambient direction; reflected to the host `dir` |
162
162
 
163
163
  Plus the shared `FormUiControl` inputs from the base (`disabled`, `readonly`, `required`, `invalid`, `pending`, `dirty`, `name`, `errors`, and the `touched` model) and the floating tunables (`sideOffset`, `alignOffset`, `avoidCollisions`, `collisionPadding`, `sticky`, `hideWhenDetached`).
@@ -322,7 +322,7 @@ readonly booking = form(this.model, (p) => required(p.stay));
322
322
  - **Native submission.** When `name` is set, two hidden inputs `<name>-start` / `<name>-end` mirror the committed endpoints as ISO `YYYY-MM-DD` for native `<form>` posts.
323
323
  - **Bounds naming.** `minDate` / `maxDate` (not `min` / `max`) for the same reason as `ForDatePicker` — and additionally because `FormUiControl.min` / `max` are typed `NonNullable<TValue>` (the range object itself), which is meaningless as a bound.
324
324
 
325
- Defaults are configured with `provideForDateRangePickerDefaults` (`sideOffset` / `collisionPadding`), and both wrapper patterns work via the exported `FOR_DATE_RANGE_PICKER_HOST_DIRECTIVE_INPUTS` / `FOR_DATE_RANGE_PICKER_HOST_DIRECTIVE_OUTPUTS` tuples — see [Wrapping form primitives](../../../docs/wrapping-form-primitives.md).
325
+ Defaults are configured with `provideForDateRangePickerDefaults` (`side` / `align` / `sideOffset` / `collisionPadding`), and both wrapper patterns work via the exported `FOR_DATE_RANGE_PICKER_HOST_DIRECTIVE_INPUTS` / `FOR_DATE_RANGE_PICKER_HOST_DIRECTIVE_OUTPUTS` tuples — see [Wrapping form primitives](../../../docs/wrapping-form-primitives.md).
326
326
 
327
327
  ## Keyboard
328
328
 
package/dialog/README.md CHANGED
@@ -349,6 +349,16 @@ Implements the [WAI-ARIA Modal Dialog pattern](https://www.w3.org/WAI/ARIA/apg/p
349
349
  - `alert: true` interrupts assistive tech aggressively — only for genuine alerts (lost connection, unsaved changes warning), not for general confirms.
350
350
  - Don't put interactive overlays (popovers, menus) outside the focus trap while a modal dialog is open — they won't be reachable. For a non-modal floating surface anchored to a trigger, use `[forPopover]` instead.
351
351
 
352
+ ## Known limitations
353
+
354
+ Two shapes are correct by design and still break something a consumer can only discover by hitting it. Both live in markup a design system produces routinely, and neither shows up in devtools: every role and `aria-*` stays correct, so the symptom is a keyboard or screen-reader one. The library-wide statement, with the same detail for every primitive, is [Shadow DOM](../shared/README.md#shadow-dom) in `forty-cdk/shared`.
355
+
356
+ **A shadow host that renders a focusable after its `<slot>` breaks the trap's `Tab` cycle.** The trap resolves its first / last pair by walking the surface's composed tree, and that walk visits slotted content after the host's whole shadow tree, whereas the browser sequences it at the `<slot>`'s position. Initial focus can land on a control that is not the visually first one, and a `Tab` at the dialog's real last control is not recognised as the cycle's end — focus leaves the surface (with the page `inert`, usually onto the browser's own UI) and the next `Tab` is pulled back to whichever control the walk thinks is first. That is the configuration you are in whenever you wrap a third-party web component, or your own `ViewEncapsulation.ShadowDom` component, inside the dialog. **Workaround:** render a host's own focusables before its `<slot>`, or project them instead of shadowing them; `initialFocus="container"` fixes the initial-focus half only, since the cycle's edges are re-resolved on every `Tab` press. Details and markup: [Focusable order](../shared/README.md#focusable-order-is-composed-only-for-a-host-that-renders-no-slot).
357
+
358
+ **A `keydown` handler inside the dialog that calls `stopPropagation()` swallows Escape.** The dismissible-layer stack observes `Escape` on `document` in the bubble phase — a deliberate trade-off recorded on `DismissibleLayerStack` — so an event stopped inside the surface never arrives, and `Escape` silently stops dismissing while the backdrop click and `[forDialogClose]` keep working. **Workaround:** narrow the `stopPropagation()` to the keys you actually handle. Keeping the dialog open on `Escape` is the separate, supported job of the vetoable `(escapeKeyDown)` output. Details: [Escape is observed on the bubble phase](../shared/README.md#escape-is-observed-on-the-bubble-phase).
359
+
360
+ A third known limit does not apply to this primitive but is easy to hit inside one: a [Tabs](../tabs) or [Stepper](../stepper) panel rendered in a dialog cannot re-measure its focusable content across a shadow boundary, so its own tab stop can go stale — see [that entry](../shared/README.md#a-panels-focusable-content-measurement-does-not-re-measure-across-a-boundary).
361
+
352
362
  ## Styling
353
363
 
354
364
  forty-cdk ships no styles. Add your own class to each piece — the `for*` selectors are the behavior API, not a styling contract (see [Styling forty-cdk](../../../docs/styling.md)). Key your CSS off the reflected `data-*` attributes listed per piece in the [API](#api) section.
package/drawer/README.md CHANGED
@@ -581,6 +581,16 @@ Implements the [WAI-ARIA Modal Dialog pattern](https://www.w3.org/WAI/ARIA/apg/p
581
581
 
582
582
  Keyboard: **Escape** closes the topmost drawer when `dismissible`; **Tab / Shift+Tab** cycles focus inside the drawer when `modal`; **Click** on `[forDrawerBackdrop]` closes when `dismissible`.
583
583
 
584
+ ## Known limitations
585
+
586
+ Two shapes are correct by design and still break something a consumer can only discover by hitting it. Both live in markup a design system produces routinely, and neither shows up in devtools: every role and `aria-*` stays correct, so the symptom is a keyboard or screen-reader one. The library-wide statement, with the same detail for every primitive, is [Shadow DOM](../shared/README.md#shadow-dom) in `forty-cdk/shared`.
587
+
588
+ **A shadow host that renders a focusable after its `<slot>` breaks the trap's `Tab` cycle.** The trap resolves its first / last pair by walking the surface's composed tree, and that walk visits slotted content after the host's whole shadow tree, whereas the browser sequences it at the `<slot>`'s position. Initial focus can land on a control that is not the visually first one, and a `Tab` at the drawer's real last control is not recognised as the cycle's end — focus leaves the surface (with the page `inert`, usually onto the browser's own UI) and the next `Tab` is pulled back to whichever control the walk thinks is first. That is the configuration you are in whenever you wrap a third-party web component, or your own `ViewEncapsulation.ShadowDom` component, inside the drawer. **Workaround:** render a host's own focusables before its `<slot>`, or project them instead of shadowing them; `initialFocus="container"` fixes the initial-focus half only, since the cycle's edges are re-resolved on every `Tab` press. Details and markup: [Focusable order](../shared/README.md#focusable-order-is-composed-only-for-a-host-that-renders-no-slot).
589
+
590
+ **A `keydown` handler inside the drawer that calls `stopPropagation()` swallows Escape.** The dismissible-layer stack observes `Escape` on `document` in the bubble phase — a deliberate trade-off recorded on `DismissibleLayerStack` — so an event stopped inside the surface never arrives, and `Escape` silently stops dismissing while swipe-to-dismiss, the backdrop click and `[forDrawerClose]` keep working. Only the topmost drawer's `Escape` is affected; see [Nested drawers](#nested-drawers) for the stacking contract. **Workaround:** narrow the `stopPropagation()` to the keys you actually handle. Keeping the drawer open on `Escape` is the separate, supported job of the vetoable `(escapeKeyDown)` output. Details: [Escape is observed on the bubble phase](../shared/README.md#escape-is-observed-on-the-bubble-phase).
591
+
592
+ A third known limit does not apply to this primitive but is easy to hit inside one: a [Tabs](../tabs) or [Stepper](../stepper) panel rendered in a drawer cannot re-measure its focusable content across a shadow boundary, so its own tab stop can go stale — see [that entry](../shared/README.md#a-panels-focusable-content-measurement-does-not-re-measure-across-a-boundary).
593
+
584
594
  ## Styling
585
595
 
586
596
  forty-cdk ships no styles. Add your own class to each piece — the `for*` selectors are the behavior API, not a styling contract (see [Styling forty-cdk](../../../docs/styling.md)). Key your CSS off the reflected `data-*` attributes listed per piece in the [API](#api) section.
@@ -69,7 +69,7 @@ import {
69
69
  `,
70
70
  })
71
71
  export class DemoOptions {
72
- readonly alignment = signal<string>('left');
72
+ readonly alignment = signal<string | null>('left');
73
73
  cut() {
74
74
  /* ... */
75
75
  }
@@ -136,8 +136,8 @@ Angular resolves `ng-template` DI at the template's **declaration** site, not wh
136
136
  | Property | Type | Description |
137
137
  | --------------------------- | --------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
138
138
  | `open` | `model<boolean>` | Two-way bindable. Whether the menu is shown.<br>**Default:** `false` |
139
- | `side` | `input<string>` | Anchor side of `[forMenuContent]` against the trigger.<br>**Default:** `'bottom'` |
140
- | `align` | `input<string>` | Alignment along `side` (`'start'` / `'center'` / `'end'`).<br>**Default:** `'start'` |
139
+ | `side` | `input<string>` | Anchor side of `[forMenuContent]` against the trigger. The default is read from `provideForDropdownMenuDefaults` for the surrounding scope.<br>**Default:** `'bottom'` |
140
+ | `align` | `input<string>` | Alignment along `side` (`'start'` / `'center'` / `'end'`). The default is read from `provideForDropdownMenuDefaults` for the surrounding scope.<br>**Default:** `'start'` |
141
141
  | `sideOffset` | `input<number>` | Gap (px) between the trigger and the content along the main axis.<br>**Default:** `4` |
142
142
  | `alignOffset` | `input<number>` | Gap (px) along the cross axis (parallel to `side`).<br>**Default:** `0` |
143
143
  | `fallbackAxisSideDirection` | `input<'none' \| 'start' \| 'end'>` | When both sides of the preferred axis overflow, lets `flip` drop the menu to a perpendicular side instead of clipping. `'none'` keeps only the opposite same-axis placement. The default is read from `provideForDropdownMenuDefaults` for the surrounding scope — set it once for the whole app rather than per call site.<br>**Default:** `'none'` |
@@ -2,16 +2,16 @@ import * as i0 from '@angular/core';
2
2
  import { InjectionToken, inject, input, booleanAttribute, model, Directive, computed, signal, ElementRef } from '@angular/core';
3
3
  import { orphanContextError, assertRootContext, Collection, injectTextDirection, moveIndex, IdGenerator, adoptHostId, hostButtonType, registerHandle, reflectDisabled, resolveListNavigation, hostLabelledBy } from 'forty-cdk/core';
4
4
 
5
- /**
6
- * DI token for the accordion's coordination surface, provided by `[forAccordion]`.
7
- *
8
- * Publicly typed as the read surface {@link ForAccordionContext}, which is the whole of
9
- * what the token promises a consumer. The pieces read the same token at an internal type
10
- * that adds the trigger-registration protocol, so a wrapper re-providing it must alias it
11
- * to the root: `{ provide: FOR_ACCORDION_CONTEXT, useExisting: MyAccordion }`, where
12
- * `MyAccordion` extends `ForAccordion`. A value that merely satisfies the declared type
13
- * resolves too, and is rejected in dev mode by the first piece to reach the protocol.
14
- */
5
+
6
+
7
+
8
+
9
+
10
+
11
+
12
+
13
+
14
+
15
15
  const FOR_ACCORDION_CONTEXT = new InjectionToken('FOR_ACCORDION_CONTEXT');
16
16
  const FOR_ACCORDION_ITEM_CONTEXT = new InjectionToken('FOR_ACCORDION_ITEM_CONTEXT');
17
17
  function injectAccordionContext(piece) {
@@ -46,76 +46,76 @@ function injectAccordionItemContext(piece) {
46
46
  return ctx;
47
47
  }
48
48
 
49
- /**
50
- * Root of the Accordion primitive. Holds the open value(s) and orchestrates
51
- * single/multiple expansion, collapse rules, and keyboard navigation between
52
- * triggers.
53
- *
54
- * Implements the [WAI-ARIA Accordion pattern](https://www.w3.org/WAI/ARIA/apg/patterns/accordion/).
55
- *
56
- * State is modeled as `value: readonly string[]` regardless of mode:
57
- * - In single mode (`multiple=false`, default), the array has 0 or 1 element.
58
- * - In multiple mode, any number of items can be open.
59
- *
60
- * @example
61
- * ```html
62
- * <div forAccordion [(value)]="open" collapsible>
63
- * <div forAccordionItem value="a">
64
- * <h3><button type="button" forAccordionTrigger>A</button></h3>
65
- * <section forAccordionContent>...</section>
66
- * </div>
67
- * <div forAccordionItem value="b">...</div>
68
- * </div>
69
- * ```
70
- */
49
+
50
+
51
+
52
+
53
+
54
+
55
+
56
+
57
+
58
+
59
+
60
+
61
+
62
+
63
+
64
+
65
+
66
+
67
+
68
+
69
+
70
+
71
71
  class ForAccordion {
72
72
  #triggers = new Collection();
73
- /** When true, multiple items can be expanded simultaneously. */
73
+
74
74
  multiple = input(false, { ...(ngDevMode ? { debugName: "multiple" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
75
- /**
76
- * When true, the whole accordion is disabled: every item's trigger reflects
77
- * the native `disabled` attribute (dropped from the Tab order and skipped by
78
- * arrow-key navigation) and cannot toggle. Composes with a per-item
79
- * `[disabled]` — an item is effectively disabled when either is set. Mirrors
80
- * the root `disabled` on `ForTabs` / `ForStepper` / `ForDisclosure`.
81
- */
75
+
76
+
77
+
78
+
79
+
80
+
81
+
82
82
  disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
83
- /**
84
- * Single mode only: when true, the open item can be collapsed by clicking
85
- * its trigger. When false, exactly one item stays open at all times once
86
- * any has been opened.
87
- */
83
+
84
+
85
+
86
+
87
+
88
88
  collapsible = input(false, { ...(ngDevMode ? { debugName: "collapsible" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
89
- /**
90
- * Layout direction of the trigger list. `'vertical'` (default) maps
91
- * ArrowUp/Down to prev/next; `'horizontal'` maps ArrowLeft/Right (with RTL
92
- * swap when `dir='rtl'`).
93
- */
89
+
90
+
91
+
92
+
93
+
94
94
  orientation = input('vertical', /* @ts-ignore */
95
95
  ...(ngDevMode ? [{ debugName: "orientation" }] : /* istanbul ignore next */ []));
96
- /**
97
- * Writing direction. Only relevant when `orientation='horizontal'`. When
98
- * unset (default `null`), the inherited ambient direction is resolved from
99
- * the nearest ancestor carrying a `dir` attribute (or `<html dir>`),
100
- * defaulting to `'ltr'`. An explicit `[dir]` always wins. The resolved
101
- * value is reflected to the host `dir` attribute and drives arrow-key
102
- * semantics.
103
- */
96
+
97
+
98
+
99
+
100
+
101
+
102
+
103
+
104
104
  _dirInput = input(null, { ...(ngDevMode ? { debugName: "_dirInput" } : /* istanbul ignore next */ {}), alias: 'dir' });
105
105
  dir = injectTextDirection(this._dirInput);
106
- /**
107
- * When true (default), arrow navigation between triggers wraps at the ends —
108
- * moving past the last trigger focuses the first and vice versa. Set `false`
109
- * to stop at the boundaries. Mirrors the `loop` input on `ForTabs` and
110
- * `ForListbox`.
111
- */
106
+
107
+
108
+
109
+
110
+
111
+
112
112
  loop = input(true, { ...(ngDevMode ? { debugName: "loop" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
113
- /**
114
- * Two-way bindable. List of currently expanded item values. In single
115
- * mode the array has 0 or 1 element. The `model()` change emitter
116
- * (`(valueChange)`) fires only on internal toggles, never on consumer
117
- * writes via `[(value)]` — observe transitions without binding back.
118
- */
113
+
114
+
115
+
116
+
117
+
118
+
119
119
  value = model([], /* @ts-ignore */
120
120
  ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
121
121
  isExpanded(itemValue) {
@@ -181,32 +181,32 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
181
181
  }]
182
182
  }], propDecorators: { multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], collapsible: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsible", required: false }] }], orientation: [{ type: i0.Input, args: [{ isSignal: true, alias: "orientation", required: false }] }], _dirInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "dir", required: false }] }], loop: [{ type: i0.Input, args: [{ isSignal: true, alias: "loop", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }] } });
183
183
 
184
- /**
185
- * One section of a `ForAccordion`. Owns the unique `value` identifying this
186
- * item to the root and exposes the per-item context that the trigger and
187
- * content read.
188
- */
184
+
185
+
186
+
187
+
188
+
189
189
  class ForAccordionItem {
190
190
  parent = injectAccordionContext('ForAccordionItem');
191
191
  #idGen = inject(IdGenerator);
192
- /** Unique identifier of this item within the accordion. Required. */
192
+
193
193
  value = input.required(/* @ts-ignore */
194
194
  ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
195
- /**
196
- * When true, this item's trigger ignores clicks and reflects the native
197
- * `disabled` attribute (not `aria-disabled`): dropped from the Tab order and
198
- * skipped by arrow-key navigation, but kept in the accessibility tree so
199
- * screen readers still announce it. See `ForAccordionTrigger` for the rationale. Bind via
200
- * `[disabled]`; read the composed {@link disabled} for state.
201
- */
195
+
196
+
197
+
198
+
199
+
200
+
201
+
202
202
  disabledInput = input(false, { ...(ngDevMode ? { debugName: "disabledInput" } : /* istanbul ignore next */ {}), transform: booleanAttribute, alias: 'disabled' });
203
- /**
204
- * Effective disabled: this item's own `[disabled]` OR'd with the root
205
- * `[forAccordion]`'s `disabled`. Everything that gates on the item's disabled
206
- * state — native attribute reflection, trigger click, arrow-navigation skip,
207
- * `data-disabled` — reads this, so disabling the whole accordion disables
208
- * every item.
209
- */
203
+
204
+
205
+
206
+
207
+
208
+
209
+
210
210
  disabled = computed(() => this.disabledInput() || this.parent.disabled(), /* @ts-ignore */
211
211
  ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
212
212
  #triggerId = signal(this.#idGen.next('for-accordion-trigger'), /* @ts-ignore */
@@ -217,15 +217,15 @@ class ForAccordionItem {
217
217
  contentId = this.#contentId.asReadonly();
218
218
  expanded = computed(() => this.parent.isExpanded(this.value()), /* @ts-ignore */
219
219
  ...(ngDevMode ? [{ debugName: "expanded" }] : /* istanbul ignore next */ []));
220
- /**
221
- * Adopts a consumer-set static `id` on the `[forAccordionTrigger]` host into
222
- * `triggerId` (preserving anchors / external `aria-labelledby` references /
223
- * label `for`) instead of letting the `[id]` host binding clobber it.
224
- */
220
+
221
+
222
+
223
+
224
+
225
225
  adoptTriggerId(el) {
226
226
  adoptHostId(el, this.#triggerId);
227
227
  }
228
- /** Adopts a consumer-set static `id` on the content host into `contentId`. */
228
+
229
229
  adoptContentId(el) {
230
230
  adoptHostId(el, this.#contentId);
231
231
  }
@@ -252,38 +252,38 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
252
252
  }]
253
253
  }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }], disabledInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }] } });
254
254
 
255
- /**
256
- * Header button for a `ForAccordionItem`. Apply on a `<button>` wrapped in a
257
- * heading element (`<h2>`–`<h6>`) so APG landmark navigation works. The
258
- * directive host-binds `type="button"` so a trigger inside a `<form>` never
259
- * submits it on toggle.
260
- *
261
- * Handles ARIA wiring, click-to-toggle, and the recommended keyboard
262
- * navigation (ArrowDown / ArrowUp / Home / End).
263
- *
264
- * `aria-controls` is emitted only while the item is expanded — mirroring the
265
- * overlay triggers' open-only gating — so the reference never dangles at an
266
- * unmounted panel under the recommended `@if (item.expanded())` mount pattern.
267
- */
255
+
256
+
257
+
258
+
259
+
260
+
261
+
262
+
263
+
264
+
265
+
266
+
267
+
268
268
  class ForAccordionTrigger {
269
269
  buttonType = hostButtonType();
270
270
  #host = inject(ElementRef);
271
271
  parent = injectAccordionContext('ForAccordionTrigger');
272
272
  item = injectAccordionItemContext('ForAccordionTrigger');
273
- /**
274
- * APG: aria-disabled is true only when the panel is open AND the accordion
275
- * disallows collapse. A real `disabled` item is reflected via the native
276
- * `disabled` attribute instead — the sanctioned exception to that rule:
277
- * the trigger is a real single-purpose `<button>`, not a roving collection
278
- * item (every trigger stays independently in the Tab order; the arrow-key
279
- * navigation is an APG-optional enhancement layered on top, not a
280
- * roving-tabindex collection), so native `disabled` is correct here. The
281
- * button stays in the accessibility tree — screen readers still announce it
282
- * as unavailable in browse mode — while being dropped from the Tab order and
283
- * the arrow-key navigation (which `focusByOffset` already skips), keeping
284
- * disabled triggers uniformly unreachable. The APG Accordion pattern does not
285
- * require disabled headers to remain focusable.
286
- */
273
+
274
+
275
+
276
+
277
+
278
+
279
+
280
+
281
+
282
+
283
+
284
+
285
+
286
+
287
287
  ariaDisabled = computed(() => {
288
288
  if (this.item.disabled()) {
289
289
  return false;
@@ -334,20 +334,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
334
334
  }]
335
335
  }], ctorParameters: () => [] });
336
336
 
337
- /**
338
- * Panel revealed by a `ForAccordionTrigger`. The directive does not manage
339
- * DOM presence — wrap with `@if (item.expanded())` so panels mount and
340
- * unmount with the expanded state and `animate.enter` / `animate.leave`
341
- * work natively. If the consumer prefers to keep the panel mounted (for
342
- * CSS-only transitions or to preserve internal state), the directive
343
- * reflects `aria-hidden="true"` and `inert` while closed so the panel is
344
- * removed from the accessibility tree and focus order automatically.
345
- *
346
- * APG note: `role="region"` adds the panel to the landmark navigation tree.
347
- * If the accordion has 6+ simultaneously expandable panels, consider not
348
- * using region — the directive will gain an opt-out input when this comes up
349
- * in real usage.
350
- */
337
+
338
+
339
+
340
+
341
+
342
+
343
+
344
+
345
+
346
+
347
+
348
+
349
+
350
+
351
351
  class ForAccordionContent {
352
352
  parent = injectAccordionContext('ForAccordionContent');
353
353
  item = injectAccordionItemContext('ForAccordionContent');
@@ -376,9 +376,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
376
376
  }]
377
377
  }], ctorParameters: () => [] });
378
378
 
379
- /**
380
- * Generated bundle index. Do not edit.
381
- */
379
+
380
+
381
+
382
382
 
383
383
  export { FOR_ACCORDION_CONTEXT, FOR_ACCORDION_ITEM_CONTEXT, ForAccordion, ForAccordionContent, ForAccordionItem, ForAccordionTrigger };
384
384
  //# sourceMappingURL=forty-cdk-accordion.mjs.map