forty-cdk 0.37.0 → 0.38.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 (80) hide show
  1. package/README.md +4 -4
  2. package/avatar/README.md +1 -1
  3. package/breadcrumbs/README.md +2 -2
  4. package/button/README.md +12 -6
  5. package/calendar/README.md +1 -1
  6. package/checkbox/README.md +3 -3
  7. package/date-field/README.md +2 -2
  8. package/drag-drop/README.md +56 -8
  9. package/drawer/README.md +25 -20
  10. package/fesm2022/forty-cdk-button.mjs +5 -4
  11. package/fesm2022/forty-cdk-button.mjs.map +1 -1
  12. package/fesm2022/forty-cdk-core.mjs +103 -39
  13. package/fesm2022/forty-cdk-core.mjs.map +1 -1
  14. package/fesm2022/forty-cdk-defaults.mjs.map +1 -1
  15. package/fesm2022/forty-cdk-dialog.mjs +2 -0
  16. package/fesm2022/forty-cdk-dialog.mjs.map +1 -1
  17. package/fesm2022/forty-cdk-drag-drop.mjs +289 -63
  18. package/fesm2022/forty-cdk-drag-drop.mjs.map +1 -1
  19. package/fesm2022/forty-cdk-drawer.mjs +124 -47
  20. package/fesm2022/forty-cdk-drawer.mjs.map +1 -1
  21. package/fesm2022/forty-cdk-field.mjs +1 -1
  22. package/fesm2022/forty-cdk-field.mjs.map +1 -1
  23. package/fesm2022/forty-cdk-listbox.mjs.map +1 -1
  24. package/fesm2022/forty-cdk-navigation-menu.mjs +10 -20
  25. package/fesm2022/forty-cdk-navigation-menu.mjs.map +1 -1
  26. package/fesm2022/forty-cdk-number-input.mjs +18 -11
  27. package/fesm2022/forty-cdk-number-input.mjs.map +1 -1
  28. package/fesm2022/forty-cdk-pagination.mjs +8 -4
  29. package/fesm2022/forty-cdk-pagination.mjs.map +1 -1
  30. package/fesm2022/forty-cdk-pane-resizer.mjs +2 -2
  31. package/fesm2022/forty-cdk-pane-resizer.mjs.map +1 -1
  32. package/fesm2022/forty-cdk-select.mjs +16 -2
  33. package/fesm2022/forty-cdk-select.mjs.map +1 -1
  34. package/fesm2022/forty-cdk-stepper.mjs.map +1 -1
  35. package/fesm2022/forty-cdk-table.mjs +3 -3
  36. package/fesm2022/forty-cdk-table.mjs.map +1 -1
  37. package/fesm2022/forty-cdk-time-picker.mjs +15 -1
  38. package/fesm2022/forty-cdk-time-picker.mjs.map +1 -1
  39. package/fesm2022/forty-cdk-virtual-reorder.mjs +2 -1
  40. package/fesm2022/forty-cdk-virtual-reorder.mjs.map +1 -1
  41. package/fesm2022/forty-cdk-virtualization.mjs +72 -12
  42. package/fesm2022/forty-cdk-virtualization.mjs.map +1 -1
  43. package/field/README.md +9 -9
  44. package/fieldset/README.md +1 -1
  45. package/file-upload/README.md +1 -1
  46. package/input/README.md +2 -2
  47. package/listbox/README.md +59 -77
  48. package/navigation-menu/README.md +37 -34
  49. package/number-input/README.md +11 -7
  50. package/otp-input/README.md +2 -2
  51. package/package.json +1 -1
  52. package/pagination/README.md +2 -1
  53. package/pane-resizer/README.md +2 -2
  54. package/progress/README.md +14 -5
  55. package/radio-group/README.md +4 -4
  56. package/stepper/README.md +8 -8
  57. package/switch/README.md +11 -11
  58. package/table/README.md +1 -1
  59. package/time-field/README.md +2 -2
  60. package/time-picker/README.md +1 -0
  61. package/toggle/README.md +11 -2
  62. package/types/forty-cdk-button.d.ts +7 -1
  63. package/types/forty-cdk-core.d.ts +58 -29
  64. package/types/forty-cdk-defaults.d.ts +6 -5
  65. package/types/forty-cdk-drag-drop.d.ts +52 -8
  66. package/types/forty-cdk-drawer.d.ts +50 -29
  67. package/types/forty-cdk-field.d.ts +3 -4
  68. package/types/forty-cdk-listbox.d.ts +3 -3
  69. package/types/forty-cdk-navigation-menu.d.ts +33 -30
  70. package/types/forty-cdk-number-input.d.ts +15 -15
  71. package/types/forty-cdk-pagination.d.ts +8 -1
  72. package/types/forty-cdk-pane-resizer.d.ts +4 -3
  73. package/types/forty-cdk-select.d.ts +18 -5
  74. package/types/forty-cdk-stepper.d.ts +11 -7
  75. package/types/forty-cdk-table.d.ts +4 -3
  76. package/types/forty-cdk-time-picker.d.ts +21 -8
  77. package/types/forty-cdk-virtual-reorder.d.ts +5 -3
  78. package/types/forty-cdk-virtualization.d.ts +52 -17
  79. package/virtual-reorder/README.md +7 -1
  80. package/virtualization/README.md +187 -30
package/README.md CHANGED
@@ -26,10 +26,10 @@ Required:
26
26
 
27
27
  Optional — install only if you use the matching entry point / primitives:
28
28
 
29
- | Peer | Needed by |
30
- | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
31
- | `@angular/forms` `^22.1.0` | Form-control primitives (`Switch`, `Checkbox`, `RadioGroup`, `Listbox`, `Select`, `Slider`, `Combobox`, …). They implement `FormValueControl` / `FormCheckboxControl` from `@angular/forms/signals` for `[formField]` auto-wiring. The contract is type-only, so the published bundle never references the package — consumers using only non-form primitives can skip it. |
32
- | `@internationalized/date` `^3.0.0` | The `forty-cdk/internationalized-date` entry point (`InternationalizedDateAdapter`, `InternationalizedDateTimeAdapter`). The date/time primitives themselves depend only on the abstract `DateAdapter` contract from `forty-cdk/date-adapter` — install this peer only when you import that entry point. |
29
+ | Peer | Needed by |
30
+ | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
31
+ | `@angular/forms` `^22.1.0` | The form-control entry points: `forty-cdk/checkbox`, `combobox`, `date-field`, `date-picker`, `input`, `listbox`, `number-input`, `otp-input`, `radio-group`, `search`, `select`, `slider`, `switch`, `time-field`, `time-picker` and `toggle`. Their controls implement `FormValueControl` / `FormCheckboxControl` from `@angular/forms/signals` for `[formField]` auto-wiring. The contract is type-only, so no published bundle references the package, but those entry points' type declarations do: a project that type-checks its dependencies (`skipLibCheck: false`) needs the peer as soon as it imports one of them. Every other entry point, `forty-cdk/field` and `forty-cdk/stepper` included, compiles without it. |
32
+ | `@internationalized/date` `^3.0.0` | The `forty-cdk/internationalized-date` entry point (`InternationalizedDateAdapter`, `InternationalizedDateTimeAdapter`). The date/time primitives themselves depend only on the abstract `DateAdapter` contract from `forty-cdk/date-adapter` — install this peer only when you import that entry point. |
33
33
 
34
34
  ### Regular dependencies
35
35
 
package/avatar/README.md CHANGED
@@ -22,7 +22,7 @@ It is headless and presentational: it tracks the load lifecycle of an `<img>` an
22
22
 
23
23
  ## Examples
24
24
 
25
- Let the image load, then break its URL: `data-status` moves between `loading`, `loaded` and `error`, and the fallback only appears once the delay has passed without an image.
25
+ The image loads, so `data-status` settles on `loaded` and the initials never render: `fallbackDelayMs` holds the fallback back for 500 ms, so a fast load never flashes it. Failed load below breaks the URL to show the `error` path.
26
26
 
27
27
  ```ts
28
28
  import { ChangeDetectionStrategy, Component } from '@angular/core';
@@ -126,10 +126,10 @@ Implements the [WAI-ARIA Breadcrumb pattern](https://www.w3.org/WAI/ARIA/apg/pat
126
126
 
127
127
  ## Styling
128
128
 
129
- forty-cdk ships no styles. Style the current item via `[aria-current="page"]`.
129
+ forty-cdk ships no styles: put your own class on each piece and key your CSS off the `aria-current="page"` the current item reflects, not off the `for*` selectors ([Styling forty-cdk](../../../docs/styling.md) explains why).
130
130
 
131
131
  ```css
132
- [forBreadcrumbItem][aria-current='page'] {
132
+ .bc-link[aria-current='page'] {
133
133
  font-weight: bold;
134
134
  color: inherit;
135
135
  text-decoration: none;
package/button/README.md CHANGED
@@ -58,20 +58,26 @@ Disabled buttons stay focusable so assistive technology can announce them. The n
58
58
  A surrounding disabled `[forFieldset]` disables the button too: its `disabled` input is OR'd with the group's, so `aria-disabled` / `data-disabled` are reflected and activation is suppressed. This matters most on a non-native host (`<div forButton>`), which a native `<fieldset disabled>` cannot reach.
59
59
 
60
60
  ```html
61
- <fieldset forFieldset [disabled]="locked()">
62
- <legend forFieldsetLegend>Account</legend>
61
+ <div forFieldset [disabled]="locked()">
62
+ <span forFieldsetLegend>Account</span>
63
63
  <button forButton (activate)="save()">Save</button>
64
- </fieldset>
64
+ </div>
65
65
  ```
66
66
 
67
+ On a native `<fieldset>` host, `[forFieldset]` sets the native `disabled` attribute, and HTML then disables every native `<button>` inside it: the button leaves the tab order and cannot be focused, whatever `forButton` does. The guarantee that a disabled button stays focusable holds for a non-native group host like the `<div>` above, and for a non-native button host (`<div forButton>`) in either group.
68
+
67
69
  ## Preserve consumer `type`
68
70
 
69
- A native `<button>` without an explicit `type` attribute defaults to `type="button"`. A consumer-set `type="submit"` is preserved:
71
+ A native `<button>` without an explicit `type` attribute defaults to `type="button"`. A consumer-set `type` is preserved, whether it is a static attribute or a binding, and later writes to a binding are followed:
70
72
 
71
73
  ```html
72
74
  <button type="submit" forButton>Submit form</button>
75
+
76
+ <button [attr.type]="kind()" forButton>Save</button>
73
77
  ```
74
78
 
79
+ A binding that resolves to `null` removes the attribute, so the button falls back to the HTML default, which is `submit` inside a form.
80
+
75
81
  ## API
76
82
 
77
83
  ### `ForButton`
@@ -118,12 +124,12 @@ Implements the [WAI-ARIA Button pattern](https://www.w3.org/WAI/ARIA/apg/pattern
118
124
  forty-cdk ships no styles: put your own class on each piece and key your CSS off the `data-*` attributes listed under [API](#api), not off the `for*` selectors ([Styling forty-cdk](../../../docs/styling.md) explains why).
119
125
 
120
126
  ```css
121
- [forButton][data-disabled] {
127
+ .btn[data-disabled] {
122
128
  opacity: 0.4;
123
129
  pointer-events: none;
124
130
  }
125
131
 
126
- [forButton][data-pressed] {
132
+ .btn[data-pressed] {
127
133
  transform: scale(0.97);
128
134
  }
129
135
  ```
@@ -612,7 +612,7 @@ import { CalendarDate, today, getLocalTimeZone } from '@internationalized/date';
612
612
  @Component({
613
613
  selector: 'app-date',
614
614
  changeDetection: ChangeDetectionStrategy.OnPush,
615
- /* ... */
615
+ template: `<p>Today is {{ date()?.toString() }}</p>`,
616
616
  })
617
617
  export class DatePage {
618
618
  readonly date = signal<CalendarDate | null>(null);
@@ -134,12 +134,12 @@ One class and one directive, three states. `disabled` and `readonly` both keep t
134
134
 
135
135
  ```ts
136
136
  import { Component, signal } from '@angular/core';
137
- import { form, required } from '@angular/forms/signals';
137
+ import { form, FormField, required } from '@angular/forms/signals';
138
138
  import { ForCheckbox } from 'forty-cdk/checkbox';
139
139
 
140
140
  @Component({
141
141
  selector: 'demo-checkout',
142
- imports: [ForCheckbox /* , FormField from @angular/forms */],
142
+ imports: [ForCheckbox, FormField],
143
143
  template: `
144
144
  <button forCheckbox class="cb-row" [formField]="checkout.acceptTerms">
145
145
  <span class="cb">
@@ -166,7 +166,7 @@ export class DemoCheckout {
166
166
  | `disabled` / `readonly` / `required` / `invalid` / `pending` | `input<boolean>` | Reflected as the matching `aria-*` / `data-*` attributes. A disabled checkbox stays focusable (per APG), with `aria-disabled="true"` + `data-disabled` and no native `disabled`; click is a no-op.<br>**Default:** — |
167
167
  | `name` | `input<string>` | Reflected on `name` (empty string omits the attribute). A native reset of the owning `<form>` restores `checked` and `indeterminate` to their first-render values.<br>**Default:** `''` |
168
168
  | `errors` | `input<readonly ValidationError.WithOptionalFieldTree[]>` | Validation errors fed by `[formField]`.<br>**Default:** — |
169
- | `touched` | `model<boolean>` | Set to `true` on blur.<br>**Default:** — |
169
+ | `touched` | `model<boolean>` | Set to `true` on blur.<br>**Default:** `false` |
170
170
 
171
171
  | Data attribute | Values |
172
172
  | --------------- | ------------------------------------------- |
@@ -99,14 +99,14 @@ With a time-capable adapter, a `granularity` finer than `'day'` appends hour / m
99
99
 
100
100
  ```ts
101
101
  import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
102
- import { form } from '@angular/forms/signals';
102
+ import { form, FormField } from '@angular/forms/signals';
103
103
  import { CalendarDate } from '@internationalized/date';
104
104
  import { ForDateField, ForDateFieldLiteral, ForDateFieldSegment } from 'forty-cdk/date-field';
105
105
 
106
106
  @Component({
107
107
  selector: 'app-dob-form',
108
108
  changeDetection: ChangeDetectionStrategy.OnPush,
109
- imports: [ForDateField, ForDateFieldSegment, ForDateFieldLiteral],
109
+ imports: [ForDateField, ForDateFieldSegment, ForDateFieldLiteral, FormField],
110
110
  template: `
111
111
  <div
112
112
  forDateField
@@ -157,7 +157,10 @@ whole gesture (and past the drop, while a settle transition runs) it answers the
157
157
  selector (`[forDraggable]`, or a composed one such as `[forTableRow]`) and repeats its `data-index`.
158
158
  `id` and `data-testid` are stripped from the clone and its whole subtree, so a hook that identifies
159
159
  a single element stays unambiguous; anything that **enumerates** items by attribute selector during
160
- a drag must filter the preview out with `:not([data-for-drag-preview])`.
160
+ a drag must filter the preview out with `:not([data-for-drag-preview])`. The clone is also made
161
+ inert before it joins the document: `name`, `form`, `autofocus` and `autoplay` are stripped, and
162
+ every `iframe`, `object` and `embed` becomes an empty box of the same size, so a cloned radio never
163
+ unchecks the item's own and no cloned media plays or frame reloads.
161
164
 
162
165
  ## Keyboard
163
166
 
@@ -263,6 +266,19 @@ unaffected.
263
266
  </li>
264
267
  ```
265
268
 
269
+ A handle carries `touch-action: none`, so a touch drag on it is not taken by the page's
270
+ scrolling, and the item around it leaves touch scrolling alone. Both follow the item: a handle
271
+ mounted or removed after the first render moves the `touch-action: none` between the handle and
272
+ the item, and a disabled item or list drops it from both.
273
+
274
+ ### Native drag
275
+
276
+ Images, links and selected text are draggable by the browser itself, and its own drag starts
277
+ before a pointer drag arms. A `[forDraggable]` or `[forFreeDrag]` therefore cancels the native
278
+ `dragstart` of every press it would turn into a pointer drag, so pressing a thumbnail inside an
279
+ item reorders the item instead of dragging a copy of the image. Content outside the item's
280
+ handles, and every press while the item or its list is disabled, keeps its native drag.
281
+
266
282
  ### Nested controls
267
283
 
268
284
  A control inside a draggable item keeps its press from starting a drag by calling
@@ -286,6 +302,28 @@ the press stays with the control; lifting with the keyboard is unaffected.
286
302
  - A canceled `pointerdown` suppresses the compatibility mouse events of that press (`mousedown`,
287
303
  `mouseup`), so the control must not rely on them. `click` still fires.
288
304
 
305
+ A `[forDraggable]` or `[forFreeDrag]` nested inside another one needs no `preventDefault()`: every
306
+ press inside it is its own, and the item around it never starts a drag from there, even while the
307
+ nested one is disabled. On a board whose columns are the items of one list and hold a list of
308
+ cards each, pressing a card drags the card, and pressing a column outside its cards drags the
309
+ column. Each item lifts with its own `[forDragPreview]` / `[forDragPlaceholder]`, never with one
310
+ declared inside a nested item.
311
+
312
+ ```html
313
+ <div forDropList orientation="horizontal" (dragDrop)="moveColumn($event)">
314
+ @for (column of columns(); track column.id) {
315
+ <section forDraggable [dragData]="column">
316
+ <h2>{{ column.title }}</h2>
317
+ <ul forDropList (dragDrop)="moveCard(column, $event)">
318
+ @for (card of column.cards; track card.id) {
319
+ <li forDraggable [dragData]="card">{{ card.title }}</li>
320
+ }
321
+ </ul>
322
+ </section>
323
+ }
324
+ </div>
325
+ ```
326
+
289
327
  ### Custom preview & placeholder
290
328
 
291
329
  Place `<ng-template forDragPreview>` and/or `<ng-template forDragPlaceholder>` as direct
@@ -398,7 +436,9 @@ always provided by the consumer via CSS; the library imposes none.
398
436
  ```
399
437
 
400
438
  With no such CSS, `animateReorder` is a graceful no-op: transforms clear instantly and the
401
- preview is destroyed promptly.
439
+ preview is destroyed promptly. Otherwise the preview is destroyed when its `transform` transition
440
+ ends, and never before that transition's `transition-delay` plus duration have elapsed, wherever
441
+ `transform` sits in a multi-property `transition` list.
402
442
 
403
443
  ## Orientation
404
444
 
@@ -453,10 +493,14 @@ the CSS `translate` property. It is the standalone counterpart to a sortable lis
453
493
  pointer-driven way to move an element freely, with optional root-element retargeting, a confinement
454
494
  boundary, axis locking, and a controllable position (see the inputs below).
455
495
 
456
- The offset composes with any `transform` the moved element already has, so a dialog centred with
457
- `transform: translate(-50%, -50%)` stays centred and moves from there. It replaces an inline
458
- `translate` property of the element's own, so position the element with `transform` or layout
459
- properties rather than `translate`.
496
+ The offset composes with the `transform` and the `translate` the moved element already has, inline
497
+ or from a stylesheet, so a dialog centred with `transform: translate(-50%, -50%)` or
498
+ `translate: -50% -50%` stays centred and moves from there. The `translate` it composes with is read
499
+ when the element first leaves its rest position, and the element's own `translate` comes back once
500
+ `position` returns to `{ x: 0, y: 0 }`.
501
+
502
+ Scrolling the page or a scroll container during a drag keeps the element under the pointer, and the
503
+ `boundary` clamp follows the scrolled geometry.
460
504
 
461
505
  ```html
462
506
  <!-- move the whole dialog by its header -->
@@ -483,7 +527,7 @@ properties rather than `translate`.
483
527
  | Output | Payload | Fires |
484
528
  | ----------- | ---------- | ---------------------------------------------------------------------------------------------- |
485
529
  | `moveStart` | `{ x; y }` | When a pointer drag starts (the lift-time position). |
486
- | `moveMove` | `{ x; y }` | On every armed move, with the live position. |
530
+ | `moveMove` | `{ x; y }` | On every armed move, and when a scroll moves the element, with the live position. |
487
531
  | `moveEnd` | `{ x; y }` | When the drag ends (commit keeps the position; cancel/Escape restores the lift-time snapshot). |
488
532
 
489
533
  `[forDragHandle]` works inside `[forFreeDrag]` exactly as it does inside `[forDraggable]`: once any
@@ -533,8 +577,12 @@ other list, because their reorder output describes a move within one dataset.
533
577
  [virtualCount]="rows().length"
534
578
  forVirtualReorder
535
579
  (itemReorder)="onReorder($event)"
580
+ role="list"
581
+ aria-label="Rows"
536
582
  >
537
- <div *forVirtualFor="let row of rows()" forDraggable [dragData]="row.id">{{ row.label }}</div>
583
+ <div *forVirtualFor="let row of rows()" role="listitem" forDraggable [dragData]="row.id">
584
+ {{ row.label }}
585
+ </div>
538
586
  </div>
539
587
  ```
540
588
 
package/drawer/README.md CHANGED
@@ -171,7 +171,7 @@ this.#drawers.open(ConfirmDrawer, { data, side: 'bottom', class: 'my-drawer' });
171
171
  }
172
172
  ```
173
173
 
174
- **Enter / exit animations.** A programmatic drawer is portaled to `document.body` and torn down imperatively, so the consumer can't attach `animate.leave` to the host the way a declarative `@if` block can. Pass `animateEnter` / `animateLeave` (CSS class names) instead: the manager applies `animateEnter` on mount (via `animate.enter`) and, on `close()`, keeps the host mounted with `animateLeave` until its CSS animations / transitions finish before tearing down. `close()` still resolves its promise and flips `isClosed()` immediately. Only the visual teardown waits. A template `animate.leave` inside the opened component never runs, because Angular does not run leave animations across the outlet the manager mounts it through; a `[forDrawerBackdrop]` takes its exit class from `backdropAnimateLeave`. Set them once with `provideForDrawerDefaults({ animateEnter, animateLeave, backdropAnimateLeave })`; a per-`open()` value wins over the default. `ForDrawerManager` is provided in root and reads the defaults from the root injector, so a `provideForDrawerDefaults` on a lazy route or in a component's `providers` reaches a programmatic drawer only when that scope's injector is passed as `injector` on the `open()` config.
174
+ **Enter / exit animations.** A programmatic drawer is portaled to `document.body` and torn down imperatively, so the consumer can't attach `animate.leave` to the host the way a declarative `@if` block can. Pass `animateEnter` / `animateLeave` (CSS class names) instead: the manager applies `animateEnter` on mount (via `animate.enter`) and, on `close()`, keeps the host mounted with `animateLeave` until its CSS animations / transitions finish before tearing down. `close()` still resolves its promise and flips `isClosed()` immediately. Only the visual teardown waits. A template `animate.leave` inside the opened component never runs, because Angular does not run leave animations across the outlet the manager mounts it through; a `[forDrawerBackdrop]` takes its exit class from `backdropAnimateLeave`. Set them once with `provideForDrawerDefaults({ animateEnter, animateLeave, backdropAnimateLeave })`; a per-`open()` value wins over the default. `ForDrawerManager` is provided in root and reads the defaults from the root injector, so a `provideForDrawerDefaults` on a lazy route or in a component's `providers` reaches a programmatic drawer only when that scope's injector is passed as `injector` on the `open()` config; every key then applies, the scale and nested tunables included.
175
175
 
176
176
  <!-- snippet: fragment -->
177
177
 
@@ -204,9 +204,9 @@ this.#drawers.open(ConfirmDrawer, {
204
204
  });
205
205
  ```
206
206
 
207
- `activeSnapPointChange` fires with the landed snap on the mount-time default and every swipe release. It is the read-back that the declarative API exposes through `[(activeSnapPoint)]`. All subscriptions are released automatically when the drawer closes.
207
+ `activeSnapPointChange` fires with the landed snap on the mount-time default and on every release that changes the snap; a release that settles back on the active snap emits nothing. It is the read-back that the declarative API exposes through `[(activeSnapPoint)]`. All subscriptions are released automatically when the drawer closes.
208
208
 
209
- **Driving the active snap point.** `ForDrawerRef.setActiveSnapPoint(snap)` moves a snap-point drawer to a new snap after open and is the programmatic equivalent of _writing_ `[(activeSnapPoint)]` on the declarative `[forDrawer]`. `ref.activeSnapPoint()` is the matching reactive read (it also reflects the drawer's own internal transitions, namely the mount-time default and every swipe release):
209
+ **Driving the active snap point.** `ForDrawerRef.setActiveSnapPoint(snap)` moves a snap-point drawer to a new snap after open and is the programmatic equivalent of _writing_ `[(activeSnapPoint)]` on the declarative `[forDrawer]`. `ref.activeSnapPoint()` is the matching reactive read (it also reflects the drawer's own internal transitions, namely the mount-time default and every release that changes the snap):
210
210
 
211
211
  <!-- snippet: fragment -->
212
212
 
@@ -337,7 +337,7 @@ export class DrawerDefaultExample {
337
337
 
338
338
  ### Snapping sheet
339
339
 
340
- Drag the sheet between peek / half / full. Release resolves to the nearest snap by position (or dismisses past the lowest one). The consumer positions each snap via CSS keyed off `data-active-snap-point`; `data-dragging` disables the transition mid-gesture. `fadeFromIndex` fades the backdrop in only once the sheet reaches the half snap.
340
+ Drag the sheet between peek / half / full. A slow release rests on the nearest snap, a flick on the first snap past the release in the flick's direction, and either dismisses past the lowest one. The consumer positions each snap via CSS keyed off `data-active-snap-point`; `data-dragging` disables the transition mid-gesture. `fadeFromIndex` fades the backdrop in only once the sheet reaches the half snap.
341
341
 
342
342
  ### Receding page
343
343
 
@@ -385,7 +385,7 @@ Press the button: the manager opens the confirmation as a bottom drawer, and the
385
385
  | `interactOutside` | `OutputEmitterRef<VetoableNativeEvent<PointerEvent \| FocusEvent>>` | Output. Composite, vetoed by either specific event.<br>**Default:** — |
386
386
  | `swipeStart` | `OutputEmitterRef<ForDrawerSwipeEvent>` | Output. Fires once on the arming pointer move; `progress` is `0`.<br>**Default:** — |
387
387
  | `swipeMove` | `OutputEmitterRef<ForDrawerSwipeEvent>` | Output. Streams `progress` ∈ [0,1] and the originating `PointerEvent`.<br>**Default:** — |
388
- | `swipeEnd` | `OutputEmitterRef<ForDrawerSwipeEndEvent>` | Output. `willClose`, `nextSnapPoint`. Directive already updated state.<br>**Default:** — |
388
+ | `swipeEnd` | `OutputEmitterRef<ForDrawerSwipeEndEvent>` | Output. `willClose`, `nextSnapPoint`. Fires once `activeSnapPoint` holds `nextSnapPoint`, and before `(dismiss)` on a dismissing release.<br>**Default:** — |
389
389
  | `swipeCancel` | `OutputEmitterRef<ForDrawerSwipeEvent>` | Output. `pointercancel` or mid-gesture direction abort; the surface springs back.<br>**Default:** — |
390
390
 
391
391
  `ForDrawerCloseReason`: `'escape' | 'backdrop' | 'pointerDownOutside' | 'focusOutside' | 'closeButton' | 'swipe' | 'programmatic'`.
@@ -394,15 +394,15 @@ Enter and exit animations are not inputs. On a declarative drawer, put `animate.
394
394
 
395
395
  > **The declarative and imperative surfaces spell this differently, on purpose.** The output is `(dismiss)`, because an output named `close` would collide with the native DOM event and break any wrapper re-exposing it through `hostDirectives`. Nothing else changes name: the imperative handle method is `ForDrawerRef.close()`, the directive selector is `[forDrawerClose]`, and the payload type is `ForDrawerCloseReason`.
396
396
 
397
- | Data attribute | Values |
398
- | ------------------------ | -------------------------------------------- |
399
- | `data-state` | `open` |
400
- | `data-side` | `top` \| `right` \| `bottom` \| `left` |
401
- | `data-active-snap-point` | the active snap point stringified, or absent |
402
- | `data-dragging` | present / absent |
403
- | `data-scale-background` | present / absent |
404
- | `data-depth` | `0` (root) \| `1` (first child) \| … |
405
- | `data-state-nested` | present / absent |
397
+ | Data attribute | Values |
398
+ | ------------------------ | ----------------------------------------------------------- |
399
+ | `data-state` | `open` |
400
+ | `data-side` | `top` \| `right` \| `bottom` \| `left` |
401
+ | `data-active-snap-point` | the active snap point as written in `snapPoints`, or absent |
402
+ | `data-dragging` | present / absent |
403
+ | `data-scale-background` | present / absent |
404
+ | `data-depth` | `0` (root) \| `1` (first child) \| … |
405
+ | `data-state-nested` | present / absent |
406
406
 
407
407
  ### `ForDrawerBackdrop`
408
408
 
@@ -441,9 +441,13 @@ Enter and exit animations are not inputs. On a declarative drawer, put `animate.
441
441
  Three accepted shapes:
442
442
 
443
443
  - `number ∈ [0, 1]`: fraction of the dismissal-axis dimension.
444
- - `'NN%'`: equivalent to a fraction (`'50%' === 0.5`).
444
+ - `'NN%'`: a percentage of that dimension, so `'50%'` rests where `0.5` does.
445
445
  - `'NNpx'`: absolute pixel size measured from the anchored edge.
446
446
 
447
+ The number in front of `%` or `px` is any finite number `Number()` reads, which is what the `ForDrawerSnapPoint` type accepts (`'.5%'` and `'1e2px'` included).
448
+
449
+ A snap point is identified by its value as written. `activeSnapPoint` is matched against `snapPoints` by identity and `data-active-snap-point` reflects the value as written, so `'50%'` and `0.5` are two different snap points even though they rest at the same place. Seed `[(activeSnapPoint)]` and key your CSS with the spelling your `snapPoints` use.
450
+
447
451
  Pass them in **strictly increasing** order (closest-to-edge first); in dev mode the directive throws `FORCDK-DRAWER-009` otherwise. Mixed units (`'200px'` next to `0.5`) can only be ordered against the live drawer size, so they are re-checked on first measurement and fail with `FORCDK-DRAWER-010`, which names the offending point and the dimension it resolved against. `fadeFromIndex` must be a valid index into `snapPoints`.
448
452
 
449
453
  ```html
@@ -456,9 +460,9 @@ Pass them in **strictly increasing** order (closest-to-edge first); in dev mode
456
460
  ></div>
457
461
  ```
458
462
 
459
- The three snaps read peek → mid → full; `snap` is the current one, written on every swipe release; the backdrop fades once the active snap reaches the second index.
463
+ The three snaps read peek → mid → full; `snap` is the current one, written on every release that changes the snap; the backdrop fades once the active snap reaches the second index.
460
464
 
461
- The `model<>()` change emitter (`(activeSnapPointChange)`) fires on internal transitions (the mount-time default and every swipe release), and stays silent on consumer writes through `[(activeSnapPoint)]`.
465
+ The `model<>()` change emitter (`(activeSnapPointChange)`) fires on internal transitions (the mount-time default and every release that changes the snap), and stays silent on consumer writes through `[(activeSnapPoint)]`.
462
466
 
463
467
  ### Positioning the snaps (CSS contract)
464
468
 
@@ -485,7 +489,7 @@ For a seamless release, transition **both** `translate` and your snap-position p
485
489
  .sheet[data-active-snap-point='148px'] {
486
490
  bottom: calc(148px - 80vh);
487
491
  }
488
- .sheet[data-active-snap-point='0.5'] {
492
+ .sheet[data-active-snap-point='50%'] {
489
493
  bottom: -40vh;
490
494
  }
491
495
  .sheet[data-active-snap-point='1'] {
@@ -495,7 +499,7 @@ For a seamless release, transition **both** `translate` and your snap-position p
495
499
 
496
500
  ### Backdrop swipe-fade (CSS contract)
497
501
 
498
- `[forDrawerBackdrop]` publishes the live swipe progress _toward the anchored edge_ as the **`--for-drawer-swipe-progress`** custom property (`0` at rest → `1` fully swiped off-screen) and mirrors the surface's **`data-dragging`** attribute. This drives the "backdrop fades out as you swipe to dismiss" cue with pure CSS and needs no `(swipeMove)` listener:
502
+ `[forDrawerBackdrop]` publishes the live swipe progress _toward the anchored edge_ as the **`--for-drawer-swipe-progress`** custom property (`0` at rest → `1` fully swiped off-screen, from whichever snap the drag started) and mirrors the surface's **`data-dragging`** attribute. This drives the "backdrop fades out as you swipe to dismiss" cue with pure CSS and needs no `(swipeMove)` listener:
499
503
 
500
504
  ```css
501
505
  .drawer-backdrop {
@@ -513,6 +517,7 @@ For a seamless release, transition **both** `translate` and your snap-position p
513
517
  ## Swipe-to-dismiss
514
518
 
515
519
  - Pointer drag toward the anchored edge translates the surface and resolves to the nearest snap (or a dismiss) on release.
520
+ - A flick, a release faster than 0.4 px/ms, resolves from where the surface is let go rather than from the snap the drag started on. Toward the edge it rests on the first snap below the release, or dismisses when there is none; away from the edge it rests on the first snap above the release, or on the largest one. A flick toward the edge from the lowest snap therefore dismisses short of `closeThreshold`, as any flick does without `snapPoints`.
516
521
  - With `snapPoints`, the drag is bidirectional: a drag **away** from the anchored edge grows the surface toward a larger snap (bounded by the largest snap), and a drag toward the edge shrinks it / dismisses past the lowest one. Without `snapPoints` the gesture is one-way (toward the edge to dismiss).
517
522
  - Dismissal and dragging are separate. With `swipeToDismiss` or `dismissible` set to `false`, a drawer with `snapPoints` still drags between them and stops at the lowest snap, and `(swipeEnd)` reports `willClose: false`; a drawer without `snapPoints` arms no drag at all.
518
523
  - `closeThreshold` (default `0.25`) is the fraction past which a release from the lowest snap dismisses. It is measured against that snap's own extent (not the full dimension), so a small "peek" snap stays dismissible without dragging it off-screen.
@@ -741,7 +746,7 @@ forty-cdk ships no styles: put your own class on each piece and key your CSS off
741
746
  | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
742
747
  | `--for-drawer-swipe-movement-x` | Written on `[forDrawer]` (the surface). Live swipe displacement in CSS px along the x axis (`0px` at rest; only the dismissal axis is ever non-zero). Compose with `translate: var(--for-drawer-swipe-movement-x, 0px) var(--for-drawer-swipe-movement-y, 0px)`. See [Positioning the snaps](#positioning-the-snaps-css-contract). |
743
748
  | `--for-drawer-swipe-movement-y` | Written on `[forDrawer]` (the surface). Live swipe displacement in CSS px along the y axis (`0px` at rest; only the dismissal axis is ever non-zero). Compose with `translate: var(--for-drawer-swipe-movement-x, 0px) var(--for-drawer-swipe-movement-y, 0px)`. See [Positioning the snaps](#positioning-the-snaps-css-contract). |
744
- | `--for-drawer-swipe-progress` | Written on `[forDrawerBackdrop]`. Swipe progress toward the anchored edge, a unitless fraction `0` (at rest) → `1` (fully swiped off-screen). Fade with `opacity: calc(1 - var(--for-drawer-swipe-progress, 0))`. See [Backdrop swipe-fade](#backdrop-swipe-fade-css-contract). |
749
+ | `--for-drawer-swipe-progress` | Written on `[forDrawerBackdrop]`. Swipe progress toward the anchored edge, a unitless fraction `0` (at rest) → `1` (fully swiped off-screen, from any snap). Fade with `opacity: calc(1 - var(--for-drawer-swipe-progress, 0))`. See [Backdrop swipe-fade](#backdrop-swipe-fade-css-contract). |
745
750
  | `--for-drawer-depth` | Written on `[forDrawer]` and `[forDrawerBackdrop]`. The drawer's nesting depth as a unitless integer, `0` for a root drawer, mirroring `data-depth`. Stack a nested drawer's backdrop above its parent with `z-index: calc(1009 + var(--for-drawer-depth) * 10)`. |
746
751
 
747
752
  ```css
@@ -112,7 +112,7 @@ function injectPressed(opts) {
112
112
  return computed(() => disabled?.() ? false : pressed());
113
113
  }
114
114
  var ForButton = class ForButton {
115
- #initialType = inject(ElementRef).nativeElement.getAttribute("type");
115
+ #host = inject(ElementRef).nativeElement;
116
116
  #fieldset = inject(FOR_FIELDSET_CONTEXT, { optional: true });
117
117
  disabled = input(false, {
118
118
  ...ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {},
@@ -126,9 +126,11 @@ var ForButton = class ForButton {
126
126
  hovered = injectHovered({ disabled: this.effectiveDisabled });
127
127
  pressed = injectPressed({ disabled: this.effectiveDisabled });
128
128
  focusVisible = computed(() => this.#focused() && this.#keyboardModality(), ...ngDevMode ? [{ debugName: "focusVisible" }] : /* istanbul ignore next */ []);
129
- resolvedType = this.#initialType ?? (this.#activation.nativeButton ? "button" : null);
130
129
  resolvedRole = this.#activation.nativeButton ? null : "button";
131
130
  resolvedTabindex = this.#activation.tabindex;
131
+ constructor() {
132
+ if (this.#activation.nativeButton && !this.#host.hasAttribute("type")) this.#host.setAttribute("type", "button");
133
+ }
132
134
  onClick(event) {
133
135
  if (this.effectiveDisabled()) {
134
136
  event.preventDefault();
@@ -181,7 +183,6 @@ var ForButton = class ForButton {
181
183
  "focusout": "onFocusOut()"
182
184
  },
183
185
  properties: {
184
- "attr.type": "resolvedType",
185
186
  "attr.role": "resolvedRole",
186
187
  "attr.tabindex": "resolvedTabindex()",
187
188
  "attr.aria-disabled": "effectiveDisabled() ? 'true' : null",
@@ -206,7 +207,6 @@ i0.ɵɵngDeclareClassMetadata({
206
207
  selector: "[forButton]",
207
208
  exportAs: "forButton",
208
209
  host: {
209
- "[attr.type]": "resolvedType",
210
210
  "[attr.role]": "resolvedRole",
211
211
  "[attr.tabindex]": "resolvedTabindex()",
212
212
  "[attr.aria-disabled]": "effectiveDisabled() ? 'true' : null",
@@ -222,6 +222,7 @@ i0.ɵɵngDeclareClassMetadata({
222
222
  }
223
223
  }]
224
224
  }],
225
+ ctorParameters: () => [],
225
226
  propDecorators: {
226
227
  disabled: [{
227
228
  type: i0.Input,
@@ -1 +1 @@
1
- {"version":3,"file":"forty-cdk-button.mjs","names":[],"sources":["../../../projects/forty-cdk/button/src/modality.ts","../../../projects/forty-cdk/button/src/focus-visible.ts","../../../projects/forty-cdk/button/src/hovered.ts","../../../projects/forty-cdk/button/src/pressed.ts","../../../projects/forty-cdk/button/src/button.ts"],"sourcesContent":["import { isPlatformBrowser } from '@angular/common';\nimport {\n DOCUMENT,\n DestroyRef,\n Injectable,\n PLATFORM_ID,\n inject,\n signal,\n type Signal,\n} from '@angular/core';\n\n/**\n * Application-scoped tracker for the last input modality the user employed —\n * the in-house implementation of input-modality (pointer vs keyboard)\n * detection. Created once per Angular bootstrap (one per SSR\n * request), tied to the root injector lifetime.\n *\n * It installs exactly **one** capture-phase `keydown` and **one** capture-phase\n * `pointerdown` listener on `document`, regardless of how many primitives read\n * its state — every `injectFocusVisible()` consumer shares this single\n * singleton, so the document is never listened to more than once. Capture phase\n * is used so the modality is settled before any overlay content can stop\n * propagation.\n *\n * Why a service rather than module-level state:\n *\n * - SSR isolation: module-level globals leak between simultaneous server\n * requests in the same Node process. A `providedIn: 'root'` service is\n * instantiated per application injector.\n * - Bootstrap-safety: `TestBed.resetTestingModule()`, micro-frontend reloads,\n * and anything else that destroys `ApplicationRef` must not leave stale\n * `document` listeners behind. The listeners are registered in the\n * constructor against a single `AbortController` and dropped by one\n * `abort()` from `DestroyRef`.\n * - SSR safety: `document` is inaccessible on the server. The service is a\n * no-op when `PLATFORM_ID` is not the browser; `keyboard` stays `false` and\n * no listener is installed.\n *\n * Internal — not re-exported from `public-api.ts`.\n */\n@Injectable({ providedIn: 'root' })\nexport class InputModality {\n readonly #document = inject(DOCUMENT);\n readonly #isBrowser = isPlatformBrowser(inject(PLATFORM_ID));\n\n readonly #keyboard = signal(false);\n\n /**\n * `true` when the most recent input modality was the keyboard, `false` after\n * a pointer interaction (or before any interaction). Stays `false` on the\n * server. A `keydown` carrying a `Meta` / `Control` / `Alt` modifier is\n * treated as a shortcut rather than keyboard navigation and does **not** flip\n * this to `true` (matching the platform `:focus-visible` heuristic); `Shift`\n * alone still counts, since `Shift`+`Tab` is legitimate keyboard navigation.\n */\n readonly keyboard: Signal<boolean> = this.#keyboard.asReadonly();\n\n readonly #onKeyDown = (event: KeyboardEvent): void => {\n if (event.metaKey || event.ctrlKey || event.altKey) {\n return;\n }\n this.#keyboard.set(true);\n };\n readonly #onPointerDown = (): void => {\n this.#keyboard.set(false);\n };\n\n constructor() {\n if (!this.#isBrowser) {\n return;\n }\n const controller = new AbortController();\n const options = { capture: true, signal: controller.signal };\n this.#document.addEventListener('keydown', this.#onKeyDown, options);\n this.#document.addEventListener('pointerdown', this.#onPointerDown, options);\n\n inject(DestroyRef).onDestroy(() => controller.abort());\n }\n}\n","import { inject, type Signal } from '@angular/core';\n\nimport { InputModality } from './modality';\n\n/**\n * Returns a `Signal<boolean>` that is `true` while the last global input\n * modality was the keyboard, and `false` after a pointer interaction (or\n * before any interaction). This is the modality half of `:focus-visible`\n * styling: a consumer that wants a focus ring only on keyboard focus combines\n * this with the element's own focused state (`:focus`, or a `focusin` /\n * `focusout` listener) — e.g. `host: { '[attr.data-focus-visible]':\n * \"focused() && focusVisible() ? '' : null\" }`.\n *\n * Backed by the application-scoped {@link InputModality} singleton, so every\n * consumer shares a single capture-phase `keydown` / `pointerdown` listener on\n * `document` no matter how many call this.\n *\n * SSR-safe: on the server the backing service installs no listener and the\n * signal stays `false`. Must be called from an injection context.\n *\n * Internal — not re-exported from `public-api.ts`.\n */\nexport function injectFocusVisible(): Signal<boolean> {\n return inject(InputModality).keyboard;\n}\n","import { isPlatformBrowser } from '@angular/common';\nimport {\n DestroyRef,\n ElementRef,\n PLATFORM_ID,\n computed,\n inject,\n signal,\n type Signal,\n} from '@angular/core';\nimport { isNonTouchPointer } from 'forty-cdk/core';\n\n/** Options for {@link injectHovered}. */\nexport interface HoveredOptions {\n /**\n * When this signal reports `true`, the returned signal is forced to `false`\n * and no interaction can set it — a disabled control is never hovered.\n */\n disabled?: Signal<boolean>;\n}\n\n/**\n * Returns a `Signal<boolean>` reflecting whether a pointing device is currently\n * hovering the host element. State is set on `pointerenter` and cleared on\n * `pointerleave`.\n *\n * Touch is suppressed through the shared `isNonTouchPointer` predicate: a\n * `pointerenter` whose `pointerType` is `'touch'` does not set the hovered\n * state, so the emulated mouse-enter a tap produces on a touchscreen never\n * leaves the element stuck in a hovered state after the finger lifts. Only\n * `'mouse'` / `'pen'` pointers report hover — this is the pen-inclusive hover\n * vocabulary, not the menu family's mouse-only `isHoverCapablePointer`.\n *\n * Attaches `pointerenter` / `pointerleave` listeners to the host element. The\n * `disabled` option short-circuits the result reactively (a pure `computed`,\n * never an `effect`-written signal) and also stops hover from arming while\n * disabled.\n *\n * SSR-safe: on the server no listener is attached and the signal stays\n * `false`. Must be called from an injection context (injects `ElementRef`,\n * `PLATFORM_ID`, `DestroyRef`).\n *\n * Internal — not re-exported from `public-api.ts`.\n */\nexport function injectHovered(opts?: HoveredOptions): Signal<boolean> {\n const disabled = opts?.disabled;\n const isBrowser = isPlatformBrowser(inject(PLATFORM_ID));\n if (!isBrowser) {\n return signal(false).asReadonly();\n }\n\n const el = inject<ElementRef<HTMLElement>>(ElementRef).nativeElement;\n const hovered = signal(false);\n\n const onPointerEnter = (event: PointerEvent): void => {\n if (!isNonTouchPointer(event)) {\n return;\n }\n if (disabled?.()) {\n return;\n }\n hovered.set(true);\n };\n const onPointerLeave = (): void => {\n hovered.set(false);\n };\n\n const controller = new AbortController();\n const options = { signal: controller.signal };\n\n el.addEventListener('pointerenter', onPointerEnter, options);\n el.addEventListener('pointerleave', onPointerLeave, options);\n\n inject(DestroyRef).onDestroy(() => controller.abort());\n\n return computed(() => (disabled?.() ? false : hovered()));\n}\n","import { isPlatformBrowser } from '@angular/common';\nimport {\n DestroyRef,\n ElementRef,\n PLATFORM_ID,\n computed,\n inject,\n signal,\n type Signal,\n} from '@angular/core';\n\n/** Options for {@link injectPressed}. */\nexport interface PressedOptions {\n /**\n * When this signal reports `true`, the returned signal is forced to `false`\n * and no interaction can set it — a disabled control is never pressed.\n */\n disabled?: Signal<boolean>;\n}\n\n/**\n * Returns a `Signal<boolean>` reflecting whether the host element is currently\n * being pressed — held down via the primary pointer button, or via the\n * <kbd>Enter</kbd> / <kbd>Space</kbd> key while focused. The state clears on\n * pointer release, the pointer leaving the element mid-press, key release, or\n * `blur` (covers focus leaving the element while a key is held).\n *\n * Attaches `pointerdown` / `pointerup` / `pointerleave` / `keydown` / `keyup` /\n * `blur` listeners to the host element. The `disabled` option short-circuits\n * the result reactively (a pure `computed`, never an `effect`-written signal)\n * and also stops a fresh press from arming while disabled.\n *\n * SSR-safe: on the server no listener is attached and the signal stays\n * `false`. Must be called from an injection context (injects `ElementRef`,\n * `PLATFORM_ID`, `DestroyRef`).\n *\n * Internal — not re-exported from `public-api.ts`.\n */\nexport function injectPressed(opts?: PressedOptions): Signal<boolean> {\n const disabled = opts?.disabled;\n const isBrowser = isPlatformBrowser(inject(PLATFORM_ID));\n if (!isBrowser) {\n return signal(false).asReadonly();\n }\n\n const el = inject<ElementRef<HTMLElement>>(ElementRef).nativeElement;\n const pressed = signal(false);\n\n const onPointerDown = (event: PointerEvent): void => {\n if (event.pointerType === 'mouse' && event.button !== 0) {\n return;\n }\n if (disabled?.()) {\n return;\n }\n pressed.set(true);\n };\n const onPointerEnd = (): void => {\n pressed.set(false);\n };\n const onKeyDown = (event: KeyboardEvent): void => {\n if (event.key !== 'Enter' && event.key !== ' ') {\n return;\n }\n if (disabled?.()) {\n return;\n }\n pressed.set(true);\n };\n const onKeyUp = (event: KeyboardEvent): void => {\n if (event.key !== 'Enter' && event.key !== ' ') {\n return;\n }\n pressed.set(false);\n };\n const onBlur = (): void => {\n pressed.set(false);\n };\n\n const controller = new AbortController();\n const options = { signal: controller.signal };\n\n el.addEventListener('pointerdown', onPointerDown, options);\n el.addEventListener('pointerup', onPointerEnd, options);\n el.addEventListener('pointerleave', onPointerEnd, options);\n el.addEventListener('keydown', onKeyDown, options);\n el.addEventListener('keyup', onKeyUp, options);\n el.addEventListener('blur', onBlur, options);\n\n inject(DestroyRef).onDestroy(() => controller.abort());\n\n return computed(() => (disabled?.() ? false : pressed()));\n}\n","import {\n Directive,\n ElementRef,\n booleanAttribute,\n computed,\n inject,\n input,\n output,\n signal,\n} from '@angular/core';\nimport { FOR_FIELDSET_CONTEXT, injectSyntheticActivation } from 'forty-cdk/core';\n\nimport { injectFocusVisible } from './focus-visible';\nimport { injectHovered } from './hovered';\nimport { injectPressed } from './pressed';\n\n/**\n * Headless implementation of the [WAI-ARIA Button pattern](https://www.w3.org/WAI/ARIA/apg/patterns/button/).\n *\n * Works on a native `<button>` host and on any arbitrary host element (e.g. `<div>`, `<span>`).\n * On a native button the platform handles Enter/Space → click synthesis and no `role` or\n * `tabindex` is emitted (the platform owns those). On a non-button host, `role=\"button\"` and\n * `tabindex=\"0\"` are applied, and the directive synthesizes a click through its single `onClick`\n * path: Enter activates on `keydown`, while Space activates on `keyup` (its `keydown` always\n * calls `preventDefault()` to stop the page scrolling, even when disabled) — matching native\n * button and APG behavior.\n *\n * Disabled stays focusable: per the APG a disabled button must remain reachable by assistive\n * technology, so the native `disabled` attribute is never set. Instead `aria-disabled=\"true\"` and\n * `data-disabled=\"\"` are reflected and the activation handler becomes a no-op. `disabled` composes\n * with a surrounding `[forFieldset]`: a disabled group disables the button too (`aria-disabled` +\n * `data-disabled`, activation suppressed), which is what makes a non-native host (`<div forButton>`)\n * behave like a native button inside a native `<fieldset disabled>`.\n *\n * Interaction state is reflected as `data-pressed`, `data-hovered`, and `data-focus-visible`\n * (present/absent boolean attributes). There is no `data-state` — this primitive has no\n * open/closed or checked/unchecked logical state.\n *\n * @example\n * ```html\n * <!-- Native button — platform handles Enter/Space -->\n * <button forButton (activate)=\"doSomething()\">Click me</button>\n *\n * <!-- Non-button host — role, tabindex, and keyboard handling added automatically -->\n * <div forButton (activate)=\"doSomething()\">Click me</div>\n *\n * <!-- Disabled: stays focusable, activation is a no-op -->\n * <button forButton [disabled]=\"true\" (activate)=\"doSomething()\">Disabled</button>\n * ```\n */\n@Directive({\n selector: '[forButton]',\n exportAs: 'forButton',\n host: {\n '[attr.type]': 'resolvedType',\n '[attr.role]': 'resolvedRole',\n '[attr.tabindex]': 'resolvedTabindex()',\n '[attr.aria-disabled]': \"effectiveDisabled() ? 'true' : null\",\n '[attr.data-disabled]': \"effectiveDisabled() ? '' : null\",\n '[attr.data-pressed]': \"pressed() ? '' : null\",\n '[attr.data-hovered]': \"hovered() ? '' : null\",\n '[attr.data-focus-visible]': \"focusVisible() ? '' : null\",\n '(click)': 'onClick($event)',\n '(keydown)': 'onKeydown($event)',\n '(keyup)': 'onKeyup($event)',\n '(focusin)': 'onFocusIn()',\n '(focusout)': 'onFocusOut()',\n },\n})\nexport class ForButton {\n readonly #initialType =\n inject<ElementRef<HTMLElement>>(ElementRef).nativeElement.getAttribute('type');\n readonly #fieldset = inject(FOR_FIELDSET_CONTEXT, { optional: true });\n\n /**\n * When `true`, activation (click, Enter, Space) is suppressed and the element\n * reflects `aria-disabled=\"true\"` + `data-disabled=\"\"`. The element remains\n * focusable so assistive technology can announce it. Read\n * {@link effectiveDisabled} for the value that actually gates behavior — it\n * also folds in a surrounding disabled `[forFieldset]`.\n */\n readonly disabled = input(false, { transform: booleanAttribute });\n\n /**\n * The button's own {@link disabled} OR'd with a surrounding disabled\n * `[forFieldset]`. This is what gates activation and drives `aria-disabled` /\n * `data-disabled`: a native `<fieldset disabled>` never reaches a non-native\n * host (`<div forButton>`), so the group's disabled state has to compose in\n * here. Mirrors `FormUiControlBase.effectiveDisabled`, so a `[forButton]` and\n * a `[forSwitch]` inside the same disabled group behave identically.\n */\n readonly effectiveDisabled = computed(\n () => this.disabled() || (this.#fieldset?.disabled() ?? false),\n );\n\n /**\n * Emitted once per user activation: a pointer click on any host, or Enter / Space\n * on a non-native-button host (native buttons synthesize click from keyboard).\n */\n readonly activate = output<void>();\n\n readonly #focused = signal(false);\n readonly #activation = injectSyntheticActivation({ disabled: this.effectiveDisabled });\n readonly #keyboardModality = injectFocusVisible();\n private readonly hovered = injectHovered({ disabled: this.effectiveDisabled });\n private readonly pressed = injectPressed({ disabled: this.effectiveDisabled });\n private readonly focusVisible = computed(() => this.#focused() && this.#keyboardModality());\n\n private readonly resolvedType =\n this.#initialType ?? (this.#activation.nativeButton ? 'button' : null);\n private readonly resolvedRole = this.#activation.nativeButton ? null : 'button';\n private readonly resolvedTabindex = this.#activation.tabindex;\n\n private onClick(event: MouseEvent): void {\n if (this.effectiveDisabled()) {\n event.preventDefault();\n event.stopImmediatePropagation();\n return;\n }\n this.activate.emit();\n }\n\n private onKeydown(event: KeyboardEvent): void {\n this.#activation.keydown(event);\n }\n\n private onKeyup(event: KeyboardEvent): void {\n this.#activation.keyup(event);\n }\n\n private onFocusIn(): void {\n this.#focused.set(true);\n }\n\n private onFocusOut(): void {\n this.#focused.set(false);\n this.#activation.reset();\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"forty-cdk-button.mjs","names":[],"sources":["../../../projects/forty-cdk/button/src/modality.ts","../../../projects/forty-cdk/button/src/focus-visible.ts","../../../projects/forty-cdk/button/src/hovered.ts","../../../projects/forty-cdk/button/src/pressed.ts","../../../projects/forty-cdk/button/src/button.ts"],"sourcesContent":["import { isPlatformBrowser } from '@angular/common';\nimport {\n DOCUMENT,\n DestroyRef,\n Injectable,\n PLATFORM_ID,\n inject,\n signal,\n type Signal,\n} from '@angular/core';\n\n/**\n * Application-scoped tracker for the last input modality the user employed —\n * the in-house implementation of input-modality (pointer vs keyboard)\n * detection. Created once per Angular bootstrap (one per SSR\n * request), tied to the root injector lifetime.\n *\n * It installs exactly **one** capture-phase `keydown` and **one** capture-phase\n * `pointerdown` listener on `document`, regardless of how many primitives read\n * its state — every `injectFocusVisible()` consumer shares this single\n * singleton, so the document is never listened to more than once. Capture phase\n * is used so the modality is settled before any overlay content can stop\n * propagation.\n *\n * Why a service rather than module-level state:\n *\n * - SSR isolation: module-level globals leak between simultaneous server\n * requests in the same Node process. A `providedIn: 'root'` service is\n * instantiated per application injector.\n * - Bootstrap-safety: `TestBed.resetTestingModule()`, micro-frontend reloads,\n * and anything else that destroys `ApplicationRef` must not leave stale\n * `document` listeners behind. The listeners are registered in the\n * constructor against a single `AbortController` and dropped by one\n * `abort()` from `DestroyRef`.\n * - SSR safety: `document` is inaccessible on the server. The service is a\n * no-op when `PLATFORM_ID` is not the browser; `keyboard` stays `false` and\n * no listener is installed.\n *\n * Internal — not re-exported from `public-api.ts`.\n */\n@Injectable({ providedIn: 'root' })\nexport class InputModality {\n readonly #document = inject(DOCUMENT);\n readonly #isBrowser = isPlatformBrowser(inject(PLATFORM_ID));\n\n readonly #keyboard = signal(false);\n\n /**\n * `true` when the most recent input modality was the keyboard, `false` after\n * a pointer interaction (or before any interaction). Stays `false` on the\n * server. A `keydown` carrying a `Meta` / `Control` / `Alt` modifier is\n * treated as a shortcut rather than keyboard navigation and does **not** flip\n * this to `true` (matching the platform `:focus-visible` heuristic); `Shift`\n * alone still counts, since `Shift`+`Tab` is legitimate keyboard navigation.\n */\n readonly keyboard: Signal<boolean> = this.#keyboard.asReadonly();\n\n readonly #onKeyDown = (event: KeyboardEvent): void => {\n if (event.metaKey || event.ctrlKey || event.altKey) {\n return;\n }\n this.#keyboard.set(true);\n };\n readonly #onPointerDown = (): void => {\n this.#keyboard.set(false);\n };\n\n constructor() {\n if (!this.#isBrowser) {\n return;\n }\n const controller = new AbortController();\n const options = { capture: true, signal: controller.signal };\n this.#document.addEventListener('keydown', this.#onKeyDown, options);\n this.#document.addEventListener('pointerdown', this.#onPointerDown, options);\n\n inject(DestroyRef).onDestroy(() => controller.abort());\n }\n}\n","import { inject, type Signal } from '@angular/core';\n\nimport { InputModality } from './modality';\n\n/**\n * Returns a `Signal<boolean>` that is `true` while the last global input\n * modality was the keyboard, and `false` after a pointer interaction (or\n * before any interaction). This is the modality half of `:focus-visible`\n * styling: a consumer that wants a focus ring only on keyboard focus combines\n * this with the element's own focused state (`:focus`, or a `focusin` /\n * `focusout` listener) — e.g. `host: { '[attr.data-focus-visible]':\n * \"focused() && focusVisible() ? '' : null\" }`.\n *\n * Backed by the application-scoped {@link InputModality} singleton, so every\n * consumer shares a single capture-phase `keydown` / `pointerdown` listener on\n * `document` no matter how many call this.\n *\n * SSR-safe: on the server the backing service installs no listener and the\n * signal stays `false`. Must be called from an injection context.\n *\n * Internal — not re-exported from `public-api.ts`.\n */\nexport function injectFocusVisible(): Signal<boolean> {\n return inject(InputModality).keyboard;\n}\n","import { isPlatformBrowser } from '@angular/common';\nimport {\n DestroyRef,\n ElementRef,\n PLATFORM_ID,\n computed,\n inject,\n signal,\n type Signal,\n} from '@angular/core';\nimport { isNonTouchPointer } from 'forty-cdk/core';\n\n/** Options for {@link injectHovered}. */\nexport interface HoveredOptions {\n /**\n * When this signal reports `true`, the returned signal is forced to `false`\n * and no interaction can set it — a disabled control is never hovered.\n */\n disabled?: Signal<boolean>;\n}\n\n/**\n * Returns a `Signal<boolean>` reflecting whether a pointing device is currently\n * hovering the host element. State is set on `pointerenter` and cleared on\n * `pointerleave`.\n *\n * Touch is suppressed through the shared `isNonTouchPointer` predicate: a\n * `pointerenter` whose `pointerType` is `'touch'` does not set the hovered\n * state, so the emulated mouse-enter a tap produces on a touchscreen never\n * leaves the element stuck in a hovered state after the finger lifts. Only\n * `'mouse'` / `'pen'` pointers report hover — this is the pen-inclusive hover\n * vocabulary, not the menu family's mouse-only `isHoverCapablePointer`.\n *\n * Attaches `pointerenter` / `pointerleave` listeners to the host element. The\n * `disabled` option short-circuits the result reactively (a pure `computed`,\n * never an `effect`-written signal) and also stops hover from arming while\n * disabled.\n *\n * SSR-safe: on the server no listener is attached and the signal stays\n * `false`. Must be called from an injection context (injects `ElementRef`,\n * `PLATFORM_ID`, `DestroyRef`).\n *\n * Internal — not re-exported from `public-api.ts`.\n */\nexport function injectHovered(opts?: HoveredOptions): Signal<boolean> {\n const disabled = opts?.disabled;\n const isBrowser = isPlatformBrowser(inject(PLATFORM_ID));\n if (!isBrowser) {\n return signal(false).asReadonly();\n }\n\n const el = inject<ElementRef<HTMLElement>>(ElementRef).nativeElement;\n const hovered = signal(false);\n\n const onPointerEnter = (event: PointerEvent): void => {\n if (!isNonTouchPointer(event)) {\n return;\n }\n if (disabled?.()) {\n return;\n }\n hovered.set(true);\n };\n const onPointerLeave = (): void => {\n hovered.set(false);\n };\n\n const controller = new AbortController();\n const options = { signal: controller.signal };\n\n el.addEventListener('pointerenter', onPointerEnter, options);\n el.addEventListener('pointerleave', onPointerLeave, options);\n\n inject(DestroyRef).onDestroy(() => controller.abort());\n\n return computed(() => (disabled?.() ? false : hovered()));\n}\n","import { isPlatformBrowser } from '@angular/common';\nimport {\n DestroyRef,\n ElementRef,\n PLATFORM_ID,\n computed,\n inject,\n signal,\n type Signal,\n} from '@angular/core';\n\n/** Options for {@link injectPressed}. */\nexport interface PressedOptions {\n /**\n * When this signal reports `true`, the returned signal is forced to `false`\n * and no interaction can set it — a disabled control is never pressed.\n */\n disabled?: Signal<boolean>;\n}\n\n/**\n * Returns a `Signal<boolean>` reflecting whether the host element is currently\n * being pressed — held down via the primary pointer button, or via the\n * <kbd>Enter</kbd> / <kbd>Space</kbd> key while focused. The state clears on\n * pointer release, the pointer leaving the element mid-press, key release, or\n * `blur` (covers focus leaving the element while a key is held).\n *\n * Attaches `pointerdown` / `pointerup` / `pointerleave` / `keydown` / `keyup` /\n * `blur` listeners to the host element. The `disabled` option short-circuits\n * the result reactively (a pure `computed`, never an `effect`-written signal)\n * and also stops a fresh press from arming while disabled.\n *\n * SSR-safe: on the server no listener is attached and the signal stays\n * `false`. Must be called from an injection context (injects `ElementRef`,\n * `PLATFORM_ID`, `DestroyRef`).\n *\n * Internal — not re-exported from `public-api.ts`.\n */\nexport function injectPressed(opts?: PressedOptions): Signal<boolean> {\n const disabled = opts?.disabled;\n const isBrowser = isPlatformBrowser(inject(PLATFORM_ID));\n if (!isBrowser) {\n return signal(false).asReadonly();\n }\n\n const el = inject<ElementRef<HTMLElement>>(ElementRef).nativeElement;\n const pressed = signal(false);\n\n const onPointerDown = (event: PointerEvent): void => {\n if (event.pointerType === 'mouse' && event.button !== 0) {\n return;\n }\n if (disabled?.()) {\n return;\n }\n pressed.set(true);\n };\n const onPointerEnd = (): void => {\n pressed.set(false);\n };\n const onKeyDown = (event: KeyboardEvent): void => {\n if (event.key !== 'Enter' && event.key !== ' ') {\n return;\n }\n if (disabled?.()) {\n return;\n }\n pressed.set(true);\n };\n const onKeyUp = (event: KeyboardEvent): void => {\n if (event.key !== 'Enter' && event.key !== ' ') {\n return;\n }\n pressed.set(false);\n };\n const onBlur = (): void => {\n pressed.set(false);\n };\n\n const controller = new AbortController();\n const options = { signal: controller.signal };\n\n el.addEventListener('pointerdown', onPointerDown, options);\n el.addEventListener('pointerup', onPointerEnd, options);\n el.addEventListener('pointerleave', onPointerEnd, options);\n el.addEventListener('keydown', onKeyDown, options);\n el.addEventListener('keyup', onKeyUp, options);\n el.addEventListener('blur', onBlur, options);\n\n inject(DestroyRef).onDestroy(() => controller.abort());\n\n return computed(() => (disabled?.() ? false : pressed()));\n}\n","import {\n Directive,\n ElementRef,\n booleanAttribute,\n computed,\n inject,\n input,\n output,\n signal,\n} from '@angular/core';\nimport { FOR_FIELDSET_CONTEXT, injectSyntheticActivation } from 'forty-cdk/core';\n\nimport { injectFocusVisible } from './focus-visible';\nimport { injectHovered } from './hovered';\nimport { injectPressed } from './pressed';\n\n/**\n * Headless implementation of the [WAI-ARIA Button pattern](https://www.w3.org/WAI/ARIA/apg/patterns/button/).\n *\n * Works on a native `<button>` host and on any arbitrary host element (e.g. `<div>`, `<span>`).\n * On a native button the platform handles Enter/Space → click synthesis and no `role` or\n * `tabindex` is emitted (the platform owns those). On a non-button host, `role=\"button\"` and\n * `tabindex=\"0\"` are applied, and the directive synthesizes a click through its single `onClick`\n * path: Enter activates on `keydown`, while Space activates on `keyup` (its `keydown` always\n * calls `preventDefault()` to stop the page scrolling, even when disabled) — matching native\n * button and APG behavior.\n *\n * A native `<button>` with no `type` attribute gets `type=\"button\"`, so it never submits a form\n * by accident. A consumer `type` is never overwritten, whether it is a static attribute, an\n * `[attr.type]` binding or a `[type]` binding, and later writes to a binding are followed. A\n * binding that resolves to `null` removes the attribute, so the button falls back to the HTML\n * default (`submit` inside a form). A non-button host gets no `type`.\n *\n * Disabled stays focusable: per the APG a disabled button must remain reachable by assistive\n * technology, so the native `disabled` attribute is never set. Instead `aria-disabled=\"true\"` and\n * `data-disabled=\"\"` are reflected and the activation handler becomes a no-op. `disabled` composes\n * with a surrounding `[forFieldset]`: a disabled group disables the button too (`aria-disabled` +\n * `data-disabled`, activation suppressed), which is what makes a non-native host (`<div forButton>`)\n * behave like a native button inside a native `<fieldset disabled>`.\n *\n * Interaction state is reflected as `data-pressed`, `data-hovered`, and `data-focus-visible`\n * (present/absent boolean attributes). There is no `data-state` — this primitive has no\n * open/closed or checked/unchecked logical state.\n *\n * @example\n * ```html\n * <!-- Native button — platform handles Enter/Space -->\n * <button forButton (activate)=\"doSomething()\">Click me</button>\n *\n * <!-- Non-button host — role, tabindex, and keyboard handling added automatically -->\n * <div forButton (activate)=\"doSomething()\">Click me</div>\n *\n * <!-- Disabled: stays focusable, activation is a no-op -->\n * <button forButton [disabled]=\"true\" (activate)=\"doSomething()\">Disabled</button>\n * ```\n */\n@Directive({\n selector: '[forButton]',\n exportAs: 'forButton',\n host: {\n '[attr.role]': 'resolvedRole',\n '[attr.tabindex]': 'resolvedTabindex()',\n '[attr.aria-disabled]': \"effectiveDisabled() ? 'true' : null\",\n '[attr.data-disabled]': \"effectiveDisabled() ? '' : null\",\n '[attr.data-pressed]': \"pressed() ? '' : null\",\n '[attr.data-hovered]': \"hovered() ? '' : null\",\n '[attr.data-focus-visible]': \"focusVisible() ? '' : null\",\n '(click)': 'onClick($event)',\n '(keydown)': 'onKeydown($event)',\n '(keyup)': 'onKeyup($event)',\n '(focusin)': 'onFocusIn()',\n '(focusout)': 'onFocusOut()',\n },\n})\nexport class ForButton {\n readonly #host = inject<ElementRef<HTMLElement>>(ElementRef).nativeElement;\n readonly #fieldset = inject(FOR_FIELDSET_CONTEXT, { optional: true });\n\n /**\n * When `true`, activation (click, Enter, Space) is suppressed and the element\n * reflects `aria-disabled=\"true\"` + `data-disabled=\"\"`. The element remains\n * focusable so assistive technology can announce it. Read\n * {@link effectiveDisabled} for the value that actually gates behavior — it\n * also folds in a surrounding disabled `[forFieldset]`.\n */\n readonly disabled = input(false, { transform: booleanAttribute });\n\n /**\n * The button's own {@link disabled} OR'd with a surrounding disabled\n * `[forFieldset]`. This is what gates activation and drives `aria-disabled` /\n * `data-disabled`: a native `<fieldset disabled>` never reaches a non-native\n * host (`<div forButton>`), so the group's disabled state has to compose in\n * here. Mirrors `FormUiControlBase.effectiveDisabled`, so a `[forButton]` and\n * a `[forSwitch]` inside the same disabled group behave identically.\n */\n readonly effectiveDisabled = computed(\n () => this.disabled() || (this.#fieldset?.disabled() ?? false),\n );\n\n /**\n * Emitted once per user activation: a pointer click on any host, or Enter / Space\n * on a non-native-button host (native buttons synthesize click from keyboard).\n */\n readonly activate = output<void>();\n\n readonly #focused = signal(false);\n readonly #activation = injectSyntheticActivation({ disabled: this.effectiveDisabled });\n readonly #keyboardModality = injectFocusVisible();\n private readonly hovered = injectHovered({ disabled: this.effectiveDisabled });\n private readonly pressed = injectPressed({ disabled: this.effectiveDisabled });\n private readonly focusVisible = computed(() => this.#focused() && this.#keyboardModality());\n\n private readonly resolvedRole = this.#activation.nativeButton ? null : 'button';\n private readonly resolvedTabindex = this.#activation.tabindex;\n\n constructor() {\n if (this.#activation.nativeButton && !this.#host.hasAttribute('type')) {\n this.#host.setAttribute('type', 'button');\n }\n }\n\n private onClick(event: MouseEvent): void {\n if (this.effectiveDisabled()) {\n event.preventDefault();\n event.stopImmediatePropagation();\n return;\n }\n this.activate.emit();\n }\n\n private onKeydown(event: KeyboardEvent): void {\n this.#activation.keydown(event);\n }\n\n private onKeyup(event: KeyboardEvent): void {\n this.#activation.keyup(event);\n }\n\n private onFocusIn(): void {\n this.#focused.set(true);\n }\n\n private onFocusOut(): void {\n this.#focused.set(false);\n this.#activation.reset();\n }\n}\n"],"mappings":"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"}