forty-cdk 0.36.0 → 0.37.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 (92) hide show
  1. package/accordion/README.md +8 -8
  2. package/calendar/README.md +15 -12
  3. package/checkbox/README.md +4 -0
  4. package/combobox/README.md +30 -7
  5. package/context-menu/README.md +9 -3
  6. package/date-field/README.md +23 -14
  7. package/date-picker/README.md +47 -28
  8. package/dialog/README.md +1 -1
  9. package/drawer/README.md +1 -1
  10. package/dropdown-menu/README.md +6 -1
  11. package/fesm2022/forty-cdk-calendar.mjs +96 -39
  12. package/fesm2022/forty-cdk-calendar.mjs.map +1 -1
  13. package/fesm2022/forty-cdk-combobox.mjs +54 -27
  14. package/fesm2022/forty-cdk-combobox.mjs.map +1 -1
  15. package/fesm2022/forty-cdk-context-menu.mjs +58 -12
  16. package/fesm2022/forty-cdk-context-menu.mjs.map +1 -1
  17. package/fesm2022/forty-cdk-core-overlay.mjs +134 -45
  18. package/fesm2022/forty-cdk-core-overlay.mjs.map +1 -1
  19. package/fesm2022/forty-cdk-core.mjs +123 -17
  20. package/fesm2022/forty-cdk-core.mjs.map +1 -1
  21. package/fesm2022/forty-cdk-date-field.mjs.map +1 -1
  22. package/fesm2022/forty-cdk-date-picker.mjs +6 -4
  23. package/fesm2022/forty-cdk-date-picker.mjs.map +1 -1
  24. package/fesm2022/forty-cdk-dialog.mjs +1 -1
  25. package/fesm2022/forty-cdk-dialog.mjs.map +1 -1
  26. package/fesm2022/forty-cdk-drawer.mjs +1 -1
  27. package/fesm2022/forty-cdk-drawer.mjs.map +1 -1
  28. package/fesm2022/forty-cdk-listbox.mjs +62 -18
  29. package/fesm2022/forty-cdk-listbox.mjs.map +1 -1
  30. package/fesm2022/forty-cdk-popover.mjs.map +1 -1
  31. package/fesm2022/forty-cdk-search.mjs +16 -1
  32. package/fesm2022/forty-cdk-search.mjs.map +1 -1
  33. package/fesm2022/forty-cdk-select.mjs +6 -4
  34. package/fesm2022/forty-cdk-select.mjs.map +1 -1
  35. package/fesm2022/forty-cdk-table-virtualization.mjs +29 -9
  36. package/fesm2022/forty-cdk-table-virtualization.mjs.map +1 -1
  37. package/fesm2022/forty-cdk-table.mjs +89 -25
  38. package/fesm2022/forty-cdk-table.mjs.map +1 -1
  39. package/fesm2022/forty-cdk-time-field.mjs.map +1 -1
  40. package/fesm2022/forty-cdk-time-picker.mjs +43 -26
  41. package/fesm2022/forty-cdk-time-picker.mjs.map +1 -1
  42. package/fesm2022/forty-cdk-toast.mjs +5 -1
  43. package/fesm2022/forty-cdk-toast.mjs.map +1 -1
  44. package/fesm2022/forty-cdk-tooltip.mjs +7 -2
  45. package/fesm2022/forty-cdk-tooltip.mjs.map +1 -1
  46. package/fesm2022/forty-cdk-tree.mjs +51 -36
  47. package/fesm2022/forty-cdk-tree.mjs.map +1 -1
  48. package/fesm2022/forty-cdk-virtual-reorder.mjs +2 -16
  49. package/fesm2022/forty-cdk-virtual-reorder.mjs.map +1 -1
  50. package/fesm2022/forty-cdk-virtualization.mjs +7 -4
  51. package/fesm2022/forty-cdk-virtualization.mjs.map +1 -1
  52. package/hover-card/README.md +17 -13
  53. package/listbox/README.md +21 -13
  54. package/menu/README.md +4 -4
  55. package/menubar/README.md +6 -6
  56. package/navigation-menu/README.md +1 -1
  57. package/package.json +1 -1
  58. package/pane-resizer/README.md +1 -1
  59. package/popover/README.md +34 -31
  60. package/radio-group/README.md +5 -1
  61. package/scroll-area/README.md +1 -1
  62. package/search/README.md +12 -6
  63. package/select/README.md +24 -4
  64. package/table/README.md +24 -16
  65. package/table-virtualization/README.md +3 -1
  66. package/time-field/README.md +23 -14
  67. package/time-picker/README.md +54 -36
  68. package/toast/README.md +1 -1
  69. package/toggle/README.md +2 -2
  70. package/toolbar/README.md +1 -1
  71. package/tooltip/README.md +26 -21
  72. package/tree/README.md +2 -0
  73. package/types/forty-cdk-calendar.d.ts +90 -28
  74. package/types/forty-cdk-combobox.d.ts +40 -24
  75. package/types/forty-cdk-context-menu.d.ts +14 -1
  76. package/types/forty-cdk-core-overlay.d.ts +74 -30
  77. package/types/forty-cdk-core.d.ts +27 -9
  78. package/types/forty-cdk-date-field.d.ts +6 -4
  79. package/types/forty-cdk-date-picker.d.ts +8 -6
  80. package/types/forty-cdk-dialog.d.ts +4 -1
  81. package/types/forty-cdk-drawer.d.ts +4 -2
  82. package/types/forty-cdk-listbox.d.ts +43 -15
  83. package/types/forty-cdk-popover.d.ts +3 -3
  84. package/types/forty-cdk-search.d.ts +8 -1
  85. package/types/forty-cdk-select.d.ts +1 -0
  86. package/types/forty-cdk-table-virtualization.d.ts +4 -3
  87. package/types/forty-cdk-table.d.ts +89 -32
  88. package/types/forty-cdk-time-field.d.ts +6 -5
  89. package/types/forty-cdk-time-picker.d.ts +19 -4
  90. package/types/forty-cdk-toast.d.ts +1 -0
  91. package/types/forty-cdk-tooltip.d.ts +32 -23
  92. package/types/forty-cdk-virtualization.d.ts +6 -6
@@ -111,14 +111,14 @@ A disabled item cannot be toggled and is skipped by the arrow keys, while stayin
111
111
 
112
112
  ### `ForAccordion`
113
113
 
114
- | Property | Type | Description |
115
- | ------------- | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
116
- | `value` | `model<readonly string[]>` | Currently open item values. In single mode the array has 0 or 1 element.<br>**Default:** — |
117
- | `multiple` | `input<boolean>` | When true, multiple items can be open simultaneously.<br>**Default:** `false` |
118
- | `collapsible` | `input<boolean>` | Single mode only: when true, the open item can be collapsed by clicking it. Otherwise once any item is open, exactly one stays open.<br>**Default:** `false` |
119
- | `disabled` | `input<boolean>` | When true, disables every item: each trigger reflects the native `disabled` attribute and cannot toggle. Composes with a per-item `[disabled]`.<br>**Default:** `false` |
120
- | `orientation` | `input<'horizontal' \| 'vertical'>` | Layout direction of the trigger list. In horizontal mode ArrowLeft/Right replace ArrowUp/Down.<br>**Default:** `'vertical'` |
121
- | `dir` | `input<'ltr' \| 'rtl'>` | Writing direction. Only relevant in horizontal mode, where it swaps the meaning of Left/Right arrows.<br>**Default:** — |
114
+ | Property | Type | Description |
115
+ | ------------- | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
116
+ | `value` | `model<readonly string[]>` | Currently open item values. In single mode the array has 0 or 1 element.<br>**Default:** — |
117
+ | `multiple` | `input<boolean>` | When true, multiple items can be open simultaneously.<br>**Default:** `false` |
118
+ | `collapsible` | `input<boolean>` | Single mode only: when true, the open item can be collapsed by clicking it. Otherwise once any item is open, exactly one stays open.<br>**Default:** `false` |
119
+ | `disabled` | `input<boolean>` | When true, disables every item: each trigger reflects the native `disabled` attribute and cannot toggle. Composes with a per-item `[disabled]`.<br>**Default:** `false` |
120
+ | `orientation` | `input<'horizontal' \| 'vertical'>` | Layout direction of the trigger list. In horizontal mode ArrowLeft/Right replace ArrowUp/Down.<br>**Default:** `'vertical'` |
121
+ | `dir` | `input<'ltr' \| 'rtl' \| null>` | Writing direction. Only relevant in horizontal mode, where it swaps the meaning of Left/Right arrows. `null` inherits the nearest `[dir]` / `<html dir>`, else `'ltr'`.<br>**Default:** `null` |
122
122
 
123
123
  | Data attribute | Values |
124
124
  | ------------------ | -------------------------- |
@@ -188,7 +188,7 @@ export class CalendarDefaultExample {
188
188
 
189
189
  ### States
190
190
 
191
- One class and one directive, three states. `disabled` turns off focus movement and selection for the whole calendar; `readonly` keeps days focusable and the grid still pages, but clicking or pressing `Enter` no longer changes the selection. Each reflects a root hook (`data-disabled` and `data-readonly`), and the example's stylesheet keys on nothing else.
191
+ One class and one directive, three states. `disabled` turns off focus movement and selection for the whole calendar; `readonly` keeps days focusable and the grid still pages, but clicking or pressing `Enter` no longer changes the selection, and a click moves only the focused day to the clicked cell. Each reflects a root hook (`data-disabled` and `data-readonly`), and the example's stylesheet keys on nothing else.
192
192
 
193
193
  ### Constraints & week start
194
194
 
@@ -253,11 +253,11 @@ Set `selectionMode="range"` and bind `[(range)]` to get date-range selection. In
253
253
 
254
254
  **Interaction model.** Click (or `Enter` / `Space`) a first cell to set the anchor; the grid enters selecting state. Click (or `Enter` / `Space`) a second cell **in either direction** to commit the range. Clicking before the anchor commits the inverted band `[click, anchor]` (matching the hover preview) rather than starting over. There is no separate "start over" gesture and no explicit Escape-to-cancel: once a range is committed, the next click begins a fresh anchor, and `range` keeps the committed range until the second click replaces it.
255
255
 
256
- **Keyboard in range mode.** `Enter` / `Space` on the focused cell sets the anchor on the first press and commits on the second (same key as single mode). While selecting, arrow / `Home` / `End` / `PageUp` / `PageDown` move the keyboard focus and update the preview cursor (the keyboard equivalent of pointer hover); moving before the anchor previews (and commits) the inverted band.
256
+ **Keyboard in range mode.** `Enter` / `Space` on the focused cell sets the anchor on the first press and commits on the second (same key as single mode). While selecting, arrow / `Home` / `End` / `PageUp` / `PageDown` move the keyboard focus and update the preview cursor (the keyboard equivalent of pointer hover); moving before the anchor previews (and commits) the inverted band. The preview follows whichever device moved last: a keyboard move, a selection or a page change takes the cursor back from a pointer resting on the grid, and the pointer takes it again when it enters another cell.
257
257
 
258
258
  **`min` / `max` / `isDateUnavailable`** still gate both endpoints. An unavailable or out-of-bounds date cannot become an anchor or an end.
259
259
 
260
- **`minRangeLength` / `maxRangeLength`.** Constrain the inclusive day count of the committed range. A click that would violate either limit is a no-op (the anchor is preserved). Both default to `null` (unconstrained).
260
+ **`minRangeLength` / `maxRangeLength`.** Constrain the inclusive day count of the committed range. A click that would violate either limit commits nothing: the anchor is preserved, and the clicked cell becomes the focused day and the end of the preview. Both default to `null` (unconstrained).
261
261
 
262
262
  **Cell facets (range mode).** Four boolean `data-*` attributes are added to `[forCalendarCell]`:
263
263
 
@@ -290,7 +290,7 @@ Set `selectionMode="range"` and bind `[(range)]` to get date-range selection. In
290
290
  }
291
291
  ```
292
292
 
293
- **`aria-selected`** in range mode is `"true"` across every committed-range cell (inclusive). During selecting (range null), it is `"false"` everywhere.
293
+ **`aria-selected`** in range mode is `"true"` across every committed-range cell (inclusive), and the grid carries `aria-multiselectable="true"`. While a new anchor is pending, `aria-selected` is `"false"` everywhere, but `range` keeps the previously committed range until the second click replaces it.
294
294
 
295
295
  **Scope.** Range mode is day-granular only: `granularity` / time is orthogonal and not supported alongside it.
296
296
 
@@ -306,7 +306,7 @@ Set `selectionMode="range"` and bind `[(range)]` to get date-range selection. In
306
306
  | `goToMonth(month)` | Set the visible month within the current visible year. `month` is **1-12**. |
307
307
  | `goToYear(year)` | Set the visible year, keeping the current visible month. |
308
308
 
309
- All three methods re-apply the user's intended day-of-month (clamped to the target month's length), clamp the result into `[min, max]`, and announce the new period politely when the visible month changes. They keep DOM focus on the caller and do not move focus into the grid. They are a no-op while the calendar is `disabled`.
309
+ All three methods re-apply the user's intended day-of-month (clamped to the target month's length), clamp the result into `[min, max]`, and announce the new period politely when the period the active view shows changes. They keep DOM focus on the caller and do not move focus into the grid. They are a no-op while the calendar is `disabled`.
310
310
 
311
311
  ### Read accessors and predicates
312
312
 
@@ -395,15 +395,17 @@ The lower-level hooks (`visibleMonthNumber()`, `visibleYear()`, `monthOptions()`
395
395
 
396
396
  `ForCalendar` supports two additional views (**month grid** and **year grid**) so users can jump quickly to a different month or year without paging one at a time. All three views share a single `[(view)]` model and the same `focusedDate` cursor.
397
397
 
398
+ The heading's `label()` follows the active view, with the same text as the view trigger: the month and year in day view, the year in month view, the block of years in year view. Every grid is named by the heading, so each one is named after the period it shows. A keyboard move or a prev / next press that changes that period announces it. Drilling down changes no value, so a `readonly` calendar still drills from a year to its months and from a month to its days.
399
+
398
400
  ### Pieces
399
401
 
400
- | Class | Selector | Role |
401
- | ------------------------ | -------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
402
- | `ForCalendarViewTrigger` | `[forCalendarViewTrigger]` | Button that cycles the view: day → month → year. Auto-disabled when the calendar is disabled. |
403
- | `ForCalendarMonthGrid` | `[forCalendarMonthGrid]` | 4×3 month grid (`role="grid"`). Exposes `rows()`, an array of `CalendarMonthRow`, each with three `CalendarYearOption`. |
404
- | `ForCalendarMonthCell` | `[forCalendarMonthCell]` | One month (`role="gridcell"`). Requires `[month]` (1–12). Click drills down to day view for that month. |
405
- | `ForCalendarYearGrid` | `[forCalendarYearGrid]` | 4×3 year grid (`role="grid"`). Exposes `rows()`, an array of `CalendarYearRow`, each with three `CalendarYearOption`. |
406
- | `ForCalendarYearCell` | `[forCalendarYearCell]` | One year (`role="gridcell"`). Requires `[year]`. Click drills down to month view for that year. |
402
+ | Class | Selector | Role |
403
+ | ------------------------ | -------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
404
+ | `ForCalendarViewTrigger` | `[forCalendarViewTrigger]` | Button that cycles the view: day → month → year. Auto-disabled when the calendar is disabled. |
405
+ | `ForCalendarMonthGrid` | `[forCalendarMonthGrid]` | 4×3 month grid (`role="grid"`). Exposes `rows()`, an array of `CalendarMonthRow`, each with three `CalendarMonthOption`. |
406
+ | `ForCalendarMonthCell` | `[forCalendarMonthCell]` | One month (`role="gridcell"`). Requires `[month]` (1–12). Click drills down to day view for that month. |
407
+ | `ForCalendarYearGrid` | `[forCalendarYearGrid]` | 4×3 year grid (`role="grid"`). Exposes `rows()`, an array of `CalendarYearRow`, each with three `CalendarYearOption`. |
408
+ | `ForCalendarYearCell` | `[forCalendarYearCell]` | One year (`role="gridcell"`). Requires `[year]`. Click drills down to month view for that year. |
407
409
 
408
410
  ### `view` model — `ForCalendar`
409
411
 
@@ -571,6 +573,7 @@ Implements the [WAI-ARIA Grid pattern](https://www.w3.org/WAI/ARIA/apg/patterns/
571
573
  - **`aria-labelledby`** wires the grid to the heading so it names the visible period. Paging the month is announced through a dedicated off-screen `aria-live="polite"` region (owned by `[forCalendar]`), so the period is read on navigation without the heading double-announcing as both a live region and the grid's label.
572
574
  - **`aria-label`** on every cell carries the full localized date (e.g. `"Monday, June 15, 2026"`) so screen readers announce the whole date, not the bare day number that stays the cell's visible content. Outside-month padding days are suffixed (`" (outside month)"`) so they are distinguishable. Override the format via `ForCalendar`'s `dateLabel` input.
573
575
  - **`aria-selected`** is always emitted (`"true"` / `"false"`); **`aria-current="date"`** marks today; **`aria-disabled`** marks unavailable dates (truthy-only).
576
+ - **`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.
574
577
  - **Roving tabindex**: exactly one cell (the focused date) is tabbable. `Tab` enters and leaves the grid in one stop.
575
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`.
576
579
 
@@ -173,6 +173,10 @@ export class DemoCheckout {
173
173
  | `data-state` | `checked` \| `unchecked` \| `indeterminate` |
174
174
  | `data-disabled` | present \| absent |
175
175
  | `data-readonly` | present \| absent |
176
+ | `data-touched` | present \| absent |
177
+ | `data-dirty` | present \| absent |
178
+ | `data-pending` | present \| absent |
179
+ | `data-invalid` | present \| absent |
176
180
 
177
181
  ### `ForCheckboxIndicator`
178
182
 
@@ -196,7 +196,23 @@ The primitive never owns the scroll container, so it virtualizes with any window
196
196
 
197
197
  ## API
198
198
 
199
- Input tables are not yet tabulated for this primitive. See the feature sections below for documented inputs and the prose descriptions of each input.
199
+ Only the positioning inputs every anchored root inherits are tabulated for this primitive so far. See the feature sections below for the other inputs and their prose descriptions.
200
+
201
+ ### `ForCombobox` positioning inputs
202
+
203
+ | Property | Type | Description |
204
+ | --------------------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
205
+ | `side` | `input<FloatingSide>` | Anchor side (`'top'` / `'right'` / `'bottom'` / `'left'`). Falls back to `provideForComboboxDefaults`.<br>**Default:** `'bottom'` |
206
+ | `align` | `input<FloatingAlign>` | Alignment along `side` (`'start'` / `'center'` / `'end'`). Falls back to `provideForComboboxDefaults`.<br>**Default:** `'start'` |
207
+ | `sideOffset` | `input<number>` | Gap (px) between the anchor and the content along the main axis. Falls back to `provideForComboboxDefaults`.<br>**Default:** `4` |
208
+ | `alignOffset` | `input<number>` | Gap (px) along the cross axis (parallel to `side`).<br>**Default:** `0` |
209
+ | `avoidCollisions` | `input<boolean>` | When `true`, `flip` and `shift` keep the content inside the viewport. Set `false` for strict positioning where overflow is acceptable.<br>**Default:** `true` |
210
+ | `collisionPadding` | `input<number>` | Padding (px) for the `flip` / `shift` / `size` collision middlewares. Falls back to `provideForComboboxDefaults`.<br>**Default:** `8` |
211
+ | `sticky` | `input<'partial' \| 'always' \| false>` | `'always'` keeps the requested placement even when the content leaves the viewport. `'partial'` and `false` let `shift` slide it back into view.<br>**Default:** `'partial'` |
212
+ | `hideWhenDetached` | `input<boolean>` | When `true`, the content carries `data-detached` while the anchor has scrolled out of every clipping ancestor, so CSS can hide it.<br>**Default:** `false` |
213
+ | `clipUntilPositioned` | `input<boolean>` | Clips the content until its first position resolves, so it never flashes at the viewport corner. Set `false` so an `animate.enter` plays from its first frame.<br>**Default:** `true` |
214
+
215
+ The content anchors to the field box when a `[forComboboxAnchor]` is present; see [Anchoring to a field box](#anchoring-to-a-field-box).
200
216
 
201
217
  ### Data attributes
202
218
 
@@ -205,6 +221,10 @@ Input tables are not yet tabulated for this primitive. See the feature sections
205
221
  | `[forCombobox]` | `data-state` | `open` \| `closed` |
206
222
  | `[forCombobox]` | `data-disabled` | present / absent |
207
223
  | `[forCombobox]` | `data-readonly` | present / absent |
224
+ | `[forCombobox]` | `data-touched` | present / absent |
225
+ | `[forCombobox]` | `data-dirty` | present / absent |
226
+ | `[forCombobox]` | `data-pending` | present / absent |
227
+ | `[forCombobox]` | `data-invalid` | present / absent |
208
228
  | `[forComboboxInput]` | `data-state` | `open` \| `closed` |
209
229
  | `[forComboboxInput]` | `data-disabled` | present / absent |
210
230
  | `[forComboboxToggle]` | `data-state` | `open` \| `closed` |
@@ -363,7 +383,9 @@ An action:
363
383
  - **never touches `value` / `options()`.** It registers in a collection separate
364
384
  from options, so `options()`, `aria-setsize`, and `aria-posinset` are
365
385
  unaffected and activation emits `(activate)` instead of mutating `[(value)]`. The
366
- consumer decides what happens and whether to close the popup afterwards.
386
+ consumer decides what happens and whether to close the popup afterwards. In the
387
+ editable anatomy a close from `(activate)` returns focus to the input, unless
388
+ your handler already moved it out of the popup.
367
389
  - **is `role="button"`, not `role="option"`.** Assistive tech announces it as an
368
390
  action, not as one of N choices.
369
391
  - **is reached by Tab, not the arrow keys** (see below), so it stays reachable no
@@ -395,10 +417,10 @@ outside-focus dismissal checks, exactly like the input.
395
417
 
396
418
  ### Action item API
397
419
 
398
- | Member | Type | Notes |
399
- | ------------ | -------------- | ---------------------------------------------------------------------------------------------------------- |
400
- | `[disabled]` | `boolean` | Drops the action out of the focus ring (`tabindex` removed), reflects `aria-disabled`, ignores activation. |
401
- | `(activate)` | `output<void>` | Fired on click / Enter / Space. Never mutates `[(value)]`. |
420
+ | Member | Type | Notes |
421
+ | ------------ | -------------- | ---------------------------------------------------------------------------------------------------------------- |
422
+ | `[disabled]` | `boolean` | Drops the action out of the focus ring (it keeps `tabindex="-1"`), reflects `aria-disabled`, ignores activation. |
423
+ | `(activate)` | `output<void>` | Fired on click / Enter / Space. Never mutates `[(value)]`. |
402
424
 
403
425
  `[forComboboxAction]` host-binds `role="button"`, `type="button"` (on a native
404
426
  `<button>` host only, leaving any other element with no `type`), a primitive-managed
@@ -575,7 +597,7 @@ Each dismiss reason emits a vetoable event from `[forCombobox]`. Call `preventDe
575
597
 
576
598
  The two anatomies have different focus models:
577
599
 
578
- - **Editable anatomy**: the input retains focus the entire time the listbox is open and on close; the active option is tracked via `aria-activedescendant`, never via `.focus()`. There is no automatic focus move, so `(autoFocusOnOpen)` / `(autoFocusOnClose)` never fire. If you need to move focus elsewhere, do it from your own keydown handler. The combobox won't fight you.
600
+ - **Editable anatomy**: the input retains focus the entire time the listbox is open and on close; the active option is tracked via `aria-activedescendant`, never via `.focus()`. The one automatic move is back to the input, when a close unmounts the popup while focus is inside it (a `[forComboboxAction]` whose `(activate)` closes the popup), gated by `[returnFocus]`. `(autoFocusOnOpen)` / `(autoFocusOnClose)` never fire. If you need to move focus elsewhere, do it from your own keydown handler. The combobox won't fight you.
579
601
  - **Picker anatomy**: focus moves into the input on open and returns to the `[forComboboxTrigger]` on close, exactly like the other trigger-anchored overlays (`[forPopover]`, `[forDropdownMenu]`, `[forSelect]`). Both moves emit a vetoable event on `[forCombobox]`; call `preventDefault()` to keep focus where it is:
580
602
 
581
603
  | Output | When | `preventDefault()` effect |
@@ -822,6 +844,7 @@ Implements the [WAI-ARIA Combobox pattern](https://www.w3.org/WAI/ARIA/apg/patte
822
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.
823
845
  - `[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.
824
846
  - `[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
+ - `[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.
825
848
  - In single mode, `aria-selected="true"` follows the activedescendant (the option Enter would activate). In multi mode it follows membership in `value()`, so every selected option carries `aria-selected="true"` simultaneously.
826
849
  - `data-state="checked" | "unchecked"` always reflects membership in `value()`, so consumers can paint a checkmark icon with pure CSS regardless of mode.
827
850
  - `data-highlighted=""` marks the option that is the current `aria-activedescendant`. Because focus stays on the `<input>`, there is no `:focus` on the option to style. `data-highlighted` is the canonical CSS hook.
@@ -88,8 +88,13 @@ The same menu vocabulary the Dropdown Menu exposes, anchored to the pointer on r
88
88
  | `sideOffset` | `input<number>` | Gap (px) between the pointer and the menu along the main axis.<br>**Default:** `0` |
89
89
  | `alignOffset` | `input<number>` | Gap (px) along the cross axis (parallel to `side`).<br>**Default:** `0` |
90
90
  | `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'` |
91
+ | `avoidCollisions` | `input<boolean>` | When `true`, `flip` and `shift` keep the content inside the viewport. Set `false` for strict positioning where overflow is acceptable.<br>**Default:** `true` |
92
+ | `collisionPadding` | `input<number>` | Padding (px) for the `flip` / `shift` / `size` collision middlewares. The default is read from `provideForContextMenuDefaults` for the surrounding scope.<br>**Default:** `8` |
93
+ | `sticky` | `input<'partial' \| 'always' \| false>` | `'always'` keeps the requested placement even when the content leaves the viewport. `'partial'` and `false` let `shift` slide it back into view.<br>**Default:** `'partial'` |
94
+ | `hideWhenDetached` | `input<boolean>` | When `true`, the content carries `data-detached` while the anchor has scrolled out of every clipping ancestor, so CSS can hide it.<br>**Default:** `false` |
95
+ | `clipUntilPositioned` | `input<boolean>` | Clips the content until its first position resolves, so it never flashes at the viewport corner. Set `false` so an `animate.enter` plays from its first frame.<br>**Default:** `true` |
91
96
  | `loop` | `input<boolean>` | Whether arrow navigation wraps.<br>**Default:** `true` |
92
- | `dir` | `input<string>` | Writing direction. In RTL the swap is automatic: ArrowLeft opens submenus and ArrowRight closes them. Inherited by every nested `[forMenuSub]` underneath unless overridden.<br>**Default:** `'ltr'` |
97
+ | `dir` | `input<'ltr' \| 'rtl' \| null>` | Writing direction. `null` inherits the nearest `[dir]` / `<html dir>`, else `'ltr'`. In RTL the swap is automatic: ArrowLeft opens submenus and ArrowRight closes them. Inherited by every nested `[forMenuSub]` underneath unless overridden.<br>**Default:** `null` |
93
98
  | `disabled` | `input<boolean>` | When `true`, the contextmenu event falls through to the native browser menu.<br>**Default:** `false` |
94
99
  | `dismissible` | `input<boolean>` | When `false`, Escape and outside interactions don't close.<br>**Default:** `true` |
95
100
  | `returnFocus` | `input<boolean>` | When `true`, focus returns on close to the trigger element, or to the focused element inside it that a keyboard activation started from.<br>**Default:** `true` |
@@ -134,10 +139,11 @@ forty-cdk ships no styles: put your own class on each piece and key your CSS off
134
139
 
135
140
  ## Behavior notes
136
141
 
137
- - **Trigger is NOT exempt** from outside-pointer / outside-focus checks. Unlike DropdownMenu (where the trigger button toggles via its own click handler), the context-menu region is treated like any other "outside" element: a left-click on the region while the menu is open closes it. Right-clicking again immediately reopens at the new position. This is a full close → open cycle, not an in-place reposition: re-right-clicking while the menu is already open tears the surface down and rebuilds it (so any `animate.enter` / `animate.leave` replays and focus resets). Keep enter/leave animations cheap, or gate expensive ones, since a user can fire this rapidly. The directive deliberately exempts nothing, so there is no smooth reposition path to attach to.
142
+ - **Trigger is NOT exempt** from outside-pointer / outside-focus checks. Unlike DropdownMenu (where the trigger button toggles via its own click handler), the context-menu region is treated like any other "outside" element: a left-click on the region while the menu is open closes it. Right-clicking again reopens the menu at the new position. When the close and the reopen land in the same task, as they do where the browser fires `contextmenu` on mouse down (macOS, Linux), the mounted surface is reused rather than rebuilt: it moves to the new point without replaying `animate.enter` / `animate.leave`, and focus moves back to its first item, honouring `(autoFocusOnOpen)`. Where `contextmenu` follows mouse up (Windows), the surface renders closed in between and is mounted again, so its animations replay. Keep enter/leave animations cheap, or gate expensive ones, since a user can fire this rapidly.
143
+ - **Nested regions.** A `[forContextMenuTrigger]` inside another one takes precedence: a right-click, `Shift+F10`, the `ContextMenu` key or a long-press on the inner region opens the inner menu only, and the outer menu opens from the parts of its region the inner one does not cover. A disabled inner region lets the outer menu open. The trigger also ignores a `contextmenu` or key press that a handler beneath it already cancelled with `preventDefault()`, so calling it on a descendant is how you keep a context menu from opening there.
138
144
  - **Virtual anchor.** Right-click captures a 0×0 rect at the pointer location. `Shift+F10` and `ContextMenu` snapshot the bounding rect of the focused element (or the trigger if focus is on it directly), so the menu floats off the element under attention. Closing a keyboard-opened menu returns focus to that same element, so a row in a roving-tabindex list keeps its position; when a menu action removed it, focus falls back to the region. Both forms feed floating-ui's `flip` and `shift` middleware, so corners and screen edges work without special-casing.
139
145
  - **Keyboard activators only fire while focus is inside the trigger.** Keyboard events dispatch to the focused element, so `Shift+F10` / `ContextMenu` anywhere outside the trigger goes to the browser default. The trigger is focusable by default (host-bound `tabindex="-1"`), so this works out of the box and focus can return to it programmatically when the menu closes. Your own `tabindex` wins over that default: use `tabindex="0"` if you want the region itself reachable via Tab.
140
- - **Native menu suppressed.** The trigger calls `event.preventDefault()` on `contextmenu` and on the keyboard activators. Set `disabled` to let the browser's native menu surface for that region.
146
+ - **Native menu suppressed.** The trigger calls `event.preventDefault()` on `contextmenu` and on the keyboard activators. After a keyboard open it also cancels the `contextmenu` the platform raises when the key is released (Windows), wherever focus has moved by then, so the browser's menu is not painted over the one that just opened. Set `disabled` to let the browser's native menu surface for that region.
141
147
  - **An open that renders nothing closes again.** When a right-click, `Shift+F10`, the `ContextMenu` key or a long-press opens a menu whose template renders no `[forMenuContent]`, the trigger closes it after the next render with reason `'programmatic'`, and dev mode logs `FORCDK-CONTEXT-MENU-002` once. The native menu cannot come back for that gesture, because it was already suppressed, so bind `[disabled]` to the same condition that renders the content: `[disabled]="!hasActions(col)"` beside `@if (hasActions(col))`.
142
148
  - **Touch long-press.** The trigger runs its own long-press timer (a `touch` `pointerdown` held ~500 ms, without lifting or moving past a small tolerance, opens the menu at the touch point). This is required because iOS Safari never fires the `contextmenu` event a long-press synthesizes elsewhere; where the browser does synthesize it (Android, desktop touch emulation) the two paths stay mutually exclusive, so the menu opens exactly once. For the press to survive on iOS, suppress the native callout / text-selection on the trigger with CSS, since otherwise the OS gesture cancels the press:
143
149
 
@@ -87,7 +87,7 @@ export class DateFieldDefaultExample {
87
87
 
88
88
  ### Date & time
89
89
 
90
- With a time-capable adapter, a `granularity` coarser than `'day'` appends hour / minute segments and the value becomes a `CalendarDateTime`. A 12-hour cycle adds an AM/PM segment you toggle with `↑` / `↓` or by typing `a` / `p`.
90
+ With a time-capable adapter, a `granularity` finer than `'day'` appends hour / minute segments and the value becomes a `CalendarDateTime`. A 12-hour cycle adds an AM/PM segment you toggle with `↑` / `↓` or by typing `a` / `p`.
91
91
 
92
92
  ### Localized segment labels
93
93
 
@@ -139,7 +139,7 @@ export class DobFormField {
139
139
 
140
140
  ### Date & time range
141
141
 
142
- With a time-capable adapter, a `granularity` coarser than `'day'` appends time segments to both endpoints and the value becomes a `CalendarDateTime` range, which is handy for a check-in to check-out with times. A 12-hour `hourCycle` adds an AM/PM segment to each side.
142
+ With a time-capable adapter, a `granularity` finer than `'day'` appends time segments to both endpoints and the value becomes a `CalendarDateTime` range, which is handy for a check-in to check-out with times. A 12-hour `hourCycle` adds an AM/PM segment to each side.
143
143
 
144
144
  ### Range in Signal Forms
145
145
 
@@ -154,7 +154,7 @@ With a time-capable adapter, a `granularity` coarser than `'day'` appends time s
154
154
  | `value` | `model<D \| null>` | Two-way bindable entered date, or `null` while any segment is empty. The `FormValueControl` backing.<br>**Default:** `null` |
155
155
  | `minDate` | `input<D \| null>` | Minimum date (inclusive). A composed value below it is clamped up. Named `minDate` (see note below).<br>**Default:** `null` |
156
156
  | `maxDate` | `input<D \| null>` | Maximum date (inclusive). A composed value above it is clamped down.<br>**Default:** `null` |
157
- | `granularity` | `input<'day' \| 'hour' \| 'minute' \| 'second'>` | Date-time precision. `'day'` is date-only; coarser-than-day appends time segments. See below.<br>**Default:** `'day'` |
157
+ | `granularity` | `input<'day' \| 'hour' \| 'minute' \| 'second'>` | Date-time precision. `'day'` is date-only; a finer granularity appends time segments. See below.<br>**Default:** `'day'` |
158
158
  | `hourCycle` | `input<12 \| 24 \| null>` | 12/24-hour cycle for the time segments. `null` → the scope's `hourCycle`, then the locale. 12-hour adds the AM/PM segment.<br>**Default:** `null` |
159
159
  | `locale` | `input<string \| null>` | BCP 47 locale driving segment order, separators, and month name. `null` → the adapter's `locale()`, then the runtime locale.<br>**Default:** `null` |
160
160
  | `placeholder` | `input<Partial<Record<SegmentType, string>>>` | Per-segment placeholder while empty. Unspecified parts fall back to the scope's `placeholder`, then to `dd` / `mm` / `yyyy` / `hh` / `mm` / `ss` / `--`.<br>**Default:** `{}` |
@@ -181,6 +181,11 @@ A native reset of the owning `<form>` returns `value` to what it held when the f
181
181
  | `[forDateField]` | `data-disabled` | present \| absent |
182
182
  | `[forDateField]` | `data-readonly` | present \| absent |
183
183
  | `[forDateField]` | `data-empty` | present \| absent |
184
+ | `[forDateField]` | `data-required` | present \| absent |
185
+ | `[forDateField]` | `data-touched` | present \| absent |
186
+ | `[forDateField]` | `data-dirty` | present \| absent |
187
+ | `[forDateField]` | `data-pending` | present \| absent |
188
+ | `[forDateField]` | `data-invalid` | present \| absent |
184
189
  | `[forDateFieldSegment]` | `data-highlighted` | present \| absent |
185
190
  | `[forDateFieldSegment]` | `data-placeholder` | present \| absent |
186
191
  | `[forDateFieldSegment]` | `data-disabled` | present \| absent |
@@ -284,7 +289,7 @@ readonly booking = form(this.model);
284
289
  | `value` | `model<DateRange<D> \| null>` | Two-way bindable committed range, or `null` while incomplete or out of order. The `FormValueControl` backing.<br>**Default:** `null` |
285
290
  | `minDate` | `input<D \| null>` | Minimum date (inclusive) for both endpoints. A composed endpoint below it is clamped up. Named `minDate` (see note below).<br>**Default:** `null` |
286
291
  | `maxDate` | `input<D \| null>` | Maximum date (inclusive) for both endpoints. A composed endpoint above it is clamped down.<br>**Default:** `null` |
287
- | `granularity` | `input<'day' \| 'hour' \| 'minute' \| 'second'>` | Date-time precision shared by both endpoints. `'day'` is date-only; coarser-than-day appends time segments.<br>**Default:** `'day'` |
292
+ | `granularity` | `input<'day' \| 'hour' \| 'minute' \| 'second'>` | Date-time precision shared by both endpoints. `'day'` is date-only; a finer granularity appends time segments.<br>**Default:** `'day'` |
288
293
  | `hourCycle` | `input<12 \| 24 \| null>` | 12/24-hour cycle for the time segments. `null` → the scope's `hourCycle`, then the locale. 12-hour adds the AM/PM segment.<br>**Default:** `null` |
289
294
  | `locale` | `input<string \| null>` | BCP 47 locale driving segment order, separators, and month name. `null` → the adapter's `locale()`, then the runtime locale.<br>**Default:** `null` |
290
295
  | `placeholder` | `input<Partial<Record<SegmentType, string>>>` | Per-segment placeholder while empty, applied to both endpoints. Unspecified parts fall back to the scope's `placeholder`.<br>**Default:** `{}` |
@@ -297,7 +302,7 @@ The endpoint groups each accept an `ariaLabel` input for their own group label,
297
302
 
298
303
  > **Why `minDate` / `maxDate`, not `min` / `max`?** Beyond the reason above, `FormUiControl.min` / `max` are additionally typed `NonNullable<TValue>` (the range object itself), which is meaningless as a bound.
299
304
 
300
- `[forDateRangeField]` reflects the same `data-disabled` / `data-readonly` / `data-empty` hooks as `[forDateField]`, plus `data-range-error`; `[forDateRangeFieldSegment]` reflects the same four segment hooks. `data-empty` marks the field only while **both** endpoints are entirely empty; a partially-filled or complete-but-disordered range is **not** empty.
305
+ `[forDateRangeField]` reflects the same root hooks as `[forDateField]` (`data-disabled` / `data-readonly` / `data-empty` / `data-required` and the four form-state hooks), plus `data-range-error`; `[forDateRangeFieldSegment]` reflects the same four segment hooks. `data-empty` marks the field only while **both** endpoints are entirely empty; a partially-filled or complete-but-disordered range is **not** empty.
301
306
 
302
307
  ### Ordering
303
308
 
@@ -315,14 +320,14 @@ Each endpoint is its own tab stop, so `Tab` moves start group → end group →
315
320
 
316
321
  Key behavior applies per segment. Horizontal arrows mirror under `dir="rtl"`.
317
322
 
318
- | Key | Behavior |
319
- | -------------------------- | --------------------------------------------------------------------------------------- |
320
- | **0–9** | Type the value; auto-advances to the next segment when full. |
321
- | **ArrowUp / ArrowDown** | Step the value. Day and month wrap; year clamps. Empty seeds from today. |
322
- | **ArrowLeft / ArrowRight** | Move to the previous / next segment (no wrap). |
323
- | **Home / End** | Jump to the segment minimum / maximum. |
324
- | **Backspace** | Delete the last entered digit; the value becomes `null` when the last digit is removed. |
325
- | **Delete** | Clear the whole segment (the value becomes `null` until refilled). |
323
+ | Key | Behavior |
324
+ | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
325
+ | **0–9** | Type the value; auto-advances to the next segment when full. |
326
+ | **ArrowUp / ArrowDown** | Step the value. Day and month wrap; year clamps. An empty date part seeds from today; an empty hour from its minimum (midnight, or 1 AM in 12-hour), minute and second from 0. |
327
+ | **ArrowLeft / ArrowRight** | Move to the previous / next segment (no wrap). |
328
+ | **Home / End** | Jump to the segment minimum / maximum. |
329
+ | **Backspace** | Delete the last entered digit; the value becomes `null` when the last digit is removed. |
330
+ | **Delete** | Clear the whole segment (the value becomes `null` until refilled). |
326
331
 
327
332
  The day clamps to the current month's length (e.g. 31 → 28 in February), and a composed value is clamped into `[minDate, maxDate]`. While focus stays in the field its segments keep what was entered, so editing one part never rewrites the others; they show the clamped value once focus leaves. Stepping the hour skips a wall-clock hour the day does not have, such as a spring-forward DST gap.
328
333
 
@@ -336,18 +341,22 @@ Composes the [WAI-ARIA Spinbutton pattern](https://www.w3.org/WAI/ARIA/apg/patte
336
341
  - **Literals are `aria-hidden`** and never focusable, so assistive tech reads only the spinbutton segments.
337
342
  - **Boolean `data-*`** on each segment, present when true and absent when false: `data-highlighted` (focused/roving), `data-placeholder` (empty), `data-disabled`, `data-readonly`.
338
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
+ - **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 }}`.
339
345
 
340
346
  ## Styling
341
347
 
342
- 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]` on the root group.
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.
343
349
 
344
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).
345
351
 
352
+ A segment is an editing host, so the browser paints a text caret in the focused one. Hide it with `caret-color: transparent` when your highlight already marks that segment.
353
+
346
354
  ```css
347
355
  .date-field-segment[data-placeholder] {
348
356
  color: GrayText;
349
357
  }
350
358
  .date-field-segment[data-highlighted] {
359
+ caret-color: transparent;
351
360
  background: Highlight;
352
361
  color: HighlightText;
353
362
  }
@@ -11,7 +11,7 @@ A trigger that opens a floating calendar to pick a date, composing ForCalendar i
11
11
 
12
12
  Reinterpreted idiomatically for modern Angular: a focusable trigger that opens a floating surface wrapping a projected [`ForCalendar`](../calendar/README.md).
13
13
 
14
- `ForDatePicker` is the root **and** the form value: it implements `FormValueControl<D | null>` from `@angular/forms/signals`, so it auto-wires with `[formField]`. The trigger is the focusable control that carries `name` / `disabled` / `invalid`; selection state flows root → projected calendar via `[(value)]`. The library reuses its existing overlay stack (trigger-anchored Popover positioning, dismissible layer, return-focus) rather than re-implementing positioning, dismissal, or focus return. The modal opt-in routes through the shared modal shell (focus trap + inert background + scroll lock).
14
+ `ForDatePicker` is the root **and** the form value: it implements `FormValueControl<D | null>` from `@angular/forms/signals`, so it auto-wires with `[formField]`. The trigger is the focusable control that carries `disabled` / `invalid` / `required` / `readonly`, while `name` submits the value through a hidden input (see [Native submission](#native-submission)); selection state flows root → projected calendar via `[(value)]`. The library reuses its existing overlay stack (trigger-anchored Popover positioning, dismissible layer, return-focus) rather than re-implementing positioning, dismissal, or focus return. The modal opt-in routes through the shared modal shell (focus trap + inert background + scroll lock).
15
15
 
16
16
  ## When to choose
17
17
 
@@ -187,7 +187,7 @@ With `granularity="minute"` and a time-capable adapter the picker becomes a date
187
187
 
188
188
  ### Constraints
189
189
 
190
- `minDate` disables every day before today and `isDateUnavailable` blocks weekends. The picker forwards both to the projected calendar, where they reflect `aria-disabled` and refuse selection while the arrow keys still travel across them. Only an available weekday can be committed.
190
+ `minDate` disables every day before today and `isDateUnavailable` blocks weekends. Forward both to the projected calendar's `[min]` / `[isDateUnavailable]`, where they reflect `aria-disabled` and refuse selection while the arrow keys still travel across them. Only an available weekday can be committed. The picker does not forward them itself: on a calendar bound without them, it clamps an out-of-range pick into `[minDate, maxDate]` and commits an unavailable date as picked.
191
191
 
192
192
  ### Range selection
193
193
 
@@ -197,35 +197,46 @@ Open the picker and click a first day: the trigger keeps its placeholder, becaus
197
197
 
198
198
  ### `ForDatePicker`
199
199
 
200
- | Property | Type | Description |
201
- | ------------------- | ------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
202
- | `value` | `model<D \| null>` | Two-way bindable selected date. `(valueChange)` fires only on internal commits.<br>**Default:** `null` |
203
- | `open` | `model<boolean>` | Two-way bindable surface visibility. `(openChange)` fires only on internal transitions.<br>**Default:** `false` |
204
- | `minDate` | `input<D \| null>` | Minimum selectable date (inclusive). Forward to the projected calendar's `[min]`.<br>**Default:** `null` |
205
- | `maxDate` | `input<D \| null>` | Maximum selectable date (inclusive). Forward to the projected calendar's `[max]`.<br>**Default:** `null` |
206
- | `isDateUnavailable` | `input<(date: D) => boolean>` | Per-date predicate. Forward to the projected calendar's `[isDateUnavailable]`.<br>**Default:** `() => false` |
207
- | `closeOnSelect` | `input<boolean>` | Close the surface after a date is picked. Honoured at `granularity="day"`, and at any granularity with `anatomy="field"`.<br>**Default:** `true` |
208
- | `anatomy` | `input<'trigger' \| 'field'>` | Which piece is the control. `'field'` makes a projected `[forDateField]` the control and the trigger a plain button; see [Field anatomy](#field-anatomy).<br>**Default:** `'trigger'` |
209
- | `granularity` | `input<'day' \| 'hour' \| 'minute' \| 'second'>` | Date-time precision. `'day'` (default) is a pure date picker; coarser-than-day off composes a time field.<br>**Default:** `'day'` |
210
- | `hourCycle` | `input<12 \| 24 \| null>` | 12/24-hour cycle for the value display; a projected time field takes `resolvedHourCycle()`.<br>**Default:** `null` → the scope's `hourCycle` (`provideForDatePickerDefaults`), then the locale |
211
- | `resolvedHourCycle` | `Signal<12 \| 24 \| null>` | The effective cycle: `hourCycle`, then the scope's, or `null` for the locale. Bind it to a projected time field's `[hourCycle]`.<br>**Default:** — |
212
- | `modal` | `input<boolean>` | Trap focus + inert background + scroll lock (centered dialog) instead of an anchored popover.<br>**Default:** `false` |
213
- | `dismissible` | `input<boolean>` | Escape / outside-pointer dismiss the surface.<br>**Default:** `true` |
214
- | `returnFocus` | `input<boolean>` | Return focus to the trigger on close.<br>**Default:** `true` |
215
- | `formatOptions` | `input<Intl.DateTimeFormatOptions>` | Options for the text rendered by `[forDatePickerValue]`.<br>**Default:** `{ year: 'numeric', month: 'long', day: 'numeric' }` |
216
- | `locale` | `input<string \| null>` | BCP 47 locale for the text rendered by `[forDatePickerValue]`. Not forwarded to the projected calendar, so bind its `[locale]` too.<br>**Default:** `null` → the adapter's `locale()`, then the runtime locale |
217
- | `placeholder` | `input<string>` | Fallback text for `[forDatePickerValue]` when empty.<br>**Default:** `''` |
218
- | `side` / `align` | `input` | Anchored placement (popover mode only). Defaults from `provideForDatePickerDefaults` / `provideForDateRangePickerDefaults`.<br>**Default:** `'bottom'` / `'start'` |
219
- | `dir` | `input<'ltr' \| 'rtl' \| null>` | Writing direction.<br>**Default:** `null` resolves the ambient direction; reflected to the host `dir` |
220
-
221
- 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`).
222
-
223
- A native reset of the owning `<form>` returns `value` to what it held when the picker first rendered. In the [field anatomy](#field-anatomy) it also drops a partly typed entry in the projected field.
200
+ | Property | Type | Description |
201
+ | --------------------- | ------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
202
+ | `value` | `model<D \| null>` | Two-way bindable selected date. `(valueChange)` fires only on internal commits.<br>**Default:** `null` |
203
+ | `open` | `model<boolean>` | Two-way bindable surface visibility. `(openChange)` fires only on internal transitions.<br>**Default:** `false` |
204
+ | `minDate` | `input<D \| null>` | Minimum selectable date (inclusive). Forward to the projected calendar's `[min]`.<br>**Default:** `null` |
205
+ | `maxDate` | `input<D \| null>` | Maximum selectable date (inclusive). Forward to the projected calendar's `[max]`.<br>**Default:** `null` |
206
+ | `isDateUnavailable` | `input<(date: D) => boolean>` | Per-date predicate. Forward to the projected calendar's `[isDateUnavailable]`.<br>**Default:** `() => false` |
207
+ | `closeOnSelect` | `input<boolean>` | Close the surface after a date is picked. Honoured at `granularity="day"`, and at any granularity with `anatomy="field"`.<br>**Default:** `true` |
208
+ | `anatomy` | `input<'trigger' \| 'field'>` | Which piece is the control. `'field'` makes a projected `[forDateField]` the control and the trigger a plain button; see [Field anatomy](#field-anatomy).<br>**Default:** `'trigger'` |
209
+ | `granularity` | `input<'day' \| 'hour' \| 'minute' \| 'second'>` | Date-time precision. `'day'` (default) is a pure date picker; a finer granularity composes a time field.<br>**Default:** `'day'` |
210
+ | `hourCycle` | `input<12 \| 24 \| null>` | 12/24-hour cycle for the value display; a projected time field takes `resolvedHourCycle()`.<br>**Default:** `null` → the scope's `hourCycle` (`provideForDatePickerDefaults`), then the locale |
211
+ | `resolvedHourCycle` | `Signal<12 \| 24 \| null>` | The effective cycle: `hourCycle`, then the scope's, or `null` for the locale. Bind it to a projected time field's `[hourCycle]`.<br>**Default:** — |
212
+ | `modal` | `input<boolean>` | Trap focus + inert background + scroll lock (centered dialog) instead of an anchored popover.<br>**Default:** `false` |
213
+ | `dismissible` | `input<boolean>` | Escape / outside-pointer dismiss the surface.<br>**Default:** `true` |
214
+ | `returnFocus` | `input<boolean>` | Return focus to the trigger on close.<br>**Default:** `true` |
215
+ | `formatOptions` | `input<Intl.DateTimeFormatOptions>` | Options for the text rendered by `[forDatePickerValue]`.<br>**Default:** `{ year: 'numeric', month: 'long', day: 'numeric' }` |
216
+ | `locale` | `input<string \| null>` | BCP 47 locale for the text rendered by `[forDatePickerValue]`. Not forwarded to the projected calendar, so bind its `[locale]` too.<br>**Default:** `null` → the adapter's `locale()`, then the runtime locale |
217
+ | `placeholder` | `input<string>` | Fallback text for `[forDatePickerValue]` when empty.<br>**Default:** `''` |
218
+ | `side` / `align` | `input` | Anchored placement (popover mode only). Defaults from `provideForDatePickerDefaults` / `provideForDateRangePickerDefaults`.<br>**Default:** `'bottom'` / `'start'` |
219
+ | `sideOffset` | `input<number>` | Gap (px) between the anchor and the content along the main axis. Defaults from `provideForDatePickerDefaults` / `provideForDateRangePickerDefaults`.<br>**Default:** `8` |
220
+ | `alignOffset` | `input<number>` | Gap (px) along the cross axis (parallel to `side`).<br>**Default:** `0` |
221
+ | `avoidCollisions` | `input<boolean>` | When `true`, `flip` and `shift` keep the content inside the viewport. Set `false` for strict positioning where overflow is acceptable.<br>**Default:** `true` |
222
+ | `collisionPadding` | `input<number>` | Padding (px) for the `flip` / `shift` / `size` collision middlewares. Defaults from `provideForDatePickerDefaults` / `provideForDateRangePickerDefaults`.<br>**Default:** `8` |
223
+ | `sticky` | `input<'partial' \| 'always' \| false>` | `'always'` keeps the requested placement even when the content leaves the viewport. `'partial'` and `false` let `shift` slide it back into view.<br>**Default:** `'partial'` |
224
+ | `hideWhenDetached` | `input<boolean>` | When `true`, the content carries `data-detached` while the anchor has scrolled out of every clipping ancestor, so CSS can hide it.<br>**Default:** `false` |
225
+ | `clipUntilPositioned` | `input<boolean>` | Clips the content until its first position resolves, so it never flashes at the viewport corner. Set `false` so an `animate.enter` plays from its first frame.<br>**Default:** `true` |
226
+ | `dir` | `input<'ltr' \| 'rtl' \| null>` | Writing direction.<br>**Default:** `null` resolves the ambient direction; reflected to the host `dir` |
227
+
228
+ Plus the shared `FormUiControl` inputs from the base (`disabled`, `readonly`, `required`, `invalid`, `pending`, `dirty`, `name`, `errors`, and the `touched` model). The positioning inputs (`side` through `clipUntilPositioned`) apply in popover mode only; `modal` centres the surface instead of anchoring it.
224
229
 
225
230
  > **`locale` only styles the trigger display.** It drives the text rendered by `[forDatePickerValue]` (both here and on `ForDateRangePicker`); it is **not** forwarded to the projected `ForCalendar`. Bind the calendar's own `[locale]` to localize its heading / weekday / cell labels, exactly as you forward `[min]` / `[max]`.
226
231
 
227
232
  > **Why `minDate` / `maxDate`, not `min` / `max`?** `ForDatePicker` is a `FormValueControl`, and `FormUiControl` reserves `min` / `max` for numeric validators (`InputSignal<number | undefined>`). A date-typed `min` / `max` would break that contract, so the date bounds use the `*Date` suffix. (`ForCalendar` is not a form control, so it keeps `min` / `max`.)
228
233
 
234
+ ### Native submission
235
+
236
+ When `name` is set, a hidden `<input name="…">` is mounted right after the `[forDatePicker]` element and mirrors the committed value as an ISO date (`YYYY-MM-DD`), or as an ISO date-time (`YYYY-MM-DDTHH:mm`, plus `:ss` at `granularity="second"`) at a granularity finer than `'day'`. It is removed while the value is `null` and disabled with the picker. The trigger itself carries no `name` attribute.
237
+
238
+ A native reset of the owning `<form>` returns `value` to what it held when the picker first rendered. In the [field anatomy](#field-anatomy) it also drops a partly typed entry in the projected field.
239
+
229
240
  ### Data attributes
230
241
 
231
242
  | Piece | Attribute | Values |
@@ -233,9 +244,17 @@ A native reset of the owning `<form>` returns `value` to what it held when the p
233
244
  | `[forDatePicker]` | `data-state` | `open` \| `closed` |
234
245
  | `[forDatePicker]` | `data-disabled` | present \| absent |
235
246
  | `[forDatePicker]` | `data-readonly` | present \| absent |
247
+ | `[forDatePicker]` | `data-touched` | present \| absent |
248
+ | `[forDatePicker]` | `data-dirty` | present \| absent |
249
+ | `[forDatePicker]` | `data-pending` | present \| absent |
250
+ | `[forDatePicker]` | `data-invalid` | present \| absent |
236
251
  | `[forDateRangePicker]` | `data-state` | `open` \| `closed` |
237
252
  | `[forDateRangePicker]` | `data-disabled` | present \| absent |
238
253
  | `[forDateRangePicker]` | `data-readonly` | present \| absent |
254
+ | `[forDateRangePicker]` | `data-touched` | present \| absent |
255
+ | `[forDateRangePicker]` | `data-dirty` | present \| absent |
256
+ | `[forDateRangePicker]` | `data-pending` | present \| absent |
257
+ | `[forDateRangePicker]` | `data-invalid` | present \| absent |
239
258
  | `[forDatePickerTrigger]` | `data-state` | `open` \| `closed` |
240
259
  | `[forDatePickerTrigger]` | `data-disabled` | present \| absent |
241
260
  | `[forDatePickerTrigger]` | `data-readonly` | present \| absent |
@@ -440,7 +459,7 @@ readonly booking = form(this.model, (p) => required(p.stay));
440
459
  - **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. A native reset of the owning `<form>` returns `value` to what it held when the picker first rendered.
441
460
  - **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.
442
461
 
443
- 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).
462
+ Its positioning inputs are the ones the [`ForDatePicker` table](#fordatepicker) lists, with the same defaults. 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).
444
463
 
445
464
  ## Keyboard
446
465
 
@@ -451,7 +470,7 @@ Defaults are configured with `provideForDateRangePickerDefaults` (`side` / `alig
451
470
  | **Escape** | Dismiss the surface and return focus to the trigger (when `dismissible`). |
452
471
  | **Tab / Shift+Tab** past either end of the surface | Non-modal: Tab moves on to the element after the trigger and Shift+Tab lands on the trigger, closing the surface when `dismissible`. Modal: focus stays trapped. |
453
472
 
454
- Inside the surface, the projected `ForCalendar` owns the full grid keyboard map (arrows / `Home` / `End` / `PageUp` / `PageDown` / `Enter` / `Space`). On open, focus lands on the calendar's focused cell (`value ?? today`, with today clamped into the calendar's `[min, max]`) in non-modal mode, or the first focusable element in modal mode.
473
+ Inside the surface, the projected `ForCalendar` owns the full grid keyboard map (arrows / `Home` / `End` / `PageUp` / `PageDown` / `Enter` / `Space`). On open, focus lands on the calendar's focused cell (`value ?? today`, with today clamped into the calendar's `[min, max]`) in both modes. When no cell can take focus, the surface itself is focused.
455
474
 
456
475
  ## Accessibility
457
476
 
package/dialog/README.md CHANGED
@@ -157,7 +157,7 @@ Open a component imperatively and await its result. The manager mounts it under
157
157
  | Property | Type | Description |
158
158
  | -------------------- | ------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
159
159
  | `dismissible` | — | When `false`, Escape, backdrop, outside-pointer, and outside-focus do not request close. The close button still does.<br>**Default:** `true` |
160
- | `modal` | — | When `false`, no `aria-modal`, no scroll lock, no focus trap, no inert siblings, and no return focus on close.<br>**Default:** `true` |
160
+ | `modal` | — | When `false`, no `aria-modal`, no scroll lock, no focus trap, no inert siblings, and no return focus on close. Read once at mount: a change while the dialog is open takes effect on the next open.<br>**Default:** `true` |
161
161
  | `alert` | — | Switches role to `alertdialog`.<br>**Default:** `false` |
162
162
  | `returnFocus` | — | Modal mode only: focus returns to the previously focused element on close. A non-modal dialog never moves focus on close.<br>**Default:** `true` |
163
163
  | `initialFocus` | — | `'first'` (first focusable inside) or `'container'` (the dialog host).<br>**Default:** `'first'` |
package/drawer/README.md CHANGED
@@ -362,7 +362,7 @@ Press the button: the manager opens the confirmation as a bottom drawer, and the
362
362
  | Property | Type | Description |
363
363
  | --------------------------- | ------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
364
364
  | `side` | `'top' \| 'right' \| 'bottom' \| 'left'` | Anchored edge. Drives swipe direction and `data-side`.<br>**Default:** `'bottom'` |
365
- | `modal` | `boolean` | `aria-modal`, scroll lock, focus trap, inert siblings, return focus on close.<br>**Default:** `true` |
365
+ | `modal` | `boolean` | `aria-modal`, scroll lock, focus trap, inert siblings, return focus on close. Read once at mount: a change while the drawer is open takes effect on the next open.<br>**Default:** `true` |
366
366
  | `dismissible` | `boolean` | Whether Escape / backdrop / outside / swipe close. When `false`, no swipe dismisses; `snapPoints` still drag between snaps.<br>**Default:** `true` |
367
367
  | `alert` | `boolean` | `role="alertdialog"`.<br>**Default:** `false` |
368
368
  | `returnFocus` | `boolean` | Modal mode only: restore focus on close. A non-modal drawer never moves focus on close.<br>**Default:** `true` |
@@ -98,8 +98,13 @@ A settings-style dropdown built from the full menu vocabulary: `forMenuGroup` wi
98
98
  | `sideOffset` | `input<number>` | Gap (px) between the trigger and the content along the main axis.<br>**Default:** `4` |
99
99
  | `alignOffset` | `input<number>` | Gap (px) along the cross axis (parallel to `side`).<br>**Default:** `0` |
100
100
  | `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, so set it once for the whole app rather than per call site.<br>**Default:** `'none'` |
101
+ | `avoidCollisions` | `input<boolean>` | When `true`, `flip` and `shift` keep the content inside the viewport. Set `false` for strict positioning where overflow is acceptable.<br>**Default:** `true` |
102
+ | `collisionPadding` | `input<number>` | Padding (px) for the `flip` / `shift` / `size` collision middlewares. The default is read from `provideForDropdownMenuDefaults` for the surrounding scope.<br>**Default:** `8` |
103
+ | `sticky` | `input<'partial' \| 'always' \| false>` | `'always'` keeps the requested placement even when the content leaves the viewport. `'partial'` and `false` let `shift` slide it back into view.<br>**Default:** `'partial'` |
104
+ | `hideWhenDetached` | `input<boolean>` | When `true`, the content carries `data-detached` while the anchor has scrolled out of every clipping ancestor, so CSS can hide it.<br>**Default:** `false` |
105
+ | `clipUntilPositioned` | `input<boolean>` | Clips the content until its first position resolves, so it never flashes at the viewport corner. Set `false` so an `animate.enter` plays from its first frame.<br>**Default:** `true` |
101
106
  | `loop` | `input<boolean>` | Whether arrow navigation wraps at the ends.<br>**Default:** `true` |
102
- | `dir` | `input<string>` | Writing direction. In RTL, ArrowLeft opens submenus and ArrowRight closes them. The swap is automatic. Inherited by every nested `[forMenuSub]` underneath unless overridden.<br>**Default:** `'ltr'` |
107
+ | `dir` | `input<'ltr' \| 'rtl' \| null>` | Writing direction. `null` inherits the nearest `[dir]` / `<html dir>`, else `'ltr'`. In RTL, ArrowLeft opens submenus and ArrowRight closes them. The swap is automatic. Inherited by every nested `[forMenuSub]` underneath unless overridden.<br>**Default:** `null` |
103
108
  | `disabled` | `input<boolean>` | When `true`, trigger interactions are ignored and every item of the menu and its submenus is disabled. An open menu stays open, and keyboard navigation, Tab and Escape keep working.<br>**Default:** `false` |
104
109
  | `dismissible` | `input<boolean>` | When `false`, Escape and outside interactions don't close.<br>**Default:** `true` |
105
110
  | `returnFocus` | `input<boolean>` | When `true`, focus returns to the trigger on close.<br>**Default:** `true` |