forty-cdk 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (220) hide show
  1. package/accordion/README.md +122 -0
  2. package/aspect-ratio/README.md +76 -0
  3. package/avatar/README.md +100 -0
  4. package/breadcrumbs/README.md +49 -0
  5. package/breakpoints/README.md +81 -0
  6. package/button/README.md +49 -0
  7. package/calendar/README.md +458 -0
  8. package/carousel/README.md +358 -0
  9. package/checkbox/README.md +146 -0
  10. package/combobox/README.md +535 -0
  11. package/context-menu/README.md +139 -0
  12. package/date-field/README.md +184 -0
  13. package/date-picker/README.md +338 -0
  14. package/dialog/README.md +388 -0
  15. package/disclosure/README.md +114 -0
  16. package/drag-drop/README.md +359 -0
  17. package/drawer/README.md +560 -0
  18. package/dropdown-menu/README.md +176 -0
  19. package/fesm2022/forty-cdk-accordion.mjs +348 -0
  20. package/fesm2022/forty-cdk-accordion.mjs.map +1 -0
  21. package/fesm2022/forty-cdk-aspect-ratio.mjs +74 -0
  22. package/fesm2022/forty-cdk-aspect-ratio.mjs.map +1 -0
  23. package/fesm2022/forty-cdk-avatar.mjs +308 -0
  24. package/fesm2022/forty-cdk-avatar.mjs.map +1 -0
  25. package/fesm2022/forty-cdk-breadcrumbs.mjs +125 -0
  26. package/fesm2022/forty-cdk-breadcrumbs.mjs.map +1 -0
  27. package/fesm2022/forty-cdk-breakpoints.mjs +117 -0
  28. package/fesm2022/forty-cdk-breakpoints.mjs.map +1 -0
  29. package/fesm2022/forty-cdk-button.mjs +134 -0
  30. package/fesm2022/forty-cdk-button.mjs.map +1 -0
  31. package/fesm2022/forty-cdk-calendar.mjs +2034 -0
  32. package/fesm2022/forty-cdk-calendar.mjs.map +1 -0
  33. package/fesm2022/forty-cdk-carousel.mjs +968 -0
  34. package/fesm2022/forty-cdk-carousel.mjs.map +1 -0
  35. package/fesm2022/forty-cdk-checkbox.mjs +226 -0
  36. package/fesm2022/forty-cdk-checkbox.mjs.map +1 -0
  37. package/fesm2022/forty-cdk-combobox.mjs +2596 -0
  38. package/fesm2022/forty-cdk-combobox.mjs.map +1 -0
  39. package/fesm2022/forty-cdk-context-menu.mjs +413 -0
  40. package/fesm2022/forty-cdk-context-menu.mjs.map +1 -0
  41. package/fesm2022/forty-cdk-core.mjs +9022 -0
  42. package/fesm2022/forty-cdk-core.mjs.map +1 -0
  43. package/fesm2022/forty-cdk-date-field.mjs +744 -0
  44. package/fesm2022/forty-cdk-date-field.mjs.map +1 -0
  45. package/fesm2022/forty-cdk-date-picker.mjs +1011 -0
  46. package/fesm2022/forty-cdk-date-picker.mjs.map +1 -0
  47. package/fesm2022/forty-cdk-dialog.mjs +707 -0
  48. package/fesm2022/forty-cdk-dialog.mjs.map +1 -0
  49. package/fesm2022/forty-cdk-disclosure.mjs +190 -0
  50. package/fesm2022/forty-cdk-disclosure.mjs.map +1 -0
  51. package/fesm2022/forty-cdk-drag-drop.mjs +1180 -0
  52. package/fesm2022/forty-cdk-drag-drop.mjs.map +1 -0
  53. package/fesm2022/forty-cdk-drawer.mjs +1641 -0
  54. package/fesm2022/forty-cdk-drawer.mjs.map +1 -0
  55. package/fesm2022/forty-cdk-dropdown-menu.mjs +350 -0
  56. package/fesm2022/forty-cdk-dropdown-menu.mjs.map +1 -0
  57. package/fesm2022/forty-cdk-field.mjs +425 -0
  58. package/fesm2022/forty-cdk-field.mjs.map +1 -0
  59. package/fesm2022/forty-cdk-fieldset.mjs +164 -0
  60. package/fesm2022/forty-cdk-fieldset.mjs.map +1 -0
  61. package/fesm2022/forty-cdk-file-upload.mjs +221 -0
  62. package/fesm2022/forty-cdk-file-upload.mjs.map +1 -0
  63. package/fesm2022/forty-cdk-hover-card.mjs +496 -0
  64. package/fesm2022/forty-cdk-hover-card.mjs.map +1 -0
  65. package/fesm2022/forty-cdk-input.mjs +274 -0
  66. package/fesm2022/forty-cdk-input.mjs.map +1 -0
  67. package/fesm2022/forty-cdk-internationalized-date.mjs +1 -1
  68. package/fesm2022/forty-cdk-internationalized-date.mjs.map +1 -1
  69. package/fesm2022/forty-cdk-listbox.mjs +1279 -0
  70. package/fesm2022/forty-cdk-listbox.mjs.map +1 -0
  71. package/fesm2022/forty-cdk-menu.mjs +1439 -0
  72. package/fesm2022/forty-cdk-menu.mjs.map +1 -0
  73. package/fesm2022/forty-cdk-menubar.mjs +787 -0
  74. package/fesm2022/forty-cdk-menubar.mjs.map +1 -0
  75. package/fesm2022/forty-cdk-meter.mjs +211 -0
  76. package/fesm2022/forty-cdk-meter.mjs.map +1 -0
  77. package/fesm2022/forty-cdk-navigation-menu.mjs +1145 -0
  78. package/fesm2022/forty-cdk-navigation-menu.mjs.map +1 -0
  79. package/fesm2022/forty-cdk-number-input.mjs +559 -0
  80. package/fesm2022/forty-cdk-number-input.mjs.map +1 -0
  81. package/fesm2022/forty-cdk-otp-input.mjs +527 -0
  82. package/fesm2022/forty-cdk-otp-input.mjs.map +1 -0
  83. package/fesm2022/forty-cdk-pagination.mjs +323 -0
  84. package/fesm2022/forty-cdk-pagination.mjs.map +1 -0
  85. package/fesm2022/forty-cdk-pane-resizer.mjs +297 -0
  86. package/fesm2022/forty-cdk-pane-resizer.mjs.map +1 -0
  87. package/fesm2022/forty-cdk-popover.mjs +698 -0
  88. package/fesm2022/forty-cdk-popover.mjs.map +1 -0
  89. package/fesm2022/forty-cdk-progress.mjs +226 -0
  90. package/fesm2022/forty-cdk-progress.mjs.map +1 -0
  91. package/fesm2022/forty-cdk-radio-group.mjs +378 -0
  92. package/fesm2022/forty-cdk-radio-group.mjs.map +1 -0
  93. package/fesm2022/forty-cdk-scroll-area.mjs +640 -0
  94. package/fesm2022/forty-cdk-scroll-area.mjs.map +1 -0
  95. package/fesm2022/forty-cdk-search.mjs +205 -0
  96. package/fesm2022/forty-cdk-search.mjs.map +1 -0
  97. package/fesm2022/forty-cdk-select.mjs +1661 -0
  98. package/fesm2022/forty-cdk-select.mjs.map +1 -0
  99. package/fesm2022/forty-cdk-separator.mjs +82 -0
  100. package/fesm2022/forty-cdk-separator.mjs.map +1 -0
  101. package/fesm2022/forty-cdk-signal-forms.mjs +97 -0
  102. package/fesm2022/forty-cdk-signal-forms.mjs.map +1 -0
  103. package/fesm2022/forty-cdk-slider.mjs +803 -0
  104. package/fesm2022/forty-cdk-slider.mjs.map +1 -0
  105. package/fesm2022/forty-cdk-stepper.mjs +886 -0
  106. package/fesm2022/forty-cdk-stepper.mjs.map +1 -0
  107. package/fesm2022/forty-cdk-switch.mjs +137 -0
  108. package/fesm2022/forty-cdk-switch.mjs.map +1 -0
  109. package/fesm2022/forty-cdk-table.mjs +1518 -0
  110. package/fesm2022/forty-cdk-table.mjs.map +1 -0
  111. package/fesm2022/forty-cdk-tabs.mjs +400 -0
  112. package/fesm2022/forty-cdk-tabs.mjs.map +1 -0
  113. package/fesm2022/forty-cdk-time-field.mjs +593 -0
  114. package/fesm2022/forty-cdk-time-field.mjs.map +1 -0
  115. package/fesm2022/forty-cdk-time-picker.mjs +1013 -0
  116. package/fesm2022/forty-cdk-time-picker.mjs.map +1 -0
  117. package/fesm2022/forty-cdk-toast.mjs +1153 -0
  118. package/fesm2022/forty-cdk-toast.mjs.map +1 -0
  119. package/fesm2022/forty-cdk-toggle.mjs +516 -0
  120. package/fesm2022/forty-cdk-toggle.mjs.map +1 -0
  121. package/fesm2022/forty-cdk-toolbar.mjs +374 -0
  122. package/fesm2022/forty-cdk-toolbar.mjs.map +1 -0
  123. package/fesm2022/forty-cdk-tooltip.mjs +672 -0
  124. package/fesm2022/forty-cdk-tooltip.mjs.map +1 -0
  125. package/fesm2022/forty-cdk-tree.mjs +2007 -0
  126. package/fesm2022/forty-cdk-tree.mjs.map +1 -0
  127. package/fesm2022/forty-cdk-virtualization.mjs +1 -1
  128. package/fesm2022/forty-cdk-virtualization.mjs.map +1 -1
  129. package/fesm2022/forty-cdk.mjs +0 -43310
  130. package/fesm2022/forty-cdk.mjs.map +1 -1
  131. package/field/README.md +97 -0
  132. package/fieldset/README.md +86 -0
  133. package/file-upload/README.md +73 -0
  134. package/hover-card/README.md +171 -0
  135. package/input/README.md +156 -0
  136. package/listbox/README.md +424 -0
  137. package/menu/README.md +181 -0
  138. package/menubar/README.md +140 -0
  139. package/meter/README.md +128 -0
  140. package/navigation-menu/README.md +253 -0
  141. package/number-input/README.md +171 -0
  142. package/otp-input/README.md +198 -0
  143. package/package.json +213 -1
  144. package/pagination/README.md +61 -0
  145. package/pane-resizer/README.md +136 -0
  146. package/popover/README.md +262 -0
  147. package/progress/README.md +115 -0
  148. package/radio-group/README.md +129 -0
  149. package/scroll-area/README.md +184 -0
  150. package/search/README.md +42 -0
  151. package/select/README.md +488 -0
  152. package/separator/README.md +84 -0
  153. package/signal-forms/README.md +72 -0
  154. package/slider/README.md +152 -0
  155. package/stepper/README.md +292 -0
  156. package/switch/README.md +116 -0
  157. package/table/README.md +769 -0
  158. package/tabs/README.md +130 -0
  159. package/time-field/README.md +157 -0
  160. package/time-picker/README.md +172 -0
  161. package/toast/README.md +398 -0
  162. package/toggle/README.md +224 -0
  163. package/toolbar/README.md +109 -0
  164. package/tooltip/README.md +274 -0
  165. package/tree/README.md +708 -0
  166. package/types/forty-cdk-accordion.d.ts +242 -0
  167. package/types/forty-cdk-aspect-ratio.d.ts +59 -0
  168. package/types/forty-cdk-avatar.d.ts +133 -0
  169. package/types/forty-cdk-breadcrumbs.d.ts +92 -0
  170. package/types/forty-cdk-breakpoints.d.ts +141 -0
  171. package/types/forty-cdk-button.d.ts +80 -0
  172. package/types/forty-cdk-calendar.d.ts +914 -0
  173. package/types/forty-cdk-carousel.d.ts +530 -0
  174. package/types/forty-cdk-checkbox.d.ts +141 -0
  175. package/types/forty-cdk-combobox.d.ts +1259 -0
  176. package/types/forty-cdk-context-menu.d.ts +313 -0
  177. package/types/forty-cdk-core.d.ts +5774 -0
  178. package/types/forty-cdk-date-field.d.ts +307 -0
  179. package/types/forty-cdk-date-picker.d.ts +622 -0
  180. package/types/forty-cdk-dialog.d.ts +546 -0
  181. package/types/forty-cdk-disclosure.d.ts +127 -0
  182. package/types/forty-cdk-drag-drop.d.ts +456 -0
  183. package/types/forty-cdk-drawer.d.ts +871 -0
  184. package/types/forty-cdk-dropdown-menu.d.ts +242 -0
  185. package/types/forty-cdk-field.d.ts +236 -0
  186. package/types/forty-cdk-fieldset.d.ts +119 -0
  187. package/types/forty-cdk-file-upload.d.ts +124 -0
  188. package/types/forty-cdk-hover-card.d.ts +320 -0
  189. package/types/forty-cdk-input.d.ts +169 -0
  190. package/types/forty-cdk-internationalized-date.d.ts +1 -1
  191. package/types/forty-cdk-listbox.d.ts +513 -0
  192. package/types/forty-cdk-menu.d.ts +629 -0
  193. package/types/forty-cdk-menubar.d.ts +451 -0
  194. package/types/forty-cdk-meter.d.ts +122 -0
  195. package/types/forty-cdk-navigation-menu.d.ts +514 -0
  196. package/types/forty-cdk-number-input.d.ts +319 -0
  197. package/types/forty-cdk-otp-input.d.ts +248 -0
  198. package/types/forty-cdk-pagination.d.ts +214 -0
  199. package/types/forty-cdk-pane-resizer.d.ts +145 -0
  200. package/types/forty-cdk-popover.d.ts +509 -0
  201. package/types/forty-cdk-progress.d.ts +143 -0
  202. package/types/forty-cdk-radio-group.d.ts +222 -0
  203. package/types/forty-cdk-scroll-area.d.ts +258 -0
  204. package/types/forty-cdk-search.d.ts +142 -0
  205. package/types/forty-cdk-select.d.ts +899 -0
  206. package/types/forty-cdk-separator.d.ts +59 -0
  207. package/types/forty-cdk-signal-forms.d.ts +58 -0
  208. package/types/forty-cdk-slider.d.ts +379 -0
  209. package/types/forty-cdk-stepper.d.ts +650 -0
  210. package/types/forty-cdk-switch.d.ts +87 -0
  211. package/types/forty-cdk-table.d.ts +723 -0
  212. package/types/forty-cdk-tabs.d.ts +235 -0
  213. package/types/forty-cdk-time-field.d.ts +307 -0
  214. package/types/forty-cdk-time-picker.d.ts +578 -0
  215. package/types/forty-cdk-toast.d.ts +598 -0
  216. package/types/forty-cdk-toggle.d.ts +310 -0
  217. package/types/forty-cdk-toolbar.d.ts +217 -0
  218. package/types/forty-cdk-tooltip.d.ts +436 -0
  219. package/types/forty-cdk-tree.d.ts +688 -0
  220. package/types/forty-cdk.d.ts +1 -19743
package/tabs/README.md ADDED
@@ -0,0 +1,130 @@
1
+ # Tabs
2
+
3
+ Headless implementation of the [WAI-ARIA Tabs pattern](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/) with selectable activation mode (automatic vs manual) and roving tabindex.
4
+
5
+ ## Pieces
6
+
7
+ | Class | Selector | Role |
8
+ | ---------------- | ------------------ | --------------------------------------------------------------------------------------------- |
9
+ | `ForTabs` | `[forTabs]` | Root. Owns `value` (selected tab), activation mode, orientation. Provides the shared context. |
10
+ | `ForTabsList` | `[forTabsList]` | `role="tablist"` container that wraps the tab buttons. |
11
+ | `ForTabsTrigger` | `[forTabsTrigger]` | One tab button. Apply on a `<button type="button">`. |
12
+ | `ForTabsContent` | `[forTabsContent]` | Panel revealed by the tab with the matching `value`. |
13
+
14
+ ## Inputs / models
15
+
16
+ ### `ForTabs`
17
+
18
+ | API | Type | Description |
19
+ | ---------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
20
+ | `value` | `model<string \| null>` | Two-way bindable. The selected tab's value, or `null` when nothing is selected. `null` is the unset state — distinct from a tab whose `value` is `''`. |
21
+ | `activationMode` | `input<'automatic' \| 'manual'>` | Default `'automatic'` (selection follows arrow focus). Use `'manual'` when panel content is expensive — user must press Space / Enter. |
22
+ | `orientation` | `input<'horizontal' \| 'vertical'>` | Default `'horizontal'`. Drives keyboard navigation and `aria-orientation`. |
23
+ | `dir` | `input<'ltr' \| 'rtl'>` | Default `'ltr'`. Swaps ArrowLeft / ArrowRight. |
24
+ | `disabled` | `input<boolean>` | When true, blocks all selection and keyboard nav. |
25
+ | `loop` | `input<boolean>` | When true (default), arrow nav wraps around past the first / last enabled trigger. Set to `false` for a non-wrapping tablist. |
26
+
27
+ ### `ForTabsTrigger`
28
+
29
+ | API | Type | Description |
30
+ | ---------- | ------------------------ | --------------------------------------------------------------------- |
31
+ | `value` | `input.required<string>` | The tab's identifier. Must match the `value` of its `ForTabsContent`. |
32
+ | `disabled` | `input<boolean>` | Disables this trigger; arrow nav skips it. |
33
+
34
+ Reflects on its host: `id`, `aria-selected`, `aria-controls` (looked up from the matching content), `aria-disabled`, `tabindex`, `data-state="active" \| "inactive"`, `data-disabled`. A disabled trigger keeps `aria-disabled="true"` + `data-disabled=""` (no native `disabled`, per APG) — announced but non-activatable, with arrow nav skipping it.
35
+
36
+ ### `ForTabsContent`
37
+
38
+ | API | Type | Description |
39
+ | -------------------- | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
40
+ | `value` | `input.required<string>` | Pairs the panel with the trigger of the same value. |
41
+ | `interactiveContent` | `input<boolean \| null>` | Overrides the automatic focusable-content detection that drives `tabindex`. Default `null` (auto-detect). `true` forces no tab stop (the panel always holds its own focusable content); `false` forces a tab stop regardless. |
42
+
43
+ Reflects: `id`, `role="tabpanel"`, `aria-labelledby` (the matching trigger's id), `tabindex="0"` **only when the panel has no focusable descendants** (APG), `aria-hidden` (when inactive), `inert` (when inactive), `data-state="active" \| "inactive"`.
44
+
45
+ The directive does **not** apply `[hidden]`. Two patterns work:
46
+
47
+ - **Leave all panels mounted** (idiomatic) — preserves scroll/input state across activations. While inactive, the directive sets `aria-hidden="true"` and `inert` so each non-selected panel is out of the accessibility tree and focus order. Hide the inactive ones visually with CSS keyed on `[data-state="inactive"]` (e.g. `display: none`).
48
+ - **Mount/unmount with `@if (active() === 'tab')`** — the panel is absent while inactive; useful for heavy panels or when you want `animate.enter` / `animate.leave`.
49
+
50
+ ## Example
51
+
52
+ ```ts
53
+ import { Component, signal } from '@angular/core';
54
+ import { ForTabs, ForTabsContent, ForTabsList, ForTabsTrigger } from 'forty-cdk/tabs';
55
+
56
+ @Component({
57
+ selector: 'demo-settings',
58
+ imports: [ForTabs, ForTabsList, ForTabsTrigger, ForTabsContent],
59
+ template: `
60
+ <div forTabs [(value)]="active">
61
+ <div forTabsList aria-label="Settings sections">
62
+ <button type="button" forTabsTrigger class="tabs-trigger" value="profile">Profile</button>
63
+ <button type="button" forTabsTrigger class="tabs-trigger" value="security">Security</button>
64
+ <button type="button" forTabsTrigger class="tabs-trigger" value="billing">Billing</button>
65
+ </div>
66
+ <div forTabsContent class="tabs-content" value="profile">…profile…</div>
67
+ <div forTabsContent class="tabs-content" value="security">…security…</div>
68
+ <div forTabsContent class="tabs-content" value="billing">…billing…</div>
69
+ </div>
70
+ `,
71
+ })
72
+ export class DemoSettings {
73
+ readonly active = signal('profile');
74
+ }
75
+ ```
76
+
77
+ ## Manual activation
78
+
79
+ ```html
80
+ <div forTabs activationMode="manual" [(value)]="active">
81
+ <!-- Arrow keys move focus only. Space / Enter activates. -->
82
+ </div>
83
+ ```
84
+
85
+ ## Styling
86
+
87
+ forty-cdk ships no styles. Add your own class to each piece — the for\* selectors are the behavior API, not a styling contract (see [Styling forty-cdk](../../../../../docs/styling.md)). Key your CSS off the reflected data-\* attributes below.
88
+
89
+ ### Data attributes
90
+
91
+ | Piece | Attribute | Values |
92
+ | ------------------ | ------------------ | -------------------------- |
93
+ | `[forTabs]` | `data-orientation` | `horizontal` \| `vertical` |
94
+ | `[forTabs]` | `data-disabled` | present \| absent |
95
+ | `[forTabsList]` | `data-orientation` | `horizontal` \| `vertical` |
96
+ | `[forTabsTrigger]` | `data-state` | `active` \| `inactive` |
97
+ | `[forTabsTrigger]` | `data-disabled` | present \| absent |
98
+ | `[forTabsTrigger]` | `data-orientation` | `horizontal` \| `vertical` |
99
+ | `[forTabsContent]` | `data-state` | `active` \| `inactive` |
100
+ | `[forTabsContent]` | `data-orientation` | `horizontal` \| `vertical` |
101
+
102
+ ```css
103
+ .tabs-trigger[data-state='active'] {
104
+ border-bottom: 2px solid currentColor;
105
+ }
106
+
107
+ .tabs-trigger[data-disabled] {
108
+ opacity: 0.5;
109
+ cursor: not-allowed;
110
+ }
111
+
112
+ .tabs-content[data-state='inactive'] {
113
+ display: none;
114
+ }
115
+ ```
116
+
117
+ ## Keyboard
118
+
119
+ - **Tab** moves focus into / out of the tablist; lands on the user-focused trigger (or the selected one, if none focused yet).
120
+ - **ArrowRight / ArrowLeft** in horizontal, **ArrowDown / ArrowUp** in vertical: move focus between triggers, wrap-around. RTL swaps Left/Right.
121
+ - **Home / End** jump to first / last enabled trigger.
122
+ - **Space / Enter** activate the focused trigger (no-op in automatic mode since arrow nav already activated it).
123
+ - Disabled triggers are skipped.
124
+
125
+ ## Accessibility notes
126
+
127
+ - **Label the tablist** via `aria-label` on `ForTabsList`, or `aria-labelledby` pointing to a heading.
128
+ - **Choose `activationMode='automatic'`** when panels render quickly; `'manual'` when activation has noticeable cost (network, heavy computation).
129
+ - **Panel `tabindex`** follows APG: a panel with **no** focusable descendants is itself a tab stop (`tabindex="0"`) so screen-reader users can focus and read it, while a panel that already contains focusable content (a form, links, buttons) is **not** a tab stop — the directive detects this automatically and reacts to subtree changes. Use `[interactiveContent]` to override the detection in either direction.
130
+ - **`aria-controls` and `aria-labelledby`** are wired automatically when triggers and contents share the same `value`. `aria-controls` is emitted only on the selected trigger — mirroring the overlay triggers' open-only gating — so the reference never dangles at an unmounted panel under the `@if (selected())` mount pattern.
@@ -0,0 +1,157 @@
1
+ # TimeField
2
+
3
+ Headless, segmented, spin-editable **time-of-day** input — the time counterpart to [DateField](../date-field/README.md). There is **no single WAI-ARIA APG pattern** for a time field; it is a composition of [Spinbuttons](https://www.w3.org/WAI/ARIA/apg/patterns/spinbutton/) inside a labelled `role="group"`. Each hour / minute / second / AM·PM part is an independent `role="spinbutton"` segment, so entry is unambiguous and locale-correct. Segment **order**, the separators between them, and whether an AM/PM segment is shown follow the runtime locale and the resolved hour cycle.
4
+
5
+ `ForTimeField` implements `FormValueControl<D | null>` from `@angular/forms/signals`, so it auto-wires with `[formField]` and auto-associates inside a `[forField]` (label / description / error) with no extra markup. The value stays `null` until every visible segment is filled.
6
+
7
+ ## Date adapter — pick a time-capable one (required)
8
+
9
+ All time math goes through the same pluggable `DateAdapter<D>` as `ForCalendar`, so the library hard-depends on **no** date library. The time field needs the adapter's optional time accessors, so provide a **time-capable** adapter:
10
+
11
+ | Provider | Date-time type `D` | Dependency |
12
+ | ------------------------------------------- | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
13
+ | `provideInternationalizedDateTimeAdapter()` | `CalendarDateTime` (`@internationalized/date`) | **Recommended.** From `forty-cdk/internationalized-date`; needs `@internationalized/date` (optional peer) |
14
+ | `provideNativeDateAdapter()` | `Date` | None (zero-dependency fallback) |
15
+
16
+ > The day-only `provideInternationalizedDateAdapter()` (`CalendarDate`) cannot carry a time — `ForTimeField` throws a descriptive error if it is the active adapter.
17
+
18
+ ```ts
19
+ import { bootstrapApplication } from '@angular/platform-browser';
20
+ import { provideInternationalizedDateTimeAdapter } from 'forty-cdk/internationalized-date';
21
+
22
+ bootstrapApplication(App, {
23
+ providers: [provideInternationalizedDateTimeAdapter()],
24
+ });
25
+ ```
26
+
27
+ When no value is bound yet, a composed value is anchored on the adapter's `today()` and carries the entered time. Bind an existing date-time as `value` to edit its time in place (the calendar day is preserved).
28
+
29
+ ## Pieces
30
+
31
+ | Class | Selector | Role |
32
+ | --------------------- | ----------------------- | -------------------------------------------------------------------------------------------------- |
33
+ | `ForTimeField` | `[forTimeField]` | Root (`role="group"`). Owns the entered parts, composes the value, and exposes `segments()`. |
34
+ | `ForTimeFieldSegment` | `[forTimeFieldSegment]` | One editable part (`role="spinbutton"`). Roving tab stop, ARIA value reflection, keyboard editing. |
35
+ | `ForTimeFieldLiteral` | `[forTimeFieldLiteral]` | A decorative separator (`:`, a space). `aria-hidden`, out of the tab order. |
36
+
37
+ ## Inputs / models — `ForTimeField`
38
+
39
+ | API | Type | Description |
40
+ | ------------- | ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
41
+ | `value` | `model<D \| null>` | Two-way bindable entered time, or `null` while any visible segment is empty. The `FormValueControl` backing. Default `null`. |
42
+ | `minTime` | `input<D \| null>` | Earliest time-of-day (inclusive). A composed value earlier in the day is clamped up. Named `minTime` — see note. Default `null`. |
43
+ | `maxTime` | `input<D \| null>` | Latest time-of-day (inclusive). A composed value later in the day is clamped down. Default `null`. |
44
+ | `hourCycle` | `input<12 \| 24 \| null>` | 12- or 24-hour cycle. Default `null` → derived from the locale. 12-hour adds the AM/PM segment. |
45
+ | `granularity` | `input<'hour' \| 'minute' \| 'second'>` | Smallest editable unit. Default `'minute'`. |
46
+ | `locale` | `input<string \| null>` | BCP 47 locale driving segment order, separators, and AM/PM names. Default `null` → runtime locale. |
47
+ | `placeholder` | `input<Partial<Record<TimeSegmentType, string>>>` | Per-segment placeholder while empty. Unspecified parts fall back to `hh` / `mm` / `ss` / `--`. Default `{}`. |
48
+ | `ariaLabel` | `input<string \| null>` | Accessible name for the group. Emits no `aria-label` while `null`. Default `null`. |
49
+ | `dir` | `input<'ltr' \| 'rtl' \| null>` | Writing direction. Default `null` resolves the ambient direction; mirrors ArrowLeft / ArrowRight segment navigation. |
50
+
51
+ Plus the shared `FormUiControl` members from `@angular/forms/signals`: `disabled`, `readonly`, `required`, `invalid`, `name`, `errors`, `touched` (bound automatically by `[formField]`).
52
+
53
+ > **Why `minTime` / `maxTime`, not `min` / `max`?** `FormUiControl.min` / `max` are reserved members typed `number | undefined` for numeric validators bound by `[formField]`. A date-time-typed `min` / `max` would break the `FormValueControl` contract, so the time bounds use distinct names. Only the time-of-day component of the bounds is considered.
54
+
55
+ ## Usage
56
+
57
+ ```ts
58
+ import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
59
+ import { CalendarDateTime } from '@internationalized/date';
60
+ import { ForTimeField, ForTimeFieldLiteral, ForTimeFieldSegment } from 'forty-cdk/time-field';
61
+
62
+ @Component({
63
+ selector: 'app-appt-time',
64
+ changeDetection: ChangeDetectionStrategy.OnPush,
65
+ imports: [ForTimeField, ForTimeFieldSegment, ForTimeFieldLiteral],
66
+ template: `
67
+ <div forTimeField [(value)]="time" [ariaLabel]="'Appointment time'" #field="forTimeField">
68
+ @for (seg of field.segments(); track seg.id) {
69
+ @if (seg.isLiteral) {
70
+ <span forTimeFieldLiteral>{{ seg.text }}</span>
71
+ } @else {
72
+ <span forTimeFieldSegment class="time-field-segment" [segment]="seg.type!">{{
73
+ seg.text
74
+ }}</span>
75
+ }
76
+ }
77
+ </div>
78
+ `,
79
+ })
80
+ export class ApptTime {
81
+ readonly time = signal<CalendarDateTime | null>(null);
82
+ }
83
+ ```
84
+
85
+ The library is styleless: style the boolean `data-*` hooks on the segments yourself — `[data-highlighted]` (the focused/roving segment), `[data-placeholder]` (empty), `[data-disabled]`, `[data-readonly]` — and `[data-empty]` / `[data-disabled]` / `[data-readonly]` on the root group.
86
+
87
+ ## Keyboard (per segment)
88
+
89
+ Horizontal arrows mirror under `dir="rtl"`.
90
+
91
+ | Key | Behavior |
92
+ | -------------------------- | -------------------------------------------------------------------------------------------------- |
93
+ | **0–9** | Type the value; auto-advances to the next segment when full. |
94
+ | **a / p** | On the AM/PM segment, set the period of the entered hour. |
95
+ | **ArrowUp / ArrowDown** | Step the value. Hour / minute / second wrap; the AM/PM segment toggles. Empty seeds from midnight. |
96
+ | **ArrowLeft / ArrowRight** | Move to the previous / next segment (no wrap). |
97
+ | **Home / End** | Jump to the segment minimum / maximum (the AM/PM segment → AM / PM). |
98
+ | **Backspace / Delete** | Clear a numeric segment (the value becomes `null` until refilled). |
99
+
100
+ The hour, minute, and second clamp to their valid ranges (hour to the cycle, minute / second to 0–59), and a composed value is clamped into `[minTime, maxTime]` by time-of-day. The AM/PM period is derived from the entered hour; clearing it is a no-op (clear or step the hour instead).
101
+
102
+ ## Scope defaults
103
+
104
+ ```ts
105
+ import { provideForTimeFieldDefaults } from 'forty-cdk/time-field';
106
+
107
+ // app config or a component's providers — localize segment labels and the
108
+ // empty-segment announcement for every nested [forTimeField].
109
+ providers: [
110
+ provideForTimeFieldDefaults({
111
+ emptySegmentText: 'Vacío',
112
+ segmentLabels: { hour: 'hora', minute: 'minuto', second: 'segundo', dayPeriod: 'AM/PM' },
113
+ }),
114
+ ];
115
+ ```
116
+
117
+ `segmentLabels` supplies each segment's default `aria-label`, keyed by part type. Unset keys keep the library default (the part name, and `'AM/PM'` for the `dayPeriod` segment), so overriding a single key never wipes the rest. A segment's own `[ariaLabel]` still wins over the scope default.
118
+
119
+ ## Styling
120
+
121
+ forty-cdk ships no styles. Add your own class to each piece — the `for*` selectors are the behavior API, not a styling contract (see [Styling forty-cdk](../../../../../docs/styling.md)). Key your CSS off the reflected `data-*` attributes below.
122
+
123
+ ### Data attributes
124
+
125
+ | Piece | Attribute | Values |
126
+ | ----------------------- | ------------------ | ----------------- |
127
+ | `[forTimeField]` | `data-disabled` | present \| absent |
128
+ | `[forTimeField]` | `data-readonly` | present \| absent |
129
+ | `[forTimeField]` | `data-empty` | present \| absent |
130
+ | `[forTimeFieldSegment]` | `data-highlighted` | present \| absent |
131
+ | `[forTimeFieldSegment]` | `data-placeholder` | present \| absent |
132
+ | `[forTimeFieldSegment]` | `data-disabled` | present \| absent |
133
+ | `[forTimeFieldSegment]` | `data-readonly` | present \| absent |
134
+
135
+ `[forTimeFieldLiteral]` carries no `data-*` hooks — it is `aria-hidden` and purely decorative; style it directly via your own class.
136
+
137
+ ```css
138
+ .time-field-segment[data-placeholder] {
139
+ color: gray;
140
+ }
141
+
142
+ .time-field-segment[data-highlighted] {
143
+ background: highlight;
144
+ }
145
+ ```
146
+
147
+ ## Accessibility notes
148
+
149
+ - **`role="group"`** on the root carries the field's accessible name (`ariaLabel`, or point native `aria-labelledby` at a visible label).
150
+ - **`role="spinbutton"`** per segment, with `aria-valuemin` / `aria-valuemax` / `aria-valuenow` reflected; the AM/PM segment also exposes a localized `aria-valuetext` ("AM" / "PM"), so screen readers read the period rather than `0` / `1`.
151
+ - **Roving tabindex**: exactly one segment is tabbable, so `Tab` enters and leaves the whole field in one stop; arrows move between segments.
152
+ - **Literals are `aria-hidden`** and never focusable — assistive tech reads only the spinbutton segments.
153
+ - **Boolean `data-*`** on each segment — `data-highlighted` (focused/roving), `data-placeholder` (empty), `data-disabled`, `data-readonly` — present when true, absent when false.
154
+
155
+ ## Wrapping in a design system
156
+
157
+ Both supported wrapper patterns — `hostDirectives` with the exported `FOR_TIME_FIELD_HOST_DIRECTIVE_INPUTS` / `FOR_TIME_FIELD_HOST_DIRECTIVE_OUTPUTS` name tuples, and subclassing — are documented in [Wrapping form primitives](../../../../../docs/wrapping-form-primitives.md).
@@ -0,0 +1,172 @@
1
+ # ForTimePicker
2
+
3
+ Headless, styleless slot-based time picker. Implements the
4
+ [WAI-ARIA Listbox pattern](https://www.w3.org/WAI/ARIA/apg/patterns/listbox/) — a
5
+ combobox trigger opens a floating listbox of generated time slots. Value is typed as
6
+ your adapter's date-time type `D`.
7
+
8
+ Requires a time-capable adapter:
9
+ [`provideNativeDateAdapter()`](../calendar/native-date-adapter.ts) or the
10
+ `@internationalized/date` adapter from `forty-cdk/internationalized-date`.
11
+
12
+ ## Usage
13
+
14
+ ```html
15
+ <div
16
+ forTimePicker
17
+ [(value)]="value"
18
+ [(open)]="open"
19
+ [step]="30"
20
+ [hourCycle]="24"
21
+ #picker="forTimePicker"
22
+ >
23
+ <button forTimePickerTrigger>
24
+ <span forTimePickerValue placeholder="Pick a time"></span>
25
+ </button>
26
+
27
+ @if (open()) {
28
+ <div forTimePickerContent>
29
+ @for (slot of picker.slots(); track slot.id) {
30
+ <div forTimePickerOption [value]="slot.value" [disabled]="slot.disabled">{{ slot.label }}</div>
31
+ }
32
+ </div>
33
+ }
34
+ </div>
35
+ ```
36
+
37
+ ## Pieces
38
+
39
+ | Piece | Selector | Role |
40
+ | ---------------------- | ------------------------ | -------------------------------------------------------------------------------------------------------------------------- |
41
+ | `ForTimePicker` | `[forTimePicker]` | Root — state, slots, context |
42
+ | `ForTimePickerTrigger` | `[forTimePickerTrigger]` | Combobox button |
43
+ | `ForTimePickerValue` | `[forTimePickerValue]` | Display element |
44
+ | `ForTimePickerContent` | `[forTimePickerContent]` | Portaled listbox |
45
+ | `ForTimePickerOption` | `[forTimePickerOption]` | Option (non-button `<div>`) |
46
+ | `ForTimePickerAnchor` | `[forTimePickerAnchor]` | Optional positioning anchor — wrap a decorated field box so the listbox aligns to the visible field instead of the trigger |
47
+
48
+ ## Inputs (root)
49
+
50
+ | Input | Type | Default | Description |
51
+ | --------------- | -------------------------------- | ---------- | -------------------------------- |
52
+ | `value` | `D \| null` | `null` | Selected time (two-way) |
53
+ | `open` | `boolean` | `false` | Open state (two-way) |
54
+ | `step` | `number` | `30` | Slot interval in minutes |
55
+ | `granularity` | `'hour' \| 'minute' \| 'second'` | `'minute'` | Selection precision |
56
+ | `hourCycle` | `12 \| 24 \| null` | `null` | Hour cycle for labels |
57
+ | `locale` | `string \| null` | `null` | BCP 47 locale for labels |
58
+ | `minTime` | `D \| null` | `null` | Earliest selectable time |
59
+ | `maxTime` | `D \| null` | `null` | Latest selectable time |
60
+ | `closeOnSelect` | `boolean` | `true` | Close on slot selection |
61
+ | `modal` | `boolean` | `false` | Modal (focus-trapped) mode |
62
+ | `dismissible` | `boolean` | `true` | Escape / outside close |
63
+ | `returnFocus` | `boolean` | `true` | Return focus to trigger on close |
64
+ | `placeholder` | `string` | `''` | Value display placeholder |
65
+ | `formatOptions` | `Intl.DateTimeFormatOptions` | `{}` | Override slot label format |
66
+
67
+ Inherits all `FormUiControl` inputs (`disabled`, `readonly`, `required`, `invalid`,
68
+ `errors`, `touched`, `name`, `pending`) for `[formField]` auto-wiring.
69
+
70
+ ## Signal Forms
71
+
72
+ ```html
73
+ <div forTimePicker [formField]="profile.meetingTime" [(open)]="open" #picker="forTimePicker">
74
+ <button forTimePickerTrigger>
75
+ <span forTimePickerValue placeholder="Pick a time"></span>
76
+ </button>
77
+ @if (open()) {
78
+ <div forTimePickerContent>
79
+ @for (slot of picker.slots(); track slot.id) {
80
+ <div forTimePickerOption [value]="slot.value" [disabled]="slot.disabled">{{ slot.label }}</div>
81
+ }
82
+ </div>
83
+ }
84
+ </div>
85
+ ```
86
+
87
+ ## Anchoring to a field box
88
+
89
+ By default the listbox is positioned against `[forTimePickerTrigger]`. When the trigger lives inside a decorated field box — padding, a prefix icon, a clear / chevron button — anchoring to the inner button makes the panel offset from the visible field's edge. Wrap the field box in `[forTimePickerAnchor]` so floating-ui positions (and sizes, via `--for-anchor-width`) the listbox against the box instead:
90
+
91
+ ```html
92
+ <div forTimePicker #picker="forTimePicker" [(value)]="value" [(open)]="open">
93
+ <div forTimePickerAnchor class="field-box">
94
+ <icon name="clock" />
95
+ <button forTimePickerTrigger>
96
+ <span forTimePickerValue placeholder="Pick a time"></span>
97
+ </button>
98
+ <button class="clear" (click)="value.set(null)">×</button>
99
+ </div>
100
+ @if (open()) {
101
+ <div forTimePickerContent style="width: var(--for-anchor-width)">
102
+ @for (slot of picker.slots(); track slot.id) {
103
+ <div forTimePickerOption [value]="slot.value" [disabled]="slot.disabled">{{ slot.label }}</div>
104
+ }
105
+ </div>
106
+ }
107
+ </div>
108
+ ```
109
+
110
+ `[forTimePickerAnchor]` changes **only** positioning. The trigger keeps `aria-haspopup` / `aria-expanded` / `aria-controls`, the click toggle, focus return on close, and its exemption from outside-pointer dismissal. Without an anchor the listbox falls back to the trigger, so existing markup is unaffected. At most one `[forTimePickerAnchor]` per `[forTimePicker]` — a second one throws `[forty-cdk/time-picker]`.
111
+
112
+ ## Date-time composition
113
+
114
+ Place `[forTimePicker]` inside `[forDatePickerContent]` alongside a `[forCalendar]`. The
115
+ `FOR_TIME_VALUE_SOURCE` token is provided by `[forTimePicker]`, and `[forDatePicker]`
116
+ resolves it automatically via `contentChild` to graft time changes onto the committed date.
117
+
118
+ ```html
119
+ <div forDatePicker granularity="minute" [(value)]="value" [(open)]="open" #dp="forDatePicker">
120
+ <button forDatePickerTrigger>...</button>
121
+ @if (open()) {
122
+ <div forDatePickerContent>
123
+ <div forCalendar [value]="dp.value()">...</div>
124
+ <div forTimePicker [value]="dp.value()" [step]="60" #tp="forTimePicker">
125
+ <button forTimePickerTrigger>...</button>
126
+ @if (tp.open()) {
127
+ <div forTimePickerContent>
128
+ @for (slot of tp.slots(); track slot.id) {
129
+ <div forTimePickerOption [value]="slot.value" [disabled]="slot.disabled">
130
+ {{ slot.label }}
131
+ </div>
132
+ }
133
+ </div>
134
+ }
135
+ </div>
136
+ </div>
137
+ }
138
+ </div>
139
+ ```
140
+
141
+ ## Wrapping with `hostDirectives`
142
+
143
+ ```typescript
144
+ import {
145
+ FOR_TIME_PICKER_HOST_DIRECTIVE_INPUTS,
146
+ FOR_TIME_PICKER_HOST_DIRECTIVE_OUTPUTS,
147
+ ForTimePicker,
148
+ } from 'forty-cdk/time-picker';
149
+
150
+ @Component({
151
+ selector: '[myTimePicker]',
152
+ hostDirectives: [
153
+ {
154
+ directive: ForTimePicker,
155
+ inputs: [...FOR_TIME_PICKER_HOST_DIRECTIVE_INPUTS],
156
+ outputs: [...FOR_TIME_PICKER_HOST_DIRECTIVE_OUTPUTS],
157
+ },
158
+ ],
159
+ })
160
+ export class MyTimePicker {}
161
+ ```
162
+
163
+ ## Keyboard interaction
164
+
165
+ | Key | Behavior |
166
+ | ----------------------- | ----------------------------------------- |
167
+ | `Enter` / `Space` | Select the focused slot |
168
+ | `ArrowDown` / `ArrowUp` | Move focus between slots |
169
+ | `Home` | Focus the first enabled slot |
170
+ | `End` | Focus the last enabled slot |
171
+ | `Tab` | Commit the focused slot and advance focus |
172
+ | `Escape` | Close without committing |