forty-cdk 0.31.0 → 0.33.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/calendar/README.md +2 -2
- package/carousel/README.md +4 -3
- package/combobox/README.md +15 -1
- package/context-menu/README.md +2 -2
- package/date-field/README.md +2 -2
- package/date-picker/README.md +14 -9
- package/drag-drop/README.md +8 -3
- package/drawer/README.md +35 -33
- 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 +33 -8
- package/fesm2022/forty-cdk-calendar.mjs.map +1 -1
- package/fesm2022/forty-cdk-carousel.mjs +32 -25
- package/fesm2022/forty-cdk-carousel.mjs.map +1 -1
- package/fesm2022/forty-cdk-combobox.mjs +69 -10
- package/fesm2022/forty-cdk-combobox.mjs.map +1 -1
- package/fesm2022/forty-cdk-context-menu.mjs +8 -3
- package/fesm2022/forty-cdk-context-menu.mjs.map +1 -1
- package/fesm2022/forty-cdk-core-overlay.mjs +156 -14
- package/fesm2022/forty-cdk-core-overlay.mjs.map +1 -1
- package/fesm2022/forty-cdk-core.mjs +274 -84
- package/fesm2022/forty-cdk-core.mjs.map +1 -1
- package/fesm2022/forty-cdk-date-picker.mjs +40 -15
- package/fesm2022/forty-cdk-date-picker.mjs.map +1 -1
- package/fesm2022/forty-cdk-defaults.mjs +33 -9
- package/fesm2022/forty-cdk-defaults.mjs.map +1 -1
- package/fesm2022/forty-cdk-dialog.mjs +12 -19
- package/fesm2022/forty-cdk-dialog.mjs.map +1 -1
- package/fesm2022/forty-cdk-drag-drop.mjs +16 -8
- package/fesm2022/forty-cdk-drag-drop.mjs.map +1 -1
- package/fesm2022/forty-cdk-drawer.mjs +153 -79
- package/fesm2022/forty-cdk-drawer.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 +119 -17
- package/fesm2022/forty-cdk-hover-card.mjs.map +1 -1
- package/fesm2022/forty-cdk-listbox.mjs +182 -11
- package/fesm2022/forty-cdk-listbox.mjs.map +1 -1
- package/fesm2022/forty-cdk-menu.mjs +15 -5
- package/fesm2022/forty-cdk-menu.mjs.map +1 -1
- package/fesm2022/forty-cdk-menubar.mjs +29 -14
- 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 +58 -45
- package/fesm2022/forty-cdk-navigation-menu.mjs.map +1 -1
- package/fesm2022/forty-cdk-number-input.mjs +44 -1
- package/fesm2022/forty-cdk-number-input.mjs.map +1 -1
- package/fesm2022/forty-cdk-otp-input.mjs +8 -1
- 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 +7 -1
- package/fesm2022/forty-cdk-popover.mjs.map +1 -1
- package/fesm2022/forty-cdk-radio-group.mjs +41 -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 +10 -17
- package/fesm2022/forty-cdk-select.mjs.map +1 -1
- package/fesm2022/forty-cdk-slider.mjs +14 -3
- package/fesm2022/forty-cdk-slider.mjs.map +1 -1
- package/fesm2022/forty-cdk-stepper.mjs +25 -5
- package/fesm2022/forty-cdk-stepper.mjs.map +1 -1
- package/fesm2022/forty-cdk-table-virtualization.mjs +16 -11
- package/fesm2022/forty-cdk-table-virtualization.mjs.map +1 -1
- package/fesm2022/forty-cdk-table.mjs +96 -25
- package/fesm2022/forty-cdk-table.mjs.map +1 -1
- package/fesm2022/forty-cdk-tabs.mjs +0 -1
- package/fesm2022/forty-cdk-tabs.mjs.map +1 -1
- package/fesm2022/forty-cdk-testing.mjs +7 -1
- package/fesm2022/forty-cdk-testing.mjs.map +1 -1
- package/fesm2022/forty-cdk-time-picker.mjs +17 -0
- package/fesm2022/forty-cdk-time-picker.mjs.map +1 -1
- package/fesm2022/forty-cdk-toast.mjs +94 -17
- package/fesm2022/forty-cdk-toast.mjs.map +1 -1
- package/fesm2022/forty-cdk-tooltip.mjs +22 -11
- package/fesm2022/forty-cdk-tooltip.mjs.map +1 -1
- package/fesm2022/forty-cdk-tree.mjs +51 -5
- package/fesm2022/forty-cdk-tree.mjs.map +1 -1
- package/fesm2022/forty-cdk-virtual-reorder.mjs +24 -10
- package/fesm2022/forty-cdk-virtual-reorder.mjs.map +1 -1
- package/fesm2022/forty-cdk-virtualization.mjs +73 -7
- package/fesm2022/forty-cdk-virtualization.mjs.map +1 -1
- package/field/README.md +1 -1
- package/fieldset/README.md +1 -1
- package/file-upload/README.md +1 -1
- package/hover-card/README.md +17 -16
- package/input/README.md +1 -1
- package/listbox/README.md +121 -3
- package/menu/README.md +2 -1
- package/menubar/README.md +11 -11
- package/meter/README.md +1 -1
- package/navigation-menu/README.md +2 -2
- package/number-input/README.md +5 -5
- package/otp-input/README.md +3 -2
- 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/select/README.md +3 -2
- package/shared/README.md +2 -1
- package/slider/README.md +3 -3
- package/stepper/README.md +12 -12
- package/table/README.md +28 -23
- package/tabs/README.md +7 -7
- package/time-field/README.md +2 -2
- package/time-picker/README.md +15 -9
- package/toast/README.md +11 -8
- package/tooltip/README.md +19 -17
- package/tree/README.md +3 -3
- package/types/forty-cdk-breakpoints.d.ts +1 -1
- package/types/forty-cdk-calendar.d.ts +26 -7
- package/types/forty-cdk-carousel.d.ts +11 -6
- package/types/forty-cdk-combobox.d.ts +32 -10
- package/types/forty-cdk-context-menu.d.ts +7 -2
- package/types/forty-cdk-core-overlay.d.ts +106 -17
- package/types/forty-cdk-core.d.ts +156 -27
- package/types/forty-cdk-date-picker.d.ts +17 -6
- package/types/forty-cdk-defaults.d.ts +32 -21
- package/types/forty-cdk-drag-drop.d.ts +14 -4
- package/types/forty-cdk-drawer.d.ts +24 -5
- package/types/forty-cdk-file-upload.d.ts +17 -3
- package/types/forty-cdk-hover-card.d.ts +64 -7
- package/types/forty-cdk-listbox.d.ts +137 -12
- package/types/forty-cdk-menu.d.ts +14 -6
- package/types/forty-cdk-menubar.d.ts +8 -2
- package/types/forty-cdk-meter.d.ts +1 -1
- package/types/forty-cdk-navigation-menu.d.ts +14 -21
- package/types/forty-cdk-number-input.d.ts +10 -1
- package/types/forty-cdk-otp-input.d.ts +5 -1
- 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 +17 -15
- 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 +5 -8
- package/types/forty-cdk-slider.d.ts +6 -5
- package/types/forty-cdk-stepper.d.ts +47 -12
- package/types/forty-cdk-table-virtualization.d.ts +3 -1
- package/types/forty-cdk-table.d.ts +33 -7
- package/types/forty-cdk-tabs.d.ts +3 -4
- package/types/forty-cdk-testing.d.ts +4 -1
- package/types/forty-cdk-time-picker.d.ts +7 -1
- package/types/forty-cdk-toast.d.ts +25 -11
- package/types/forty-cdk-tooltip.d.ts +7 -4
- package/types/forty-cdk-tree.d.ts +10 -4
- package/types/forty-cdk-virtual-reorder.d.ts +15 -6
- package/types/forty-cdk-virtualization.d.ts +42 -9
- package/virtual-reorder/README.md +3 -1
- package/virtualization/README.md +23 -12
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/calendar/README.md
CHANGED
|
@@ -214,7 +214,7 @@ Click the heading button to cycle from day → month → year view. Click a mont
|
|
|
214
214
|
| ------------------- | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
215
215
|
| `value` | `model<D \| null>` | Two-way bindable selected date, or `null`. Used in `selectionMode="single"`. `(valueChange)` fires only on internal selection.<br>**Default:** `null` |
|
|
216
216
|
| `selectionMode` | `input<'single' \| 'range'>` | `'single'` (default) keeps the single-date `value` flow. `'range'` switches to anchor → commit and exposes `range`.<br>**Default:** `'single'` |
|
|
217
|
-
| `range` | `model<DateRange<D> \| null>` | Two-way bindable committed range. Only used in `selectionMode="range"`. `(rangeChange)` fires only on internal commits
|
|
217
|
+
| `range` | `model<DateRange<D> \| null>` | Two-way bindable committed range. Only used in `selectionMode="range"`. `(rangeChange)` fires only on internal commits.<br>**Default:** `null` |
|
|
218
218
|
| `minRangeLength` | `input<number \| null>` | Minimum inclusive day count. A commit shorter than this is a no-op.<br>**Default:** `null` (no minimum) |
|
|
219
219
|
| `maxRangeLength` | `input<number \| null>` | Maximum inclusive day count. A commit longer than this is a no-op.<br>**Default:** `null` (no maximum) |
|
|
220
220
|
| `min` | `input<D \| null>` | Minimum selectable date (inclusive). Earlier dates are unavailable.<br>**Default:** `null` |
|
|
@@ -251,7 +251,7 @@ Set `selectionMode="range"` and bind `[(range)]` to get date-range selection. In
|
|
|
251
251
|
</div>
|
|
252
252
|
```
|
|
253
253
|
|
|
254
|
-
**Interaction model.** Click (or `Enter` / `Space`) a first cell to set the anchor; the grid enters selecting state. Click (or `Enter` / `Space`) a second cell **in either direction** to commit the range. Clicking before the anchor commits the inverted band `[click, anchor]` (matching the hover preview) rather than starting over. There is no separate "start over" gesture and no explicit Escape-to-cancel: once a range is committed, the next click begins a fresh anchor.
|
|
254
|
+
**Interaction model.** Click (or `Enter` / `Space`) a first cell to set the anchor; the grid enters selecting state. Click (or `Enter` / `Space`) a second cell **in either direction** to commit the range. Clicking before the anchor commits the inverted band `[click, anchor]` (matching the hover preview) rather than starting over. There is no separate "start over" gesture and no explicit Escape-to-cancel: once a range is committed, the next click begins a fresh anchor, and `range` keeps the committed range until the second click replaces it.
|
|
255
255
|
|
|
256
256
|
**Keyboard in range mode.** `Enter` / `Space` on the focused cell sets the anchor on the first press and commits on the second (same key as single mode). While selecting, arrow / `Home` / `End` / `PageUp` / `PageDown` move the keyboard focus and update the preview cursor (the keyboard equivalent of pointer hover); moving before the anchor previews (and commits) the inverted band.
|
|
257
257
|
|
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/combobox/README.md
CHANGED
|
@@ -549,6 +549,15 @@ The `autocompleteMode` input mirrors the WAI-ARIA `aria-autocomplete` property:
|
|
|
549
549
|
|
|
550
550
|
Inline completion preserves the user's typed prefix as unselected and selects the appended remainder, so the next keystroke replaces the selection (matching native browser autofill behavior). Backspace deletes the selection without re-completing, so the user can always shorten the query.
|
|
551
551
|
|
|
552
|
+
The completed text is a suggestion: `query()` keeps the typed prefix until the user accepts it, and accepting writes the full label into `query` (emitting `(queryChange)`). These gestures accept a pending completion:
|
|
553
|
+
|
|
554
|
+
- **Tab**, before the listbox closes and focus moves on.
|
|
555
|
+
- **Enter** when no option is activated (the listbox is closed, or open with nothing highlighted).
|
|
556
|
+
- A caret move that keeps the completed text: **ArrowLeft** / **ArrowRight**, and **Home** / **End** while the listbox is closed. While it is open, Home and End move the highlight instead and the completion stays pending.
|
|
557
|
+
- A **click** in the input, and **blur**.
|
|
558
|
+
|
|
559
|
+
Typing replaces the suggestion, Backspace removes it, and Escape on an open listbox closes it and restores the typed prefix. Accepting never opens the listbox, so a completion accepted on blur does not reopen a popup the same click dismissed.
|
|
560
|
+
|
|
552
561
|
> **Pure `'inline'` needs a warm cache.** `'inline'` never opens the popup (per APG, `aria-autocomplete="inline"` has no listbox), so in the default `@if (open())` anatomy no `[forComboboxOption]` ever renders and the label cache starts cold. A first keystroke into a combobox that has never been opened completes against nothing; inline completion only works once the options have rendered at least once (the user opened the popup via ArrowDown or `[openOnFocus]`, warming the cache). If completion must work from the very first keystroke, use `'both'`, which opens the popup, so the options render and the cache warms. Leaving `[forComboboxContent]` permanently mounted instead is not a supported shape and warns in dev mode: mount **is** the open state for this surface, so it never runs `animate.enter` / `animate.leave`, and its dismissible layer stays active while closed.
|
|
553
562
|
|
|
554
563
|
## Dismiss events
|
|
@@ -707,6 +716,8 @@ readonly v = injectVirtualizer({
|
|
|
707
716
|
|
|
708
717
|
This uses the library's own [`injectVirtualizer`](../virtualization/README.md) core: `v.virtualItems()` is the windowed slice, `v.totalSize()` the spacer height, `v.range()` feeds `[visibleRange]`, and `v.scrollToIndex(idx)` brings an absolute index into view. The scroll container belongs to the consumer's virtualizer, not the directive (here `[forComboboxContent]` is the scroll element).
|
|
709
718
|
|
|
719
|
+
`[totalCount]` is also the result count the message pieces read: `[forComboboxStatus]`'s `count()` reports it rather than the size of the rendered window, so `{{ status.count() }} results.` announces the full count once and stays put while the window scrolls, and `[forComboboxEmpty]` stays hidden while a window with results has not rendered yet.
|
|
720
|
+
|
|
710
721
|
When `[totalCount]` is omitted, the directive falls back to `options().length` and behaves exactly as before: `aria-setsize` is left to the platform default and navigation never emits `(scrollToIndex)`.
|
|
711
722
|
|
|
712
723
|
> **Disabled options off-screen.** The directive learns an option's `disabled` only when it's been rendered at least once. While the consumer can pre-mark disabled rows with their own filter (most apps do), arrow nav cannot skip an off-screen disabled option it has never seen: it will land on it, the option will mount, and the next arrow press skips. Mark disabled rows in the source array if this matters.
|
|
@@ -792,9 +803,12 @@ Focus stays in the input throughout: arrow keys move the listbox's _active desce
|
|
|
792
803
|
| **Tab** _(open, no action)_ | Close the listbox and let Tab flow to the next focusable. |
|
|
793
804
|
| **Tab / Shift+Tab** _(open, action present)_ | Move focus around the input↔actions ring without dismissing (see [Action items](#action-items)). |
|
|
794
805
|
| **Backspace** _(empty input, multi only)_ | Focus the last chip; a second Backspace there removes it. |
|
|
806
|
+
| **ArrowLeft / ArrowRight** | Move the caret, accepting a pending inline completion. |
|
|
795
807
|
| Printable keys | Update `query`. With `'inline'` / `'both'` autocomplete, complete the rest of the first match into the input as selected text. |
|
|
796
808
|
|
|
797
|
-
|
|
809
|
+
With `'inline'` / `'both'` autocomplete, Tab, Enter with no option activated, and Home / End while closed also accept a pending completion (see [Autocomplete modes](#autocomplete-modes)).
|
|
810
|
+
|
|
811
|
+
Hovering an option also makes it the activedescendant, so mouse and keyboard intent stay synchronized. For a short window after the directive scrolls an option into view (arrow navigation, the auto-highlight seed after the list changes, and the reveal on open), a hover is ignored, so an option the scroll slides under a resting cursor cannot take the highlight.
|
|
798
812
|
|
|
799
813
|
## Accessibility
|
|
800
814
|
|
package/context-menu/README.md
CHANGED
|
@@ -92,7 +92,7 @@ The same menu vocabulary the Dropdown Menu exposes, anchored to the pointer on r
|
|
|
92
92
|
| `dir` | `input<string>` | Writing direction. In RTL the swap is automatic: ArrowLeft opens submenus and ArrowRight closes them. Inherited by every nested `[forMenuSub]` underneath unless overridden.<br>**Default:** `'ltr'` |
|
|
93
93
|
| `disabled` | `input<boolean>` | When `true`, the contextmenu event falls through to the native browser menu.<br>**Default:** `false` |
|
|
94
94
|
| `dismissible` | `input<boolean>` | When `false`, Escape and outside interactions don't close.<br>**Default:** `true` |
|
|
95
|
-
| `returnFocus` | `input<boolean>` | When `true`, focus returns to the trigger element
|
|
95
|
+
| `returnFocus` | `input<boolean>` | When `true`, focus returns on close to the trigger element, or to the focused element inside it that a keyboard activation started from.<br>**Default:** `true` |
|
|
96
96
|
| `ariaLabel` | `input<string \| null>` | Accessible name reflected as `aria-label` on `[forMenuContent]`. The root's only name hook for a context menu: the right-click region is never used as an `aria-labelledby` target.<br>**Default:** `null` |
|
|
97
97
|
| `escapeKeyDown` | `output<VetoableNativeEvent<KeyboardEvent>>` | Output. Escape pressed while the menu is the topmost dismissible layer.<br>**Default:** — |
|
|
98
98
|
| `pointerDownOutside` | `output<VetoableNativeEvent<PointerEvent>>` | Output. Pointer-down on a target outside content + trigger.<br>**Default:** — |
|
|
@@ -135,7 +135,7 @@ forty-cdk ships no styles: put your own class on each piece and key your CSS off
|
|
|
135
135
|
## Behavior notes
|
|
136
136
|
|
|
137
137
|
- **Trigger is NOT exempt** from outside-pointer / outside-focus checks. Unlike DropdownMenu (where the trigger button toggles via its own click handler), the context-menu region is treated like any other "outside" element: a left-click on the region while the menu is open closes it. Right-clicking again immediately reopens at the new position. This is a full close → open cycle, not an in-place reposition: re-right-clicking while the menu is already open tears the surface down and rebuilds it (so any `animate.enter` / `animate.leave` replays and focus resets). Keep enter/leave animations cheap, or gate expensive ones, since a user can fire this rapidly. The directive deliberately exempts nothing, so there is no smooth reposition path to attach to.
|
|
138
|
-
- **Virtual anchor.** Right-click captures a 0×0 rect at the pointer location. `Shift+F10` and `ContextMenu` snapshot the bounding rect of the focused element (or the trigger if focus is on it directly), so the menu floats off the element under attention. Both forms feed floating-ui's `flip` and `shift` middleware, so corners and screen edges work without special-casing.
|
|
138
|
+
- **Virtual anchor.** Right-click captures a 0×0 rect at the pointer location. `Shift+F10` and `ContextMenu` snapshot the bounding rect of the focused element (or the trigger if focus is on it directly), so the menu floats off the element under attention. Closing a keyboard-opened menu returns focus to that same element, so a row in a roving-tabindex list keeps its position; when a menu action removed it, focus falls back to the region. Both forms feed floating-ui's `flip` and `shift` middleware, so corners and screen edges work without special-casing.
|
|
139
139
|
- **Keyboard activators only fire while focus is inside the trigger.** Keyboard events dispatch to the focused element, so `Shift+F10` / `ContextMenu` anywhere outside the trigger goes to the browser default. The trigger is focusable by default (host-bound `tabindex="-1"`), so this works out of the box and focus can return to it programmatically when the menu closes. Your own `tabindex` wins over that default: use `tabindex="0"` if you want the region itself reachable via Tab.
|
|
140
140
|
- **Native menu suppressed.** The trigger calls `event.preventDefault()` on `contextmenu` and on the keyboard activators. Set `disabled` to let the browser's native menu surface for that region.
|
|
141
141
|
- **An open that renders nothing closes again.** When a right-click, `Shift+F10`, the `ContextMenu` key or a long-press opens a menu whose template renders no `[forMenuContent]`, the trigger closes it after the next render with reason `'programmatic'`, and dev mode logs `FORCDK-CONTEXT-MENU-002` once. The native menu cannot come back for that gesture, because it was already suppressed, so bind `[disabled]` to the same condition that renders the content: `[disabled]="!hasActions(col)"` beside `@if (hasActions(col))`.
|
package/date-field/README.md
CHANGED
|
@@ -220,7 +220,7 @@ providers: [
|
|
|
220
220
|
|
|
221
221
|
`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
222
|
|
|
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]`.
|
|
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. 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
224
|
|
|
225
225
|
`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
226
|
|
|
@@ -313,7 +313,7 @@ Key behavior applies per segment. Horizontal arrows mirror under `dir="rtl"`.
|
|
|
313
313
|
| **Backspace** | Delete the last entered digit; the value becomes `null` when the last digit is removed. |
|
|
314
314
|
| **Delete** | Clear the whole segment (the value becomes `null` until refilled). |
|
|
315
315
|
|
|
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]`.
|
|
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]`. 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
317
|
|
|
318
318
|
## Accessibility
|
|
319
319
|
|
package/date-picker/README.md
CHANGED
|
@@ -48,7 +48,7 @@ All date math and formatting go through a `DateAdapter<D>`, shared with `ForCale
|
|
|
48
48
|
</div>
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
-
Bind the projected `[forCalendar]` to the picker: `[(value)]` to the same date signal, and forward `[min]` / `[max]` / `[isDateUnavailable]` from the picker's accessors (`#picker="forDatePicker"`). The picker observes the calendar's selection through a `contentChild` query and never mutates the calendar, so picking a date sets the value, flips `touched`, and (when `closeOnSelect` is on) closes the surface.
|
|
51
|
+
Bind the projected `[forCalendar]` to the picker: `[(value)]` to the same date signal, and forward `[min]` / `[max]` / `[isDateUnavailable]` from the picker's accessors (`#picker="forDatePicker"`). The picker observes the calendar's selection through a `contentChild` query and never mutates the calendar, so picking a date sets the value, flips `touched`, and (when `closeOnSelect` is on) closes the surface. The calendar takes the picker's `readonly` and `disabled` without a binding, so a read-only or disabled picker lets no pick through and paints no selection, whichever way the calendar is bound.
|
|
52
52
|
|
|
53
53
|
Presence in the DOM is yours: wrap `[forDatePickerContent]` in `@if (picker.open())`, and `animate.enter` / `animate.leave` drive its transitions.
|
|
54
54
|
|
|
@@ -191,7 +191,7 @@ With `granularity="minute"` and a time-capable adapter the picker becomes a date
|
|
|
191
191
|
|
|
192
192
|
### Range selection
|
|
193
193
|
|
|
194
|
-
Open the picker and click a first day: the trigger keeps its placeholder, because the value `[formField]` reads
|
|
194
|
+
Open the picker and click a first day: the trigger keeps its placeholder, because the value `[formField]` reads does not change until a second click commits the range. Close the panel with only one end picked and the required error appears. [`ForDateRangePicker`](#range-selection--fordaterangepicker) documents the root, its bounds and native submission.
|
|
195
195
|
|
|
196
196
|
## API
|
|
197
197
|
|
|
@@ -231,6 +231,9 @@ Plus the shared `FormUiControl` inputs from the base (`disabled`, `readonly`, `r
|
|
|
231
231
|
| `[forDatePicker]` | `data-state` | `open` \| `closed` |
|
|
232
232
|
| `[forDatePicker]` | `data-disabled` | present \| absent |
|
|
233
233
|
| `[forDatePicker]` | `data-readonly` | present \| absent |
|
|
234
|
+
| `[forDateRangePicker]` | `data-state` | `open` \| `closed` |
|
|
235
|
+
| `[forDateRangePicker]` | `data-disabled` | present \| absent |
|
|
236
|
+
| `[forDateRangePicker]` | `data-readonly` | present \| absent |
|
|
234
237
|
| `[forDatePickerTrigger]` | `data-state` | `open` \| `closed` |
|
|
235
238
|
| `[forDatePickerTrigger]` | `data-disabled` | present \| absent |
|
|
236
239
|
| `[forDatePickerTrigger]` | `data-readonly` | present \| absent |
|
|
@@ -383,7 +386,7 @@ The anatomy is explicit, so a date field placed inside the surface of a trigger-
|
|
|
383
386
|
|
|
384
387
|
For date-range selection use the dedicated `ForDateRangePicker` root (selector `[forDateRangePicker]`). It is the root **and** the form value, implementing `FormValueControl<DateRange<D> | null>`, so the committed range auto-wires with `[formField]` exactly like any other control.
|
|
385
388
|
|
|
386
|
-
It reuses the same pieces (`[forDatePickerTrigger]`, `[forDatePickerContent]`, `[forDatePickerValue]`, `[forDatePickerAnchor]`) through a shared base, and provides `FOR_DATE_PICKER_CONTEXT` so they resolve under it. Project a `[forCalendar]` in `selectionMode="range"` and bind its range to the picker's `value`; the two-click anchor → commit flow
|
|
389
|
+
It reuses the same pieces (`[forDatePickerTrigger]`, `[forDatePickerContent]`, `[forDatePickerValue]`, `[forDatePickerAnchor]`) through a shared base, and provides `FOR_DATE_PICKER_CONTEXT` so they resolve under it. Project a `[forCalendar]` in `selectionMode="range"` and bind its range to the picker's `value`; the two-click anchor → commit flow leaves `value` unchanged until both endpoints are chosen, so the form never sees a half-entered range, and abandoning a new selection with Escape or an outside click keeps the range committed before it. `start <= end` is an invariant. Range is day-granular (no time composition).
|
|
387
390
|
|
|
388
391
|
<!-- snippet: fragment -->
|
|
389
392
|
|
|
@@ -428,7 +431,7 @@ readonly booking = form(this.model, (p) => required(p.stay));
|
|
|
428
431
|
```
|
|
429
432
|
|
|
430
433
|
- **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.
|
|
431
|
-
- **Validity.** `start <= end` is guaranteed by construction and is never an error. Forward `minDate` / `maxDate` to the calendar's `[min]` / `[max]
|
|
434
|
+
- **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).
|
|
432
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.
|
|
433
436
|
- **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.
|
|
434
437
|
|
|
@@ -436,12 +439,14 @@ Defaults are configured with `provideForDateRangePickerDefaults` (`side` / `alig
|
|
|
436
439
|
|
|
437
440
|
## Keyboard
|
|
438
441
|
|
|
439
|
-
| Key
|
|
440
|
-
|
|
|
441
|
-
| **Enter / Space** on trigger
|
|
442
|
-
| **
|
|
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
|
+
| **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. |
|
|
443
448
|
|
|
444
|
-
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`) in non-modal mode, or the first focusable element in modal mode.
|
|
449
|
+
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.
|
|
445
450
|
|
|
446
451
|
## Accessibility
|
|
447
452
|
|
package/drag-drop/README.md
CHANGED
|
@@ -446,11 +446,16 @@ scope. `[animateReorder]` (FLIP) reflows by DOM order and needs no change in mix
|
|
|
446
446
|
## Free drag
|
|
447
447
|
|
|
448
448
|
`[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 `
|
|
449
|
+
**no `[forDropList]` required**. It never commits a reorder; it just moves the element around via
|
|
450
|
+
the CSS `translate` property. It is the standalone counterpart to a sortable list item: a
|
|
451
451
|
pointer-driven way to move an element freely, with optional root-element retargeting, a confinement
|
|
452
452
|
boundary, axis locking, and a controllable position (see the inputs below).
|
|
453
453
|
|
|
454
|
+
The offset composes with any `transform` the moved element already has, so a dialog centred with
|
|
455
|
+
`transform: translate(-50%, -50%)` stays centred and moves from there. It replaces an inline
|
|
456
|
+
`translate` property of the element's own, so position the element with `transform` or layout
|
|
457
|
+
properties rather than `translate`.
|
|
458
|
+
|
|
454
459
|
```html
|
|
455
460
|
<!-- move the whole dialog by its header -->
|
|
456
461
|
<div class="dialog">
|
|
@@ -467,7 +472,7 @@ boundary, axis locking, and a controllable position (see the inputs below).
|
|
|
467
472
|
|
|
468
473
|
| Input | Type | Default | Meaning |
|
|
469
474
|
| ------------- | ------------------------------- | ------------- | -------------------------------------------------------------------------------------------------- |
|
|
470
|
-
| `disabled` | `boolean` | `false` | When true, the element can't be dragged (it stays focusable;
|
|
475
|
+
| `disabled` | `boolean` | `false` | When true, the element can't be dragged (it stays focusable; its position doesn't change). |
|
|
471
476
|
| `rootElement` | `HTMLElement \| string \| null` | `null` | The element actually moved. A `closest()` selector resolves an ancestor. `null` moves the host. |
|
|
472
477
|
| `boundary` | `HTMLElement \| string \| null` | `null` | Confine the moved element fully inside this element (or `closest()` selector). `null` = unbounded. |
|
|
473
478
|
| `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
|
@@ -153,7 +153,7 @@ class DemoHost {
|
|
|
153
153
|
|
|
154
154
|
`injectDrawerData<T>()` is typed `T | null`: the manager provides `null` when `open()` is called without `data`, so guard (`data?.message`) before dereferencing the payload. `await ref.closed` resolves `{ reason, result }`, whose `reason` (a `ForDrawerCloseReason`) tells apart an imperative `close()` (`'programmatic'`) from Escape / backdrop / outside / swipe / close-button dismissals.
|
|
155
155
|
|
|
156
|
-
Drawers opened by the manager
|
|
156
|
+
Drawers opened by the manager share the LIFO dismissible layer with declarative ones, so Escape closes the topmost drawer first whichever API opened it. Nesting is resolved from where a drawer is declared, not from the order drawers open in: a declarative `[forDrawer]` inside a programmatic drawer's component is its child (`data-depth="1"`, and the programmatic parent reflects `data-state-nested`), but a drawer the manager opens over a declarative one is a root, so it reflects `data-depth="0"` and its declarative neighbour gets no `data-state-nested`.
|
|
157
157
|
|
|
158
158
|
`ForDrawerManager.openCount` is a `Signal<number>` of the drawers it has open, counting a closed one until its exit animation finishes. `closeAll(result?)` closes every one of them, topmost first, with reason `'programmatic'`: each `ref.closed` resolves as it does for `ref.close()`, exit animations still play, and focus ends where the bottom drawer returns it.
|
|
159
159
|
|
|
@@ -359,39 +359,39 @@ Press the button: the manager opens the confirmation as a bottom drawer, and the
|
|
|
359
359
|
|
|
360
360
|
### `ForDrawer`
|
|
361
361
|
|
|
362
|
-
| Property | Type | Description
|
|
363
|
-
| --------------------------- | ------------------------------------------------------------------- |
|
|
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`
|
|
366
|
-
| `dismissible` | `boolean` | Whether Escape / backdrop / outside / swipe close.<br>**Default:** `true`
|
|
367
|
-
| `alert` | `boolean` | `role="alertdialog"`.<br>**Default:** `false`
|
|
368
|
-
| `returnFocus` | `boolean` | Restore focus on close.<br>**Default:** `true`
|
|
369
|
-
| `initialFocus` | `'first' \| 'container'` | **Default:** `'first'`
|
|
370
|
-
| `ariaLabel` | `string \| null` | Use when no visible title is rendered.<br>**Default:** `null`
|
|
371
|
-
| `
|
|
372
|
-
| `
|
|
373
|
-
| `
|
|
374
|
-
| `
|
|
375
|
-
| `
|
|
376
|
-
| `
|
|
377
|
-
| `
|
|
378
|
-
| `
|
|
379
|
-
| `
|
|
380
|
-
| `
|
|
381
|
-
| `
|
|
382
|
-
| `
|
|
383
|
-
| `
|
|
384
|
-
| `
|
|
385
|
-
| `
|
|
386
|
-
| `
|
|
387
|
-
| `
|
|
388
|
-
| `
|
|
389
|
-
| `
|
|
390
|
-
| `swipeEnd` | `OutputEmitterRef<ForDrawerSwipeEndEvent>` | Output. `willClose`, `nextSnapPoint`. Directive already updated state.<br>**Default:** — |
|
|
391
|
-
| `swipeCancel` | `OutputEmitterRef<ForDrawerSwipeEvent>` | Output. `pointercancel` or mid-gesture direction abort; the surface springs back.<br>**Default:** — |
|
|
362
|
+
| Property | Type | Description |
|
|
363
|
+
| --------------------------- | ------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
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` |
|
|
366
|
+
| `dismissible` | `boolean` | Whether Escape / backdrop / outside / swipe close. When `false`, no swipe dismisses; `snapPoints` still drag between snaps.<br>**Default:** `true` |
|
|
367
|
+
| `alert` | `boolean` | `role="alertdialog"`.<br>**Default:** `false` |
|
|
368
|
+
| `returnFocus` | `boolean` | Restore focus on close.<br>**Default:** `true` |
|
|
369
|
+
| `initialFocus` | `'first' \| 'container'` | **Default:** `'first'` |
|
|
370
|
+
| `ariaLabel` | `string \| null` | Use when no visible title is rendered.<br>**Default:** `null` |
|
|
371
|
+
| `autoFocusOnOpen` | `(e: VetoableEvent) => void` \| `undefined` | `event.preventDefault()` skips the imperative focus move.<br>**Default:** — |
|
|
372
|
+
| `autoFocusOnClose` | `(e: VetoableEvent) => void` \| `undefined` | Fires on every close path regardless of mode. In non-modal mode the directive doesn't move focus, so the veto is informational; in modal mode `event.preventDefault()` skips return-focus.<br>**Default:** — |
|
|
373
|
+
| `swipeToDismiss` | `boolean` | Whether a swipe toward the anchored edge dismisses. When `false`, `snapPoints` still drag between snaps and stop at the lowest one. No drag arms under `prefers-reduced-motion: reduce`.<br>**Default:** `true` |
|
|
374
|
+
| `closeThreshold` | `number` | Fraction past which a release dismisses, measured against the full dimension without `snapPoints` and against the lowest snap's extent with them.<br>**Default:** `0.25` |
|
|
375
|
+
| `handleOnly` | `boolean` | Swipe arms only on the registered `[forDrawerHandle]`.<br>**Default:** `false` |
|
|
376
|
+
| `snapPoints` | `ReadonlyArray<ForDrawerSnapPoint>` | `number ∈ [0,1]` \| `'NN%'` \| `'NNpx'`. Strictly increasing.<br>**Default:** — |
|
|
377
|
+
| `activeSnapPoint` | `ModelSignal<ForDrawerSnapPoint \| null>` | Two-way bindable. Initialised to `snapPoints[0]` on mount when null.<br>**Default:** `null` |
|
|
378
|
+
| `fadeFromIndex` | `number` | Backdrop reflects `data-fade-from-active` once active >= this index.<br>**Default:** — |
|
|
379
|
+
| `scaleBackground` | `boolean` | Asks `[forDrawerWrapper]` to scale + translate behind the drawer.<br>**Default:** `false` |
|
|
380
|
+
| `setBackgroundColorOnScale` | `boolean` | Paints `<body>` to mask the gap between scaled wrapper and viewport edge.<br>**Default:** `true` |
|
|
381
|
+
| `dismiss` | `OutputEmitterRef<ForDrawerCloseReason>` | Output. Wire to `(dismiss)="open.set(false)"`.<br>**Default:** — |
|
|
382
|
+
| `escapeKeyDown` | `OutputEmitterRef<VetoableNativeEvent<KeyboardEvent>>` | Output. `preventDefault()` suppresses auto-close.<br>**Default:** — |
|
|
383
|
+
| `pointerDownOutside` | `OutputEmitterRef<VetoableNativeEvent<PointerEvent>>` | Output. `preventDefault()` suppresses auto-close.<br>**Default:** — |
|
|
384
|
+
| `focusOutside` | `OutputEmitterRef<VetoableNativeEvent<FocusEvent>>` | Output. `preventDefault()` suppresses auto-close.<br>**Default:** — |
|
|
385
|
+
| `interactOutside` | `OutputEmitterRef<VetoableNativeEvent<PointerEvent \| FocusEvent>>` | Output. Composite, vetoed by either specific event.<br>**Default:** — |
|
|
386
|
+
| `swipeStart` | `OutputEmitterRef<ForDrawerSwipeEvent>` | Output. Fires once on the arming pointer move; `progress` is `0`.<br>**Default:** — |
|
|
387
|
+
| `swipeMove` | `OutputEmitterRef<ForDrawerSwipeEvent>` | Output. Streams `progress` ∈ [0,1] and the originating `PointerEvent`.<br>**Default:** — |
|
|
388
|
+
| `swipeEnd` | `OutputEmitterRef<ForDrawerSwipeEndEvent>` | Output. `willClose`, `nextSnapPoint`. Directive already updated state.<br>**Default:** — |
|
|
389
|
+
| `swipeCancel` | `OutputEmitterRef<ForDrawerSwipeEvent>` | Output. `pointercancel` or mid-gesture direction abort; the surface springs back.<br>**Default:** — |
|
|
392
390
|
|
|
393
391
|
`ForDrawerCloseReason`: `'escape' | 'backdrop' | 'pointerDownOutside' | 'focusOutside' | 'closeButton' | 'swipe' | 'programmatic'`.
|
|
394
392
|
|
|
393
|
+
Enter and exit animations are not inputs. On a declarative drawer, put `animate.enter` / `animate.leave` on the `[forDrawer]` host inside its `@if`; on a programmatic one, pass `animateEnter` / `animateLeave` to [`ForDrawerManager.open()`](#programmatic--fordrawermanageropen).
|
|
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
397
|
| Data attribute | Values |
|
|
@@ -444,7 +444,7 @@ Three accepted shapes:
|
|
|
444
444
|
- `'NN%'`: equivalent to a fraction (`'50%' === 0.5`).
|
|
445
445
|
- `'NNpx'`: absolute pixel size measured from the anchored edge.
|
|
446
446
|
|
|
447
|
-
Pass them in **strictly increasing** order (closest-to-edge first); 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`.
|
|
447
|
+
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
448
|
|
|
449
449
|
```html
|
|
450
450
|
<div
|
|
@@ -514,10 +514,12 @@ For a seamless release, transition **both** `translate` and your snap-position p
|
|
|
514
514
|
|
|
515
515
|
- Pointer drag toward the anchored edge translates the surface and resolves to the nearest snap (or a dismiss) on release.
|
|
516
516
|
- 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
|
+
- 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.
|
|
517
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.
|
|
518
519
|
- `handleOnly: true` confines the gesture to a registered `[forDrawerHandle]`, leaving the rest of the surface free for content scroll.
|
|
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.
|
|
519
521
|
- Gestures starting inside a scrollable element that hasn't reached its edge are NOT treated as swipes (the helper defers to inner scroll).
|
|
520
|
-
- **`prefers-reduced-motion: reduce`** disables the
|
|
522
|
+
- **`prefers-reduced-motion: reduce`** disables the drag entirely, snap dragging included. Escape, backdrop, outside-pointer, and close button continue to work.
|
|
521
523
|
|
|
522
524
|
## Scale background
|
|
523
525
|
|
|
@@ -199,17 +199,18 @@ class ForAvatarImage {
|
|
|
199
199
|
const parent = this.#parent;
|
|
200
200
|
const host = this.#host;
|
|
201
201
|
let destroyed = false;
|
|
202
|
-
|
|
202
|
+
const destroyRef = inject(DestroyRef);
|
|
203
|
+
destroyRef.onDestroy(() => (destroyed = true));
|
|
203
204
|
afterNextRender(() => {
|
|
204
205
|
this.#syncFromAttr();
|
|
205
|
-
|
|
206
|
-
|
|
206
|
+
if (destroyed || typeof MutationObserver === 'undefined')
|
|
207
|
+
return;
|
|
207
208
|
const observer = new MutationObserver(() => {
|
|
208
209
|
this.#syncFromAttr();
|
|
209
210
|
});
|
|
210
211
|
observer.observe(host, { attributes: true, attributeFilter: ['src'] });
|
|
211
|
-
|
|
212
|
-
}
|
|
212
|
+
destroyRef.onDestroy(() => observer.disconnect());
|
|
213
|
+
});
|
|
213
214
|
|
|
214
215
|
|
|
215
216
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"forty-cdk-avatar.mjs","sources":["../../../projects/forty-cdk/avatar/src/avatar-context.ts","../../../projects/forty-cdk/avatar/src/avatar.ts","../../../projects/forty-cdk/avatar/src/avatar-image.ts","../../../projects/forty-cdk/avatar/src/avatar-fallback.ts","../../../projects/forty-cdk/avatar/src/forty-cdk-avatar.ts"],"sourcesContent":["import { inject, InjectionToken, type Signal } from '@angular/core';\n\nimport { assertRootContext, orphanContextError } from 'forty-cdk/core';\n\n/**\n * Lifecycle stages of the avatar's image:\n * - `idle`: no `src` set yet.\n * - `loading`: `src` set, waiting for the network.\n * - `loaded`: image rendered successfully.\n * - `error`: image failed to load.\n */\nexport type ForAvatarStatus = 'idle' | 'loading' | 'loaded' | 'error';\n\n/** Read surface `[forAvatar]` publishes through {@link FOR_AVATAR_CONTEXT}. */\nexport interface ForAvatarContext {\n /** Current status of the avatar's image. */\n readonly status: Signal<ForAvatarStatus>;\n /**\n * `true` when the consumer should render the fallback. Drives `@if` in\n * the consumer's template. Honors `fallbackDelayMs` for `idle` / `loading`\n * states; `error` flips it to `true` immediately.\n */\n readonly shouldShowFallback: Signal<boolean>;\n}\n\n/**\n * The avatar's piece-coordination surface: the one call `[forAvatarImage]`\n * makes so the root can transition its state machine.\n *\n * **Not** part of {@link ForAvatarContext} and never exported from\n * `public-api.ts` — a consumer drives the status by binding `[src]`, never by\n * reporting a transition into the root.\n */\nexport interface AvatarPieceContext {\n /** Reported by `[forAvatarImage]` so the root can transition its state machine. */\n reportStatus(status: ForAvatarStatus): void;\n}\n\n/**\n * The avatar's internal coordination surface: everything\n * {@link ForAvatarContext} publishes plus the {@link AvatarPieceContext} call.\n *\n * Never exported from `public-api.ts`. It is the type the pieces read\n * {@link FOR_AVATAR_CONTEXT} at, so a consumer who injects that token gets the\n * read surface while `[forAvatarImage]` gets the transition channel. `ForAvatar`\n * declares `reportStatus` TS-`private`, which keeps it out of the emitted\n * `.d.ts` while `useExisting` still satisfies this contract at runtime.\n */\nexport interface AvatarContext extends ForAvatarContext, AvatarPieceContext {}\n\n/**\n * DI token for the avatar's coordination surface, provided by `[forAvatar]`.\n *\n * Publicly typed as the read surface {@link ForAvatarContext}, which is the whole of\n * what the token promises a consumer. `[forAvatarImage]` reads the same token at an\n * internal type that adds the status-report channel, so a wrapper re-providing it must\n * alias it to the root: `{ provide: FOR_AVATAR_CONTEXT, useExisting: MyAvatar }`, where\n * `MyAvatar` extends `ForAvatar`. A value that merely satisfies the declared type\n * resolves too, and is rejected in dev mode by the first piece to reach the channel.\n */\nexport const FOR_AVATAR_CONTEXT = new InjectionToken<ForAvatarContext>('FOR_AVATAR_CONTEXT');\n\n/**\n * The constant half of both resolvers' {@link assertRootContext} calls, so the\n * fallback and the image state the same requirement.\n */\nconst ROOT_ASSERTION = {\n entryPoint: 'avatar',\n token: 'FOR_AVATAR_CONTEXT',\n root: '[forAvatar]',\n};\n\n/**\n * Injects the nearest {@link ForAvatarContext}, throwing a descriptive error\n * when used outside a `[forAvatar]` element.\n *\n * @param piece Name of the calling directive, used in the error message.\n */\nexport function injectAvatarContext(piece: string): AvatarContext {\n const ctx = inject(FOR_AVATAR_CONTEXT, { optional: true });\n if (!ctx) {\n throw orphanContextError({\n code: 'FORCDK-AVATAR-001',\n piece,\n root: '[forAvatar]',\n token: 'FOR_AVATAR_CONTEXT',\n });\n }\n const widened = ctx as AvatarContext;\n assertRootContext({\n ...ROOT_ASSERTION,\n piece,\n probe: () => widened.reportStatus,\n });\n return widened;\n}\n\n/**\n * `[forAvatarImage]`'s own resolver. It reports a distinct failure from the rest\n * of the anatomy — the image is the piece that drives the state machine, so an\n * orphaned one silently never transitions — and therefore carries its own code\n * rather than routing through {@link injectAvatarContext}.\n */\nexport function injectAvatarImageContext(): AvatarContext {\n const ctx = inject(FOR_AVATAR_CONTEXT, { optional: true });\n if (!ctx) {\n throw orphanContextError({\n code: 'FORCDK-AVATAR-002',\n piece: 'ForAvatarImage',\n root: '[forAvatar]',\n token: 'FOR_AVATAR_CONTEXT',\n });\n }\n const widened = ctx as AvatarContext;\n assertRootContext({\n ...ROOT_ASSERTION,\n piece: 'ForAvatarImage',\n probe: () => widened.reportStatus,\n });\n return widened;\n}\n","import { isPlatformBrowser } from '@angular/common';\nimport {\n computed,\n DestroyRef,\n Directive,\n effect,\n inject,\n input,\n linkedSignal,\n numberAttribute,\n PLATFORM_ID,\n signal,\n} from '@angular/core';\n\nimport { FOR_AVATAR_CONTEXT, type ForAvatarContext, type ForAvatarStatus } from './avatar-context';\nimport { FOR_AVATAR_DEFAULTS } from 'forty-cdk/defaults';\n\n/**\n * Headless avatar root. Tracks the load lifecycle of an inner\n * `[forAvatarImage]` and exposes `status` + `shouldShowFallback` so the\n * consumer can swap the image for fallback content (initials, generic icon)\n * via `@if`.\n *\n * No ARIA role is imposed — avatars are typically presentational. If the\n * surrounding component conveys identity (a person's name in the header,\n * a chat bubble), the consumer should provide an `aria-label` on the\n * relevant ancestor or pair the avatar with visible text.\n *\n * @example\n * ```html\n * <span forAvatar #a=\"forAvatar\" fallbackDelayMs=\"500\">\n * <img forAvatarImage [src]=\"user.avatarUrl\" [alt]=\"user.name\" />\n * @if (a.shouldShowFallback()) {\n * <span forAvatarFallback>{{ user.initials }}</span>\n * }\n * </span>\n * ```\n */\n@Directive({\n selector: '[forAvatar]',\n exportAs: 'forAvatar',\n host: {\n '[attr.data-status]': 'status()',\n },\n providers: [{ provide: FOR_AVATAR_CONTEXT, useExisting: ForAvatar }],\n})\nexport class ForAvatar implements ForAvatarContext {\n readonly #defaults = inject(FOR_AVATAR_DEFAULTS);\n\n /**\n * Milliseconds to defer the fallback for fast loads, avoiding a brief\n * \"initials → image\" flicker. Set to `0` (default) to show immediately\n * during `idle` / `loading`, or to e.g. `500` to skip rendering during\n * quick cached loads. `error` always shows the fallback immediately.\n * The default is read from `provideForAvatarDefaults` for the\n * surrounding scope.\n */\n readonly fallbackDelayMs = input(this.#defaults.fallbackDelayMs, {\n transform: (value: unknown) => {\n const parsed = numberAttribute(value);\n return Number.isFinite(parsed) ? parsed : this.#defaults.fallbackDelayMs;\n },\n });\n\n readonly #status = signal<ForAvatarStatus>('idle');\n readonly status = this.#status.asReadonly();\n\n // Whether the deferred-fallback timer has fired for the current status/delay.\n // `linkedSignal` resets it to `false` whenever the source changes (the\n // canonical replacement for resetting state inside an effect); the timer\n // callback in the constructor flips it to `true`.\n readonly #timerElapsed = linkedSignal<{ status: ForAvatarStatus; delay: number }, boolean>({\n source: () => ({ status: this.#status(), delay: this.fallbackDelayMs() }),\n computation: () => false,\n });\n #timer: ReturnType<typeof setTimeout> | null = null;\n readonly #isBrowser = isPlatformBrowser(inject(PLATFORM_ID));\n\n readonly shouldShowFallback = computed(() => {\n const status = this.#status();\n if (status === 'loaded') return false;\n if (status === 'error') return true;\n // idle | loading: an immediate (delay <= 0) gate shows the fallback right\n // away; a deferred gate waits for the timer armed in the effect below.\n if (this.fallbackDelayMs() <= 0) return true;\n return this.#timerElapsed();\n });\n\n private reportStatus(status: ForAvatarStatus): void {\n this.#status.set(status);\n }\n\n constructor() {\n // Imperative timer integration: the effect's only job is to (re)arm a\n // setTimeout for the deferred-fallback case (idle | loading with a positive\n // delay). The synchronous branches (loaded / error / delay <= 0) are pure\n // derivations and live in `shouldShowFallback`. The signal write happens\n // inside the timer callback, which runs outside the effect's reactive\n // scope, so this does NOT create a self-cycle in the reactive graph.\n effect(() => {\n const status = this.#status();\n const delay = this.fallbackDelayMs();\n this.#cancelTimer();\n if (this.#isBrowser && (status === 'idle' || status === 'loading') && delay > 0) {\n this.#timer = setTimeout(() => {\n this.#timer = null;\n this.#timerElapsed.set(true);\n }, delay);\n }\n });\n\n inject(DestroyRef).onDestroy(() => this.#cancelTimer());\n }\n\n #cancelTimer(): void {\n if (this.#timer !== null) {\n clearTimeout(this.#timer);\n this.#timer = null;\n }\n }\n}\n","import { isPlatformBrowser } from '@angular/common';\nimport {\n afterNextRender,\n DestroyRef,\n Directive,\n ElementRef,\n inject,\n output,\n PLATFORM_ID,\n} from '@angular/core';\n\nimport { type ForAvatarStatus, injectAvatarImageContext } from './avatar-context';\n\n/**\n * Wraps an `<img>` and reports its load lifecycle to the surrounding\n * `[forAvatar]`. The native `src` / `alt` bindings are owned by the\n * consumer — this directive only observes.\n *\n * Reflects `data-status` on the image element so the consumer can hide it\n * via CSS while loading or after an error (e.g. `img[data-status=\"error\"]\n * { display: none }`).\n */\n@Directive({\n selector: 'img[forAvatarImage]',\n exportAs: 'forAvatarImage',\n host: {\n '[attr.data-status]': 'status()',\n '(load)': 'onLoad()',\n '(error)': 'onError()',\n },\n})\nexport class ForAvatarImage {\n readonly #host = inject<ElementRef<HTMLImageElement>>(ElementRef).nativeElement;\n readonly #parent = injectAvatarImageContext();\n\n /**\n * Emits whenever the image lifecycle transitions to a new state. Useful\n * if the consumer wants to log analytics, swap to a different src on\n * error, etc.\n */\n readonly loadStatusChange = output<ForAvatarStatus>();\n\n constructor() {\n const parent = this.#parent;\n const host = this.#host;\n\n let destroyed = false;\n inject(DestroyRef).onDestroy(() => (destroyed = true));\n\n afterNextRender(() => {\n this.#syncFromAttr();\n });\n\n if (isPlatformBrowser(inject(PLATFORM_ID)) && typeof MutationObserver !== 'undefined') {\n const observer = new MutationObserver(() => {\n this.#syncFromAttr();\n });\n observer.observe(host, { attributes: true, attributeFilter: ['src'] });\n inject(DestroyRef).onDestroy(() => observer.disconnect());\n }\n\n // Re-emit upstream whenever the parent's status changes due to our writes.\n // De-dupe on the (request, status) pair rather than status alone: a new\n // `src` opens a fresh request token, so an identical status (e.g. a second\n // image that also `loaded`) still re-reports for the new request, while a\n // repeated status within the same request is suppressed.\n let last: ForAvatarStatus | null = null;\n let lastReportedToken = -1;\n const emit = (status: ForAvatarStatus): void => {\n if (destroyed) return;\n if (last === status && lastReportedToken === this.#requestToken) return;\n last = status;\n lastReportedToken = this.#requestToken;\n parent.reportStatus(status);\n this.loadStatusChange.emit(status);\n };\n this.#emit = emit;\n }\n\n protected status(): ForAvatarStatus {\n return this.#parent.status();\n }\n\n protected onLoad(): void {\n // Ignore a `load` that fires for a request the host has already moved past\n // (the native event can arrive after `src` was reassigned).\n if (!this.#isCurrentRequest()) return;\n if (!this.#host.complete) return;\n this.#emit('loaded');\n }\n\n protected onError(): void {\n if (!this.#isCurrentRequest()) return;\n if (!this.#host.complete) return;\n this.#emit('error');\n }\n\n /**\n * Cheap string-identity fast-path: true when the `src` the host currently\n * carries is the one the latest `#syncFromAttr` observed, so a native\n * load/error event has not drifted away from the in-flight request. This\n * proves src-attribute stability only; request *completeness* (a late event\n * from a superseded resource that happens to share the current src) is\n * verified separately in onLoad/onError via `this.#host.complete`.\n */\n #isCurrentRequest(): boolean {\n return this.#host.getAttribute('src') === this.#requestSrc;\n }\n\n #syncFromAttr(): void {\n const src = this.#host.getAttribute('src');\n this.#requestToken++;\n this.#requestSrc = src;\n if (!src) {\n this.#emit('idle');\n return;\n }\n if (this.#host.complete) {\n // Cached image: load/error events may not fire — derive from naturalWidth.\n if (this.#host.naturalWidth > 0) {\n this.#emit('loaded');\n return;\n }\n // A complete image with zero intrinsic width is ambiguous: it may be a\n // broken image OR a valid SVG with no intrinsic dimensions. Don't\n // pessimistically flag `error` and hide a valid image — verify with\n // `decode()`, which resolves for a decodable (even zero-size) image and\n // rejects for a broken one. Stay `loading` until it settles.\n this.#emit('loading');\n this.#verifyCachedZeroSize(src);\n return;\n }\n this.#emit('loading');\n }\n\n #verifyCachedZeroSize(src: string): void {\n const decode = this.#host.decode?.bind(this.#host);\n if (!decode) {\n // No `decode()` (older engines / jsdom): keep the legacy heuristic.\n this.#emit('error');\n return;\n }\n decode().then(\n () => {\n if (this.#host.getAttribute('src') === src) {\n this.#emit('loaded');\n }\n },\n () => {\n if (this.#host.getAttribute('src') === src) {\n this.#emit('error');\n }\n },\n );\n }\n\n // Initialized in constructor.\n #emit!: (status: ForAvatarStatus) => void;\n\n /** Monotonic id bumped on every `src` change so stale events are ignored. */\n #requestToken = 0;\n /** The `src` the latest `#syncFromAttr` observed, used to match native events. */\n #requestSrc: string | null = null;\n}\n","import { Directive } from '@angular/core';\n\nimport { injectAvatarContext } from './avatar-context';\n\n/**\n * Marker for the avatar's fallback content (initials, generic icon, …).\n * Reflects `data-status` from the parent `[forAvatar]` so the consumer can\n * style for the `error` state separately from `idle` / `loading` if needed.\n *\n * The decision of *whether* to render the fallback lives on the root —\n * drive `@if` with `forAvatar.shouldShowFallback()`.\n */\n@Directive({\n selector: '[forAvatarFallback]',\n exportAs: 'forAvatarFallback',\n host: {\n '[attr.data-status]': 'parent.status()',\n },\n})\nexport class ForAvatarFallback {\n protected readonly parent = injectAvatarContext('ForAvatarFallback');\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"forty-cdk-avatar.mjs","sources":["../../../projects/forty-cdk/avatar/src/avatar-context.ts","../../../projects/forty-cdk/avatar/src/avatar.ts","../../../projects/forty-cdk/avatar/src/avatar-image.ts","../../../projects/forty-cdk/avatar/src/avatar-fallback.ts","../../../projects/forty-cdk/avatar/src/forty-cdk-avatar.ts"],"sourcesContent":["import { inject, InjectionToken, type Signal } from '@angular/core';\n\nimport { assertRootContext, orphanContextError } from 'forty-cdk/core';\n\n/**\n * Lifecycle stages of the avatar's image:\n * - `idle`: no `src` set yet.\n * - `loading`: `src` set, waiting for the network.\n * - `loaded`: image rendered successfully.\n * - `error`: image failed to load.\n */\nexport type ForAvatarStatus = 'idle' | 'loading' | 'loaded' | 'error';\n\n/** Read surface `[forAvatar]` publishes through {@link FOR_AVATAR_CONTEXT}. */\nexport interface ForAvatarContext {\n /** Current status of the avatar's image. */\n readonly status: Signal<ForAvatarStatus>;\n /**\n * `true` when the consumer should render the fallback. Drives `@if` in\n * the consumer's template. Honors `fallbackDelayMs` for `idle` / `loading`\n * states; `error` flips it to `true` immediately.\n */\n readonly shouldShowFallback: Signal<boolean>;\n}\n\n/**\n * The avatar's piece-coordination surface: the one call `[forAvatarImage]`\n * makes so the root can transition its state machine.\n *\n * **Not** part of {@link ForAvatarContext} and never exported from\n * `public-api.ts` — a consumer drives the status by binding `[src]`, never by\n * reporting a transition into the root.\n */\nexport interface AvatarPieceContext {\n /** Reported by `[forAvatarImage]` so the root can transition its state machine. */\n reportStatus(status: ForAvatarStatus): void;\n}\n\n/**\n * The avatar's internal coordination surface: everything\n * {@link ForAvatarContext} publishes plus the {@link AvatarPieceContext} call.\n *\n * Never exported from `public-api.ts`. It is the type the pieces read\n * {@link FOR_AVATAR_CONTEXT} at, so a consumer who injects that token gets the\n * read surface while `[forAvatarImage]` gets the transition channel. `ForAvatar`\n * declares `reportStatus` TS-`private`, which keeps it out of the emitted\n * `.d.ts` while `useExisting` still satisfies this contract at runtime.\n */\nexport interface AvatarContext extends ForAvatarContext, AvatarPieceContext {}\n\n/**\n * DI token for the avatar's coordination surface, provided by `[forAvatar]`.\n *\n * Publicly typed as the read surface {@link ForAvatarContext}, which is the whole of\n * what the token promises a consumer. `[forAvatarImage]` reads the same token at an\n * internal type that adds the status-report channel, so a wrapper re-providing it must\n * alias it to the root: `{ provide: FOR_AVATAR_CONTEXT, useExisting: MyAvatar }`, where\n * `MyAvatar` extends `ForAvatar`. A value that merely satisfies the declared type\n * resolves too, and is rejected in dev mode by the first piece to reach the channel.\n */\nexport const FOR_AVATAR_CONTEXT = new InjectionToken<ForAvatarContext>('FOR_AVATAR_CONTEXT');\n\n/**\n * The constant half of both resolvers' {@link assertRootContext} calls, so the\n * fallback and the image state the same requirement.\n */\nconst ROOT_ASSERTION = {\n entryPoint: 'avatar',\n token: 'FOR_AVATAR_CONTEXT',\n root: '[forAvatar]',\n};\n\n/**\n * Injects the nearest {@link ForAvatarContext}, throwing a descriptive error\n * when used outside a `[forAvatar]` element.\n *\n * @param piece Name of the calling directive, used in the error message.\n */\nexport function injectAvatarContext(piece: string): AvatarContext {\n const ctx = inject(FOR_AVATAR_CONTEXT, { optional: true });\n if (!ctx) {\n throw orphanContextError({\n code: 'FORCDK-AVATAR-001',\n piece,\n root: '[forAvatar]',\n token: 'FOR_AVATAR_CONTEXT',\n });\n }\n const widened = ctx as AvatarContext;\n assertRootContext({\n ...ROOT_ASSERTION,\n piece,\n probe: () => widened.reportStatus,\n });\n return widened;\n}\n\n/**\n * `[forAvatarImage]`'s own resolver. It reports a distinct failure from the rest\n * of the anatomy — the image is the piece that drives the state machine, so an\n * orphaned one silently never transitions — and therefore carries its own code\n * rather than routing through {@link injectAvatarContext}.\n */\nexport function injectAvatarImageContext(): AvatarContext {\n const ctx = inject(FOR_AVATAR_CONTEXT, { optional: true });\n if (!ctx) {\n throw orphanContextError({\n code: 'FORCDK-AVATAR-002',\n piece: 'ForAvatarImage',\n root: '[forAvatar]',\n token: 'FOR_AVATAR_CONTEXT',\n });\n }\n const widened = ctx as AvatarContext;\n assertRootContext({\n ...ROOT_ASSERTION,\n piece: 'ForAvatarImage',\n probe: () => widened.reportStatus,\n });\n return widened;\n}\n","import { isPlatformBrowser } from '@angular/common';\nimport {\n computed,\n DestroyRef,\n Directive,\n effect,\n inject,\n input,\n linkedSignal,\n numberAttribute,\n PLATFORM_ID,\n signal,\n} from '@angular/core';\n\nimport { FOR_AVATAR_CONTEXT, type ForAvatarContext, type ForAvatarStatus } from './avatar-context';\nimport { FOR_AVATAR_DEFAULTS } from 'forty-cdk/defaults';\n\n/**\n * Headless avatar root. Tracks the load lifecycle of an inner\n * `[forAvatarImage]` and exposes `status` + `shouldShowFallback` so the\n * consumer can swap the image for fallback content (initials, generic icon)\n * via `@if`.\n *\n * No ARIA role is imposed — avatars are typically presentational. If the\n * surrounding component conveys identity (a person's name in the header,\n * a chat bubble), the consumer should provide an `aria-label` on the\n * relevant ancestor or pair the avatar with visible text.\n *\n * @example\n * ```html\n * <span forAvatar #a=\"forAvatar\" fallbackDelayMs=\"500\">\n * <img forAvatarImage [src]=\"user.avatarUrl\" [alt]=\"user.name\" />\n * @if (a.shouldShowFallback()) {\n * <span forAvatarFallback>{{ user.initials }}</span>\n * }\n * </span>\n * ```\n */\n@Directive({\n selector: '[forAvatar]',\n exportAs: 'forAvatar',\n host: {\n '[attr.data-status]': 'status()',\n },\n providers: [{ provide: FOR_AVATAR_CONTEXT, useExisting: ForAvatar }],\n})\nexport class ForAvatar implements ForAvatarContext {\n readonly #defaults = inject(FOR_AVATAR_DEFAULTS);\n\n /**\n * Milliseconds to defer the fallback for fast loads, avoiding a brief\n * \"initials → image\" flicker. Set to `0` (default) to show immediately\n * during `idle` / `loading`, or to e.g. `500` to skip rendering during\n * quick cached loads. `error` always shows the fallback immediately.\n * The default is read from `provideForAvatarDefaults` for the\n * surrounding scope.\n */\n readonly fallbackDelayMs = input(this.#defaults.fallbackDelayMs, {\n transform: (value: unknown) => {\n const parsed = numberAttribute(value);\n return Number.isFinite(parsed) ? parsed : this.#defaults.fallbackDelayMs;\n },\n });\n\n readonly #status = signal<ForAvatarStatus>('idle');\n readonly status = this.#status.asReadonly();\n\n // Whether the deferred-fallback timer has fired for the current status/delay.\n // `linkedSignal` resets it to `false` whenever the source changes (the\n // canonical replacement for resetting state inside an effect); the timer\n // callback in the constructor flips it to `true`.\n readonly #timerElapsed = linkedSignal<{ status: ForAvatarStatus; delay: number }, boolean>({\n source: () => ({ status: this.#status(), delay: this.fallbackDelayMs() }),\n computation: () => false,\n });\n #timer: ReturnType<typeof setTimeout> | null = null;\n readonly #isBrowser = isPlatformBrowser(inject(PLATFORM_ID));\n\n readonly shouldShowFallback = computed(() => {\n const status = this.#status();\n if (status === 'loaded') return false;\n if (status === 'error') return true;\n // idle | loading: an immediate (delay <= 0) gate shows the fallback right\n // away; a deferred gate waits for the timer armed in the effect below.\n if (this.fallbackDelayMs() <= 0) return true;\n return this.#timerElapsed();\n });\n\n private reportStatus(status: ForAvatarStatus): void {\n this.#status.set(status);\n }\n\n constructor() {\n // Imperative timer integration: the effect's only job is to (re)arm a\n // setTimeout for the deferred-fallback case (idle | loading with a positive\n // delay). The synchronous branches (loaded / error / delay <= 0) are pure\n // derivations and live in `shouldShowFallback`. The signal write happens\n // inside the timer callback, which runs outside the effect's reactive\n // scope, so this does NOT create a self-cycle in the reactive graph.\n effect(() => {\n const status = this.#status();\n const delay = this.fallbackDelayMs();\n this.#cancelTimer();\n if (this.#isBrowser && (status === 'idle' || status === 'loading') && delay > 0) {\n this.#timer = setTimeout(() => {\n this.#timer = null;\n this.#timerElapsed.set(true);\n }, delay);\n }\n });\n\n inject(DestroyRef).onDestroy(() => this.#cancelTimer());\n }\n\n #cancelTimer(): void {\n if (this.#timer !== null) {\n clearTimeout(this.#timer);\n this.#timer = null;\n }\n }\n}\n","import { afterNextRender, DestroyRef, Directive, ElementRef, inject, output } from '@angular/core';\n\nimport { type ForAvatarStatus, injectAvatarImageContext } from './avatar-context';\n\n/**\n * Wraps an `<img>` and reports its load lifecycle to the surrounding\n * `[forAvatar]`. The native `src` / `alt` bindings are owned by the\n * consumer — this directive only observes.\n *\n * Reflects `data-status` on the image element so the consumer can hide it\n * via CSS while loading or after an error (e.g. `img[data-status=\"error\"]\n * { display: none }`).\n */\n@Directive({\n selector: 'img[forAvatarImage]',\n exportAs: 'forAvatarImage',\n host: {\n '[attr.data-status]': 'status()',\n '(load)': 'onLoad()',\n '(error)': 'onError()',\n },\n})\nexport class ForAvatarImage {\n readonly #host = inject<ElementRef<HTMLImageElement>>(ElementRef).nativeElement;\n readonly #parent = injectAvatarImageContext();\n\n /**\n * Emits whenever the image lifecycle transitions to a new state. Useful\n * if the consumer wants to log analytics, swap to a different src on\n * error, etc.\n */\n readonly loadStatusChange = output<ForAvatarStatus>();\n\n constructor() {\n const parent = this.#parent;\n const host = this.#host;\n\n let destroyed = false;\n const destroyRef = inject(DestroyRef);\n destroyRef.onDestroy(() => (destroyed = true));\n\n afterNextRender(() => {\n this.#syncFromAttr();\n if (destroyed || typeof MutationObserver === 'undefined') return;\n const observer = new MutationObserver(() => {\n this.#syncFromAttr();\n });\n observer.observe(host, { attributes: true, attributeFilter: ['src'] });\n destroyRef.onDestroy(() => observer.disconnect());\n });\n\n // Re-emit upstream whenever the parent's status changes due to our writes.\n // De-dupe on the (request, status) pair rather than status alone: a new\n // `src` opens a fresh request token, so an identical status (e.g. a second\n // image that also `loaded`) still re-reports for the new request, while a\n // repeated status within the same request is suppressed.\n let last: ForAvatarStatus | null = null;\n let lastReportedToken = -1;\n const emit = (status: ForAvatarStatus): void => {\n if (destroyed) return;\n if (last === status && lastReportedToken === this.#requestToken) return;\n last = status;\n lastReportedToken = this.#requestToken;\n parent.reportStatus(status);\n this.loadStatusChange.emit(status);\n };\n this.#emit = emit;\n }\n\n protected status(): ForAvatarStatus {\n return this.#parent.status();\n }\n\n protected onLoad(): void {\n // Ignore a `load` that fires for a request the host has already moved past\n // (the native event can arrive after `src` was reassigned).\n if (!this.#isCurrentRequest()) return;\n if (!this.#host.complete) return;\n this.#emit('loaded');\n }\n\n protected onError(): void {\n if (!this.#isCurrentRequest()) return;\n if (!this.#host.complete) return;\n this.#emit('error');\n }\n\n /**\n * Cheap string-identity fast-path: true when the `src` the host currently\n * carries is the one the latest `#syncFromAttr` observed, so a native\n * load/error event has not drifted away from the in-flight request. This\n * proves src-attribute stability only; request *completeness* (a late event\n * from a superseded resource that happens to share the current src) is\n * verified separately in onLoad/onError via `this.#host.complete`.\n */\n #isCurrentRequest(): boolean {\n return this.#host.getAttribute('src') === this.#requestSrc;\n }\n\n #syncFromAttr(): void {\n const src = this.#host.getAttribute('src');\n this.#requestToken++;\n this.#requestSrc = src;\n if (!src) {\n this.#emit('idle');\n return;\n }\n if (this.#host.complete) {\n // Cached image: load/error events may not fire — derive from naturalWidth.\n if (this.#host.naturalWidth > 0) {\n this.#emit('loaded');\n return;\n }\n // A complete image with zero intrinsic width is ambiguous: it may be a\n // broken image OR a valid SVG with no intrinsic dimensions. Don't\n // pessimistically flag `error` and hide a valid image — verify with\n // `decode()`, which resolves for a decodable (even zero-size) image and\n // rejects for a broken one. Stay `loading` until it settles.\n this.#emit('loading');\n this.#verifyCachedZeroSize(src);\n return;\n }\n this.#emit('loading');\n }\n\n #verifyCachedZeroSize(src: string): void {\n const decode = this.#host.decode?.bind(this.#host);\n if (!decode) {\n // No `decode()` (older engines / jsdom): keep the legacy heuristic.\n this.#emit('error');\n return;\n }\n decode().then(\n () => {\n if (this.#host.getAttribute('src') === src) {\n this.#emit('loaded');\n }\n },\n () => {\n if (this.#host.getAttribute('src') === src) {\n this.#emit('error');\n }\n },\n );\n }\n\n // Initialized in constructor.\n #emit!: (status: ForAvatarStatus) => void;\n\n /** Monotonic id bumped on every `src` change so stale events are ignored. */\n #requestToken = 0;\n /** The `src` the latest `#syncFromAttr` observed, used to match native events. */\n #requestSrc: string | null = null;\n}\n","import { Directive } from '@angular/core';\n\nimport { injectAvatarContext } from './avatar-context';\n\n/**\n * Marker for the avatar's fallback content (initials, generic icon, …).\n * Reflects `data-status` from the parent `[forAvatar]` so the consumer can\n * style for the `error` state separately from `idle` / `loading` if needed.\n *\n * The decision of *whether* to render the fallback lives on the root —\n * drive `@if` with `forAvatar.shouldShowFallback()`.\n */\n@Directive({\n selector: '[forAvatarFallback]',\n exportAs: 'forAvatarFallback',\n host: {\n '[attr.data-status]': 'parent.status()',\n },\n})\nexport class ForAvatarFallback {\n protected readonly parent = injectAvatarContext('ForAvatarFallback');\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
|