forty-cdk 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/accordion/README.md +80 -71
  2. package/aspect-ratio/README.md +24 -22
  3. package/avatar/README.md +50 -33
  4. package/breadcrumbs/README.md +34 -8
  5. package/breakpoints/README.md +16 -12
  6. package/button/README.md +55 -18
  7. package/calendar/README.md +105 -83
  8. package/carousel/README.md +153 -112
  9. package/checkbox/README.md +60 -37
  10. package/combobox/README.md +106 -95
  11. package/context-menu/README.md +55 -37
  12. package/date-field/README.md +110 -60
  13. package/date-picker/README.md +126 -59
  14. package/date-range-field/README.md +251 -0
  15. package/dialog/README.md +83 -67
  16. package/disclosure/README.md +65 -59
  17. package/drag-drop/README.md +60 -5
  18. package/drawer/README.md +118 -92
  19. package/dropdown-menu/README.md +62 -49
  20. package/fesm2022/forty-cdk-combobox.mjs +39 -1
  21. package/fesm2022/forty-cdk-combobox.mjs.map +1 -1
  22. package/fesm2022/forty-cdk-core.mjs +1140 -341
  23. package/fesm2022/forty-cdk-core.mjs.map +1 -1
  24. package/fesm2022/forty-cdk-date-field.mjs +43 -345
  25. package/fesm2022/forty-cdk-date-field.mjs.map +1 -1
  26. package/fesm2022/forty-cdk-date-picker.mjs +589 -266
  27. package/fesm2022/forty-cdk-date-picker.mjs.map +1 -1
  28. package/fesm2022/forty-cdk-date-range-field.mjs +682 -0
  29. package/fesm2022/forty-cdk-date-range-field.mjs.map +1 -0
  30. package/fesm2022/forty-cdk-hover-card.mjs +34 -4
  31. package/fesm2022/forty-cdk-hover-card.mjs.map +1 -1
  32. package/fesm2022/forty-cdk-listbox.mjs +6 -2
  33. package/fesm2022/forty-cdk-listbox.mjs.map +1 -1
  34. package/fesm2022/forty-cdk-table.mjs +202 -6
  35. package/fesm2022/forty-cdk-table.mjs.map +1 -1
  36. package/fesm2022/forty-cdk-time-field.mjs +45 -202
  37. package/fesm2022/forty-cdk-time-field.mjs.map +1 -1
  38. package/fesm2022/forty-cdk-time-range-field.mjs +671 -0
  39. package/fesm2022/forty-cdk-time-range-field.mjs.map +1 -0
  40. package/fesm2022/forty-cdk-toast.mjs +22 -1
  41. package/fesm2022/forty-cdk-toast.mjs.map +1 -1
  42. package/fesm2022/forty-cdk-tooltip.mjs +57 -17
  43. package/fesm2022/forty-cdk-tooltip.mjs.map +1 -1
  44. package/fesm2022/forty-cdk-virtualization.mjs +338 -4
  45. package/fesm2022/forty-cdk-virtualization.mjs.map +1 -1
  46. package/field/README.md +62 -21
  47. package/fieldset/README.md +35 -15
  48. package/file-upload/README.md +38 -14
  49. package/hover-card/README.md +104 -82
  50. package/input/README.md +81 -47
  51. package/listbox/README.md +215 -194
  52. package/menu/README.md +53 -29
  53. package/menubar/README.md +65 -39
  54. package/meter/README.md +67 -57
  55. package/navigation-menu/README.md +128 -115
  56. package/number-input/README.md +86 -68
  57. package/otp-input/README.md +78 -65
  58. package/package.json +9 -1
  59. package/pagination/README.md +57 -9
  60. package/pane-resizer/README.md +68 -51
  61. package/popover/README.md +110 -97
  62. package/progress/README.md +47 -39
  63. package/radio-group/README.md +78 -57
  64. package/scroll-area/README.md +81 -39
  65. package/search/README.md +39 -10
  66. package/select/README.md +141 -118
  67. package/separator/README.md +28 -26
  68. package/slider/README.md +95 -64
  69. package/stepper/README.md +100 -87
  70. package/switch/README.md +61 -40
  71. package/table/README.md +175 -46
  72. package/tabs/README.md +87 -70
  73. package/time-field/README.md +110 -58
  74. package/time-picker/README.md +74 -51
  75. package/time-range-field/README.md +256 -0
  76. package/toast/README.md +104 -68
  77. package/toggle/README.md +125 -104
  78. package/toolbar/README.md +97 -43
  79. package/tooltip/README.md +152 -131
  80. package/tree/README.md +120 -80
  81. package/types/forty-cdk-combobox.d.ts +25 -0
  82. package/types/forty-cdk-core.d.ts +546 -150
  83. package/types/forty-cdk-date-field.d.ts +12 -16
  84. package/types/forty-cdk-date-picker.d.ts +374 -155
  85. package/types/forty-cdk-date-range-field.d.ts +390 -0
  86. package/types/forty-cdk-hover-card.d.ts +8 -2
  87. package/types/forty-cdk-table.d.ts +81 -2
  88. package/types/forty-cdk-time-field.d.ts +16 -18
  89. package/types/forty-cdk-time-range-field.d.ts +389 -0
  90. package/types/forty-cdk-toast.d.ts +33 -0
  91. package/types/forty-cdk-tooltip.d.ts +21 -12
  92. package/types/forty-cdk-virtualization.d.ts +70 -3
  93. package/virtualization/README.md +52 -20
@@ -1,12 +1,14 @@
1
1
  # Calendar
2
2
 
3
- Headless single-date calendar grid following the [WAI-ARIA Grid pattern](https://www.w3.org/WAI/ARIA/apg/patterns/grid/) — the date table at the heart of the APG [Date Picker Dialog](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/examples/datepicker-dialog/) example. Roving-tabindex focus management, full grid keyboard interaction (arrows / `Home` / `End` / `PageUp` / `PageDown` / `Shift+PageUp` / `Shift+PageDown`), focus paging across month boundaries, `aria-current="date"` on today, `min` / `max` / per-date availability, RTL arrow mirroring, and a pluggable, date-library-agnostic `DateAdapter<D>`.
3
+ A single-date calendar grid implementing the APG Grid pattern over a pluggable date adapter: roving-tabindex day navigation, month / year paging, and min / max / per-date availability.
4
+
5
+ Headless and styleless — the date table at the heart of the APG [Date Picker Dialog](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/examples/datepicker-dialog/) example. Full grid keyboard interaction (arrows / `Home` / `End` / `PageUp` / `PageDown` / `Shift+PageUp` / `Shift+PageDown`), focus paging across month boundaries, `aria-current="date"` on today, RTL arrow mirroring, and a pluggable, date-library-agnostic `DateAdapter<D>`.
4
6
 
5
7
  `ForCalendar` is the grid widget, **not a form value** — it exposes `[(value)]` as a `model<D | null>`. The form-control contract (`FormValueControl<D>`) arrives with the follow-up `ForDatePicker` / `ForDateField`.
6
8
 
7
- ## Date adapter — pick one (required)
9
+ ## Date adapter
8
10
 
9
- All date math goes through a `DateAdapter<D>`, so the library hard-depends on **no** date library. Provide exactly one adapter in your application (or component) providers:
11
+ All date math goes through a `DateAdapter<D>`, so the library hard-depends on **no** date library. Provide exactly one adapter in your application (or component) providers (required):
10
12
 
11
13
  | Provider | Date type `D` | Dependency |
12
14
  | --------------------------------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------------------- |
@@ -24,37 +26,35 @@ bootstrapApplication(App, {
24
26
 
25
27
  `@internationalized/date` is a widely-used immutable date primitive; it works in every browser today with no polyfill, and its reference-equality-on-mutation makes it signal-friendly. Both `@internationalized/date` adapters operate on the **Gregorian** calendar today — `createDate` always builds a Gregorian date, so the grid stays Gregorian regardless of the runtime locale. True non-Gregorian calendar systems are deferred to the planned `Temporal.PlainDate` adapter ([#354](https://github.com/tutkli/forty-cdk/issues/354)), a non-breaking addition once the Temporal API is broadly available across browsers — the `DateAdapter<D>` seam means adopting it later is a drop-in, not a migration.
26
28
 
27
- ## Pieces
28
-
29
- | Class | Selector | Role |
30
- | ----------------------- | ------------------------- | --------------------------------------------------------------------------------------------------- |
31
- | `ForCalendar` | `[forCalendar]` | Root. Owns `value`, the focused date, the visible month, and the shared context. |
32
- | `ForCalendarHeading` | `[forCalendarHeading]` | Month/year title and the grid's `aria-labelledby` target; its text is set to the visible period. |
33
- | `ForCalendarPrevButton` | `[forCalendarPrevButton]` | Pages to the previous month. Auto-disabled at the `min` bound. |
34
- | `ForCalendarNextButton` | `[forCalendarNextButton]` | Pages to the next month. Auto-disabled at the `max` bound. |
35
- | `ForCalendarGrid` | `[forCalendarGrid]` | Date table (`role="grid"`, `aria-labelledby` the heading). Exposes `weekDays()` / `weeks()`. |
36
- | `ForCalendarGridHeader` | `[forCalendarGridHeader]` | Header rowgroup (`role="rowgroup"`) holding the weekday `columnheader`s. Also exposes `weekDays()`. |
37
- | `ForCalendarCell` | `[forCalendarCell]` | One day (`role="gridcell"`). Roving tab stop, ARIA state, and keyboard / click interaction. |
38
-
39
- ## Inputs / models — `ForCalendar`
40
-
41
- | API | Type | Description |
42
- | ------------------- | -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
43
- | `value` | `model<D \| null>` | Two-way bindable selected date, or `null`. Used in `selectionMode="single"`. `(valueChange)` fires only on internal selection. Default `null`. |
44
- | `selectionMode` | `input<'single' \| 'range'>` | `'single'` (default) keeps the single-date `value` flow. `'range'` switches to anchor → commit and exposes `range`. |
45
- | `range` | `model<CalendarDateRange<D> \| null>` | Two-way bindable committed range. Only used in `selectionMode="range"`. `(rangeChange)` fires only on internal commits/clears. Default `null`. |
46
- | `minRangeLength` | `input<number \| null>` | Minimum inclusive day count. A commit shorter than this is a no-op. Default `null` (no minimum). |
47
- | `maxRangeLength` | `input<number \| null>` | Maximum inclusive day count. A commit longer than this is a no-op. Default `null` (no maximum). |
48
- | `min` | `input<D \| null>` | Minimum selectable date (inclusive). Earlier dates are unavailable. Default `null`. |
49
- | `max` | `input<D \| null>` | Maximum selectable date (inclusive). Later dates are unavailable. Default `null`. |
50
- | `isDateUnavailable` | `input<(date: D) => boolean>` | Per-date predicate marking a date unavailable (present but not selectable). Default `() => false`. |
51
- | `dateLabel` | `input<CalendarDateLabelFormatter<D>>` | Formats each gridcell's `aria-label` (full accessible date). Default: localized full date, outside-month days suffixed. |
52
- | `disabled` | `input<boolean>` | Disables the whole calendar (no focus movement, no selection). Reflected as `data-disabled`. |
53
- | `readonly` | `input<boolean>` | Read-only: dates stay focusable, selection is blocked. Reflected as `data-readonly`. |
54
- | `firstDayOfWeek` | `input<number \| null>` | First column's weekday, **0-6** (`0` = Sunday). Default `null` → the adapter's value (or `provideForCalendarDefaults`). |
55
- | `dir` | `input<'ltr' \| 'rtl' \| null>` | Writing direction. Default `null` resolves the ambient direction; reflected to the host `dir` and mirrors horizontal arrows. |
56
-
57
- ## Usage
29
+ ## Anatomy
30
+
31
+ ```html
32
+ <div forCalendar [(value)]="date">
33
+ <header>
34
+ <button forCalendarPrevButton [ariaLabel]="'Previous month'">‹</button>
35
+ <h2 forCalendarHeading #heading="forCalendarHeading">{{ heading.label() }}</h2>
36
+ <button forCalendarNextButton [ariaLabel]="'Next month'">›</button>
37
+ </header>
38
+
39
+ <table forCalendarGrid #grid="forCalendarGrid">
40
+ <thead forCalendarGridHeader>
41
+ <tr>
42
+ <!-- @for (day of grid.weekDays(); track day.key) -->
43
+ <th scope="col" [attr.aria-label]="day.long">{{ day.short }}</th>
44
+ </tr>
45
+ </thead>
46
+ <tbody>
47
+ <!-- @for (week of grid.weeks(); track week.key) -->
48
+ <tr>
49
+ <!-- @for (cell of week.days; track cell.key) -->
50
+ <td forCalendarCell [date]="cell.date">{{ cell.label }}</td>
51
+ </tr>
52
+ </tbody>
53
+ </table>
54
+ </div>
55
+ ```
56
+
57
+ ## Examples
58
58
 
59
59
  ```ts
60
60
  import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
@@ -117,6 +117,40 @@ export class DatePage {
117
117
 
118
118
  The library is styleless: style the boolean `data-*` hooks on `[forCalendarCell]` yourself (`[data-selected]`, `[data-today]`, `[data-outside-month]`, `[data-highlighted]`, and `:not([data-disabled])` for the enabled state).
119
119
 
120
+ ## API
121
+
122
+ ### `ForCalendar`
123
+
124
+ | Property | Type | Description |
125
+ | ------------------- | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
126
+ | `value` | `model<D \| null>` | Two-way bindable selected date, or `null`. Used in `selectionMode="single"`. `(valueChange)` fires only on internal selection.<br>**Default:** `null` |
127
+ | `selectionMode` | `input<'single' \| 'range'>` | `'single'` (default) keeps the single-date `value` flow. `'range'` switches to anchor → commit and exposes `range`.<br>**Default:** `'single'` |
128
+ | `range` | `model<CalendarDateRange<D> \| null>` | Two-way bindable committed range. Only used in `selectionMode="range"`. `(rangeChange)` fires only on internal commits/clears.<br>**Default:** `null` |
129
+ | `minRangeLength` | `input<number \| null>` | Minimum inclusive day count. A commit shorter than this is a no-op.<br>**Default:** `null` (no minimum) |
130
+ | `maxRangeLength` | `input<number \| null>` | Maximum inclusive day count. A commit longer than this is a no-op.<br>**Default:** `null` (no maximum) |
131
+ | `min` | `input<D \| null>` | Minimum selectable date (inclusive). Earlier dates are unavailable.<br>**Default:** `null` |
132
+ | `max` | `input<D \| null>` | Maximum selectable date (inclusive). Later dates are unavailable.<br>**Default:** `null` |
133
+ | `isDateUnavailable` | `input<(date: D) => boolean>` | Per-date predicate marking a date unavailable (present but not selectable).<br>**Default:** `() => false` |
134
+ | `dateLabel` | `input<CalendarDateLabelFormatter<D>>` | Formats each gridcell's `aria-label` (full accessible date).<br>**Default:** localized full date, outside-month days suffixed |
135
+ | `disabled` | `input<boolean>` | Disables the whole calendar (no focus movement, no selection). Reflected as `data-disabled`.<br>**Default:** — |
136
+ | `readonly` | `input<boolean>` | Read-only: dates stay focusable, selection is blocked. Reflected as `data-readonly`.<br>**Default:** — |
137
+ | `firstDayOfWeek` | `input<number \| null>` | First column's weekday, **0-6** (`0` = Sunday).<br>**Default:** `null` → the adapter's value (or `provideForCalendarDefaults`) |
138
+ | `dir` | `input<'ltr' \| 'rtl' \| null>` | Writing direction.<br>**Default:** `null` resolves the ambient direction; reflected to the host `dir` and mirrors horizontal arrows |
139
+
140
+ ### Data attributes
141
+
142
+ | Piece | Attribute | Values |
143
+ | ------------------------- | -------------------- | ----------------- |
144
+ | `[forCalendar]` | `data-disabled` | present \| absent |
145
+ | `[forCalendar]` | `data-readonly` | present \| absent |
146
+ | `[forCalendarPrevButton]` | `data-disabled` | present \| absent |
147
+ | `[forCalendarNextButton]` | `data-disabled` | present \| absent |
148
+ | `[forCalendarCell]` | `data-selected` | present \| absent |
149
+ | `[forCalendarCell]` | `data-today` | present \| absent |
150
+ | `[forCalendarCell]` | `data-highlighted` | present \| absent |
151
+ | `[forCalendarCell]` | `data-disabled` | present \| absent |
152
+ | `[forCalendarCell]` | `data-outside-month` | present \| absent |
153
+
120
154
  ## Range selection
121
155
 
122
156
  Set `selectionMode="range"` and bind `[(range)]` to get date-range selection. In range mode the single-date `value` is unused and stays `null`.
@@ -245,7 +279,7 @@ The default window is **anchored to the current year** (not the visible year), s
245
279
 
246
280
  The lower-level hooks (`visibleMonthNumber()`, `visibleYear()`, `monthOptions()`, `goToMonth()`, `goToYear()`, `isYearDisabled()`) in the table above remain available for any other UI.
247
281
 
248
- ## View switching (month / year picker)
282
+ ## View switching
249
283
 
250
284
  `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.
251
285
 
@@ -364,23 +398,44 @@ Auto-disabled when the entire previous / next page would be outside `[min, max]`
364
398
  | `[forCalendarYearCell]` | `data-highlighted` | present \| absent |
365
399
  | `[forCalendarYearCell]` | `data-disabled` | present \| absent |
366
400
 
367
- ## Styling
401
+ ## Scope defaults
368
402
 
369
- forty-cdk ships no styles. Add your own class to each piece — the `forCalendar*` selectors are the behavior API, not a styling contract (see [Styling forty-cdk](../../../../../docs/styling.md)). Key your CSS off the reflected `data-*` attributes below.
403
+ ```ts
404
+ import { provideForCalendarDefaults } from 'forty-cdk/calendar';
370
405
 
371
- ### Data attributes
406
+ // app config or a component's providers — Monday-first weeks for this scope
407
+ providers: [provideForCalendarDefaults({ firstDayOfWeek: 1 })];
408
+ ```
372
409
 
373
- | Piece | Attribute | Values |
374
- | ------------------------- | -------------------- | ----------------- |
375
- | `[forCalendar]` | `data-disabled` | present \| absent |
376
- | `[forCalendar]` | `data-readonly` | present \| absent |
377
- | `[forCalendarPrevButton]` | `data-disabled` | present \| absent |
378
- | `[forCalendarNextButton]` | `data-disabled` | present \| absent |
379
- | `[forCalendarCell]` | `data-selected` | present \| absent |
380
- | `[forCalendarCell]` | `data-today` | present \| absent |
381
- | `[forCalendarCell]` | `data-highlighted` | present \| absent |
382
- | `[forCalendarCell]` | `data-disabled` | present \| absent |
383
- | `[forCalendarCell]` | `data-outside-month` | present \| absent |
410
+ ## Keyboard
411
+
412
+ LTR (horizontal arrows mirror under `dir="rtl"`):
413
+
414
+ | Key | Behavior |
415
+ | --------------------------------- | ------------------------------------------------------------- |
416
+ | **ArrowLeft / ArrowRight** | Previous / next day. |
417
+ | **ArrowUp / ArrowDown** | Same weekday, previous / next week. |
418
+ | **Home / End** | First / last day of the focused week. |
419
+ | **PageUp / PageDown** | Same day-of-month, previous / next month (re-pages the grid). |
420
+ | **Shift+PageUp / Shift+PageDown** | Same month, previous / next year. |
421
+ | **Enter / Space** | Select the focused date. |
422
+
423
+ Focus that crosses a month boundary re-pages the visible grid and keeps the focused cell in view. Day-of-month is constrained when paging (e.g. Jan 31 → Feb 28). Paging (the prev / next buttons and `PageUp` / `PageDown`) also clamps the focused date into the `[min, max]` range, so it never lands on a cell outside the selectable bounds; day / week arrows still move freely across unavailable dates so keyboard navigation is never trapped.
424
+
425
+ ## Accessibility
426
+
427
+ Implements the [WAI-ARIA Grid pattern](https://www.w3.org/WAI/ARIA/apg/patterns/grid/).
428
+
429
+ - **`role="grid"`** on the table, `columnheader` weekday headers, `gridcell` days — the APG Date Picker Dialog technique over a real `<table>`.
430
+ - **`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.
431
+ - **`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.
432
+ - **`aria-selected`** is always emitted (`"true"` / `"false"`); **`aria-current="date"`** marks today; **`aria-disabled`** marks unavailable dates (truthy-only).
433
+ - **Roving tabindex**: exactly one cell (the focused date) is tabbable. `Tab` enters and leaves the grid in one stop.
434
+ - **Boolean `data-*`** on the cell — `data-selected`, `data-today`, `data-highlighted` (the focused/roving cell), `data-disabled`, `data-outside-month` — present when true, absent when false.
435
+
436
+ ## Styling
437
+
438
+ forty-cdk ships no styles. Add your own class to each piece — the `forCalendar*` selectors are the behavior API, not a styling contract (see [Styling forty-cdk](../../../../../docs/styling.md)). Key your CSS off the reflected `data-*` attributes listed under [Data attributes](#data-attributes).
384
439
 
385
440
  ```css
386
441
  .calendar-cell {
@@ -402,31 +457,7 @@ forty-cdk ships no styles. Add your own class to each piece — the `forCalendar
402
457
  }
403
458
  ```
404
459
 
405
- ## Keyboard
406
-
407
- LTR (horizontal arrows mirror under `dir="rtl"`):
408
-
409
- | Key | Behavior |
410
- | --------------------------------- | ------------------------------------------------------------- |
411
- | **ArrowLeft / ArrowRight** | Previous / next day. |
412
- | **ArrowUp / ArrowDown** | Same weekday, previous / next week. |
413
- | **Home / End** | First / last day of the focused week. |
414
- | **PageUp / PageDown** | Same day-of-month, previous / next month (re-pages the grid). |
415
- | **Shift+PageUp / Shift+PageDown** | Same month, previous / next year. |
416
- | **Enter / Space** | Select the focused date. |
417
-
418
- Focus that crosses a month boundary re-pages the visible grid and keeps the focused cell in view. Day-of-month is constrained when paging (e.g. Jan 31 → Feb 28). Paging (the prev / next buttons and `PageUp` / `PageDown`) also clamps the focused date into the `[min, max]` range, so it never lands on a cell outside the selectable bounds; day / week arrows still move freely across unavailable dates so keyboard navigation is never trapped.
419
-
420
- ## Scope defaults
421
-
422
- ```ts
423
- import { provideForCalendarDefaults } from 'forty-cdk/calendar';
424
-
425
- // app config or a component's providers — Monday-first weeks for this scope
426
- providers: [provideForCalendarDefaults({ firstDayOfWeek: 1 })];
427
- ```
428
-
429
- ## SSR / hydration
460
+ ## SSR
430
461
 
431
462
  The active adapter's `today()` and `format()` resolve against the **runtime** time zone and default locale. Rendered on the server they reflect the _server's_ environment, so a render near midnight (or under a different server locale) can disagree with the browser by up to a day — `ForCalendar` reads `today()` once to mark the `data-today` / `aria-current="date"` cell, so the mismatch surfaces there as a hydration error and a flicker. For SSR, pin a fixed "today" / time zone / locale for both environments, or defer the today-highlight so it only computes client-side:
432
463
 
@@ -447,12 +478,3 @@ export class DatePage {
447
478
  }
448
479
  }
449
480
  ```
450
-
451
- ## Accessibility notes
452
-
453
- - **`role="grid"`** on the table, `columnheader` weekday headers, `gridcell` days — the APG Date Picker Dialog technique over a real `<table>`.
454
- - **`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.
455
- - **`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.
456
- - **`aria-selected`** is always emitted (`"true"` / `"false"`); **`aria-current="date"`** marks today; **`aria-disabled`** marks unavailable dates (truthy-only).
457
- - **Roving tabindex**: exactly one cell (the focused date) is tabbable. `Tab` enters and leaves the grid in one stop.
458
- - **Boolean `data-*`** on the cell — `data-selected`, `data-today`, `data-highlighted` (the focused/roving cell), `data-disabled`, `data-outside-month` — present when true, absent when false.
@@ -1,11 +1,34 @@
1
1
  # ForCarousel
2
2
 
3
- Headless, styleless carousel primitive implementing the
4
- [WAI-ARIA APG Carousel pattern](https://www.w3.org/WAI/ARIA/apg/patterns/carousel/).
5
- It ships slide tracking, keyboard navigation, focus management, and ARIA; you
6
- supply the markup and CSS.
3
+ 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.
7
4
 
8
- ## Usage
5
+ Headless and styleless: it ships slide tracking, keyboard navigation, focus management, and ARIA; you supply the markup and CSS.
6
+
7
+ ## Anatomy
8
+
9
+ ```html
10
+ <div forCarousel [(activeIndex)]="index" loop ariaLabel="Featured products">
11
+ <button forCarouselPrevious aria-label="Previous slide">‹</button>
12
+
13
+ <div forCarouselViewport>
14
+ <div forCarouselTrack>
15
+ <!-- one [forCarouselSlide] per item -->
16
+ <div forCarouselSlide>Slide content</div>
17
+ </div>
18
+ </div>
19
+
20
+ <button forCarouselNext aria-label="Next slide">›</button>
21
+
22
+ <div forCarouselIndicators ariaLabel="Choose slide to display">
23
+ <!-- one [forCarouselIndicator] per slide, same order -->
24
+ <button forCarouselIndicator></button>
25
+ </div>
26
+ </div>
27
+ ```
28
+
29
+ ## Examples
30
+
31
+ ### Basic carousel
9
32
 
10
33
  ```html
11
34
  <div
@@ -44,49 +67,6 @@ desynchronizes the active-indicator state and is dev-guarded by a `console.warn`
44
67
  in development builds. Grouped or summarized indicators (fewer dots than slides)
45
68
  are not supported.
46
69
 
47
- ## Example CSS
48
-
49
- The directive publishes geometry as CSS custom properties on the root element
50
- so they cascade to the track. The consumer applies the transform and transition.
51
-
52
- ```css
53
- [forCarouselViewport] {
54
- overflow: hidden;
55
- }
56
- [forCarouselTrack] {
57
- display: flex;
58
- transform: translateX(var(--for-carousel-offset));
59
- transition: transform 300ms ease;
60
- }
61
- [forCarousel][data-orientation='vertical'] [forCarouselTrack] {
62
- flex-direction: column;
63
- transform: translateY(var(--for-carousel-offset));
64
- }
65
- [forCarouselSlide] {
66
- flex: 0 0 calc(100% / var(--for-carousel-slides-per-view));
67
- }
68
- @media (prefers-reduced-motion: reduce) {
69
- [forCarouselTrack] {
70
- transition: none;
71
- }
72
- }
73
- ```
74
-
75
- ## CSS custom properties
76
-
77
- The following properties are set on the `[forCarousel]` host and cascade to
78
- children, unless noted otherwise:
79
-
80
- | Property | Host | Value | Notes |
81
- | -------------------------------- | ----------------------- | ------------- | --------------------------------------------------------------------------------------------------------------- |
82
- | `--for-carousel-offset` | `[forCarousel]` | e.g. `-100%` | Pure arithmetic from `activeIndex`, `slidesPerView`, `align`. |
83
- | `--for-carousel-active-index` | `[forCarousel]` | integer | Current `activeIndex`. |
84
- | `--for-carousel-slide-count` | `[forCarousel]` | integer | Total registered slides. |
85
- | `--for-carousel-slides-per-view` | `[forCarousel]` | integer | From the `slidesPerView` input. |
86
- | `--for-carousel-viewport-width` | `[forCarousel]` | e.g. `640px` | Measured via `ResizeObserver`. Absent on the server and before first measurement. |
87
- | `--for-carousel-viewport-height` | `[forCarousel]` | e.g. `400px` | Same as above, for the block axis. |
88
- | `--for-carousel-drag` | `[forCarouselViewport]` | e.g. `-128px` | Live px offset along the primary axis during a drag; absent at rest and under `prefers-reduced-motion: reduce`. |
89
-
90
70
  ## Autoplay
91
71
 
92
72
  Add the `autoplay` attribute and a `[forCarouselRotationControl]` as the **first
@@ -130,23 +110,6 @@ the defaults with `startLabel` / `stopLabel` inputs:
130
110
  <button forCarouselRotationControl startLabel="Play slideshow" stopLabel="Pause slideshow"></button>
131
111
  ```
132
112
 
133
- **Styling hooks:**
134
-
135
- ```css
136
- [forCarouselRotationControl]::before {
137
- content: '▶';
138
- }
139
- [forCarouselRotationControl][data-playing]::before {
140
- content: '⏸';
141
- }
142
- ```
143
-
144
- | Attribute | When present |
145
- | --------------- | ------------------------------------------------------- |
146
- | `data-playing` | On `[forCarouselRotationControl]` — user intent is "on" |
147
- | `data-rotating` | On `[forCarousel]` — actively rotating right now |
148
- | `data-autoplay` | On `[forCarousel]` — the `autoplay` input is `true` |
149
-
150
113
  **Programmatic control** via `exportAs`:
151
114
 
152
115
  ```html
@@ -224,39 +187,59 @@ horizontal carousels (allows vertical page scroll) and `pan-x` for vertical
224
187
  carousels (allows horizontal page scroll). A mostly-cross-axis swipe is never
225
188
  captured, so page scrolling on the perpendicular axis is unaffected.
226
189
 
227
- ### Styling hooks
190
+ ## Localizing the default labels
228
191
 
229
- | Attribute | Host | When present |
230
- | --------------- | ----------------------- | -------------------------------------- |
231
- | `data-dragging` | `[forCarouselViewport]` | Present while a drag gesture is armed. |
192
+ Each slide's default `aria-label` is the positional `"N of M"` string, and each
193
+ indicator's is `"Go to slide N"`. Localize both centrally with
194
+ `provideForCarouselDefaults` instead of setting `ariaLabel` on every slide and
195
+ indicator:
196
+
197
+ ```ts
198
+ providers: [
199
+ provideForCarouselDefaults({
200
+ slideLabel: (position, total) => `Diapositiva ${position} de ${total}`,
201
+ indicatorLabel: (position) => `Ir a la diapositiva ${position}`,
202
+ }),
203
+ ];
204
+ ```
232
205
 
233
- ### Inputs on `[forCarouselDrag]`
206
+ `position` is the 1-based slide index and `total` is the slide count. Overrides
207
+ merge with the parent scope, so you can localize just the labels and inherit the
208
+ rest of the defaults. A per-element `ariaLabel` on `[forCarouselSlide]` /
209
+ `[forCarouselIndicator]` still takes precedence over the localized default.
234
210
 
235
- | Input | Type | Default | Description |
236
- | ---------- | --------- | ------- | ---------------------------------------------------------------------------- |
237
- | `disabled` | `boolean` | `false` | Disable pointer drag without removing the directive. Removes `touch-action`. |
211
+ ## API
238
212
 
239
- ## Inputs
213
+ ### `ForCarousel`
240
214
 
241
215
  All inputs are on `[forCarousel]` unless noted.
242
216
 
243
- | Input | Type | Default | Description |
244
- | ------------------------------------------------ | ------------------------------ | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- |
245
- | `activeIndex` | `model<number>` | `0` | Two-way bindable current slide index. |
246
- | `orientation` | `'horizontal' \| 'vertical'` | `'horizontal'` | Scroll axis. |
247
- | `loop` | `boolean` | `false` | Wrap-around at the boundaries. |
248
- | `align` | `'start' \| 'center' \| 'end'` | `'start'` | Alignment of the active slide. |
249
- | `slidesPerView` | `number` | `1` | Visible slides at once. |
250
- | `containScroll` | `boolean` | `false` | Clamp the track offset so trailing slides sit flush at the viewport edge (no overscroll) when `slidesPerView > 1` and not looping. |
251
- | `autoplay` | `boolean` | `false` | Enable auto-rotation (suppressed by `prefers-reduced-motion: reduce`). |
252
- | `autoplayInterval` | `number` | `5000` | Ms between automatic slide advances. `<= 0` disables the timer. |
253
- | `ariaLabel` | `string \| null` | `null` | Accessible label for the carousel root. |
254
- | `dir` | `'ltr' \| 'rtl' \| null` | `null` (inherits) | Writing direction. |
255
- | `ariaLabel` (on `[forCarouselIndicators]`) | `string \| null` | `null` | Label for the picker group. |
256
- | `ariaLabel` (on `[forCarouselSlide]`) | `string \| null` | `null` | Override the positional "N of M" label. |
257
- | `disabled` (on `[forCarouselIndicator]`) | `boolean` | `false` | Disable this indicator. |
258
- | `startLabel` (on `[forCarouselRotationControl]`) | `string` | `'Start automatic slide show'` | Accessible name while rotation is stopped. |
259
- | `stopLabel` (on `[forCarouselRotationControl]`) | `string` | `'Stop automatic slide show'` | Accessible name while rotation is playing. |
217
+ | Property | Type | Description |
218
+ | ------------------------------------------------ | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
219
+ | `activeIndex` | `model<number>` | Two-way bindable current slide index.<br>**Default:** `0` |
220
+ | `orientation` | `'horizontal' \| 'vertical'` | Scroll axis.<br>**Default:** `'horizontal'` |
221
+ | `loop` | `boolean` | Wrap-around at the boundaries.<br>**Default:** `false` |
222
+ | `align` | `'start' \| 'center' \| 'end'` | Alignment of the active slide.<br>**Default:** `'start'` |
223
+ | `slidesPerView` | `number` | Visible slides at once.<br>**Default:** `1` |
224
+ | `containScroll` | `boolean` | Clamp the track offset so trailing slides sit flush at the viewport edge (no overscroll) when `slidesPerView > 1` and not looping.<br>**Default:** `false` |
225
+ | `autoplay` | `boolean` | Enable auto-rotation (suppressed by `prefers-reduced-motion: reduce`).<br>**Default:** `false` |
226
+ | `autoplayInterval` | `number` | Ms between automatic slide advances. `<= 0` disables the timer.<br>**Default:** `5000` |
227
+ | `ariaLabel` | `string \| null` | Accessible label for the carousel root.<br>**Default:** `null` |
228
+ | `dir` | `'ltr' \| 'rtl' \| null` | Writing direction.<br>**Default:** `null` (inherits) |
229
+ | `ariaLabel` (on `[forCarouselIndicators]`) | `string \| null` | Label for the picker group.<br>**Default:** `null` |
230
+ | `ariaLabel` (on `[forCarouselSlide]`) | `string \| null` | Override the positional "N of M" label.<br>**Default:** `null` |
231
+ | `disabled` (on `[forCarouselIndicator]`) | `boolean` | Disable this indicator.<br>**Default:** `false` |
232
+ | `startLabel` (on `[forCarouselRotationControl]`) | `string` | Accessible name while rotation is stopped.<br>**Default:** `'Start automatic slide show'` |
233
+ | `stopLabel` (on `[forCarouselRotationControl]`) | `string` | Accessible name while rotation is playing.<br>**Default:** `'Stop automatic slide show'` |
234
+
235
+ Reflected on the `[forCarousel]` host:
236
+
237
+ | Data attribute | Values |
238
+ | ------------------ | --------------------------------- |
239
+ | `data-orientation` | `horizontal` \| `vertical` |
240
+ | `data-align` | `start` \| `center` \| `end` |
241
+ | `data-autoplay` | present when `autoplay` is `true` |
242
+ | `data-rotating` | present while actively rotating |
260
243
 
261
244
  ### Contain scroll
262
245
 
@@ -275,28 +258,19 @@ same clamped view simply share the same visual position. The clamp has no effect
275
258
  when `loop` is enabled (the entire range is valid when wrapping) or when
276
259
  `slidesPerView` is `1` (a single-view carousel never overscrolls).
277
260
 
278
- ## Localizing the default labels
261
+ ### `ForCarouselDrag` inputs
279
262
 
280
- Each slide's default `aria-label` is the positional `"N of M"` string, and each
281
- indicator's is `"Go to slide N"`. Localize both centrally with
282
- `provideForCarouselDefaults` instead of setting `ariaLabel` on every slide and
283
- indicator:
263
+ | Property | Type | Description |
264
+ | ---------- | --------- | ---------------------------------------------------------------------------------------------------- |
265
+ | `disabled` | `boolean` | Disable pointer drag without removing the directive. Removes `touch-action`.<br>**Default:** `false` |
284
266
 
285
- ```ts
286
- providers: [
287
- provideForCarouselDefaults({
288
- slideLabel: (position, total) => `Diapositiva ${position} de ${total}`,
289
- indicatorLabel: (position) => `Ir a la diapositiva ${position}`,
290
- }),
291
- ];
292
- ```
267
+ Reflected on the `[forCarouselViewport]` host this directive is applied to:
293
268
 
294
- `position` is the 1-based slide index and `total` is the slide count. Overrides
295
- merge with the parent scope, so you can localize just the labels and inherit the
296
- rest of the defaults. A per-element `ariaLabel` on `[forCarouselSlide]` /
297
- `[forCarouselIndicator]` still takes precedence over the localized default.
269
+ | Data attribute | Values |
270
+ | --------------- | ------------------------------------- |
271
+ | `data-dragging` | present while a drag gesture is armed |
298
272
 
299
- ## Keyboard interaction (indicator group)
273
+ ## Keyboard
300
274
 
301
275
  Keyboard navigation lives on the indicator group. Only the current
302
276
  indicator is in the tab order. Arrow keys move focus and activate the target slide
@@ -310,7 +284,9 @@ automatically.
310
284
  | `End` | Last indicator and slide. |
311
285
  | `Enter` / `Space` | Activate the focused indicator (via native button). |
312
286
 
313
- ## Accessibility notes
287
+ ## Accessibility
288
+
289
+ Implements the [WAI-ARIA Carousel pattern](https://www.w3.org/WAI/ARIA/apg/patterns/carousel/).
314
290
 
315
291
  - The root carries `role="group"` and `aria-roledescription="carousel"`. The `ariaLabel` input
316
292
  should describe the carousel's purpose without using the word "carousel" (APG guidance).
@@ -330,7 +306,72 @@ automatically.
330
306
  `aria-roledescription`, `aria-label`, and `aria-hidden` toggle carry the screen-reader
331
307
  experience for non-auto-rotating carousels.
332
308
 
333
- ## Reduced-motion
309
+ ## Styling
310
+
311
+ forty-cdk ships no styles. Add your own class to each piece — the `for*` selectors are the behavior API, not a styling contract (see [Styling forty-cdk](../../../../../docs/styling.md)). The directive publishes geometry as CSS custom properties on the root element so they cascade to the track. The consumer applies the transform and transition.
312
+
313
+ ```css
314
+ [forCarouselViewport] {
315
+ overflow: hidden;
316
+ }
317
+ [forCarouselTrack] {
318
+ display: flex;
319
+ transform: translateX(var(--for-carousel-offset));
320
+ transition: transform 300ms ease;
321
+ }
322
+ [forCarousel][data-orientation='vertical'] [forCarouselTrack] {
323
+ flex-direction: column;
324
+ transform: translateY(var(--for-carousel-offset));
325
+ }
326
+ [forCarouselSlide] {
327
+ flex: 0 0 calc(100% / var(--for-carousel-slides-per-view));
328
+ }
329
+ @media (prefers-reduced-motion: reduce) {
330
+ [forCarouselTrack] {
331
+ transition: none;
332
+ }
333
+ }
334
+ ```
335
+
336
+ ### CSS custom properties
337
+
338
+ The following properties are set on the `[forCarousel]` host and cascade to
339
+ children, unless noted otherwise:
340
+
341
+ | Property | Host | Value | Notes |
342
+ | -------------------------------- | ----------------------- | ------------- | --------------------------------------------------------------------------------------------------------------- |
343
+ | `--for-carousel-offset` | `[forCarousel]` | e.g. `-100%` | Pure arithmetic from `activeIndex`, `slidesPerView`, `align`. |
344
+ | `--for-carousel-active-index` | `[forCarousel]` | integer | Current `activeIndex`. |
345
+ | `--for-carousel-slide-count` | `[forCarousel]` | integer | Total registered slides. |
346
+ | `--for-carousel-slides-per-view` | `[forCarousel]` | integer | From the `slidesPerView` input. |
347
+ | `--for-carousel-viewport-width` | `[forCarousel]` | e.g. `640px` | Measured via `ResizeObserver`. Absent on the server and before first measurement. |
348
+ | `--for-carousel-viewport-height` | `[forCarousel]` | e.g. `400px` | Same as above, for the block axis. |
349
+ | `--for-carousel-drag` | `[forCarouselViewport]` | e.g. `-128px` | Live px offset along the primary axis during a drag; absent at rest and under `prefers-reduced-motion: reduce`. |
350
+
351
+ ### Autoplay styling hooks
352
+
353
+ ```css
354
+ [forCarouselRotationControl]::before {
355
+ content: '▶';
356
+ }
357
+ [forCarouselRotationControl][data-playing]::before {
358
+ content: '⏸';
359
+ }
360
+ ```
361
+
362
+ | Attribute | When present |
363
+ | --------------- | ------------------------------------------------------- |
364
+ | `data-playing` | On `[forCarouselRotationControl]` — user intent is "on" |
365
+ | `data-rotating` | On `[forCarousel]` — actively rotating right now |
366
+ | `data-autoplay` | On `[forCarousel]` — the `autoplay` input is `true` |
367
+
368
+ ### Drag styling hooks
369
+
370
+ | Attribute | Host | When present |
371
+ | --------------- | ----------------------- | -------------------------------------- |
372
+ | `data-dragging` | `[forCarouselViewport]` | Present while a drag gesture is armed. |
373
+
374
+ ### Reduced-motion
334
375
 
335
376
  The directive performs no animation itself. Add the following CSS to disable the transition
336
377
  for users who prefer reduced motion:
@@ -343,7 +384,7 @@ for users who prefer reduced motion:
343
384
  }
344
385
  ```
345
386
 
346
- ## RTL support
387
+ ### RTL support
347
388
 
348
389
  Arrow-key direction (ArrowLeft/ArrowRight) is automatically swapped in RTL — handled by
349
390
  `resolveListNavigation` and the reflected `dir` attribute. The **visual** track direction