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
@@ -0,0 +1,140 @@
1
+ # Menubar
2
+
3
+ Headless implementation of the [WAI-ARIA Menubar pattern](https://www.w3.org/WAI/ARIA/apg/patterns/menubar/): a horizontal (or vertical) bar of triggers, each opening a dropdown menu, with cross-menu ArrowLeft / ArrowRight navigation, hover-after-first-open, and roving tabindex among triggers.
4
+
5
+ ## Usage
6
+
7
+ ```ts
8
+ import { Component, signal } from '@angular/core';
9
+ import { ForMenuContent, ForMenuItem, ForMenuSeparator } from 'forty-cdk/menu';
10
+ import { ForMenubar, ForMenubarTrigger } from 'forty-cdk/menubar';
11
+
12
+ @Component({
13
+ selector: 'demo-menubar',
14
+ imports: [ForMenubar, ForMenubarTrigger, ForMenuContent, ForMenuItem, ForMenuSeparator],
15
+ template: `
16
+ <div forMenubar [(value)]="open" aria-label="Main">
17
+ <button forMenubarTrigger value="file">File</button>
18
+ @if (open() === 'file') {
19
+ <div forMenuContent animate.leave="fade-out">
20
+ <button forMenuItem (activate)="newDoc()">New</button>
21
+ <button forMenuItem (activate)="openDoc()">Open…</button>
22
+ <hr forMenuSeparator />
23
+ <button forMenuItem (activate)="quit()">Quit</button>
24
+ </div>
25
+ }
26
+
27
+ <button forMenubarTrigger value="edit">Edit</button>
28
+ @if (open() === 'edit') {
29
+ <div forMenuContent>
30
+ <button forMenuItem (activate)="undo()">Undo</button>
31
+ <button forMenuItem (activate)="redo()">Redo</button>
32
+ </div>
33
+ }
34
+
35
+ <button forMenubarTrigger value="view" disabled>View</button>
36
+ </div>
37
+ `,
38
+ })
39
+ export class DemoMenubar {
40
+ readonly open = signal<string>('');
41
+ newDoc() {}
42
+ openDoc() {}
43
+ quit() {}
44
+ undo() {}
45
+ redo() {}
46
+ }
47
+ ```
48
+
49
+ `@if (open() === '<value>')` controls each menu's mount, so Angular's `animate.enter` / `animate.leave` fire on the natural mount cycle. `[(value)]` is two-way bindable; the menubar flips it on trigger interaction, item activation, Escape, outside dismissal, and cross-menu navigation.
50
+
51
+ ## Pieces
52
+
53
+ | Class | Selector | Role |
54
+ | ------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
55
+ | `ForMenubar` | `[forMenubar]` | Root. Owns `value` (the open trigger), orientation, dir, loop, disabled. Provides the `ForMenubarContext` and a multiplexed `ForMenuContext` to `[forMenuContent]`. |
56
+ | `ForMenubarTrigger` | `[forMenubarTrigger]` | A trigger button. `role="menuitem"` with `aria-haspopup="menu"` / `aria-expanded` / `aria-controls`. Participates in roving tabindex and trigger-row keyboard. |
57
+
58
+ The menu surface, items, separators, groups, and submenus come from the [`menu/`](../menu/README.md) folder — same primitives as `[forDropdownMenu]` and `[forContextMenu]`. The bar simply pumps a different `ForMenuContext` whose anchor / side / ids reflect the active trigger.
59
+
60
+ ## Inputs (`ForMenubar`)
61
+
62
+ | API | Default | Description |
63
+ | ------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
64
+ | `value` | `''` | Two-way bindable. The open trigger's `value`, or `''` when none. |
65
+ | `orientation` | `'horizontal'` | `'horizontal' \| 'vertical'`. Drives the trigger-row arrow keys (Left/Right horizontal, Up/Down vertical). |
66
+ | `dir` | `'ltr'` | Writing direction. RTL inverts ArrowLeft / ArrowRight on the trigger row and inside the open menu. |
67
+ | `loop` | `true` | When `true`, trigger-row navigation and cross-menu nav wrap at the ends. |
68
+ | `disabled` | `false` | When `true`, every trigger interaction is a no-op. |
69
+ | `dismissible` | `true` | When `false`, the open menu ignores Escape, outside interaction, and pointer-leave — it stays pinned open until `value` is flipped (consumer write, trigger / item interaction, or cross-menu nav). |
70
+ | `closeDelay` | `150` | ms before the open menu closes after the pointer leaves the bar (and any open menu). Defaults from `provideForMenubarDefaults`. |
71
+ | `ariaLabel` | `null` | Accessible name for the menubar (`<div forMenubar aria-label="Main">` works too). |
72
+
73
+ ## Inputs (`ForMenubarTrigger`)
74
+
75
+ | API | Default | Description |
76
+ | ----------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
77
+ | `value` | required | Identifier for the trigger. The menubar's `value` model holds this when the menu is open. |
78
+ | `disabled` | `false` | Per-trigger disabled, in addition to the menubar's `disabled`. |
79
+ | `side` / `align` / `sideOffset` / `alignOffset` / `avoidCollisions` / `collisionPadding` / `arrowPadding` / `sticky` / `hideWhenDetached` | (floating-ui defaults) | Forwarded to the multiplexed `[forMenuContent]` when this trigger's menu is the one open. Same surface as `[forDropdownMenu]`. |
80
+ | `ariaLabel` | `null` | Manual `aria-label` on `[forMenuContent]` if the trigger isn't a meaningful name. |
81
+
82
+ ## Trigger keyboard
83
+
84
+ | Key | Behavior |
85
+ | --------------------------- | ------------------------------------------------------------------------------------------- |
86
+ | `Click` / `Enter` / `Space` | Toggle this trigger's menu. On open, focus moves to the first enabled item. |
87
+ | `ArrowDown` | Open and focus the first enabled item. |
88
+ | `ArrowUp` | Open and focus the last enabled item. |
89
+ | `ArrowLeft` / `ArrowRight` | Move focus to the previous / next enabled trigger. RTL inverts. |
90
+ | `Home` / `End` | Focus the first / last enabled trigger. |
91
+ | `Typeahead` | Printable keys focus the first sibling trigger whose label starts with the buffered string. |
92
+
93
+ ## In-menu keyboard
94
+
95
+ Inside an open menu, the standard `[forMenuContent]` keyboard applies — see [`menu/README.md`](../menu/README.md). The menubar adds:
96
+
97
+ | Key | Behavior |
98
+ | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
99
+ | `ArrowLeft` / `ArrowRight` (on a top-level item, no submenu open) | Close the current menu and open the previous / next sibling menu, focusing its first item. RTL inverts. |
100
+ | `Escape` | Close the menu and return focus to its trigger. |
101
+ | `Tab` / `Shift+Tab` | Close the menu and return focus to its trigger; the natural tab sequence then exits the menubar. |
102
+
103
+ Submenus opened from a top-level menu work as in `[forDropdownMenu]` — Escape collapses one level at a time, the open-key opens, the close-key collapses upward. When the submenu's parent is the top of a menubar, the close-key collapses the parent and switches to the previous sibling menu.
104
+
105
+ ## Styling
106
+
107
+ 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.
108
+
109
+ ### Data attributes
110
+
111
+ | Piece | Attribute | Values |
112
+ | --------------------- | ------------------ | -------------------------- |
113
+ | `[forMenubar]` | `data-state` | `open` \| `closed` |
114
+ | `[forMenubar]` | `data-orientation` | `horizontal` \| `vertical` |
115
+ | `[forMenubar]` | `data-disabled` | present \| absent |
116
+ | `[forMenubarTrigger]` | `data-state` | `open` \| `closed` |
117
+ | `[forMenubarTrigger]` | `data-orientation` | `horizontal` \| `vertical` |
118
+ | `[forMenubarTrigger]` | `data-disabled` | present \| absent |
119
+
120
+ > Each trigger's menu surface is the shared `[forMenuContent]` (from [`menu/`](../menu/README.md)), which **portals to `document.body`**. Style it with global CSS or a class — scoped/`:host` styles won't reach it. The portaled content also exposes the shared positioner custom properties (`--for-anchor-width` / `-height`, `--for-available-width` / `-height`, `--for-content-transform-origin`); see [Styling floating content](../../../../../docs/styling-floating-content.md) for the full list and how to use them.
121
+
122
+ ```css
123
+ .menubar-trigger[data-state='open'] {
124
+ background: var(--accent);
125
+ }
126
+
127
+ .menubar-trigger[data-disabled] {
128
+ opacity: 0.5;
129
+ }
130
+ ```
131
+
132
+ ## Behavior notes
133
+
134
+ - **One open at a time.** Opening trigger `B` while `A` is open implicitly closes `A` and opens `B` with its first item focused.
135
+ - **First open is intentional, subsequent are hover.** While no menu is open, hovering a trigger does _not_ auto-open, and keyboard focus alone never opens a menu. After the user opens any menu via click / keyboard, hovering a sibling trigger opens it instantly (no delay).
136
+ - **Hover-leave dismisses.** Once a menu is open, moving the pointer off the bar (and away from the open menu) closes it after `closeDelay` (default `150`ms). Re-entering the bar, a trigger, or the open menu before the delay elapses cancels the pending close, so travelling from a trigger down into its menu keeps it open. Touch / pen pointers don't trigger this — they dismiss by tapping outside. Set `[dismissible]="false"` to pin the menu open regardless.
137
+ - **Dismissal.** Escape, an outside pointer interaction, and pointer-leave all close the open menu when `dismissible` is `true` (default). `[dismissible]="false"` suppresses all three.
138
+ - **Roving tabindex.** Only one trigger is in the tab sequence at a time — the open trigger, the most-recently-focused trigger, or the first enabled one when nothing's focused.
139
+ - **Mount equals open.** Each menu's `[forMenuContent]` is wrapped in `@if (value() === '<id>')`, so `animate.enter` / `animate.leave` fire on mount / unmount. The directive does not toggle `[hidden]`.
140
+ - **Disabled triggers stay focusable** (per APG) — they still reflect `data-disabled=""` and `aria-disabled="true"` and are skipped by ArrowLeft / ArrowRight, typeahead, and cross-menu nav.
@@ -0,0 +1,128 @@
1
+ # Meter
2
+
3
+ Headless implementation of the [WAI-ARIA Meter pattern](https://www.w3.org/WAI/ARIA/apg/patterns/meter/), mirroring the HTML5 `<meter>` element.
4
+
5
+ A meter represents a **measurement** within a known range — battery, disk space, score, queue depth — _not_ progress on a task. Use [Progress](../progress) for the latter.
6
+
7
+ ## Pieces
8
+
9
+ | Class | Selector | Role |
10
+ | ------------------- | --------------------- | -------------------------------------------------------------------------------------- |
11
+ | `ForMeter` | `[forMeter]` | Root. Reflects `role="meter"`, ARIA value attributes, and the computed `data-quality`. |
12
+ | `ForMeterIndicator` | `[forMeterIndicator]` | Visual fill. Reflects `data-quality`, `data-percentage`, and `--for-meter-percentage`. |
13
+
14
+ ## Inputs / models
15
+
16
+ | API | Type | Description |
17
+ | --------------- | ------------------------------------------ | ----------------------------------------------------------------------------------------------------- |
18
+ | `value` | `model<number>` | Two-way bindable. Clamped to `[min, max]` for ARIA / data-\* output; the model retains the raw write. |
19
+ | `min` | `input<number>` | Lower bound. Default `0`. |
20
+ | `max` | `input<number>` | Upper bound. Default `100`. |
21
+ | `low` | `input<number \| null>` | Lower boundary of the "comfortable" range. Default `null` (= `min`). |
22
+ | `high` | `input<number \| null>` | Upper boundary of the "comfortable" range. Default `null` (= `max`). |
23
+ | `optimum` | `input<number \| null>` | Ideal point. Default `null` (= midpoint). Drives the quality classification. |
24
+ | `getValueLabel` | `input<((v, min, max) => string) \| null>` | Override for `aria-valuetext`. |
25
+
26
+ ## Quality algorithm
27
+
28
+ The `data-quality` reflection follows the HTML5 spec:
29
+
30
+ | Optimum sits in | `value` in | Quality |
31
+ | --------------- | --------------------- | ---------------- |
32
+ | middle | `[low, high]` | `optimum` |
33
+ | middle | outside `[low, high]` | `sub-optimum` |
34
+ | below `low` | below `low` | `optimum` |
35
+ | below `low` | `[low, high]` | `sub-optimum` |
36
+ | below `low` | above `high` | `even-less-good` |
37
+ | above `high` | above `high` | `optimum` |
38
+ | above `high` | `[low, high]` | `sub-optimum` |
39
+ | above `high` | below `low` | `even-less-good` |
40
+
41
+ ## Usage
42
+
43
+ ```ts
44
+ import { Component, signal } from '@angular/core';
45
+ import { ForMeter, ForMeterIndicator } from 'forty-cdk/meter';
46
+
47
+ @Component({
48
+ selector: 'demo-disk',
49
+ imports: [ForMeter, ForMeterIndicator],
50
+ template: `
51
+ <label for="disk">Disk usage</label>
52
+ <div id="disk" forMeter class="meter" [value]="used()" [low]="20" [high]="80" [optimum]="40">
53
+ <div forMeterIndicator class="meter-indicator"></div>
54
+ </div>
55
+ <output>{{ used() }}%</output>
56
+ `,
57
+ styles: [
58
+ `
59
+ .meter {
60
+ position: relative;
61
+ height: 8px;
62
+ width: 200px;
63
+ background: #f1f1f1;
64
+ border-radius: 4px;
65
+ overflow: hidden;
66
+ }
67
+ .meter-indicator {
68
+ height: 100%;
69
+ width: var(--for-meter-percentage, 0%);
70
+ transition: width 200ms;
71
+ }
72
+ .meter-indicator[data-quality='optimum'] {
73
+ background: #16a34a;
74
+ }
75
+ .meter-indicator[data-quality='sub-optimum'] {
76
+ background: #ca8a04;
77
+ }
78
+ .meter-indicator[data-quality='even-less-good'] {
79
+ background: #dc2626;
80
+ }
81
+ `,
82
+ ],
83
+ })
84
+ export class DemoDisk {
85
+ readonly used = signal(72);
86
+ }
87
+ ```
88
+
89
+ ## Styling
90
+
91
+ 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.
92
+
93
+ ### Data attributes
94
+
95
+ | Piece | Attribute | Values |
96
+ | --------------------- | ----------------- | ------------------------------------------------------ |
97
+ | `[forMeter]` | `data-quality` | `optimum` &#124; `sub-optimum` &#124; `even-less-good` |
98
+ | `[forMeter]` | `data-value` | current value, clamped to `[min, max]` |
99
+ | `[forMeter]` | `data-min` | lower bound |
100
+ | `[forMeter]` | `data-max` | upper bound |
101
+ | `[forMeter]` | `data-percentage` | `value` as a number in `0`–`100` |
102
+ | `[forMeterIndicator]` | `data-quality` | `optimum` &#124; `sub-optimum` &#124; `even-less-good` |
103
+ | `[forMeterIndicator]` | `data-value` | current value, clamped to `[min, max]` |
104
+ | `[forMeterIndicator]` | `data-min` | lower bound |
105
+ | `[forMeterIndicator]` | `data-max` | upper bound |
106
+ | `[forMeterIndicator]` | `data-percentage` | `value` as a number in `0`–`100` |
107
+
108
+ ### CSS custom properties
109
+
110
+ | Property | Meaning |
111
+ | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------- |
112
+ | `--for-meter-percentage` | `value` as a CSS percentage of `[min, max]` (`0%`–`100%`), set on `[forMeterIndicator]`. Drive `width` / `transform` from it. |
113
+
114
+ ```css
115
+ .meter-indicator {
116
+ width: var(--for-meter-percentage, 0%);
117
+ }
118
+ .meter-indicator[data-quality='even-less-good'] {
119
+ background: #dc2626;
120
+ }
121
+ ```
122
+
123
+ ## Accessibility notes
124
+
125
+ - **`role="meter"`** announces the current value as a fraction of the range. Pair with a visible label and `aria-labelledby` (or `aria-label`) for context — "Disk usage 72 of 100".
126
+ - **Always determinate.** Unlike `<progress>`, a meter must always have a known value. There is no indeterminate mode in HTML5 / ARIA.
127
+ - **Don't use Meter as Progress.** Screen readers announce the two roles differently (and assistive guidance differs); pick the right primitive for the meaning.
128
+ - **Quality is for CSS only.** `data-quality` is a styling hook; assistive tech reads `aria-valuenow` / `aria-valuetext`, not the quality bucket.
@@ -0,0 +1,253 @@
1
+ # NavigationMenu
2
+
3
+ Headless implementation of the [WAI-ARIA Disclosure Navigation Menu pattern](https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/examples/disclosure-navigation/) — a `<nav>` of disclosures, **not** an ARIA `menu`.
4
+
5
+ Triggers are buttons with `aria-expanded` / `aria-controls`, content panels are landmarks with links, Tab moves through links, Escape closes and returns focus.
6
+
7
+ ## Pieces
8
+
9
+ | Class | Selector | Role |
10
+ | ---------------------------- | ------------------------------ | --------------------------------------------------------------------- |
11
+ | `ForNavigationMenu` | `[forNavigationMenu]` | Root. Owns open state, delays, dismiss layer. |
12
+ | `ForNavigationMenuList` | `[forNavigationMenuList]` | Optional layout wrapper. |
13
+ | `ForNavigationMenuItem` | `[forNavigationMenuItem]` | Pairs one trigger with one content panel. |
14
+ | `ForNavigationMenuTrigger` | `[forNavigationMenuTrigger]` | Button that toggles its panel. |
15
+ | `ForNavigationMenuContent` | `[forNavigationMenuContent]` | Panel mounted via `@if`. Carries `aria-labelledby`. |
16
+ | `ForNavigationMenuLink` | `[forNavigationMenuLink]` | Decorative wrapper that reflects `aria-current` on active links. |
17
+ | `ForNavigationMenuIndicator` | `[forNavigationMenuIndicator]` | Optional follower (underline / pill) positioned via CSS custom props. |
18
+ | `ForNavigationMenuViewport` | `[forNavigationMenuViewport]` | Optional shared surface for mega-menu animations. |
19
+
20
+ ## Inputs (root)
21
+
22
+ | API | Type | Description |
23
+ | ------------------- | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
24
+ | `value` | `model<string>` | Two-way bindable. Open item id, or `''`. |
25
+ | `orientation` | `input<'horizontal' \| 'vertical'>` | Default `'horizontal'`. |
26
+ | `dir` | `input<WritingDirection>` | RTL inverts ArrowLeft / ArrowRight. |
27
+ | `loop` | `input<boolean>` | Whether arrow nav wraps. Default `true`. |
28
+ | `disabled` | `input<boolean>` | Disables the whole menu. |
29
+ | `ariaLabel` | `input<string \| null>` | Reactive `aria-label` for the `<nav>`. Default `null` (and empty string) emits no attribute; prefer native `aria-labelledby` when a visible label exists. |
30
+ | `delayDuration` | `input<number>` | ms before hover/focus opens. Default `200`. |
31
+ | `closeDelay` | `input<number>` | ms before pointer-leave closes. Default `150`. |
32
+ | `skipDelayDuration` | `input<number>` | ms after a peer closes during which the next open is instant. Default `300`. |
33
+
34
+ ## Usage
35
+
36
+ ```ts
37
+ import { Component, signal } from '@angular/core';
38
+ import {
39
+ ForNavigationMenu,
40
+ ForNavigationMenuContent,
41
+ ForNavigationMenuIndicator,
42
+ ForNavigationMenuItem,
43
+ ForNavigationMenuLink,
44
+ ForNavigationMenuList,
45
+ ForNavigationMenuTrigger,
46
+ } from 'forty-cdk/navigation-menu';
47
+
48
+ @Component({
49
+ selector: 'demo-nav',
50
+ imports: [
51
+ ForNavigationMenu,
52
+ ForNavigationMenuList,
53
+ ForNavigationMenuItem,
54
+ ForNavigationMenuTrigger,
55
+ ForNavigationMenuContent,
56
+ ForNavigationMenuLink,
57
+ ForNavigationMenuIndicator,
58
+ ],
59
+ template: `
60
+ <nav forNavigationMenu aria-label="Main" [(value)]="open">
61
+ <ul forNavigationMenuList>
62
+ <li forNavigationMenuItem value="products">
63
+ <button forNavigationMenuTrigger class="navigation-menu-trigger">Products</button>
64
+ @if (open() === 'products') {
65
+ <div
66
+ forNavigationMenuContent
67
+ class="navigation-menu-content"
68
+ animate.enter="fade-in"
69
+ animate.leave="fade-out"
70
+ >
71
+ <a href="/p/web" forNavigationMenuLink>Web</a>
72
+ <a href="/p/mobile" forNavigationMenuLink active>Mobile</a>
73
+ </div>
74
+ }
75
+ </li>
76
+ <li forNavigationMenuItem value="company">
77
+ <button forNavigationMenuTrigger class="navigation-menu-trigger">Company</button>
78
+ @if (open() === 'company') {
79
+ <div forNavigationMenuContent class="navigation-menu-content">
80
+ <a href="/about" forNavigationMenuLink>About</a>
81
+ <a href="/jobs" forNavigationMenuLink>Careers</a>
82
+ </div>
83
+ }
84
+ </li>
85
+ <span forNavigationMenuIndicator class="navigation-menu-indicator"></span>
86
+ </ul>
87
+ </nav>
88
+ `,
89
+ })
90
+ export class DemoNav {
91
+ readonly open = signal('');
92
+ }
93
+ ```
94
+
95
+ ## Styling
96
+
97
+ 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.
98
+
99
+ ### Data attributes
100
+
101
+ | Piece | Attribute | Values |
102
+ | ------------------------------ | ------------------ | ---------------------------------------------------- |
103
+ | `[forNavigationMenu]` | `data-state` | `open` \| `closed` |
104
+ | `[forNavigationMenu]` | `data-orientation` | `horizontal` \| `vertical` |
105
+ | `[forNavigationMenu]` | `data-disabled` | present \| absent |
106
+ | `[forNavigationMenuList]` | `data-orientation` | `horizontal` \| `vertical` |
107
+ | `[forNavigationMenuItem]` | `data-state` | `open` \| `closed` |
108
+ | `[forNavigationMenuItem]` | `data-disabled` | present \| absent |
109
+ | `[forNavigationMenuTrigger]` | `data-state` | `open` \| `closed` |
110
+ | `[forNavigationMenuTrigger]` | `data-disabled` | present \| absent |
111
+ | `[forNavigationMenuContent]` | `data-state` | `open` \| `closed` |
112
+ | `[forNavigationMenuContent]` | `data-motion` | `from-start` \| `from-end` \| `to-start` \| `to-end` |
113
+ | `[forNavigationMenuLink]` | `data-active` | present \| absent |
114
+ | `[forNavigationMenuIndicator]` | `data-state` | `visible` \| `hidden` |
115
+ | `[forNavigationMenuIndicator]` | `data-orientation` | `horizontal` \| `vertical` |
116
+ | `[forNavigationMenuViewport]` | `data-state` | `open` \| `closed` |
117
+ | `[forNavigationMenuViewport]` | `data-orientation` | `horizontal` \| `vertical` |
118
+
119
+ `data-motion` is absent on first open and last close, where there is no peer trigger to compare against.
120
+
121
+ ### CSS custom properties
122
+
123
+ `[forNavigationMenuIndicator]` exposes the active trigger's geometry (relative to `[forNavigationMenuList]`) so the indicator visual can be driven entirely from CSS. The optional shared `[forNavigationMenuViewport]` exposes the active panel's natural size so consumers can transition `width` / `height` between trigger groups.
124
+
125
+ | Property | Meaning |
126
+ | ---------------------------------------- | ------------------------------------------------------ |
127
+ | `--for-navigation-menu-indicator-x` | Horizontal offset of the active trigger (px). |
128
+ | `--for-navigation-menu-indicator-y` | Vertical offset of the active trigger (px). |
129
+ | `--for-navigation-menu-indicator-width` | Active trigger width (px). |
130
+ | `--for-navigation-menu-indicator-height` | Active trigger height (px). |
131
+ | `--for-navigation-menu-viewport-width` | Active content's natural width (px), on the Viewport. |
132
+ | `--for-navigation-menu-viewport-height` | Active content's natural height (px), on the Viewport. |
133
+
134
+ ```css
135
+ .navigation-menu-trigger svg {
136
+ transition: transform 150ms;
137
+ }
138
+ .navigation-menu-trigger[data-state='open'] svg {
139
+ transform: rotate(180deg);
140
+ }
141
+
142
+ .navigation-menu-indicator {
143
+ transform: translateX(var(--for-navigation-menu-indicator-x));
144
+ width: var(--for-navigation-menu-indicator-width);
145
+ transition:
146
+ transform 200ms,
147
+ width 200ms;
148
+ }
149
+ .navigation-menu-indicator[data-state='hidden'] {
150
+ opacity: 0;
151
+ }
152
+ ```
153
+
154
+ ## Keyboard
155
+
156
+ | Key | Behavior |
157
+ | ---------------------------------------------- | -------------------------------------------------------------------------- |
158
+ | Tab | Moves into the trigger row. Inside an open panel, moves through its links. |
159
+ | Enter / Space | Toggles the focused trigger. |
160
+ | ArrowDown (horizontal) / ArrowRight (vertical) | Opens the focused trigger. |
161
+ | ArrowLeft / ArrowRight (horizontal) | Moves focus across triggers. |
162
+ | ArrowUp / ArrowDown (vertical) | Moves focus across triggers. |
163
+ | Home / End | Jump to first / last enabled trigger. |
164
+ | Escape | Closes and returns focus to the trigger. |
165
+
166
+ ## Accessibility notes
167
+
168
+ - **Not an ARIA menu.** This implements the _disclosure_ pattern: `<nav>` + buttons + landmark panels. ARIA `role="menu"` is for application menus where Tab leaves but arrows do everything. Site navigation expects Tab to move through links, which is what this primitive supports.
169
+ - **Focus alone does not open.** A trigger opens on hover, click, Enter / Space, or the cross-axis arrow (ArrowDown horizontal / ArrowRight vertical) — never on plain focus. This matches the APG disclosure-navigation pattern: Tabbing across the trigger row does not auto-expand panels, and the return-focus after Escape cannot synchronously re-open the panel that just closed.
170
+ - **Trigger labels are mandatory.** Each `[forNavigationMenuTrigger]` needs visible text or an `aria-label`. The directive does not invent one.
171
+ - **Content panels are mounted via `@if`.** The directive does not apply `[hidden]`; visibility is the consumer's call. Use `animate.enter` / `animate.leave` for transitions.
172
+ - **Indicator follows the active trigger.** A `ResizeObserver` (browser-only) watches the active trigger and the surrounding list and re-measures only when the active trigger switches or one of those boxes resizes — reactive, not per-render polling. Consumers drive the visual via the `--for-navigation-menu-indicator-x|y|width|height` custom properties.
173
+ - **`data-state` on the root.** The `[forNavigationMenu]` host reflects `data-state="open"` whenever any item is open and `"closed"` otherwise — same vocabulary as the trigger / content / item / indicator pieces, useful for top-level CSS hooks (e.g. dimming the rest of the page while the menu is open).
174
+ - **Tab-out closes.** Per APG, moving focus past the last / before the first focusable inside the nav closes any open panel. The root listens for `focusout` and closes when `relatedTarget` falls outside the `<nav>`. Escape and outside pointerdown are already handled by the dismissable layer; this covers the keyboard-Tab case it can't see.
175
+
176
+ ## Mega-menu (shared `Viewport`)
177
+
178
+ For Stripe / Vercel / Linear-style mega menus that share a single panel between trigger groups, drop a `[forNavigationMenuViewport]` inside the menu and let it host the active content. The Viewport is fully opt-in: with no Viewport in the markup the menu behaves exactly as the disclosure recipe above.
179
+
180
+ When present, each `[forNavigationMenuContent]` re-parents its host into the Viewport on mount. The Viewport exposes the active panel's natural size as CSS custom properties so consumers can transition `width` / `height` between groups, and each Content reflects `data-motion` so consumers can author directional slide / fade animations:
181
+
182
+ | Attribute / variable | Where | Meaning |
183
+ | ---------------------------------------- | ---------------- | --------------------------------------------------------- |
184
+ | `data-state="open" \| "closed"` | Viewport host | Whether any content is currently mounted in the Viewport. |
185
+ | `--for-navigation-menu-viewport-width` | Viewport host | Active content's natural width (px). |
186
+ | `--for-navigation-menu-viewport-height` | Viewport host | Active content's natural height (px). |
187
+ | `data-motion="from-start" \| "from-end"` | Entering Content | Side the previous trigger sat on, relative to this one. |
188
+ | `data-motion="to-start" \| "to-end"` | Leaving Content | Side the new trigger sits on, relative to this one. |
189
+
190
+ `from-start` / `to-start` map to the logical inline-start (left in LTR, right in RTL); writing the keyframes with logical CSS properties (e.g. `inset-inline-start`) makes the animation work in both directions automatically. `data-motion` is absent on first open and last close, where there is no peer trigger to compare against.
191
+
192
+ ```html
193
+ <nav forNavigationMenu [(value)]="open" aria-label="Main">
194
+ <ul forNavigationMenuList>
195
+ <li forNavigationMenuItem value="products">
196
+ <button forNavigationMenuTrigger class="navigation-menu-trigger">Products</button>
197
+ @if (open() === 'products') {
198
+ <div forNavigationMenuContent class="navigation-menu-content">…</div>
199
+ }
200
+ </li>
201
+ <li forNavigationMenuItem value="solutions">
202
+ <button forNavigationMenuTrigger class="navigation-menu-trigger">Solutions</button>
203
+ @if (open() === 'solutions') {
204
+ <div forNavigationMenuContent class="navigation-menu-content">…</div>
205
+ }
206
+ </li>
207
+ </ul>
208
+
209
+ <!-- Single shared surface. Content panels re-parent into here on open. -->
210
+ <div forNavigationMenuViewport class="navigation-menu-viewport"></div>
211
+ </nav>
212
+ ```
213
+
214
+ ```css
215
+ .navigation-menu-viewport {
216
+ position: relative;
217
+ width: var(--for-navigation-menu-viewport-width);
218
+ height: var(--for-navigation-menu-viewport-height);
219
+ transition:
220
+ width 200ms,
221
+ height 200ms;
222
+ }
223
+
224
+ .navigation-menu-content {
225
+ position: absolute;
226
+ inset-inline-start: 0;
227
+ top: 0;
228
+ }
229
+ .navigation-menu-content[data-motion='from-start'] {
230
+ animation: slide-in-from-start 200ms;
231
+ }
232
+ .navigation-menu-content[data-motion='from-end'] {
233
+ animation: slide-in-from-end 200ms;
234
+ }
235
+ .navigation-menu-content[data-motion='to-start'] {
236
+ animation: slide-out-to-start 200ms;
237
+ }
238
+ .navigation-menu-content[data-motion='to-end'] {
239
+ animation: slide-out-to-end 200ms;
240
+ }
241
+ ```
242
+
243
+ The leaving content stays mounted as long as the consumer's `@if` keeps it (typically via `animate.leave`), so two panels can briefly overlap inside the Viewport and cross-fade or slide past each other.
244
+
245
+ ### Panel order is deterministic
246
+
247
+ The Viewport owns panel ordering: each Content is inserted in its **trigger's document order**, never simply appended in mount order. During an overlapping A→B transition — where the leaving A is still mounted while B enters — the Viewport's child order always matches trigger order, so the panel whose trigger comes first in the DOM is always the first child, regardless of which panel mounted last. Author cross-fade / slide stacking against that order (paired with the `data-motion` hook) rather than against mount timing.
248
+
249
+ Measurement always tracks the **active** panel. The Viewport's `--for-navigation-menu-viewport-width` / `--for-navigation-menu-viewport-height` reflect the entering panel as soon as it becomes active; a non-active panel kept mounted by `animate.leave` is intentionally no longer measured, so a leaving panel's size never drives the Viewport box mid-transition.
250
+
251
+ ## Limitations (v1)
252
+
253
+ - Submenús anidados — not implemented; tracked separately.