forty-cdk 0.32.0 → 0.34.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/breakpoints/README.md +6 -2
- package/carousel/README.md +4 -3
- package/checkbox/README.md +1 -1
- package/combobox/README.md +11 -9
- package/date-field/README.md +14 -3
- package/date-picker/README.md +14 -8
- package/dialog/README.md +46 -25
- package/drag-drop/README.md +12 -5
- package/drawer/README.md +6 -5
- package/dropdown-menu/README.md +1 -1
- package/fesm2022/forty-cdk-accordion.mjs +1 -2
- package/fesm2022/forty-cdk-accordion.mjs.map +1 -1
- package/fesm2022/forty-cdk-avatar.mjs +6 -5
- package/fesm2022/forty-cdk-avatar.mjs.map +1 -1
- package/fesm2022/forty-cdk-breakpoints.mjs.map +1 -1
- package/fesm2022/forty-cdk-calendar.mjs +5 -8
- package/fesm2022/forty-cdk-calendar.mjs.map +1 -1
- package/fesm2022/forty-cdk-carousel.mjs +35 -28
- package/fesm2022/forty-cdk-carousel.mjs.map +1 -1
- package/fesm2022/forty-cdk-checkbox.mjs +3 -1
- package/fesm2022/forty-cdk-checkbox.mjs.map +1 -1
- package/fesm2022/forty-cdk-combobox.mjs +118 -47
- package/fesm2022/forty-cdk-combobox.mjs.map +1 -1
- package/fesm2022/forty-cdk-context-menu.mjs +9 -6
- package/fesm2022/forty-cdk-context-menu.mjs.map +1 -1
- package/fesm2022/forty-cdk-core-overlay.mjs +709 -351
- package/fesm2022/forty-cdk-core-overlay.mjs.map +1 -1
- package/fesm2022/forty-cdk-core.mjs +1274 -511
- package/fesm2022/forty-cdk-core.mjs.map +1 -1
- package/fesm2022/forty-cdk-date-field.mjs +71 -5
- package/fesm2022/forty-cdk-date-field.mjs.map +1 -1
- package/fesm2022/forty-cdk-date-picker.mjs +161 -67
- package/fesm2022/forty-cdk-date-picker.mjs.map +1 -1
- package/fesm2022/forty-cdk-defaults.mjs +49 -9
- package/fesm2022/forty-cdk-defaults.mjs.map +1 -1
- package/fesm2022/forty-cdk-dialog.mjs +14 -10
- package/fesm2022/forty-cdk-dialog.mjs.map +1 -1
- package/fesm2022/forty-cdk-drag-drop.mjs +16 -6
- package/fesm2022/forty-cdk-drag-drop.mjs.map +1 -1
- package/fesm2022/forty-cdk-drawer.mjs +104 -87
- package/fesm2022/forty-cdk-drawer.mjs.map +1 -1
- package/fesm2022/forty-cdk-dropdown-menu.mjs +7 -3
- package/fesm2022/forty-cdk-dropdown-menu.mjs.map +1 -1
- package/fesm2022/forty-cdk-field.mjs +23 -12
- package/fesm2022/forty-cdk-field.mjs.map +1 -1
- package/fesm2022/forty-cdk-fieldset.mjs +4 -6
- package/fesm2022/forty-cdk-fieldset.mjs.map +1 -1
- package/fesm2022/forty-cdk-file-upload.mjs +23 -11
- package/fesm2022/forty-cdk-file-upload.mjs.map +1 -1
- package/fesm2022/forty-cdk-hover-card.mjs +49 -66
- package/fesm2022/forty-cdk-hover-card.mjs.map +1 -1
- package/fesm2022/forty-cdk-listbox.mjs +48 -52
- package/fesm2022/forty-cdk-listbox.mjs.map +1 -1
- package/fesm2022/forty-cdk-menu.mjs +76 -55
- package/fesm2022/forty-cdk-menu.mjs.map +1 -1
- package/fesm2022/forty-cdk-menubar.mjs +16 -61
- package/fesm2022/forty-cdk-menubar.mjs.map +1 -1
- package/fesm2022/forty-cdk-meter.mjs +2 -8
- package/fesm2022/forty-cdk-meter.mjs.map +1 -1
- package/fesm2022/forty-cdk-navigation-menu.mjs +66 -49
- package/fesm2022/forty-cdk-navigation-menu.mjs.map +1 -1
- package/fesm2022/forty-cdk-number-input.mjs +19 -10
- package/fesm2022/forty-cdk-number-input.mjs.map +1 -1
- package/fesm2022/forty-cdk-otp-input.mjs +34 -21
- package/fesm2022/forty-cdk-otp-input.mjs.map +1 -1
- package/fesm2022/forty-cdk-pagination.mjs +28 -10
- package/fesm2022/forty-cdk-pagination.mjs.map +1 -1
- package/fesm2022/forty-cdk-pane-resizer.mjs +21 -5
- package/fesm2022/forty-cdk-pane-resizer.mjs.map +1 -1
- package/fesm2022/forty-cdk-popover.mjs +9 -11
- package/fesm2022/forty-cdk-popover.mjs.map +1 -1
- package/fesm2022/forty-cdk-radio-group.mjs +42 -16
- package/fesm2022/forty-cdk-radio-group.mjs.map +1 -1
- package/fesm2022/forty-cdk-scroll-area.mjs +15 -6
- package/fesm2022/forty-cdk-scroll-area.mjs.map +1 -1
- package/fesm2022/forty-cdk-select.mjs +75 -79
- package/fesm2022/forty-cdk-select.mjs.map +1 -1
- package/fesm2022/forty-cdk-slider.mjs +16 -4
- package/fesm2022/forty-cdk-slider.mjs.map +1 -1
- package/fesm2022/forty-cdk-stepper.mjs +20 -10
- package/fesm2022/forty-cdk-stepper.mjs.map +1 -1
- package/fesm2022/forty-cdk-switch.mjs +2 -1
- package/fesm2022/forty-cdk-switch.mjs.map +1 -1
- package/fesm2022/forty-cdk-table-virtualization.mjs +6 -0
- package/fesm2022/forty-cdk-table-virtualization.mjs.map +1 -1
- package/fesm2022/forty-cdk-table.mjs +211 -99
- package/fesm2022/forty-cdk-table.mjs.map +1 -1
- package/fesm2022/forty-cdk-tabs.mjs +1 -2
- package/fesm2022/forty-cdk-tabs.mjs.map +1 -1
- package/fesm2022/forty-cdk-testing.mjs +69 -35
- package/fesm2022/forty-cdk-testing.mjs.map +1 -1
- package/fesm2022/forty-cdk-time-field.mjs +95 -13
- package/fesm2022/forty-cdk-time-field.mjs.map +1 -1
- package/fesm2022/forty-cdk-time-picker.mjs +95 -50
- package/fesm2022/forty-cdk-time-picker.mjs.map +1 -1
- package/fesm2022/forty-cdk-toast.mjs +11 -4
- package/fesm2022/forty-cdk-toast.mjs.map +1 -1
- package/fesm2022/forty-cdk-toggle.mjs +20 -9
- package/fesm2022/forty-cdk-toggle.mjs.map +1 -1
- package/fesm2022/forty-cdk-toolbar.mjs +25 -9
- package/fesm2022/forty-cdk-toolbar.mjs.map +1 -1
- package/fesm2022/forty-cdk-tooltip.mjs +39 -67
- package/fesm2022/forty-cdk-tooltip.mjs.map +1 -1
- package/fesm2022/forty-cdk-tree.mjs +114 -95
- package/fesm2022/forty-cdk-tree.mjs.map +1 -1
- package/fesm2022/forty-cdk-virtual-reorder.mjs +19 -2
- package/fesm2022/forty-cdk-virtual-reorder.mjs.map +1 -1
- package/fesm2022/forty-cdk-virtualization.mjs +19 -5
- package/fesm2022/forty-cdk-virtualization.mjs.map +1 -1
- package/field/README.md +11 -11
- package/fieldset/README.md +2 -2
- package/file-upload/README.md +1 -1
- package/hover-card/README.md +6 -4
- package/input/README.md +2 -0
- package/listbox/README.md +16 -4
- package/menu/README.md +2 -2
- package/menubar/README.md +13 -10
- package/meter/README.md +1 -1
- package/navigation-menu/README.md +3 -2
- package/number-input/README.md +2 -0
- package/otp-input/README.md +7 -5
- package/package.json +1 -1
- package/pagination/README.md +4 -5
- package/pane-resizer/README.md +2 -2
- package/popover/README.md +1 -1
- package/radio-group/README.md +16 -16
- package/scroll-area/README.md +4 -4
- package/search/README.md +1 -1
- package/select/README.md +9 -6
- package/shared/README.md +14 -3
- package/slider/README.md +4 -4
- package/stepper/README.md +3 -3
- package/switch/README.md +1 -1
- package/table/README.md +48 -33
- package/tabs/README.md +1 -1
- package/testing/README.md +11 -11
- package/time-field/README.md +14 -3
- package/time-picker/README.md +8 -2
- package/toast/README.md +3 -1
- package/toggle/README.md +2 -2
- package/toolbar/README.md +1 -1
- package/tooltip/README.md +2 -2
- package/tree/README.md +27 -13
- package/types/forty-cdk-breakpoints.d.ts +1 -1
- package/types/forty-cdk-carousel.d.ts +11 -6
- package/types/forty-cdk-combobox.d.ts +57 -39
- package/types/forty-cdk-context-menu.d.ts +3 -0
- package/types/forty-cdk-core-overlay.d.ts +595 -440
- package/types/forty-cdk-core.d.ts +413 -170
- package/types/forty-cdk-date-field.d.ts +29 -0
- package/types/forty-cdk-date-picker.d.ts +52 -12
- package/types/forty-cdk-defaults.d.ts +93 -25
- package/types/forty-cdk-dialog.d.ts +28 -29
- package/types/forty-cdk-drag-drop.d.ts +9 -6
- package/types/forty-cdk-drawer.d.ts +64 -50
- package/types/forty-cdk-dropdown-menu.d.ts +6 -2
- package/types/forty-cdk-field.d.ts +24 -15
- package/types/forty-cdk-fieldset.d.ts +2 -2
- package/types/forty-cdk-file-upload.d.ts +17 -3
- package/types/forty-cdk-hover-card.d.ts +17 -6
- package/types/forty-cdk-listbox.d.ts +6 -3
- package/types/forty-cdk-menu.d.ts +49 -27
- package/types/forty-cdk-menubar.d.ts +16 -57
- package/types/forty-cdk-meter.d.ts +1 -1
- package/types/forty-cdk-navigation-menu.d.ts +17 -22
- package/types/forty-cdk-otp-input.d.ts +9 -3
- package/types/forty-cdk-pagination.d.ts +16 -6
- package/types/forty-cdk-pane-resizer.d.ts +16 -2
- package/types/forty-cdk-popover.d.ts +15 -17
- package/types/forty-cdk-radio-group.d.ts +13 -5
- package/types/forty-cdk-scroll-area.d.ts +11 -5
- package/types/forty-cdk-select.d.ts +36 -17
- package/types/forty-cdk-slider.d.ts +6 -5
- package/types/forty-cdk-stepper.d.ts +14 -10
- package/types/forty-cdk-table.d.ts +47 -8
- package/types/forty-cdk-testing.d.ts +31 -19
- package/types/forty-cdk-time-field.d.ts +53 -2
- package/types/forty-cdk-time-picker.d.ts +69 -8
- package/types/forty-cdk-toast.d.ts +11 -4
- package/types/forty-cdk-toggle.d.ts +5 -2
- package/types/forty-cdk-toolbar.d.ts +19 -2
- package/types/forty-cdk-tooltip.d.ts +12 -8
- package/types/forty-cdk-tree.d.ts +14 -8
- package/types/forty-cdk-virtualization.d.ts +16 -6
- package/virtual-reorder/README.md +1 -1
- package/virtualization/README.md +8 -4
package/breakpoints/README.md
CHANGED
|
@@ -109,6 +109,8 @@ The default map gives you fully-typed names out of the box (`up('md')` autocompl
|
|
|
109
109
|
|
|
110
110
|
```ts
|
|
111
111
|
// breakpoints.ts
|
|
112
|
+
import { injectBreakpoints } from 'forty-cdk/breakpoints';
|
|
113
|
+
|
|
112
114
|
export const appBreakpoints = {
|
|
113
115
|
mobile: 0,
|
|
114
116
|
tablet: 640,
|
|
@@ -116,12 +118,14 @@ export const appBreakpoints = {
|
|
|
116
118
|
desktop: 1280,
|
|
117
119
|
} as const;
|
|
118
120
|
|
|
119
|
-
declare module 'forty-cdk' {
|
|
121
|
+
declare module 'forty-cdk/breakpoints' {
|
|
120
122
|
interface BreakpointRegistry extends Record<keyof typeof appBreakpoints, true> {}
|
|
121
123
|
}
|
|
124
|
+
|
|
125
|
+
export const injectIsDesktop = () => injectBreakpoints().up('desktop');
|
|
122
126
|
```
|
|
123
127
|
|
|
124
|
-
Now `injectBreakpoints()` autocompletes `'mobile' | 'tablet' | 'laptop' | 'desktop'` across the whole app.
|
|
128
|
+
Now `injectBreakpoints()` autocompletes `'mobile' | 'tablet' | 'laptop' | 'desktop'` across the whole app, and `up('desktop')` compiles without a cast. Augment `'forty-cdk/breakpoints'`, the entry point that declares the registry: the bare `'forty-cdk'` package exports nothing, so a block naming it declares an unrelated interface and the names stay the Tailwind scale.
|
|
125
129
|
|
|
126
130
|
## API
|
|
127
131
|
|
package/carousel/README.md
CHANGED
|
@@ -483,8 +483,9 @@ Implements the [WAI-ARIA Carousel pattern](https://www.w3.org/WAI/ARIA/apg/patte
|
|
|
483
483
|
or localize the default format and both role descriptions app-wide with
|
|
484
484
|
`provideForCarouselDefaults` (see
|
|
485
485
|
[Localizing the default labels](#localizing-the-default-labels)).
|
|
486
|
-
-
|
|
487
|
-
accessibility tree and focus order.
|
|
486
|
+
- Slides outside the viewport receive `aria-hidden="true"` and `inert` to remove them from
|
|
487
|
+
the accessibility tree and focus order. The visible set follows the laid-out offset, so
|
|
488
|
+
`align` and `containScroll` decide which slides stay reachable.
|
|
488
489
|
- The indicator group should be labelled (e.g. `ariaLabel="Choose slide to display"`).
|
|
489
490
|
- The current indicator is marked with `aria-current="true"`.
|
|
490
491
|
- Prev/next buttons never use the native `disabled` attribute. At a boundary without `loop`
|
|
@@ -532,7 +533,7 @@ children, unless noted otherwise:
|
|
|
532
533
|
| Property | Host | Value | Notes |
|
|
533
534
|
| --------------------------------- | ----------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
534
535
|
| `--for-carousel-offset` | `[forCarousel]` | e.g. `-100%` | Pure arithmetic from `activeIndex`, `slidesPerView`, `align`. |
|
|
535
|
-
| `--for-carousel-active-index` | `[forCarousel]` | integer | Current `activeIndex
|
|
536
|
+
| `--for-carousel-active-index` | `[forCarousel]` | integer | Current `activeIndex`, clamped to the registered slides. |
|
|
536
537
|
| `--for-carousel-slide-count` | `[forCarousel]` | integer | Total registered slides. |
|
|
537
538
|
| `--for-carousel-slides-per-view` | `[forCarousel]` | integer | From the `slidesPerView` input. |
|
|
538
539
|
| `--for-carousel-viewport-width` | `[forCarousel]` | e.g. `640px` | Measured via `ResizeObserver`. Absent on the server and before first measurement. |
|
package/checkbox/README.md
CHANGED
|
@@ -149,7 +149,7 @@ export class DemoCheckout {
|
|
|
149
149
|
| `checked` | `model<boolean>` | Two-way bindable on/off. Required by `FormCheckboxControl`.<br>**Default:** — |
|
|
150
150
|
| `indeterminate` | `model<boolean>` | Two-way bindable. When true, `aria-checked="mixed"` regardless of `checked`. Click clears it. UI-only and not part of the form value.<br>**Default:** — |
|
|
151
151
|
| `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:** — |
|
|
152
|
-
| `name` | `input<string>` | Reflected on `name` (empty string omits the attribute).<br>**Default:** `''`
|
|
152
|
+
| `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:** `''` |
|
|
153
153
|
| `errors` | `input<readonly ValidationError.WithOptionalFieldTree[]>` | Validation errors fed by `[formField]`.<br>**Default:** — |
|
|
154
154
|
| `touched` | `model<boolean>` | Set to `true` on blur.<br>**Default:** — |
|
|
155
155
|
|
package/combobox/README.md
CHANGED
|
@@ -220,7 +220,7 @@ Input tables are not yet tabulated for this primitive. See the feature sections
|
|
|
220
220
|
| `[forComboboxChip]` | `data-disabled` | present / absent |
|
|
221
221
|
| `[forComboboxSeparator]` | `data-orientation` | `horizontal` \| `vertical` |
|
|
222
222
|
|
|
223
|
-
Focus stays on the `<input>` the whole time the listbox is open, so options never get `:focus`. `data-highlighted` is the canonical hook for styling the keyboard-active option. A mouse press anywhere on the popup that lands on no focusable element (padding, a group label, an empty or status row) does not move focus either, so typing and the arrow keys keep working after it; the trade-off is that popup text cannot be selected with the mouse, as in a native combobox. A press on a
|
|
223
|
+
Focus stays on the `<input>` the whole time the listbox is open, so options never get `:focus`. `data-highlighted` is the canonical hook for styling the keyboard-active option. A mouse press anywhere on the popup that lands on no focusable element (padding, a group label, an empty or status row) does not move focus either, so typing and the arrow keys keep working after it; the trade-off is that popup text cannot be selected with the mouse, as in a native combobox. A press on a `[forComboboxAction]` still focuses it in every browser. A press on your own `<button>` inside the popup is left to the browser, and macOS Safari and Firefox do not focus a `<button>` on a press.
|
|
224
224
|
|
|
225
225
|
## Mount/visibility convention
|
|
226
226
|
|
|
@@ -280,7 +280,7 @@ The default (editable) anatomy is a text field that filters in place. A **picker
|
|
|
280
280
|
Add two parts:
|
|
281
281
|
|
|
282
282
|
- **`[forComboboxTrigger]`**: a real `<button>` outside the `@if (open())`. It opens the panel, becomes the default positioning anchor, and is where focus returns on close.
|
|
283
|
-
- **`[forComboboxList]`**: the `role="listbox"` element nested inside `[forComboboxContent]`, next to the input. It owns the options and the labelled role; `[forComboboxContent]` becomes
|
|
283
|
+
- **`[forComboboxList]`**: the `role="listbox"` element nested inside `[forComboboxContent]`, next to the input. It owns the options and the labelled role; `[forComboboxContent]` becomes the `role="dialog"` the trigger opens, named by the trigger.
|
|
284
284
|
|
|
285
285
|
```html
|
|
286
286
|
<div forCombobox #combobox="forCombobox" [(value)]="value" [(query)]="query" [(open)]="open">
|
|
@@ -292,20 +292,20 @@ Add two parts:
|
|
|
292
292
|
@for (item of filtered(); track item.id) {
|
|
293
293
|
<div forComboboxOption [value]="item.id" [label]="item.label">{{ item.label }}</div>
|
|
294
294
|
}
|
|
295
|
-
<div forComboboxEmpty>No matches.</div>
|
|
296
295
|
</div>
|
|
296
|
+
<div forComboboxEmpty>No matches.</div>
|
|
297
297
|
</div>
|
|
298
298
|
}
|
|
299
299
|
</div>
|
|
300
300
|
```
|
|
301
301
|
|
|
302
|
-
Why the list part is required, not optional: a `role="listbox"` may only own `option` / `group` children (`aria-required-owned-elements`). Nesting the input inside a listbox would be invalid, so the listbox role moves to `[forComboboxList]` and the input sits beside it
|
|
302
|
+
Why the list part is required, not optional: a `role="listbox"` may only own `option` / `group` children (`aria-required-owned-elements`). Nesting the input inside a listbox would be invalid, so the listbox role moves to `[forComboboxList]` and the input sits beside it inside the dialog surface. Put non-option chrome (`[forComboboxInput]`, `[forComboboxEmpty]`, `[forComboboxStatus]`) inside `[forComboboxContent]` but **outside** `[forComboboxList]`.
|
|
303
303
|
|
|
304
304
|
**Focus hand-off.** Registering a trigger opts the combobox into the standard trigger-anchored focus model: on open, focus moves into the input (the search field inside the panel); on close, focus returns to the trigger. Both moves are vetoable via `(autoFocusOnOpen)` / `(autoFocusOnClose)` on `[forCombobox]`, and the return is gated by `[returnFocus]` (default `true`). Escape stays owned by the input. See [Focus & the `(autoFocusOnOpen)` / `(autoFocusOnClose)` hooks](#focus--the-autofocusonopen--autofocusonclose-hooks).
|
|
305
305
|
|
|
306
306
|
**A trigger that registers late still owns the hand-off.** The trigger does not have to be declared before `[forComboboxContent]`, and does not have to exist when the panel first mounts. You can project it through `<ng-content>` from a wrapper, put it under a `@defer`, or gate it on an `@if` over loaded data. One caveat: focus moving _into_ the panel is a mount-time event, so a trigger that arrives while the panel is **already open** does not retroactively pull focus out of wherever you left it. From that moment on it does own the return focus, `(autoFocusOnClose)` and the Escape fallback, and the next open moves focus into the input as usual.
|
|
307
307
|
|
|
308
|
-
**Trigger keyboard.** Click / Enter / Space toggle (open moves focus into the input). ArrowDown
|
|
308
|
+
**Trigger keyboard.** Click / Enter / Space toggle (open moves focus into the input). ArrowDown and ArrowUp open with the committed selection highlighted; with nothing selected, ArrowDown lands on the first enabled option and ArrowUp on the last.
|
|
309
309
|
|
|
310
310
|
**Anchor preference.** With a trigger present the panel anchors to it by default. An explicit `[forComboboxAnchor]` still wins (explicit anchor → trigger → input), so you can wrap a decorated trigger box and anchor against it.
|
|
311
311
|
|
|
@@ -536,7 +536,7 @@ When the input is empty (no query) and the user presses Backspace, focus jumps t
|
|
|
536
536
|
| Highlight the selection on open | `first` | `openHighlight="selected"` |
|
|
537
537
|
| Restore label on close (single only) | `false` | `[restoreQueryOnClose]="true"` |
|
|
538
538
|
|
|
539
|
-
`openHighlight` decides where the editable anatomy's highlight lands when the listbox opens from focus, click, ArrowDown / ArrowUp or `openOverlay()` without an argument. With `'selected'`, a single-select combobox showing "Spain" reopens on "Spain" rather than on the first option; with nothing selected, ArrowDown still lands on the first option and ArrowUp on the last. Opening from a typed query always highlights the first match, because the list is a filter result there, and the picker anatomy always opens on the selection. Both inputs take their default from the scope: `provideForComboboxDefaults({ openHighlight: 'selected', restoreQueryOnClose: true })`.
|
|
539
|
+
`openHighlight` decides where the editable anatomy's highlight lands when the listbox opens from focus, click, ArrowDown / ArrowUp or `openOverlay()` without an argument. With `'selected'`, a single-select combobox showing "Spain" reopens on "Spain" rather than on the first option; with nothing selected, ArrowDown still lands on the first option and ArrowUp on the last. Opening from a typed query always highlights the first match, because the list is a filter result there, and the picker anatomy always opens on the selection. With [virtualization](#virtualization) the selection is not honoured: an open that would land on it highlights the first rendered enabled option instead. Both inputs take their default from the scope: `provideForComboboxDefaults({ openHighlight: 'selected', restoreQueryOnClose: true })`.
|
|
540
540
|
|
|
541
541
|
## Autocomplete modes
|
|
542
542
|
|
|
@@ -663,6 +663,8 @@ How navigation flows when virtualizing:
|
|
|
663
663
|
3. Your virtualizer scrolls; the directive's `@for` mounts the option for index 999.
|
|
664
664
|
4. As soon as that option registers (at the matching `posInSet`), the directive seeds `aria-activedescendant` to its id.
|
|
665
665
|
|
|
666
|
+
Opening seeds the highlight from the rendered window only: the first rendered enabled option, or the last for an ArrowUp with nothing selected. An open that would land on the committed selection (`openHighlight="selected"`, the picker anatomy, `[forComboboxToggle]`) lands on the first rendered enabled option too, because the selection may sit outside the window and the directive cannot know its index.
|
|
667
|
+
|
|
666
668
|
Inline autocomplete matches against the most recently rendered window overlaid with the position snapshot, so completion against off-screen labels still works. `selected().label` reads a separate, selection-keyed cache instead. That cache is bounded by the selection, so the label of a selected option survives close / re-open and any number of query rebuilds, but it is **not** resolved from the position snapshot: a value that enters the selection while its option is outside the rendered window (a `[(value)]` write restoring a saved selection, say) falls back to `[itemToStringLabel]` until that option renders once. Supply `[itemToStringLabel]` whenever the selection can be seeded from outside the list.
|
|
667
669
|
|
|
668
670
|
```html
|
|
@@ -782,7 +784,7 @@ Static and `@for`-rendered options share the same registry, navigation order (DO
|
|
|
782
784
|
</div>
|
|
783
785
|
```
|
|
784
786
|
|
|
785
|
-
For a legacy `<form action="…">` flow, set `[name]`, and the directive mirrors `[(value)]` into N `<input type="hidden">` siblings (one per array entry; zero when empty). String values land verbatim in the hidden input; object values default to `JSON.stringify` (override via `[itemToFormValue]`, see below).
|
|
787
|
+
For a legacy `<form action="…">` flow, set `[name]`, and the directive mirrors `[(value)]` into N `<input type="hidden">` siblings (one per array entry; zero when empty). String values land verbatim in the hidden input; object values default to `JSON.stringify` (override via `[itemToFormValue]`, see below). A native reset of the owning `<form>` returns `value` and `query` to what they held when the combobox first rendered, and the input shows that query again.
|
|
786
788
|
|
|
787
789
|
A single-select field is modeled as the same `readonly T[]`, kept at length ≤ 1, and bound with `[formField]` directly, so single mode needs no adapter. A `FieldTree<T | null>` cannot bind here; map to that shape at the edge that needs it. See [the selection value-type contract](../../../docs/selection-value-type-contract.md).
|
|
788
790
|
|
|
@@ -815,9 +817,9 @@ Hovering an option also makes it the activedescendant, so mouse and keyboard int
|
|
|
815
817
|
Implements the [WAI-ARIA Combobox pattern](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/).
|
|
816
818
|
|
|
817
819
|
- Apply the input directive to an actual `<input>`: the browser's caret and selection semantics are what make inline autocomplete work, and screen readers expect a real text field for `role="combobox"`.
|
|
818
|
-
- `role="listbox"` lives on `[forComboboxContent]` in the editable anatomy and on `[forComboboxList]` in the picker anatomy; the input's `aria-controls` targets whichever carries it. In the picker anatomy the popup surface (`[forComboboxContent]`) is role-
|
|
820
|
+
- `role="listbox"` lives on `[forComboboxContent]` in the editable anatomy and on `[forComboboxList]` in the picker anatomy; the input's `aria-controls` targets whichever carries it. In the picker anatomy the popup surface (`[forComboboxContent]`) is a `role="dialog"` named by the trigger (`aria-labelledby`; a static `aria-label` or `aria-labelledby` on it wins), so it can hold the input next to the list without an `aria-required-owned-elements` violation. The "editable + list" shape, with no trigger, leaves it role-less.
|
|
819
821
|
- `aria-multiselectable="true"` (multi mode) and the labelled role (`aria-label` / `aria-labelledby`, pointing at the input) sit on whichever element carries `role="listbox"`: content in the editable anatomy, the list in the picker anatomy.
|
|
820
|
-
- `[forComboboxTrigger]` (picker anatomy) is a real `<button>` reflecting `aria-haspopup="
|
|
822
|
+
- `[forComboboxTrigger]` (picker anatomy) is a real `<button>` reflecting `aria-haspopup="dialog"`, `aria-expanded`, `aria-controls` (the dialog surface, while open), an `id` (a static one is kept), and native `disabled` from the combobox's effective disabled. The popup is a dialog rather than a listbox because focus moves into the search input inside it. It is exempt from the popup's outside-pointer dismissal layer, like the input.
|
|
821
823
|
- `[forComboboxContent]` and `[forComboboxList]` cancel a `mousedown` that would otherwise move focus onto the surface or blur the input, so a press between options leaves focus in the input. A press on a focusable descendant is let through.
|
|
822
824
|
- `[forComboboxToggle]` (editable anatomy) is a real `<button>` with `tabindex="-1"`, a localizable `aria-label`, `aria-expanded`, `aria-controls` (the listbox, while open) and native `disabled`. It cancels `mousedown` so focus stays in the input, and it is exempt from the outside-pointer dismissal layer.
|
|
823
825
|
- In single mode, `aria-selected="true"` follows the activedescendant (the option Enter would activate). In multi mode it follows membership in `value()`, so every selected option carries `aria-selected="true"` simultaneously.
|
package/date-field/README.md
CHANGED
|
@@ -163,6 +163,15 @@ With a time-capable adapter, a `granularity` coarser than `'day'` appends time s
|
|
|
163
163
|
|
|
164
164
|
Plus the shared `FormUiControl` members from `@angular/forms/signals`: `disabled`, `readonly`, `required`, `invalid`, `name`, `errors`, `touched` (bound automatically by `[formField]`).
|
|
165
165
|
|
|
166
|
+
| Method | Description |
|
|
167
|
+
| --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
168
|
+
| `clear()` | Sets `value` to `null` and empties every segment, a partly typed entry included. Call it from a Clear button. |
|
|
169
|
+
| `reset()` | Drops a partly typed entry and shows the current `value`. Signal Forms calls it on `form().reset()`, so a form reset clears the segments with no extra wiring. |
|
|
170
|
+
|
|
171
|
+
A partly typed date already reports `value()` as `null`, so writing `null` through `[(value)]` changes nothing and leaves the typed digits on screen. Clear the field with `clear()` instead, which you can reach through the `#field="forDateField"` template reference.
|
|
172
|
+
|
|
173
|
+
A native reset of the owning `<form>` returns `value` to what it held when the field first rendered and runs `reset()`, so a partly typed entry goes too and the hidden input submits that value again.
|
|
174
|
+
|
|
166
175
|
> **Why `minDate` / `maxDate`, not `min` / `max`?** `FormUiControl.min` / `max` are reserved members typed `number | undefined` for numeric validators bound by `[formField]`. A date-typed `min` / `max` would break the `FormValueControl` contract, so the date bounds use distinct names.
|
|
167
176
|
|
|
168
177
|
### Data attributes
|
|
@@ -200,7 +209,7 @@ Set `granularity` to `'hour'`, `'minute'`, or `'second'` (`granularity > 'day'`)
|
|
|
200
209
|
</div>
|
|
201
210
|
```
|
|
202
211
|
|
|
203
|
-
On the AM/PM segment, `a` / `p` set the period and ArrowUp / ArrowDown toggle it
|
|
212
|
+
On the AM/PM segment, `a` / `p` set the period and ArrowUp / ArrowDown toggle it (on an empty segment ArrowUp sets PM and ArrowDown sets AM). The period is never derived from the entered hour: it stays empty until you set it, and Delete / Backspace leave it as it is. In a locale whose 12-hour clock counts 0–11 (`ja-JP`), the hour segment does too. The value stays `null` until every visible segment (date **and** time) is filled.
|
|
204
213
|
|
|
205
214
|
## Scoped defaults
|
|
206
215
|
|
|
@@ -220,7 +229,7 @@ providers: [
|
|
|
220
229
|
|
|
221
230
|
`segmentLabels` supplies each segment's default `aria-label`, keyed by part type. Unset keys keep the library default (the part name, and `'AM/PM'` for the `dayPeriod` segment), so overriding a single key never wipes the rest. A segment's own `[ariaLabel]` still wins over the scope default.
|
|
222
231
|
|
|
223
|
-
`placeholder` is the text an empty segment shows, keyed the same way. A field's own `[placeholder]` wins for the parts it names and only those, and a part neither names keeps the letter-repeat default. `provideForDateRangeFieldDefaults` takes the same keys for `[forDateRangeField]`.
|
|
232
|
+
`placeholder` is the text an empty segment shows, keyed the same way. A field's own `[placeholder]` wins for the parts it names and only those, and a part neither names keeps the letter-repeat default. A nested `provideForDateFieldDefaults` merges both records entry by entry with its parent scope, so a feature overriding only `dayPeriod` keeps the labels and placeholders the application root localized. `provideForDateRangeFieldDefaults` takes the same keys for `[forDateRangeField]`.
|
|
224
233
|
|
|
225
234
|
`hourCycle` sets the 12- or 24-hour cycle of every field that doesn't bind `[hourCycle]`, so a product on a 24-hour clock sets it once instead of on each field. Its fallback `null` derives the cycle from the locale.
|
|
226
235
|
|
|
@@ -284,6 +293,8 @@ readonly booking = form(this.model);
|
|
|
284
293
|
|
|
285
294
|
The endpoint groups each accept an `ariaLabel` input for their own group label, falling back to the scope defaults (`'Start date'` / `'End date'`). Plus the shared `FormUiControl` members bound automatically by `[formField]`.
|
|
286
295
|
|
|
296
|
+
`ForDateRangeField` exposes the same `clear()` and `reset()` as `ForDateField`. They drop a half-entered range and an out-of-order one, its `data-range-error` included, which writing `null` through `[(value)]` cannot do: such a range already reports `value()` as `null`. Without them, finishing the end endpoint later would commit a range built from a start the user thought was gone. A native reset of the owning `<form>` returns the range to what it held when the field first rendered and runs `reset()`.
|
|
297
|
+
|
|
287
298
|
> **Why `minDate` / `maxDate`, not `min` / `max`?** Beyond the reason above, `FormUiControl.min` / `max` are additionally typed `NonNullable<TValue>` (the range object itself), which is meaningless as a bound.
|
|
288
299
|
|
|
289
300
|
`[forDateRangeField]` reflects the same `data-disabled` / `data-readonly` / `data-empty` hooks as `[forDateField]`, plus `data-range-error`; `[forDateRangeFieldSegment]` reflects the same four segment hooks. `data-empty` marks the field only while **both** endpoints are entirely empty; a partially-filled or complete-but-disordered range is **not** empty.
|
|
@@ -313,7 +324,7 @@ Key behavior applies per segment. Horizontal arrows mirror under `dir="rtl"`.
|
|
|
313
324
|
| **Backspace** | Delete the last entered digit; the value becomes `null` when the last digit is removed. |
|
|
314
325
|
| **Delete** | Clear the whole segment (the value becomes `null` until refilled). |
|
|
315
326
|
|
|
316
|
-
The day clamps to the current month's length (e.g. 31 → 28 in February), and a composed value is clamped into `[minDate, maxDate]`.
|
|
327
|
+
The day clamps to the current month's length (e.g. 31 → 28 in February), and a composed value is clamped into `[minDate, maxDate]`. While focus stays in the field its segments keep what was entered, so editing one part never rewrites the others; they show the clamped value once focus leaves. Stepping the hour skips a wall-clock hour the day does not have, such as a spring-forward DST gap.
|
|
317
328
|
|
|
318
329
|
## Accessibility
|
|
319
330
|
|
package/date-picker/README.md
CHANGED
|
@@ -220,6 +220,8 @@ Open the picker and click a first day: the trigger keeps its placeholder, becaus
|
|
|
220
220
|
|
|
221
221
|
Plus the shared `FormUiControl` inputs from the base (`disabled`, `readonly`, `required`, `invalid`, `pending`, `dirty`, `name`, `errors`, and the `touched` model) and the floating tunables (`sideOffset`, `alignOffset`, `avoidCollisions`, `collisionPadding`, `sticky`, `hideWhenDetached`).
|
|
222
222
|
|
|
223
|
+
A native reset of the owning `<form>` returns `value` to what it held when the picker first rendered. In the [field anatomy](#field-anatomy) it also drops a partly typed entry in the projected field.
|
|
224
|
+
|
|
223
225
|
> **`locale` only styles the trigger display.** It drives the text rendered by `[forDatePickerValue]` (both here and on `ForDateRangePicker`); it is **not** forwarded to the projected `ForCalendar`. Bind the calendar's own `[locale]` to localize its heading / weekday / cell labels, exactly as you forward `[min]` / `[max]`.
|
|
224
226
|
|
|
225
227
|
> **Why `minDate` / `maxDate`, not `min` / `max`?** `ForDatePicker` is a `FormValueControl`, and `FormUiControl` reserves `min` / `max` for numeric validators (`InputSignal<number | undefined>`). A date-typed `min` / `max` would break that contract, so the date bounds use the `*Date` suffix. (`ForCalendar` is not a form control, so it keeps `min` / `max`.)
|
|
@@ -294,6 +296,8 @@ Set `granularity` to `'hour'`, `'minute'`, or `'second'` to turn the picker into
|
|
|
294
296
|
|
|
295
297
|
Bind the calendar **and** the time field **one-way** to `picker.value()` (not `[(value)]`). The picker is the single source of truth: when one-way bound to a timed value the calendar preserves the time-of-day on its own selection, and the picker re-grafts the previously entered time as a defensive fallback for the case where the calendar value was null or midnight (reading its own value, which the one-way children never clobber); a time-field edit emits a full date-time the picker mirrors in. A date-time picker never closes on a calendar selection, so the user can go on to set the time.
|
|
296
298
|
|
|
299
|
+
Like the calendar, the projected time field or time picker takes the picker's `readonly` and `disabled` without a binding, so a read-only or disabled picker's time source accepts no edit and shows only the committed time. A typed time the picker clamps into `[minDate, maxDate]` behaves as in a standalone time field: while focus stays in the field its segments keep what was entered, and they show the clamped value once focus leaves.
|
|
300
|
+
|
|
297
301
|
The content is a field boundary. Inside a [`[forField]`](../field/README.md#how-the-control-connects) the time field does not register with the field, which keeps reflecting the picker (its label target, `invalid` state and errors) while the panel is open.
|
|
298
302
|
|
|
299
303
|
```html
|
|
@@ -379,6 +383,7 @@ With the field adopted:
|
|
|
379
383
|
- **The field is the control.** A surrounding `[forField]` names and describes the field's `role="group"`, `aria-invalid` lands there, a label press focuses its first segment, and the picker's `focus()` goes there too.
|
|
380
384
|
- **The trigger is a plain button.** It drops `role="combobox"` and the form-control `aria-*` state, and keeps `aria-haspopup="dialog"`, `aria-expanded` and `aria-controls`. Give it a name of its own.
|
|
381
385
|
- **A pick closes the surface at any granularity.** The time is typed in the field rather than in the surface, so a picked day keeps the field's time and the surface closes (unless `closeOnSelect` is off).
|
|
386
|
+
- **A press on a segment closes the surface.** Only the trigger and the field's labels are exempt from outside dismissal, so pressing a segment to start typing closes the calendar and leaves focus on that segment.
|
|
382
387
|
|
|
383
388
|
The anatomy is explicit, so a date field placed inside the surface of a trigger-anatomy picker changes nothing. With `anatomy="field"` and no projected `[forDateField]`, opening the calendar or calling `focus()` throws `FORCDK-DATE-PICKER-007` in dev mode.
|
|
384
389
|
|
|
@@ -432,18 +437,19 @@ readonly booking = form(this.model, (p) => required(p.stay));
|
|
|
432
437
|
|
|
433
438
|
- **Form value.** The committed `DateRange<D> | null` is the `value` model. `null` is the empty state. Pair it with `required(p.stay)` so `invalid()` flips when the form demands a range and none is committed. `touched` fires on commit and on close, exactly like the single-date picker.
|
|
434
439
|
- **Validity.** `start <= end` is guaranteed by construction and is never an error. Forward `minDate` / `maxDate` to the calendar's `[min]` / `[max]` so out-of-range days are disabled in the grid; a range picked in a calendar without them is clamped into `[minDate, maxDate]` on commit. Forward `minRangeLength` / `maxRangeLength` to the calendar's `[minRangeLength]` / `[maxRangeLength]` (a too-short / too-long range is rejected as a no-op by the calendar's two-click flow).
|
|
435
|
-
- **Native submission.** When `name` is set, two hidden inputs `<name>-start` / `<name>-end` mirror the committed endpoints as ISO `YYYY-MM-DD` for native `<form>` posts.
|
|
440
|
+
- **Native submission.** When `name` is set, two hidden inputs `<name>-start` / `<name>-end` mirror the committed endpoints as ISO `YYYY-MM-DD` for native `<form>` posts. A native reset of the owning `<form>` returns `value` to what it held when the picker first rendered.
|
|
436
441
|
- **Bounds naming.** `minDate` / `maxDate` (not `min` / `max`) for the same reason as `ForDatePicker`, and additionally because `FormUiControl.min` / `max` are typed `NonNullable<TValue>` (the range object itself), which is meaningless as a bound.
|
|
437
442
|
|
|
438
443
|
Defaults are configured with `provideForDateRangePickerDefaults` (`side` / `align` / `sideOffset` / `collisionPadding`), and both wrapper patterns work via the exported `FOR_DATE_RANGE_PICKER_HOST_DIRECTIVE_INPUTS` / `FOR_DATE_RANGE_PICKER_HOST_DIRECTIVE_OUTPUTS` tuples. See [Wrapping form primitives](../../../docs/wrapping-form-primitives.md).
|
|
439
444
|
|
|
440
445
|
## Keyboard
|
|
441
446
|
|
|
442
|
-
| Key | Behavior
|
|
443
|
-
| -------------------------------------------------- |
|
|
444
|
-
| **Enter / Space** on trigger | Open the surface (native button activation).
|
|
445
|
-
| **ArrowDown / Alt+ArrowDown / ArrowUp** on trigger | Open the surface and move focus to the calendar.
|
|
446
|
-
| **Escape** | Dismiss the surface and return focus to the trigger (when `dismissible`).
|
|
447
|
+
| Key | Behavior |
|
|
448
|
+
| -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
449
|
+
| **Enter / Space** on trigger | Open the surface (native button activation). |
|
|
450
|
+
| **ArrowDown / Alt+ArrowDown / ArrowUp** on trigger | Open the surface and move focus to the calendar. |
|
|
451
|
+
| **Escape** | Dismiss the surface and return focus to the trigger (when `dismissible`). |
|
|
452
|
+
| **Tab / Shift+Tab** past either end of the surface | Non-modal: Tab moves on to the element after the trigger and Shift+Tab lands on the trigger, closing the surface when `dismissible`. Modal: focus stays trapped. |
|
|
447
453
|
|
|
448
454
|
Inside the surface, the projected `ForCalendar` owns the full grid keyboard map (arrows / `Home` / `End` / `PageUp` / `PageDown` / `Enter` / `Space`). On open, focus lands on the calendar's focused cell (`value ?? today`, with today clamped into the calendar's `[min, max]`) in non-modal mode, or the first focusable element in modal mode.
|
|
449
455
|
|
|
@@ -455,7 +461,7 @@ Implements the [WAI-ARIA Date Picker Dialog pattern](https://www.w3.org/WAI/ARIA
|
|
|
455
461
|
- **`role="dialog"`** on the surface, named by `[ariaLabel]` (or `aria-labelledby` the trigger when no label is set). `aria-modal="true"` only in modal mode (truthy-only).
|
|
456
462
|
- **Form-control ARIA** (`aria-readonly` / `aria-required` / `aria-invalid` / `aria-busy`) is reflected on the focusable trigger so assistive tech announces validity on the element that takes focus, alongside the `data-readonly` styling hook. The disabled state is the exception. It reflects through one channel only: the native `disabled` attribute (plus `data-disabled`), never `aria-disabled`.
|
|
457
463
|
- **Inside a `[forField]` the labelled element is the trigger**, not the `[forDatePicker]` / `[forDateRangePicker]` wrapper: the field's `controlId` and its `aria-labelledby` / `aria-describedby` / `aria-errormessage` land on `[forDatePickerTrigger]`, so `[forLabel]`'s `for` points at the element that takes focus, clicking a non-`<label>` `[forLabel]` opens the surface, and Signal Forms' focus-on-error reaches the trigger. `role="combobox"` takes its name from the author, so this is the channel that names the control. The root's `[ariaLabel]` names the `role="dialog"` surface instead. In the [field anatomy](#field-anatomy) the same association lands on the date field's `role="group"` instead, and a label press focuses its first segment.
|
|
458
|
-
- **Focus management**: focus enters the surface on open (the calendar's roving cell in non-modal mode) and returns to the trigger on close, both vetoable via `(autoFocusOnOpen)` / `(autoFocusOnClose)`.
|
|
464
|
+
- **Focus management**: focus enters the surface on open (the calendar's roving cell in non-modal mode) and returns to the trigger on close, both vetoable via `(autoFocusOnOpen)` / `(autoFocusOnClose)`. The one exception in non-modal mode is an **outside-interaction close**, meaning a pointer-down or focus-out that lands outside the surface. After such a close, focus stays where the interaction moved it instead of snapping back to the trigger, so a click on the next form control keeps the user's typing there. Escape, a pick and programmatic closes still return focus.
|
|
459
465
|
- **Dismissal**: Escape (`(escapeKeyDown)`) and outside-pointer (`(pointerDownOutside)` / `(interactOutside)`) close the surface, each vetoable.
|
|
460
466
|
|
|
461
467
|
## Styling
|
|
@@ -479,4 +485,4 @@ forty-cdk ships no styles: put your own class on each piece and key your CSS off
|
|
|
479
485
|
|
|
480
486
|
## Wrapping in a design system
|
|
481
487
|
|
|
482
|
-
[Wrapping form primitives](../../../docs/wrapping-form-primitives.md) documents both supported wrapper patterns: `hostDirectives` with the exported `FOR_DATE_PICKER_HOST_DIRECTIVE_INPUTS` / `FOR_DATE_PICKER_HOST_DIRECTIVE_OUTPUTS` name tuples, and subclassing.
|
|
488
|
+
[Wrapping form primitives](../../../docs/wrapping-form-primitives.md) documents both supported wrapper patterns: `hostDirectives` with the exported `FOR_DATE_PICKER_HOST_DIRECTIVE_INPUTS` / `FOR_DATE_PICKER_HOST_DIRECTIVE_OUTPUTS` name tuples, and subclassing. A subclass spreads [`provideForDatePicker(MyDatePicker)`](../../../docs/wrapping-form-primitives.md#date-and-time-roots-spread-their-provider-helper) or `provideForDateRangePicker(MyDateRangePicker)` as its `providers`, because the root also provides tokens its projected calendar, field and time control resolve that cannot be imported.
|
package/dialog/README.md
CHANGED
|
@@ -157,9 +157,9 @@ Open a component imperatively and await its result. The manager mounts it under
|
|
|
157
157
|
| Property | Type | Description |
|
|
158
158
|
| -------------------- | ------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
159
159
|
| `dismissible` | — | When `false`, Escape, backdrop, outside-pointer, and outside-focus do not request close. The close button still does.<br>**Default:** `true` |
|
|
160
|
-
| `modal` | — | When `false`, no `aria-modal`, no scroll lock, no focus trap.<br>**Default:** `true`
|
|
160
|
+
| `modal` | — | When `false`, no `aria-modal`, no scroll lock, no focus trap, no inert siblings, and no return focus on close.<br>**Default:** `true` |
|
|
161
161
|
| `alert` | — | Switches role to `alertdialog`.<br>**Default:** `false` |
|
|
162
|
-
| `returnFocus` | — |
|
|
162
|
+
| `returnFocus` | — | Modal mode only: focus returns to the previously focused element on close. A non-modal dialog never moves focus on close.<br>**Default:** `true` |
|
|
163
163
|
| `initialFocus` | — | `'first'` (first focusable inside) or `'container'` (the dialog host).<br>**Default:** `'first'` |
|
|
164
164
|
| `ariaLabel` | — | Manual `aria-label` if no `[forDialogTitle]` is rendered.<br>**Default:** `null` |
|
|
165
165
|
| `dismiss` | `OutputEmitterRef<ForDialogCloseReason>` | Output. Dialog wants to be unmounted. Reasons: `'escape'`, `'backdrop'`, `'pointerDownOutside'`, `'focusOutside'`, `'closeButton'`, `'programmatic'`. Spelled `dismiss` rather than `close` on purpose (see [Behavior notes](#behavior-notes)).<br>**Default:** — |
|
|
@@ -298,7 +298,22 @@ this.dialogs.open(ConfirmDialog, {
|
|
|
298
298
|
}
|
|
299
299
|
```
|
|
300
300
|
|
|
301
|
-
|
|
301
|
+
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. The exit animation is configured with `animateLeave` for the host and `backdropAnimateLeave` for a `[forDialogBackdrop]`, whose enter stays an `animate.enter` on the element itself.
|
|
302
|
+
|
|
303
|
+
Set them once with `provideForDialogDefaults({ animateEnter, animateLeave, backdropAnimateLeave })`; a per-`open()` value always wins over the default. `ForDialogManager` is provided in root and reads the defaults from the root injector, so a `provideForDialogDefaults` on a lazy route or in a component's `providers` reaches a programmatic dialog only when that scope's injector is passed as `injector`. The opened component is then parented on it too, so the scope's other providers reach it as well.
|
|
304
|
+
|
|
305
|
+
<!-- snippet: fragment -->
|
|
306
|
+
|
|
307
|
+
```ts
|
|
308
|
+
class ScopedHost {
|
|
309
|
+
readonly #dialogs = inject(ForDialogManager);
|
|
310
|
+
readonly #injector = inject(Injector);
|
|
311
|
+
|
|
312
|
+
confirm(): void {
|
|
313
|
+
this.#dialogs.open(ConfirmDialog, { injector: this.#injector });
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
```
|
|
302
317
|
|
|
303
318
|
| Symbol | Description |
|
|
304
319
|
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
@@ -309,26 +324,30 @@ Set them once for a scope with `provideForDialogDefaults({ animateEnter, animate
|
|
|
309
324
|
|
|
310
325
|
### `ForDialogOpenConfig`
|
|
311
326
|
|
|
312
|
-
| Field
|
|
313
|
-
|
|
|
314
|
-
| `data`
|
|
315
|
-
| `dismissible`
|
|
316
|
-
| `modal`
|
|
317
|
-
| `alert`
|
|
318
|
-
| `returnFocus`
|
|
319
|
-
| `
|
|
320
|
-
| `
|
|
321
|
-
| `
|
|
322
|
-
| `
|
|
323
|
-
| `
|
|
324
|
-
| `
|
|
325
|
-
| `
|
|
326
|
-
| `
|
|
327
|
-
| `
|
|
328
|
-
| `
|
|
329
|
-
| `
|
|
330
|
-
| `
|
|
331
|
-
| `
|
|
327
|
+
| Field | Default | Description |
|
|
328
|
+
| ---------------------- | --------- | ----------------------------------------------------------------------------------------------------------------- |
|
|
329
|
+
| `data` | — | Payload available as `injectDialogData<T>()`. |
|
|
330
|
+
| `dismissible` | `true` | When `false`, Escape, backdrop, outside-pointer and outside-focus do not close the dialog. |
|
|
331
|
+
| `modal` | `true` | Sets `aria-modal`, locks body scroll, traps focus, inerts siblings and returns focus on close. |
|
|
332
|
+
| `alert` | `false` | Use `role="alertdialog"` instead of `"dialog"`. |
|
|
333
|
+
| `returnFocus` | `true` | Modal mode only: focus returns to the previously focused element on close. |
|
|
334
|
+
| `returnFocusTarget` | — | Element focus returns to. Omitted, the trigger that opened the modal chain; `null`, the element focused at mount. |
|
|
335
|
+
| `initialFocus` | `'first'` | `'first'` finds first focusable; `'container'` focuses the host. |
|
|
336
|
+
| `ariaLabel` | — | Manual accessible name when no title element is rendered. |
|
|
337
|
+
| `container` | — | Portal target for the surface and its backdrop, `document.body` when omitted. Pair with `modal: false`. |
|
|
338
|
+
| `animateEnter` | — | CSS class applied on mount (via `animate.enter`) to play an enter animation. |
|
|
339
|
+
| `animateLeave` | — | CSS class applied on close; the host stays mounted until its animation finishes, then tears down. |
|
|
340
|
+
| `backdropAnimateLeave` | — | CSS class applied to the `[forDialogBackdrop]` on close; it stays mounted until its animation finishes. |
|
|
341
|
+
| `class` | — | CSS class(es) applied to the overlay root (the `[forDialog]` host). Single or space-separated string. |
|
|
342
|
+
| `classList` | — | CSS class(es) applied to the overlay root, as an array or space-separated string. Merged with `class`. |
|
|
343
|
+
| `providers` | `[]` | Extra providers for the opened component's injector. |
|
|
344
|
+
| `injector` | — | Caller scope: scoped `provideForDialogDefaults` and providers are read from it instead of the root. |
|
|
345
|
+
| `autoFocusOnOpen` | — | Callback. Receives a `VetoableEvent`; `event.preventDefault()` skips the imperative initial focus move. |
|
|
346
|
+
| `autoFocusOnClose` | — | Callback. Receives a `VetoableEvent`; `event.preventDefault()` skips the return-focus on close. |
|
|
347
|
+
| `escapeKeyDown` | — | Callback. `VetoableNativeEvent<KeyboardEvent>`; `preventDefault()` suppresses the Escape close. |
|
|
348
|
+
| `pointerDownOutside` | — | Callback. `VetoableNativeEvent<PointerEvent>`; `preventDefault()` suppresses the outside-pointer close. |
|
|
349
|
+
| `focusOutside` | — | Callback. `VetoableNativeEvent<FocusEvent>`; `preventDefault()` suppresses the outside-focus close. |
|
|
350
|
+
| `interactOutside` | — | Callback. Composite `VetoableNativeEvent<PointerEvent \| FocusEvent>`; shares the veto of the two above. |
|
|
332
351
|
|
|
333
352
|
The four dismiss callbacks mirror the declarative `(escapeKeyDown)` / `(pointerDownOutside)` / `(focusOutside)` / `(interactOutside)` outputs exactly, with the same events and the same veto semantics. See [Per-channel dismissal](#per-channel-dismissal-escape-only-dialogs).
|
|
334
353
|
|
|
@@ -372,6 +391,8 @@ forty-cdk ships no styles: put your own class on each piece and key your CSS off
|
|
|
372
391
|
}
|
|
373
392
|
```
|
|
374
393
|
|
|
394
|
+
The backdrop is portaled immediately before its own dialog, so with no `z-index` on either the dialog paints above it. Set one only when other positioned content on the page has to sit beneath the pair.
|
|
395
|
+
|
|
375
396
|
**Stacked dialogs.** Each surface and its backdrop publish their stacking position as `data-depth` and `--for-dialog-depth` (`0` for the first dialog, `1` above it, …), shared by declarative and managed dialogs. One rule then covers every level, with each backdrop one step below its own dialog and above every dialog underneath it:
|
|
376
397
|
|
|
377
398
|
```css
|
|
@@ -406,14 +427,14 @@ forty-cdk ships no styles: put your own class on each piece and key your CSS off
|
|
|
406
427
|
|
|
407
428
|
- **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 `ForDialogRef.close()`, the directive selector is `[forDialogClose]`, and the payload type is `ForDialogCloseReason`.
|
|
408
429
|
- **Mount equals open**. The directive does not manage `[hidden]` or any visibility attribute. The consumer's `@if (open())` controls presence, and `animate.enter` / `animate.leave` handle the visual transition. That is why `[forDialog]`, `[forDialogBackdrop]`, and `[forDialogClose]` carry a static `data-state="open"`: because the host only exists inside `@if (open())`, the element is present iff the dialog is open, so the attribute can never be `closed`. Exit styling is the consumer's `animate.leave`, not a `[data-state="closed"]` selector. Only `[forDialogTrigger]`, which stays mounted, toggles `open` / `closed`.
|
|
409
|
-
- **Portal**: the dialog box is moved to `document.body` on first render (or to `container` when set). The backdrop portals
|
|
430
|
+
- **Portal**: the dialog box is moved to `document.body` on first render (or to `container` when set). The backdrop portals to the same `container`, `document.body` by default, immediately before its dialog. CSS scoped to ancestors won't apply, so use global styles or classes.
|
|
410
431
|
- **Body scroll lock** is refcounted: stacking dialogs (or a dialog + a future overlay using the same lock) only restore on the last unlock.
|
|
411
432
|
- **Focus trap** scopes Tab inside the dialog box while `modal`. It does NOT itself mark the rest of the page `inert`. That is the inert-siblings utility's job (next bullet).
|
|
412
433
|
- **Inert siblings**. When `modal`, every direct child of `document.body` other than the dialog box (and its backdrop) gets `inert` and `aria-hidden="true"` while open, and is restored on close. This is what `aria-modal="true"` alone is missing, since Safari + VoiceOver and several other AT pairings otherwise still announce siblings of an aria-modal node. Stacking is order-safe: when a second modal opens on top, the first becomes inert; closing the top dialog re-activates the underlying one.
|
|
413
434
|
- **Vetoable dismissals**. Each of `(escapeKeyDown)`, `(pointerDownOutside)`, `(focusOutside)`, `(interactOutside)` fires before the corresponding `(dismiss)`. Call `preventDefault()` on the event to keep the dialog open (e.g. to ask "are you sure?" first).
|
|
414
435
|
- **Focus callbacks are inputs, not outputs.** The `autoFocusOnOpen` / `autoFocusOnClose` shape mirrors `ForDialogManager`'s `config.autoFocusOn*` callbacks and guarantees the `autoFocusOnClose` callback fires reliably on every close path, including a direct `open.set(false)` that bypasses the `(dismiss)` output. The trigger-anchored overlays (Popover, DropdownMenu, Select, …) expose the same pair as **outputs** instead, because every close transition there runs through their own `[(open)]` model, so no path can bypass the emitter.
|
|
415
436
|
- **The close button** (`[forDialogClose]`) always requests close, regardless of `dismissible`. Reason emitted is `'closeButton'`.
|
|
416
|
-
- **Both flows share the same engine**: the focus trap, scroll lock, dismissible layer
|
|
437
|
+
- **Both flows share the same engine**: `ForDialogManager.open()` renders the same `[forDialog]` directive, so the focus trap, scroll lock, dismissible layer and portal are the directive's own. Behavior is identical.
|
|
417
438
|
|
|
418
439
|
### Per-channel dismissal (Escape-only dialogs)
|
|
419
440
|
|
package/drag-drop/README.md
CHANGED
|
@@ -199,8 +199,10 @@ name and leave "sortable" to `aria-sort`.
|
|
|
199
199
|
|
|
200
200
|
Applying the move in `(dragDrop)` destroys or re-inserts the lifted element, which would otherwise
|
|
201
201
|
leave focus on `<body>`. So after a **keyboard** drop whose lifted item held focus, `[forDropList]`
|
|
202
|
-
restores focus to the item
|
|
203
|
-
|
|
202
|
+
restores focus on the next render to the item whose `[dragData]` is the dropped item, falling back to the
|
|
203
|
+
item at `currentIndex` in the target container when no item carries it any more. That is the item the user
|
|
204
|
+
just placed, whether it stayed in this list or transferred to a connected one, and whether the `@for`
|
|
205
|
+
tracks by item identity or by `$index`.
|
|
204
206
|
|
|
205
207
|
Two escape hatches:
|
|
206
208
|
|
|
@@ -446,11 +448,16 @@ scope. `[animateReorder]` (FLIP) reflows by DOM order and needs no change in mix
|
|
|
446
448
|
## Free drag
|
|
447
449
|
|
|
448
450
|
`[forFreeDrag]` repositions its host element (or a resolved `rootElement`) by pointer drag, with
|
|
449
|
-
**no `[forDropList]` required**. It never commits a reorder; it just moves the element around via
|
|
450
|
-
CSS `
|
|
451
|
+
**no `[forDropList]` required**. It never commits a reorder; it just moves the element around via
|
|
452
|
+
the CSS `translate` property. It is the standalone counterpart to a sortable list item: a
|
|
451
453
|
pointer-driven way to move an element freely, with optional root-element retargeting, a confinement
|
|
452
454
|
boundary, axis locking, and a controllable position (see the inputs below).
|
|
453
455
|
|
|
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`.
|
|
460
|
+
|
|
454
461
|
```html
|
|
455
462
|
<!-- move the whole dialog by its header -->
|
|
456
463
|
<div class="dialog">
|
|
@@ -467,7 +474,7 @@ boundary, axis locking, and a controllable position (see the inputs below).
|
|
|
467
474
|
|
|
468
475
|
| Input | Type | Default | Meaning |
|
|
469
476
|
| ------------- | ------------------------------- | ------------- | -------------------------------------------------------------------------------------------------- |
|
|
470
|
-
| `disabled` | `boolean` | `false` | When true, the element can't be dragged (it stays focusable;
|
|
477
|
+
| `disabled` | `boolean` | `false` | When true, the element can't be dragged (it stays focusable; its position doesn't change). |
|
|
471
478
|
| `rootElement` | `HTMLElement \| string \| null` | `null` | The element actually moved. A `closest()` selector resolves an ancestor. `null` moves the host. |
|
|
472
479
|
| `boundary` | `HTMLElement \| string \| null` | `null` | Confine the moved element fully inside this element (or `closest()` selector). `null` = unbounded. |
|
|
473
480
|
| `lockAxis` | `'x' \| 'y' \| null` | `null` | Constrain movement to one axis. `'x'` pins lift-time `y`, `'y'` pins lift-time `x`. |
|
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.
|
|
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.
|
|
175
175
|
|
|
176
176
|
<!-- snippet: fragment -->
|
|
177
177
|
|
|
@@ -362,10 +362,10 @@ Press the button: the manager opens the confirmation as a bottom drawer, and the
|
|
|
362
362
|
| Property | Type | Description |
|
|
363
363
|
| --------------------------- | ------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
364
364
|
| `side` | `'top' \| 'right' \| 'bottom' \| 'left'` | Anchored edge. Drives swipe direction and `data-side`.<br>**Default:** `'bottom'` |
|
|
365
|
-
| `modal` | `boolean` | `aria-modal`, scroll lock, focus trap, inert siblings.<br>**Default:** `true`
|
|
365
|
+
| `modal` | `boolean` | `aria-modal`, scroll lock, focus trap, inert siblings, return focus on close.<br>**Default:** `true` |
|
|
366
366
|
| `dismissible` | `boolean` | Whether Escape / backdrop / outside / swipe close. When `false`, no swipe dismisses; `snapPoints` still drag between snaps.<br>**Default:** `true` |
|
|
367
367
|
| `alert` | `boolean` | `role="alertdialog"`.<br>**Default:** `false` |
|
|
368
|
-
| `returnFocus` | `boolean` |
|
|
368
|
+
| `returnFocus` | `boolean` | Modal mode only: restore focus on close. A non-modal drawer never moves focus on close.<br>**Default:** `true` |
|
|
369
369
|
| `initialFocus` | `'first' \| 'container'` | **Default:** `'first'` |
|
|
370
370
|
| `ariaLabel` | `string \| null` | Use when no visible title is rendered.<br>**Default:** `null` |
|
|
371
371
|
| `autoFocusOnOpen` | `(e: VetoableEvent) => void` \| `undefined` | `event.preventDefault()` skips the imperative focus move.<br>**Default:** — |
|
|
@@ -517,14 +517,15 @@ For a seamless release, transition **both** `translate` and your snap-position p
|
|
|
517
517
|
- 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
518
|
- `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.
|
|
519
519
|
- `handleOnly: true` confines the gesture to a registered `[forDrawerHandle]`, leaving the rest of the surface free for content scroll.
|
|
520
|
-
-
|
|
520
|
+
- On touch the browser would otherwise claim the swipe as a scroll. The handle always carries `touch-action: none`, and while the whole surface arms the drag (`handleOnly` off, and a drag the drawer can arm) the surface carries an inline `touch-action` that leaves the browser only the other axis: `pan-x` for a `top` / `bottom` drawer, `pan-y` for `left` / `right`. A scroller inside the surface keeps its own native scrolling, so your CSS needs no `touch-action` rule of its own.
|
|
521
|
+
- Gestures whose press lands inside a scrollable element that hasn't reached its edge are NOT treated as swipes (the helper defers to inner scroll). The edge is read in the scroller's own writing direction, with a 1 px tolerance for fractional scroll offsets.
|
|
521
522
|
- **`prefers-reduced-motion: reduce`** disables the drag entirely, snap dragging included. Escape, backdrop, outside-pointer, and close button continue to work.
|
|
522
523
|
|
|
523
524
|
## Scale background
|
|
524
525
|
|
|
525
526
|
Opt in to the "viewport recedes behind the drawer" effect: when the drawer opens, the rest of the app shrinks slightly and rounds its corners to read as a layered surface. Two pieces required:
|
|
526
527
|
|
|
527
|
-
1. Apply `[forDrawerWrapper]` on the element that wraps the rest of the app (typically the root shell).
|
|
528
|
+
1. Apply `[forDrawerWrapper]` on the element that wraps the rest of the app (typically the root shell). Keep one wrapper per page: the most recently mounted one is scaled, and a second one still mounted once the render settles warns in dev mode.
|
|
528
529
|
2. Set `[scaleBackground]="true"` on the drawer that should drive the effect.
|
|
529
530
|
|
|
530
531
|
```html
|
package/dropdown-menu/README.md
CHANGED
|
@@ -100,7 +100,7 @@ A settings-style dropdown built from the full menu vocabulary: `forMenuGroup` wi
|
|
|
100
100
|
| `fallbackAxisSideDirection` | `input<'none' \| 'start' \| 'end'>` | When both sides of the preferred axis overflow, lets `flip` drop the menu to a perpendicular side instead of clipping. `'none'` keeps only the opposite same-axis placement. The default is read from `provideForDropdownMenuDefaults` for the surrounding scope, so set it once for the whole app rather than per call site.<br>**Default:** `'none'` |
|
|
101
101
|
| `loop` | `input<boolean>` | Whether arrow navigation wraps at the ends.<br>**Default:** `true` |
|
|
102
102
|
| `dir` | `input<string>` | Writing direction. In RTL, ArrowLeft opens submenus and ArrowRight closes them. The swap is automatic. Inherited by every nested `[forMenuSub]` underneath unless overridden.<br>**Default:** `'ltr'` |
|
|
103
|
-
| `disabled` | `input<boolean>` | When `true`, trigger interactions are ignored.<br>**Default:** `false`
|
|
103
|
+
| `disabled` | `input<boolean>` | When `true`, trigger interactions are ignored and every item of the menu and its submenus is disabled. An open menu stays open, and keyboard navigation, Tab and Escape keep working.<br>**Default:** `false` |
|
|
104
104
|
| `dismissible` | `input<boolean>` | When `false`, Escape and outside interactions don't close.<br>**Default:** `true` |
|
|
105
105
|
| `returnFocus` | `input<boolean>` | When `true`, focus returns to the trigger on close.<br>**Default:** `true` |
|
|
106
106
|
| `ariaLabel` | `input<string \| null>` | Manual `aria-label` on `[forMenuContent]` if the trigger isn't a meaningful name.<br>**Default:** `null` |
|
|
@@ -356,7 +356,7 @@ class ForAccordionContent {
|
|
|
356
356
|
this.item.adoptContentId(inject(ElementRef).nativeElement);
|
|
357
357
|
}
|
|
358
358
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForAccordionContent, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
359
|
-
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForAccordionContent, isStandalone: true, selector: "[forAccordionContent]", host: { properties: { "id": "item.contentId()", "attr.role": "\"region\"", "attr.aria-
|
|
359
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForAccordionContent, isStandalone: true, selector: "[forAccordionContent]", host: { properties: { "id": "item.contentId()", "attr.role": "\"region\"", "attr.aria-hidden": "item.expanded() ? null : \"true\"", "attr.inert": "item.expanded() ? null : \"\"", "attr.data-state": "item.expanded() ? \"open\" : \"closed\"", "attr.data-disabled": "item.disabled() ? \"\" : null", "attr.data-orientation": "parent.orientation()" } }, exportAs: ["forAccordionContent"], ngImport: i0 });
|
|
360
360
|
}
|
|
361
361
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForAccordionContent, decorators: [{
|
|
362
362
|
type: Directive,
|
|
@@ -366,7 +366,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
|
|
|
366
366
|
host: {
|
|
367
367
|
'[id]': 'item.contentId()',
|
|
368
368
|
'[attr.role]': '"region"',
|
|
369
|
-
'[attr.aria-labelledby]': 'labelledBy()',
|
|
370
369
|
'[attr.aria-hidden]': 'item.expanded() ? null : "true"',
|
|
371
370
|
'[attr.inert]': 'item.expanded() ? null : ""',
|
|
372
371
|
'[attr.data-state]': 'item.expanded() ? "open" : "closed"',
|