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.
- package/README.md +4 -4
- package/avatar/README.md +1 -1
- package/breadcrumbs/README.md +2 -2
- package/button/README.md +12 -6
- package/calendar/README.md +1 -1
- package/checkbox/README.md +3 -3
- package/date-field/README.md +2 -2
- package/drag-drop/README.md +56 -8
- package/drawer/README.md +25 -20
- package/fesm2022/forty-cdk-button.mjs +5 -4
- package/fesm2022/forty-cdk-button.mjs.map +1 -1
- package/fesm2022/forty-cdk-core.mjs +103 -39
- package/fesm2022/forty-cdk-core.mjs.map +1 -1
- package/fesm2022/forty-cdk-defaults.mjs.map +1 -1
- package/fesm2022/forty-cdk-dialog.mjs +2 -0
- package/fesm2022/forty-cdk-dialog.mjs.map +1 -1
- package/fesm2022/forty-cdk-drag-drop.mjs +289 -63
- package/fesm2022/forty-cdk-drag-drop.mjs.map +1 -1
- package/fesm2022/forty-cdk-drawer.mjs +124 -47
- package/fesm2022/forty-cdk-drawer.mjs.map +1 -1
- package/fesm2022/forty-cdk-field.mjs +1 -1
- package/fesm2022/forty-cdk-field.mjs.map +1 -1
- package/fesm2022/forty-cdk-listbox.mjs.map +1 -1
- package/fesm2022/forty-cdk-navigation-menu.mjs +10 -20
- package/fesm2022/forty-cdk-navigation-menu.mjs.map +1 -1
- package/fesm2022/forty-cdk-number-input.mjs +18 -11
- package/fesm2022/forty-cdk-number-input.mjs.map +1 -1
- package/fesm2022/forty-cdk-pagination.mjs +8 -4
- package/fesm2022/forty-cdk-pagination.mjs.map +1 -1
- package/fesm2022/forty-cdk-pane-resizer.mjs +2 -2
- package/fesm2022/forty-cdk-pane-resizer.mjs.map +1 -1
- package/fesm2022/forty-cdk-select.mjs +16 -2
- package/fesm2022/forty-cdk-select.mjs.map +1 -1
- package/fesm2022/forty-cdk-stepper.mjs.map +1 -1
- package/fesm2022/forty-cdk-table.mjs +3 -3
- package/fesm2022/forty-cdk-table.mjs.map +1 -1
- package/fesm2022/forty-cdk-time-picker.mjs +15 -1
- package/fesm2022/forty-cdk-time-picker.mjs.map +1 -1
- package/fesm2022/forty-cdk-virtual-reorder.mjs +2 -1
- package/fesm2022/forty-cdk-virtual-reorder.mjs.map +1 -1
- package/fesm2022/forty-cdk-virtualization.mjs +72 -12
- package/fesm2022/forty-cdk-virtualization.mjs.map +1 -1
- package/field/README.md +9 -9
- package/fieldset/README.md +1 -1
- package/file-upload/README.md +1 -1
- package/input/README.md +2 -2
- package/listbox/README.md +59 -77
- package/navigation-menu/README.md +37 -34
- package/number-input/README.md +11 -7
- package/otp-input/README.md +2 -2
- package/package.json +1 -1
- package/pagination/README.md +2 -1
- package/pane-resizer/README.md +2 -2
- package/progress/README.md +14 -5
- package/radio-group/README.md +4 -4
- package/stepper/README.md +8 -8
- package/switch/README.md +11 -11
- package/table/README.md +1 -1
- package/time-field/README.md +2 -2
- package/time-picker/README.md +1 -0
- package/toggle/README.md +11 -2
- package/types/forty-cdk-button.d.ts +7 -1
- package/types/forty-cdk-core.d.ts +58 -29
- package/types/forty-cdk-defaults.d.ts +6 -5
- package/types/forty-cdk-drag-drop.d.ts +52 -8
- package/types/forty-cdk-drawer.d.ts +50 -29
- package/types/forty-cdk-field.d.ts +3 -4
- package/types/forty-cdk-listbox.d.ts +3 -3
- package/types/forty-cdk-navigation-menu.d.ts +33 -30
- package/types/forty-cdk-number-input.d.ts +15 -15
- package/types/forty-cdk-pagination.d.ts +8 -1
- package/types/forty-cdk-pane-resizer.d.ts +4 -3
- package/types/forty-cdk-select.d.ts +18 -5
- package/types/forty-cdk-stepper.d.ts +11 -7
- package/types/forty-cdk-table.d.ts +4 -3
- package/types/forty-cdk-time-picker.d.ts +21 -8
- package/types/forty-cdk-virtual-reorder.d.ts +5 -3
- package/types/forty-cdk-virtualization.d.ts +52 -17
- package/virtual-reorder/README.md +7 -1
- 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` |
|
|
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
|
-
|
|
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';
|
package/breadcrumbs/README.md
CHANGED
|
@@ -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
|
|
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
|
-
[
|
|
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
|
-
<
|
|
62
|
-
<
|
|
61
|
+
<div forFieldset [disabled]="locked()">
|
|
62
|
+
<span forFieldsetLegend>Account</span>
|
|
63
63
|
<button forButton (activate)="save()">Save</button>
|
|
64
|
-
</
|
|
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
|
|
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
|
-
[
|
|
127
|
+
.btn[data-disabled] {
|
|
122
128
|
opacity: 0.4;
|
|
123
129
|
pointer-events: none;
|
|
124
130
|
}
|
|
125
131
|
|
|
126
|
-
[
|
|
132
|
+
.btn[data-pressed] {
|
|
127
133
|
transform: scale(0.97);
|
|
128
134
|
}
|
|
129
135
|
```
|
package/calendar/README.md
CHANGED
|
@@ -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);
|
package/checkbox/README.md
CHANGED
|
@@ -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
|
|
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
|
| --------------- | ------------------------------------------- |
|
package/date-field/README.md
CHANGED
|
@@ -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
|
package/drag-drop/README.md
CHANGED
|
@@ -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
|
|
457
|
-
`transform: translate(-50%, -50%)`
|
|
458
|
-
`translate
|
|
459
|
-
|
|
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">
|
|
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
|
|
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
|
|
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.
|
|
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`.
|
|
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
|
|
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%'`:
|
|
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
|
|
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
|
|
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='
|
|
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
|
-
#
|
|
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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"}
|