forty-cdk 0.38.0 → 0.39.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 (89) hide show
  1. package/avatar/README.md +2 -0
  2. package/button/README.md +1 -1
  3. package/calendar/README.md +9 -3
  4. package/carousel/README.md +1 -1
  5. package/combobox/README.md +23 -21
  6. package/date-field/README.md +7 -7
  7. package/date-picker/README.md +7 -8
  8. package/dropdown-menu/README.md +3 -1
  9. package/fesm2022/forty-cdk-carousel.mjs +2 -2
  10. package/fesm2022/forty-cdk-carousel.mjs.map +1 -1
  11. package/fesm2022/forty-cdk-combobox.mjs +232 -141
  12. package/fesm2022/forty-cdk-combobox.mjs.map +1 -1
  13. package/fesm2022/forty-cdk-context-menu.mjs.map +1 -1
  14. package/fesm2022/forty-cdk-core-overlay.mjs +23 -6
  15. package/fesm2022/forty-cdk-core-overlay.mjs.map +1 -1
  16. package/fesm2022/forty-cdk-core.mjs +12 -12
  17. package/fesm2022/forty-cdk-core.mjs.map +1 -1
  18. package/fesm2022/forty-cdk-date-field.mjs.map +1 -1
  19. package/fesm2022/forty-cdk-date-picker.mjs +5 -5
  20. package/fesm2022/forty-cdk-date-picker.mjs.map +1 -1
  21. package/fesm2022/forty-cdk-drag-drop.mjs +10 -14
  22. package/fesm2022/forty-cdk-drag-drop.mjs.map +1 -1
  23. package/fesm2022/forty-cdk-drawer.mjs +5 -5
  24. package/fesm2022/forty-cdk-drawer.mjs.map +1 -1
  25. package/fesm2022/forty-cdk-dropdown-menu.mjs.map +1 -1
  26. package/fesm2022/forty-cdk-field.mjs +2 -2
  27. package/fesm2022/forty-cdk-field.mjs.map +1 -1
  28. package/fesm2022/forty-cdk-fieldset.mjs +2 -2
  29. package/fesm2022/forty-cdk-fieldset.mjs.map +1 -1
  30. package/fesm2022/forty-cdk-file-upload.mjs +6 -3
  31. package/fesm2022/forty-cdk-file-upload.mjs.map +1 -1
  32. package/fesm2022/forty-cdk-listbox.mjs +199 -135
  33. package/fesm2022/forty-cdk-listbox.mjs.map +1 -1
  34. package/fesm2022/forty-cdk-menu.mjs.map +1 -1
  35. package/fesm2022/forty-cdk-navigation-menu.mjs +27 -25
  36. package/fesm2022/forty-cdk-navigation-menu.mjs.map +1 -1
  37. package/fesm2022/forty-cdk-select.mjs +227 -157
  38. package/fesm2022/forty-cdk-select.mjs.map +1 -1
  39. package/fesm2022/forty-cdk-slider.mjs +2 -2
  40. package/fesm2022/forty-cdk-slider.mjs.map +1 -1
  41. package/fesm2022/forty-cdk-table-virtualization.mjs.map +1 -1
  42. package/fesm2022/forty-cdk-table.mjs +5 -5
  43. package/fesm2022/forty-cdk-table.mjs.map +1 -1
  44. package/fesm2022/forty-cdk-time-field.mjs.map +1 -1
  45. package/fesm2022/forty-cdk-time-picker.mjs +2 -2
  46. package/fesm2022/forty-cdk-time-picker.mjs.map +1 -1
  47. package/fesm2022/forty-cdk-toast.mjs +3 -3
  48. package/fesm2022/forty-cdk-toast.mjs.map +1 -1
  49. package/fesm2022/forty-cdk-toggle.mjs.map +1 -1
  50. package/fesm2022/forty-cdk-toolbar.mjs.map +1 -1
  51. package/fesm2022/forty-cdk-tree.mjs +174 -111
  52. package/fesm2022/forty-cdk-tree.mjs.map +1 -1
  53. package/fesm2022/forty-cdk-virtualization.mjs +2 -1
  54. package/fesm2022/forty-cdk-virtualization.mjs.map +1 -1
  55. package/listbox/README.md +27 -21
  56. package/menubar/README.md +1 -1
  57. package/navigation-menu/README.md +2 -2
  58. package/number-input/README.md +9 -1
  59. package/package.json +1 -1
  60. package/progress/README.md +9 -1
  61. package/radio-group/README.md +2 -2
  62. package/select/README.md +18 -12
  63. package/stepper/README.md +1 -1
  64. package/switch/README.md +19 -6
  65. package/table/README.md +42 -36
  66. package/table-virtualization/README.md +1 -1
  67. package/time-field/README.md +6 -6
  68. package/time-picker/README.md +8 -3
  69. package/toggle/README.md +1 -1
  70. package/tree/README.md +25 -14
  71. package/types/forty-cdk-carousel.d.ts +2 -1
  72. package/types/forty-cdk-combobox.d.ts +176 -63
  73. package/types/forty-cdk-context-menu.d.ts +1 -1
  74. package/types/forty-cdk-core-overlay.d.ts +12 -1
  75. package/types/forty-cdk-core.d.ts +48 -42
  76. package/types/forty-cdk-date-field.d.ts +6 -2
  77. package/types/forty-cdk-dropdown-menu.d.ts +1 -1
  78. package/types/forty-cdk-file-upload.d.ts +4 -0
  79. package/types/forty-cdk-listbox.d.ts +177 -63
  80. package/types/forty-cdk-menu.d.ts +1 -1
  81. package/types/forty-cdk-navigation-menu.d.ts +6 -0
  82. package/types/forty-cdk-select.d.ts +197 -86
  83. package/types/forty-cdk-table.d.ts +1 -1
  84. package/types/forty-cdk-time-field.d.ts +5 -4
  85. package/types/forty-cdk-toggle.d.ts +4 -3
  86. package/types/forty-cdk-toolbar.d.ts +2 -2
  87. package/types/forty-cdk-tree.d.ts +303 -61
  88. package/virtual-reorder/README.md +1 -1
  89. package/virtualization/README.md +3 -1
package/avatar/README.md CHANGED
@@ -100,6 +100,8 @@ When the image errors, the directive flips `shouldShowFallback()` and the initia
100
100
 
101
101
  The directive does not impose a `role`. Pair the avatar with visible name text or `aria-label` on the surrounding element when identity matters. Set `alt=""` on the `<img>` for purely decorative avatars next to a name, or provide a meaningful `alt` description if the avatar stands alone.
102
102
 
103
+ A meaningful `alt` names the avatar only while the image is shown: once it fails, the image is hidden and the fallback initials are all that is left to announce. When an avatar stands alone, name the root instead (`role="img"` with an `aria-label`) and give the `<img>` `alt=""`, so the fallback carries the same name as the loaded image.
104
+
103
105
  ## Styling
104
106
 
105
107
  forty-cdk ships no styles: put your own class on each piece and key your CSS off the `data-*` attributes listed under [API](#api), not off the `for*` selectors ([Styling forty-cdk](../../../docs/styling.md) explains why).
package/button/README.md CHANGED
@@ -5,7 +5,7 @@ archetype: [composable-ui]
5
5
  apgUrl: https://www.w3.org/WAI/ARIA/apg/patterns/button/
6
6
  ---
7
7
 
8
- # ForButton
8
+ # Button
9
9
 
10
10
  Turns any element (a native `<button>` or a custom host like `<div>` / `<span>`) into an accessible button with keyboard activation. Disabled stays focusable (aria-disabled, never the native attribute) and pressed / hovered / focus-visible are reflected as data-\* hooks.
11
11
 
@@ -341,12 +341,18 @@ The recommended path is `[forCalendarMonthSelect]` and `[forCalendarYearSelect]`
341
341
  />
342
342
  </svg>
343
343
  </button>
344
- <select forCalendarMonthSelect #m="forCalendarMonthSelect">
344
+ <select forCalendarMonthSelect #m="forCalendarMonthSelect" aria-label="Month">
345
345
  @for (opt of m.options(); track opt.value) {
346
346
  <option [value]="opt.value" [disabled]="opt.disabled">{{ opt.label }}</option>
347
347
  }
348
348
  </select>
349
- <select forCalendarYearSelect #y="forCalendarYearSelect" [minYear]="1900" [maxYear]="2100">
349
+ <select
350
+ forCalendarYearSelect
351
+ #y="forCalendarYearSelect"
352
+ aria-label="Year"
353
+ [minYear]="1900"
354
+ [maxYear]="2100"
355
+ >
350
356
  @for (opt of y.years(); track opt.value) {
351
357
  <option [value]="opt.value" [disabled]="opt.disabled">{{ opt.value }}</option>
352
358
  }
@@ -575,7 +581,7 @@ Implements the [WAI-ARIA Grid pattern](https://www.w3.org/WAI/ARIA/apg/patterns/
575
581
  - **`aria-selected`** is always emitted (`"true"` / `"false"`); **`aria-current="date"`** marks today; **`aria-disabled`** marks unavailable dates (truthy-only).
576
582
  - **`aria-multiselectable="true"`** is set on the day grid in `selectionMode="range"`, where a committed range selects several cells, and is absent in single mode.
577
583
  - **Roving tabindex**: exactly one cell (the focused date) is tabbable. `Tab` enters and leaves the grid in one stop.
578
- - **Boolean `data-*`** on the cell, present when true and absent when false: `data-selected`, `data-today`, `data-highlighted` (the focused/roving cell), `data-disabled` and `data-outside-month`.
584
+ - **Boolean `data-*`** on the cell, present when true and absent when false: `data-selected`, `data-today`, `data-highlighted` (the roving tab-stop cell, kept after focus leaves the grid), `data-disabled` and `data-outside-month`.
579
585
 
580
586
  ## Styling
581
587
 
@@ -5,7 +5,7 @@ archetype: [composable-ui]
5
5
  apgUrl: https://www.w3.org/WAI/ARIA/apg/patterns/carousel/
6
6
  ---
7
7
 
8
- # ForCarousel
8
+ # Carousel
9
9
 
10
10
  A slideshow of content panels with previous / next controls, an indicator group, optional looping and multi-slide views, and an accessible autoplay mode with a pause control.
11
11
 
@@ -115,17 +115,11 @@ const COUNTRIES = [
115
115
  ForComboboxClear,
116
116
  ],
117
117
  template: `
118
- <div
119
- forCombobox
120
- #combobox="forCombobox"
121
- class="combobox"
122
- [(query)]="query"
123
- [(value)]="value"
124
- ariaLabel="Country search"
125
- >
118
+ <div forCombobox #combobox="forCombobox" class="combobox" [(query)]="query" [(value)]="value">
126
119
  <div class="combobox-single">
127
120
  <input
128
121
  forComboboxInput
122
+ aria-label="Country search"
129
123
  class="combobox-input combobox-input--boxed"
130
124
  placeholder="Search countries…"
131
125
  />
@@ -192,7 +186,7 @@ Click the `[forComboboxTrigger]` button, type into the search field inside the p
192
186
 
193
187
  ### Virtualized (1,000 options)
194
188
 
195
- The primitive never owns the scroll container, so it virtualizes with any windowing strategy. The demo uses a dependency-free one. The consumer renders only the visible window and wires `[totalCount]`, `[visibleRange]` and `[forComboboxOption][posInSet]`; `(scrollToIndex)` fires when navigation targets a row outside the window.
189
+ The primitive never owns the scroll container, so it virtualizes with any windowing strategy. The demo uses a dependency-free one. The consumer renders only the visible window, adds `forComboboxVirtualized` to the combobox, and wires its `[totalCount]` and `[visibleRange]` plus `[forComboboxOption][posInSet]`; `(scrollToIndex)` fires when navigation targets a row outside the window.
196
190
 
197
191
  ## API
198
192
 
@@ -225,6 +219,9 @@ The content anchors to the field box when a `[forComboboxAnchor]` is present; se
225
219
  | `[forCombobox]` | `data-dirty` | present / absent |
226
220
  | `[forCombobox]` | `data-pending` | present / absent |
227
221
  | `[forCombobox]` | `data-invalid` | present / absent |
222
+ | `[forComboboxTrigger]` | `data-state` | `open` \| `closed` |
223
+ | `[forComboboxTrigger]` | `data-disabled` | present / absent |
224
+ | `[forComboboxTrigger]` | `data-readonly` | present / absent |
228
225
  | `[forComboboxInput]` | `data-state` | `open` \| `closed` |
229
226
  | `[forComboboxInput]` | `data-disabled` | present / absent |
230
227
  | `[forComboboxToggle]` | `data-state` | `open` \| `closed` |
@@ -299,7 +296,7 @@ The default (editable) anatomy is a text field that filters in place. A **picker
299
296
 
300
297
  Add two parts:
301
298
 
302
- - **`[forComboboxTrigger]`**: a real `<button>` outside the `@if (open())`. It opens the panel, becomes the default positioning anchor, and is where focus returns on close.
299
+ - **`[forComboboxTrigger]`**: a real `<button>` outside the `@if (open())`. It opens the panel, becomes the default positioning anchor, and is where focus returns on close. It carries `role="combobox"` and announces the form state (`aria-required`, `aria-invalid`, `aria-busy`, `aria-readonly`), because while the panel is closed it is the only focusable part of the field. Give it a label: a combobox is not named by its text.
303
300
  - **`[forComboboxList]`**: the `role="listbox"` element nested inside `[forComboboxContent]`, next to the input. It owns the options and the labelled role; `[forComboboxContent]` becomes the `role="dialog"` the trigger opens, named by the trigger.
304
301
 
305
302
  ```html
@@ -669,14 +666,17 @@ The same three inputs cover multi mode + chips: bind `<span forComboboxChip [val
669
666
 
670
667
  ## Virtualization
671
668
 
672
- For very large option sets (1k+) the consumer can render only the visible window and let the directive coordinate navigation across the full source. Three additive inputs / one output cover the wiring; non-virtualized usage is unchanged.
669
+ For very large option sets (1k+) the consumer can render only the visible window and let the directive coordinate navigation across the full source. The wiring lives on `ForComboboxVirtualized` (`[forComboboxVirtualized]`, `exportAs: 'forComboboxVirtualized'`), placed on the same element as `[forCombobox]` and imported from `forty-cdk/combobox` next to `ForCombobox`. Non-virtualized usage is unchanged, and a combobox without the directive never bundles the virtualized navigation engine.
673
670
 
674
- | Input / Output | Purpose |
675
- | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
676
- | `[totalCount]: number \| undefined` | Length of the **filtered** source array. Drives `aria-setsize` and lets navigation walk past the rendered window. Leave `undefined` (default) for non-virtualized lists. |
677
- | `[visibleRange]: [start, end) \| undefined` | Inclusive-exclusive index range currently rendered in the DOM. Pulled from your virtualizer's `getVirtualItems()`. |
678
- | `[forComboboxOption][posInSet]` | Absolute index of this option in the source array. Required when virtualizing; the directive folds option data into a snapshot keyed by this index so it survives unmount. |
679
- | `(scrollToIndex)` | Emitted when arrow keys (or Home / End) need to land on an option whose absolute index falls outside `visibleRange()`. Wire to the virtualizer's `scrollToIndex(idx)`. |
671
+ | `ForComboboxVirtualized` member | Purpose |
672
+ | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
673
+ | `[totalCount]: number \| undefined` | Length of the **filtered** source array. Drives `aria-setsize` and lets navigation walk past the rendered window. Leave `undefined` (default) for non-virtualized lists. |
674
+ | `[visibleRange]: [start, end) \| undefined` | Inclusive-exclusive index range currently rendered in the DOM. Pulled from your virtualizer's `getVirtualItems()`. |
675
+ | `[dataVersion]: unknown` | Bind any value that changes when the source is re-sorted or refreshed without its length changing, so navigation never lands on a stale off-window entry. |
676
+ | `(scrollToIndex)` | Emitted when arrow keys (or Home / End) need to land on an option whose absolute index falls outside `visibleRange()`. Wire to the virtualizer's `scrollToIndex(idx)`. |
677
+ | `invalidateSnapshot()` | The imperative twin of `[dataVersion]`, for a same-length refresh you cannot express through the input. No-op while `totalCount` is unset. |
678
+
679
+ Each rendered option also binds `[forComboboxOption][posInSet]`, its absolute index in the source array. It is required when virtualizing: the directive keeps option data keyed by this index so it survives unmount.
680
680
 
681
681
  How navigation flows when virtualizing:
682
682
 
@@ -692,6 +692,7 @@ Inline autocomplete matches against the most recently rendered window overlaid w
692
692
  ```html
693
693
  <div
694
694
  forCombobox
695
+ forComboboxVirtualized
695
696
  [(query)]="query"
696
697
  [(value)]="value"
697
698
  [(open)]="open"
@@ -742,11 +743,11 @@ This uses the library's own [`injectVirtualizer`](../virtualization/README.md) c
742
743
 
743
744
  `[totalCount]` is also the result count the message pieces read: `[forComboboxStatus]`'s `count()` reports it rather than the size of the rendered window, so `{{ status.count() }} results.` announces the full count once and stays put while the window scrolls, and `[forComboboxEmpty]` stays hidden while a window with results has not rendered yet.
744
745
 
745
- When `[totalCount]` is omitted, the directive falls back to `options().length` and behaves exactly as before: `aria-setsize` is left to the platform default and navigation never emits `(scrollToIndex)`.
746
+ When `[forComboboxVirtualized]` is absent or its `[totalCount]` is omitted, the combobox falls back to `options().length` and behaves exactly as before: `aria-setsize` is left to the platform default and navigation never emits `(scrollToIndex)`.
746
747
 
747
748
  > **Disabled options off-screen.** The directive learns an option's `disabled` only when it's been rendered at least once. While the consumer can pre-mark disabled rows with their own filter (most apps do), arrow nav cannot skip an off-screen disabled option it has never seen: it will land on it, the option will mount, and the next arrow press skips. Mark disabled rows in the source array if this matters.
748
749
 
749
- > **Listbox virtualization** ships the same contract: `[forListbox]` defaults to roving tabindex (DOM focus on the actual option element) and switches to the `aria-activedescendant` model when you set `[totalCount]`. See the [Listbox README "Virtualization"](../listbox/README.md#virtualization) section.
750
+ > **Listbox virtualization** ships the same contract: `[forListbox]` defaults to roving tabindex (DOM focus on the actual option element) and switches to the `aria-activedescendant` model when you add `[forListboxVirtualized]` with `[totalCount]`. See the [Listbox README "Virtualization"](../listbox/README.md#virtualization) section.
750
751
 
751
752
  ## Writing direction
752
753
 
@@ -838,10 +839,11 @@ Hovering an option also makes it the activedescendant, so mouse and keyboard int
838
839
 
839
840
  Implements the [WAI-ARIA Combobox pattern](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/).
840
841
 
842
+ - **Name the input.** `role="combobox"` takes its name from the author, and a placeholder is only a fallback that disappears once the user types. The root's `ariaLabel` names the listbox, not the input. Give `[forComboboxInput]` a `<label for>`, an `aria-label` or an `aria-labelledby`, or wrap the combobox in `[forField]` with a `[forLabel]`. In the picker anatomy, label `[forComboboxTrigger]` the same way, for example `aria-labelledby` pointing at a visible label and at the element showing the value.
841
843
  - Apply the input directive to an actual `<input>`: the browser's caret and selection semantics are what make inline autocomplete work, and screen readers expect a real text field for `role="combobox"`.
842
844
  - `role="listbox"` lives on `[forComboboxContent]` in the editable anatomy and on `[forComboboxList]` in the picker anatomy; the input's `aria-controls` targets whichever carries it. In the picker anatomy the popup surface (`[forComboboxContent]`) is a `role="dialog"` named by the trigger (`aria-labelledby`; a static `aria-label` or `aria-labelledby` on it wins), so it can hold the input next to the list without an `aria-required-owned-elements` violation. The "editable + list" shape, with no trigger, leaves it role-less.
843
845
  - `aria-multiselectable="true"` (multi mode) and the labelled role (`aria-label` / `aria-labelledby`, pointing at the input) sit on whichever element carries `role="listbox"`: content in the editable anatomy, the list in the picker anatomy.
844
- - `[forComboboxTrigger]` (picker anatomy) is a real `<button>` reflecting `aria-haspopup="dialog"`, `aria-expanded`, `aria-controls` (the dialog surface, while open), an `id` (a static one is kept), and native `disabled` from the combobox's effective disabled. The popup is a dialog rather than a listbox because focus moves into the search input inside it. It is exempt from the popup's outside-pointer dismissal layer, like the input.
846
+ - `[forComboboxTrigger]` (picker anatomy) is a real `<button>` with `role="combobox"`, reflecting `aria-haspopup="dialog"`, `aria-expanded`, `aria-controls` (the dialog surface, while open), an `id` (a static one is kept), and native `disabled` from the combobox's effective disabled. It also reflects `aria-required`, `aria-invalid`, `aria-busy` and `aria-readonly` (truthy-only) from the root, so a closed picker announces a required, failing or validating field: the input carrying the same state only exists while the panel is open. It keeps the role while the dialog is open, next to the `role="combobox"` search input inside it. The popup is a dialog rather than a listbox because focus moves into the search input inside it. It is exempt from the popup's outside-pointer dismissal layer, like the input.
845
847
  - `[forComboboxContent]` and `[forComboboxList]` cancel a `mousedown` that would otherwise move focus onto the surface or blur the input, so a press between options leaves focus in the input. A press on a focusable descendant is let through.
846
848
  - `[forComboboxToggle]` (editable anatomy) is a real `<button>` with `tabindex="-1"`, a localizable `aria-label`, `aria-expanded`, `aria-controls` (the listbox, while open) and native `disabled`. It cancels `mousedown` so focus stays in the input, and it is exempt from the outside-pointer dismissal layer.
847
849
  - `[forComboboxClear]` has `tabindex="-1"` and a localizable `aria-label`. It cancels `mousedown` too, and is exempt from the outside-pointer dismissal layer, so clearing an open listbox leaves it open with focus in the input and the combobox untouched.
@@ -885,4 +887,4 @@ forty-cdk ships no styles: put your own class on each piece and key your CSS off
885
887
 
886
888
  ## Wrapping in a design system
887
889
 
888
- [Wrapping form primitives](../../../docs/wrapping-form-primitives.md) documents both supported wrapper patterns: `hostDirectives` with the exported `FOR_COMBOBOX_HOST_DIRECTIVE_INPUTS` / `FOR_COMBOBOX_HOST_DIRECTIVE_OUTPUTS` name tuples, and subclassing.
890
+ [Wrapping form primitives](../../../docs/wrapping-form-primitives.md) documents both supported wrapper patterns: `hostDirectives` with the exported `FOR_COMBOBOX_HOST_DIRECTIVE_INPUTS` / `FOR_COMBOBOX_HOST_DIRECTIVE_OUTPUTS` name tuples, and subclassing. A virtualized wrapper adds `ForComboboxVirtualized` as a second `hostDirectives` entry and re-exposes its surface with `FOR_COMBOBOX_VIRTUALIZED_HOST_DIRECTIVE_INPUTS` / `FOR_COMBOBOX_VIRTUALIZED_HOST_DIRECTIVE_OUTPUTS`.
@@ -135,7 +135,7 @@ export class DobFormField {
135
135
 
136
136
  ### Date range
137
137
 
138
- `ForDateRangeField` is the range variant, shipped from this same entry point. Two labelled endpoint groups share locale, granularity and bounds; each is its own tab stop, so `Tab` steps start → end while the arrows move between segments inside one endpoint.
138
+ `ForDateRangeField` is the range variant, shipped from this same entry point. Two labelled endpoint groups share locale, granularity and bounds; each is its own tab stop, so `Tab` steps start → end while the arrows move between segments inside one endpoint. Type an end date before the start and a message under the field says so, read from `disordered()`, since the red border alone is a colour cue only.
139
139
 
140
140
  ### Date & time range
141
141
 
@@ -143,7 +143,7 @@ With a time-capable adapter, a `granularity` finer than `'day'` appends time seg
143
143
 
144
144
  ### Range in Signal Forms
145
145
 
146
- `ForDateRangeField` implements `FormValueControl<DateRange | null>`, so `[formField]` binds the committed range into the form and pulls validation back out. A half-entered or out-of-order range keeps `value()` null, so a `required` field stays invalid until both endpoints are filled and ordered.
146
+ `ForDateRangeField` implements `FormValueControl<DateRange | null>`, so `[formField]` binds the committed range into the form and pulls validation back out. A half-entered or out-of-order range keeps `value()` null, so a `required` field stays invalid until both endpoints are filled and ordered. The error message tells the two cases apart through `disordered()`.
147
147
 
148
148
  ## API
149
149
 
@@ -191,7 +191,7 @@ A native reset of the owning `<form>` returns `value` to what it held when the f
191
191
  | `[forDateFieldSegment]` | `data-disabled` | present \| absent |
192
192
  | `[forDateFieldSegment]` | `data-readonly` | present \| absent |
193
193
 
194
- `data-empty` marks the field only while **every** editable segment is empty (nothing has been entered); a partially-typed field is **not** empty. `data-placeholder` marks each individual segment that is still empty. `data-highlighted` is the current roving-tabindex segment and the only focus hook the consumer gets, shared with the other roving primitives. `[forDateFieldLiteral]` carries no data-\* attributes (it is `aria-hidden` and out of the tab order).
194
+ `data-empty` marks the field only while **every** editable segment is empty (nothing has been entered); a partially-typed field is **not** empty. `data-placeholder` marks each individual segment that is still empty. `data-highlighted` marks the roving tab-stop segment, the vocabulary shared with the other roving primitives. It stays on that segment after focus leaves the field, so a style that reads as focus is scoped to the field's `:focus-within`, and to each endpoint group's in a range field, where each endpoint keeps its own. `[forDateFieldLiteral]` carries no data-\* attributes (it is `aria-hidden` and out of the tab order).
195
195
 
196
196
  ## Date-time field
197
197
 
@@ -306,7 +306,7 @@ The endpoint groups each accept an `ariaLabel` input for their own group label,
306
306
 
307
307
  ### Ordering
308
308
 
309
- The two endpoints are typed independently, so order is not guaranteed by construction the way the picker's two-click flow guarantees it. The field preserves the `DateRange` `end >= start` invariant by **never emitting an out-of-order range**: when both endpoints are complete but `start > end`, the typed segments are kept (not silently rewritten), `value` stays `null`, and the root reflects `aria-invalid="true"` + `data-range-error` so the disorder is perceivable and stylable. Editing either endpoint back into order emits the range.
309
+ The two endpoints are typed independently, so order is not guaranteed by construction the way the picker's two-click flow guarantees it. The field preserves the `DateRange` `end >= start` invariant by **never emitting an out-of-order range**: when both endpoints are complete but `start > end`, the typed segments are kept (not silently rewritten), `value` stays `null`, and the root reflects `aria-invalid="true"` + `data-range-error` so the disorder is perceivable and stylable. Editing either endpoint back into order emits the range. The border colour alone is not an error message, so read the same state from `disordered()` (through `#range="forDateRangeField"`) and say it in words, as the Date range example does.
310
310
 
311
311
  ### Range keyboard and accessibility
312
312
 
@@ -339,13 +339,13 @@ Composes the [WAI-ARIA Spinbutton pattern](https://www.w3.org/WAI/ARIA/apg/patte
339
339
  - **`role="spinbutton"`** per segment, with `aria-valuemin` / `aria-valuemax` / `aria-valuenow` reflected; the month segment also exposes a localized `aria-valuetext` ("March"), so screen readers read the name rather than the number.
340
340
  - **Roving tabindex**: exactly one segment is tabbable, so `Tab` enters and leaves the whole field in one stop; arrows move between segments.
341
341
  - **Literals are `aria-hidden`** and never focusable, so assistive tech reads only the spinbutton segments.
342
- - **Boolean `data-*`** on each segment, present when true and absent when false: `data-highlighted` (focused/roving), `data-placeholder` (empty), `data-disabled`, `data-readonly`.
342
+ - **Boolean `data-*`** on each segment, present when true and absent when false: `data-highlighted` (the roving tab stop, kept after blur), `data-placeholder` (empty), `data-disabled`, `data-readonly`.
343
343
  - **`aria-readonly` belongs on the segments, not the group.** WAI-ARIA supports it on `role="spinbutton"` but not on `role="group"`, so each segment carries `aria-readonly="true"` while the group reflects the `data-readonly` styling hook only.
344
344
  - **On-screen keyboards work.** A segment of a field that is neither disabled nor read-only is a `contenteditable="plaintext-only"` editing host with `inputmode="numeric"`, so tapping it on a touch device opens the numeric keypad (the AM/PM segment opens the text keyboard). What the keyboard or an input method types goes through the [Keyboard](#keyboard) map above and is never inserted into the segment, which keeps rendering `{{ seg.text }}`.
345
345
 
346
346
  ## Styling
347
347
 
348
- The library is styleless, so style the boolean `data-*` hooks yourself: `[data-highlighted]` (the focused/roving segment), `[data-placeholder]` (empty), `[data-disabled]` and `[data-readonly]` on the segments, and `[data-empty]` / `[data-disabled]` / `[data-readonly]` / `[data-required]` plus the form-state hooks `[data-touched]` / `[data-dirty]` / `[data-pending]` / `[data-invalid]` on the root group.
348
+ The library is styleless, so style the boolean `data-*` hooks yourself: `[data-highlighted]` (the roving tab-stop segment, kept after focus leaves the field), `[data-placeholder]` (empty), `[data-disabled]` and `[data-readonly]` on the segments, and `[data-empty]` / `[data-disabled]` / `[data-readonly]` / `[data-required]` plus the form-state hooks `[data-touched]` / `[data-dirty]` / `[data-pending]` / `[data-invalid]` on the root group.
349
349
 
350
350
  forty-cdk ships no styles: put your own class on each piece and key your CSS off the `data-*` attributes listed under [Data attributes](#data-attributes), not off the `for*` selectors ([Styling forty-cdk](../../../docs/styling.md) explains why).
351
351
 
@@ -355,7 +355,7 @@ A segment is an editing host, so the browser paints a text caret in the focused
355
355
  .date-field-segment[data-placeholder] {
356
356
  color: GrayText;
357
357
  }
358
- .date-field-segment[data-highlighted] {
358
+ .date-field:focus-within .date-field-segment[data-highlighted] {
359
359
  caret-color: transparent;
360
360
  background: Highlight;
361
361
  color: HighlightText;
@@ -94,14 +94,13 @@ import { provideInternationalizedDateAdapter } from 'forty-cdk/internationalized
94
94
  ],
95
95
  providers: [...provideInternationalizedDateAdapter()],
96
96
  template: `
97
- <div
98
- forDatePicker
99
- #picker="forDatePicker"
100
- [(value)]="date"
101
- placeholder="Pick a date"
102
- ariaLabel="Choose a date"
103
- >
104
- <button forDatePickerTrigger type="button" class="date-picker-trigger">
97
+ <div forDatePicker #picker="forDatePicker" [(value)]="date" placeholder="Pick a date">
98
+ <button
99
+ forDatePickerTrigger
100
+ aria-label="Choose a date"
101
+ type="button"
102
+ class="date-picker-trigger"
103
+ >
105
104
  <span forDatePickerValue class="date-picker-value"></span>
106
105
  <svg class="date-picker-chevron" viewBox="0 0 24 24" aria-hidden="true">
107
106
  <path
@@ -65,7 +65,9 @@ import { ForMenuContent, ForMenuItem, ForMenuSeparator } from 'forty-cdk/menu';
65
65
  <hr forMenuSeparator class="dropdown-menu-separator" />
66
66
  <button forMenuItem class="dropdown-menu-item">Downloads</button>
67
67
  <button forMenuItem class="dropdown-menu-item">Bookmarks</button>
68
- <button forMenuItem class="dropdown-menu-item" disabled>Sync (signed out)</button>
68
+ <button forMenuItem class="dropdown-menu-item" [disabled]="true">
69
+ Sync (signed out)
70
+ </button>
69
71
  <hr forMenuSeparator class="dropdown-menu-separator" />
70
72
  <button forMenuItem class="dropdown-menu-item dropdown-menu-item--danger">
71
73
  Clear browsing data
@@ -1,5 +1,5 @@
1
1
  import * as i0 from "@angular/core";
2
- import { DestroyRef, Directive, ElementRef, InjectionToken, PLATFORM_ID, booleanAttribute, computed, effect, inject, input, isDevMode, model, numberAttribute, signal, untracked } from "@angular/core";
2
+ import { DestroyRef, Directive, ElementRef, InjectionToken, PLATFORM_ID, booleanAttribute, computed, effect, inject, input, model, numberAttribute, signal, untracked } from "@angular/core";
3
3
  import { isPlatformBrowser } from "@angular/common";
4
4
  import { Collection, FLICK_STALE_VELOCITY_MS, FLICK_VELOCITY_PX_PER_MS, assertRootContext, attachSwipeDismiss, clamp, createSingleSlot, firstEnabledHost, flickVelocity, fortyWarn, hostAriaLabel, hostButtonType, hostId, injectElementSize, injectPauseController, injectPrefersReducedMotion, injectTextDirection, isScrollableAtEdge, moveIndex, navigateFromRovingItem, orphanContextError, registerHandle, resolveText, resolveTextInput, selectionTabStop } from "forty-cdk/core";
5
5
  import { FOR_CAROUSEL_DEFAULTS, FOR_CAROUSEL_DEFAULTS as FOR_CAROUSEL_DEFAULTS$1, provideForCarouselDefaults } from "forty-cdk/defaults";
@@ -101,7 +101,7 @@ var ForCarousel = class ForCarousel {
101
101
  untracked(() => this.#syncTimer(rotating, interval));
102
102
  });
103
103
  this.#destroyRef.onDestroy(() => this.#clearTimer());
104
- if (isDevMode()) {
104
+ if (typeof ngDevMode === "undefined" || ngDevMode) {
105
105
  let warned = false;
106
106
  effect(() => {
107
107
  const slides = this.slideCount();