forty-cdk 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/accordion/README.md +122 -0
- package/aspect-ratio/README.md +76 -0
- package/avatar/README.md +100 -0
- package/breadcrumbs/README.md +49 -0
- package/breakpoints/README.md +81 -0
- package/button/README.md +49 -0
- package/calendar/README.md +458 -0
- package/carousel/README.md +358 -0
- package/checkbox/README.md +146 -0
- package/combobox/README.md +535 -0
- package/context-menu/README.md +139 -0
- package/date-field/README.md +184 -0
- package/date-picker/README.md +338 -0
- package/dialog/README.md +388 -0
- package/disclosure/README.md +114 -0
- package/drag-drop/README.md +359 -0
- package/drawer/README.md +560 -0
- package/dropdown-menu/README.md +176 -0
- package/fesm2022/forty-cdk-accordion.mjs +348 -0
- package/fesm2022/forty-cdk-accordion.mjs.map +1 -0
- package/fesm2022/forty-cdk-aspect-ratio.mjs +74 -0
- package/fesm2022/forty-cdk-aspect-ratio.mjs.map +1 -0
- package/fesm2022/forty-cdk-avatar.mjs +308 -0
- package/fesm2022/forty-cdk-avatar.mjs.map +1 -0
- package/fesm2022/forty-cdk-breadcrumbs.mjs +125 -0
- package/fesm2022/forty-cdk-breadcrumbs.mjs.map +1 -0
- package/fesm2022/forty-cdk-breakpoints.mjs +117 -0
- package/fesm2022/forty-cdk-breakpoints.mjs.map +1 -0
- package/fesm2022/forty-cdk-button.mjs +134 -0
- package/fesm2022/forty-cdk-button.mjs.map +1 -0
- package/fesm2022/forty-cdk-calendar.mjs +2034 -0
- package/fesm2022/forty-cdk-calendar.mjs.map +1 -0
- package/fesm2022/forty-cdk-carousel.mjs +968 -0
- package/fesm2022/forty-cdk-carousel.mjs.map +1 -0
- package/fesm2022/forty-cdk-checkbox.mjs +226 -0
- package/fesm2022/forty-cdk-checkbox.mjs.map +1 -0
- package/fesm2022/forty-cdk-combobox.mjs +2596 -0
- package/fesm2022/forty-cdk-combobox.mjs.map +1 -0
- package/fesm2022/forty-cdk-context-menu.mjs +413 -0
- package/fesm2022/forty-cdk-context-menu.mjs.map +1 -0
- package/fesm2022/forty-cdk-core.mjs +9022 -0
- package/fesm2022/forty-cdk-core.mjs.map +1 -0
- package/fesm2022/forty-cdk-date-field.mjs +744 -0
- package/fesm2022/forty-cdk-date-field.mjs.map +1 -0
- package/fesm2022/forty-cdk-date-picker.mjs +1011 -0
- package/fesm2022/forty-cdk-date-picker.mjs.map +1 -0
- package/fesm2022/forty-cdk-dialog.mjs +707 -0
- package/fesm2022/forty-cdk-dialog.mjs.map +1 -0
- package/fesm2022/forty-cdk-disclosure.mjs +190 -0
- package/fesm2022/forty-cdk-disclosure.mjs.map +1 -0
- package/fesm2022/forty-cdk-drag-drop.mjs +1180 -0
- package/fesm2022/forty-cdk-drag-drop.mjs.map +1 -0
- package/fesm2022/forty-cdk-drawer.mjs +1641 -0
- package/fesm2022/forty-cdk-drawer.mjs.map +1 -0
- package/fesm2022/forty-cdk-dropdown-menu.mjs +350 -0
- package/fesm2022/forty-cdk-dropdown-menu.mjs.map +1 -0
- package/fesm2022/forty-cdk-field.mjs +425 -0
- package/fesm2022/forty-cdk-field.mjs.map +1 -0
- package/fesm2022/forty-cdk-fieldset.mjs +164 -0
- package/fesm2022/forty-cdk-fieldset.mjs.map +1 -0
- package/fesm2022/forty-cdk-file-upload.mjs +221 -0
- package/fesm2022/forty-cdk-file-upload.mjs.map +1 -0
- package/fesm2022/forty-cdk-hover-card.mjs +496 -0
- package/fesm2022/forty-cdk-hover-card.mjs.map +1 -0
- package/fesm2022/forty-cdk-input.mjs +274 -0
- package/fesm2022/forty-cdk-input.mjs.map +1 -0
- package/fesm2022/forty-cdk-internationalized-date.mjs +1 -1
- package/fesm2022/forty-cdk-internationalized-date.mjs.map +1 -1
- package/fesm2022/forty-cdk-listbox.mjs +1279 -0
- package/fesm2022/forty-cdk-listbox.mjs.map +1 -0
- package/fesm2022/forty-cdk-menu.mjs +1439 -0
- package/fesm2022/forty-cdk-menu.mjs.map +1 -0
- package/fesm2022/forty-cdk-menubar.mjs +787 -0
- package/fesm2022/forty-cdk-menubar.mjs.map +1 -0
- package/fesm2022/forty-cdk-meter.mjs +211 -0
- package/fesm2022/forty-cdk-meter.mjs.map +1 -0
- package/fesm2022/forty-cdk-navigation-menu.mjs +1145 -0
- package/fesm2022/forty-cdk-navigation-menu.mjs.map +1 -0
- package/fesm2022/forty-cdk-number-input.mjs +559 -0
- package/fesm2022/forty-cdk-number-input.mjs.map +1 -0
- package/fesm2022/forty-cdk-otp-input.mjs +527 -0
- package/fesm2022/forty-cdk-otp-input.mjs.map +1 -0
- package/fesm2022/forty-cdk-pagination.mjs +323 -0
- package/fesm2022/forty-cdk-pagination.mjs.map +1 -0
- package/fesm2022/forty-cdk-pane-resizer.mjs +297 -0
- package/fesm2022/forty-cdk-pane-resizer.mjs.map +1 -0
- package/fesm2022/forty-cdk-popover.mjs +698 -0
- package/fesm2022/forty-cdk-popover.mjs.map +1 -0
- package/fesm2022/forty-cdk-progress.mjs +226 -0
- package/fesm2022/forty-cdk-progress.mjs.map +1 -0
- package/fesm2022/forty-cdk-radio-group.mjs +378 -0
- package/fesm2022/forty-cdk-radio-group.mjs.map +1 -0
- package/fesm2022/forty-cdk-scroll-area.mjs +640 -0
- package/fesm2022/forty-cdk-scroll-area.mjs.map +1 -0
- package/fesm2022/forty-cdk-search.mjs +205 -0
- package/fesm2022/forty-cdk-search.mjs.map +1 -0
- package/fesm2022/forty-cdk-select.mjs +1661 -0
- package/fesm2022/forty-cdk-select.mjs.map +1 -0
- package/fesm2022/forty-cdk-separator.mjs +82 -0
- package/fesm2022/forty-cdk-separator.mjs.map +1 -0
- package/fesm2022/forty-cdk-signal-forms.mjs +97 -0
- package/fesm2022/forty-cdk-signal-forms.mjs.map +1 -0
- package/fesm2022/forty-cdk-slider.mjs +803 -0
- package/fesm2022/forty-cdk-slider.mjs.map +1 -0
- package/fesm2022/forty-cdk-stepper.mjs +886 -0
- package/fesm2022/forty-cdk-stepper.mjs.map +1 -0
- package/fesm2022/forty-cdk-switch.mjs +137 -0
- package/fesm2022/forty-cdk-switch.mjs.map +1 -0
- package/fesm2022/forty-cdk-table.mjs +1518 -0
- package/fesm2022/forty-cdk-table.mjs.map +1 -0
- package/fesm2022/forty-cdk-tabs.mjs +400 -0
- package/fesm2022/forty-cdk-tabs.mjs.map +1 -0
- package/fesm2022/forty-cdk-time-field.mjs +593 -0
- package/fesm2022/forty-cdk-time-field.mjs.map +1 -0
- package/fesm2022/forty-cdk-time-picker.mjs +1013 -0
- package/fesm2022/forty-cdk-time-picker.mjs.map +1 -0
- package/fesm2022/forty-cdk-toast.mjs +1153 -0
- package/fesm2022/forty-cdk-toast.mjs.map +1 -0
- package/fesm2022/forty-cdk-toggle.mjs +516 -0
- package/fesm2022/forty-cdk-toggle.mjs.map +1 -0
- package/fesm2022/forty-cdk-toolbar.mjs +374 -0
- package/fesm2022/forty-cdk-toolbar.mjs.map +1 -0
- package/fesm2022/forty-cdk-tooltip.mjs +672 -0
- package/fesm2022/forty-cdk-tooltip.mjs.map +1 -0
- package/fesm2022/forty-cdk-tree.mjs +2007 -0
- package/fesm2022/forty-cdk-tree.mjs.map +1 -0
- package/fesm2022/forty-cdk-virtualization.mjs +1 -1
- package/fesm2022/forty-cdk-virtualization.mjs.map +1 -1
- package/fesm2022/forty-cdk.mjs +0 -43310
- package/fesm2022/forty-cdk.mjs.map +1 -1
- package/field/README.md +97 -0
- package/fieldset/README.md +86 -0
- package/file-upload/README.md +73 -0
- package/hover-card/README.md +171 -0
- package/input/README.md +156 -0
- package/listbox/README.md +424 -0
- package/menu/README.md +181 -0
- package/menubar/README.md +140 -0
- package/meter/README.md +128 -0
- package/navigation-menu/README.md +253 -0
- package/number-input/README.md +171 -0
- package/otp-input/README.md +198 -0
- package/package.json +213 -1
- package/pagination/README.md +61 -0
- package/pane-resizer/README.md +136 -0
- package/popover/README.md +262 -0
- package/progress/README.md +115 -0
- package/radio-group/README.md +129 -0
- package/scroll-area/README.md +184 -0
- package/search/README.md +42 -0
- package/select/README.md +488 -0
- package/separator/README.md +84 -0
- package/signal-forms/README.md +72 -0
- package/slider/README.md +152 -0
- package/stepper/README.md +292 -0
- package/switch/README.md +116 -0
- package/table/README.md +769 -0
- package/tabs/README.md +130 -0
- package/time-field/README.md +157 -0
- package/time-picker/README.md +172 -0
- package/toast/README.md +398 -0
- package/toggle/README.md +224 -0
- package/toolbar/README.md +109 -0
- package/tooltip/README.md +274 -0
- package/tree/README.md +708 -0
- package/types/forty-cdk-accordion.d.ts +242 -0
- package/types/forty-cdk-aspect-ratio.d.ts +59 -0
- package/types/forty-cdk-avatar.d.ts +133 -0
- package/types/forty-cdk-breadcrumbs.d.ts +92 -0
- package/types/forty-cdk-breakpoints.d.ts +141 -0
- package/types/forty-cdk-button.d.ts +80 -0
- package/types/forty-cdk-calendar.d.ts +914 -0
- package/types/forty-cdk-carousel.d.ts +530 -0
- package/types/forty-cdk-checkbox.d.ts +141 -0
- package/types/forty-cdk-combobox.d.ts +1259 -0
- package/types/forty-cdk-context-menu.d.ts +313 -0
- package/types/forty-cdk-core.d.ts +5774 -0
- package/types/forty-cdk-date-field.d.ts +307 -0
- package/types/forty-cdk-date-picker.d.ts +622 -0
- package/types/forty-cdk-dialog.d.ts +546 -0
- package/types/forty-cdk-disclosure.d.ts +127 -0
- package/types/forty-cdk-drag-drop.d.ts +456 -0
- package/types/forty-cdk-drawer.d.ts +871 -0
- package/types/forty-cdk-dropdown-menu.d.ts +242 -0
- package/types/forty-cdk-field.d.ts +236 -0
- package/types/forty-cdk-fieldset.d.ts +119 -0
- package/types/forty-cdk-file-upload.d.ts +124 -0
- package/types/forty-cdk-hover-card.d.ts +320 -0
- package/types/forty-cdk-input.d.ts +169 -0
- package/types/forty-cdk-internationalized-date.d.ts +1 -1
- package/types/forty-cdk-listbox.d.ts +513 -0
- package/types/forty-cdk-menu.d.ts +629 -0
- package/types/forty-cdk-menubar.d.ts +451 -0
- package/types/forty-cdk-meter.d.ts +122 -0
- package/types/forty-cdk-navigation-menu.d.ts +514 -0
- package/types/forty-cdk-number-input.d.ts +319 -0
- package/types/forty-cdk-otp-input.d.ts +248 -0
- package/types/forty-cdk-pagination.d.ts +214 -0
- package/types/forty-cdk-pane-resizer.d.ts +145 -0
- package/types/forty-cdk-popover.d.ts +509 -0
- package/types/forty-cdk-progress.d.ts +143 -0
- package/types/forty-cdk-radio-group.d.ts +222 -0
- package/types/forty-cdk-scroll-area.d.ts +258 -0
- package/types/forty-cdk-search.d.ts +142 -0
- package/types/forty-cdk-select.d.ts +899 -0
- package/types/forty-cdk-separator.d.ts +59 -0
- package/types/forty-cdk-signal-forms.d.ts +58 -0
- package/types/forty-cdk-slider.d.ts +379 -0
- package/types/forty-cdk-stepper.d.ts +650 -0
- package/types/forty-cdk-switch.d.ts +87 -0
- package/types/forty-cdk-table.d.ts +723 -0
- package/types/forty-cdk-tabs.d.ts +235 -0
- package/types/forty-cdk-time-field.d.ts +307 -0
- package/types/forty-cdk-time-picker.d.ts +578 -0
- package/types/forty-cdk-toast.d.ts +598 -0
- package/types/forty-cdk-toggle.d.ts +310 -0
- package/types/forty-cdk-toolbar.d.ts +217 -0
- package/types/forty-cdk-tooltip.d.ts +436 -0
- package/types/forty-cdk-tree.d.ts +688 -0
- package/types/forty-cdk.d.ts +1 -19743
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# ContextMenu
|
|
2
|
+
|
|
3
|
+
> New to overlays in forty-cdk? [Your first overlay](../../../../../docs/your-first-overlay.md) walks a Popover from empty markup to styled-and-animated and explains the `@if` / open-state model and the portal → global CSS rule.
|
|
4
|
+
|
|
5
|
+
Headless right-click menu — variant of the [WAI-ARIA Menu pattern](https://www.w3.org/WAI/ARIA/apg/patterns/menubar/) opened via the `contextmenu` event (right-click, long-press on touch) and via the keyboard activators `Shift+F10` and the dedicated `ContextMenu` key. The native browser context menu is suppressed.
|
|
6
|
+
|
|
7
|
+
Pointer activations anchor the menu at the cursor; keyboard activations anchor it at the bounding rect of the focused element so screen-reader / keyboard-only users get the menu next to whatever they're working on. Floating-ui's virtual element handles either case — placement, flip, and shift middleware still apply, so the menu is repositioned to stay on-screen automatically.
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { Component, signal } from '@angular/core';
|
|
13
|
+
import { ForContextMenu, ForContextMenuTrigger } from 'forty-cdk/context-menu';
|
|
14
|
+
import { ForMenuContent, ForMenuItem } from 'forty-cdk/menu';
|
|
15
|
+
|
|
16
|
+
@Component({
|
|
17
|
+
selector: 'demo-context',
|
|
18
|
+
imports: [ForContextMenu, ForContextMenuTrigger, ForMenuContent, ForMenuItem],
|
|
19
|
+
template: `
|
|
20
|
+
<div forContextMenu #menu="forContextMenu">
|
|
21
|
+
<div forContextMenuTrigger class="canvas context-menu-trigger">
|
|
22
|
+
Right-click anywhere here.
|
|
23
|
+
</div>
|
|
24
|
+
@if (menu.open()) {
|
|
25
|
+
<div forMenuContent animate.leave="fade-out">
|
|
26
|
+
<button forMenuItem (activate)="rename()">Rename</button>
|
|
27
|
+
<button forMenuItem (activate)="duplicate()">Duplicate</button>
|
|
28
|
+
<button forMenuItem (activate)="delete()">Delete</button>
|
|
29
|
+
</div>
|
|
30
|
+
}
|
|
31
|
+
</div>
|
|
32
|
+
`,
|
|
33
|
+
})
|
|
34
|
+
export class DemoContext {
|
|
35
|
+
rename() {
|
|
36
|
+
/* ... */
|
|
37
|
+
}
|
|
38
|
+
duplicate() {
|
|
39
|
+
/* ... */
|
|
40
|
+
}
|
|
41
|
+
delete() {
|
|
42
|
+
/* ... */
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The trigger is focusable out of the box: `[forContextMenuTrigger]` host-binds a default `tabindex="-1"` so focus returns there programmatically when the menu closes — no consumer setup required. Override it with your own `tabindex` (e.g. `tabindex="0"` to put the region in the Tab order) and it wins.
|
|
48
|
+
|
|
49
|
+
### `#menu="forContextMenu"` vs. `[(open)]`
|
|
50
|
+
|
|
51
|
+
The minimal "right-click → show menu" case needs **neither** a separate `open` signal **nor** a two-way binding. `[forContextMenu]` is `exportAs: 'forContextMenu'`, so expose the directive instance with a template reference variable — `#menu="forContextMenu"` — and drive the `@if` straight off its own `open()` signal, as above. The contextmenu gesture, item activation, Escape, and outside dismissal all flip it.
|
|
52
|
+
|
|
53
|
+
Reach for the explicit `[(open)]="mySignal"` model binding only when the component class needs to read or drive open state — open it programmatically, persist it, or react to it elsewhere:
|
|
54
|
+
|
|
55
|
+
```html
|
|
56
|
+
<div forContextMenu [(open)]="open">
|
|
57
|
+
<div forContextMenuTrigger class="context-menu-trigger">Right-click anywhere here.</div>
|
|
58
|
+
@if (open()) {
|
|
59
|
+
<div forMenuContent>…</div>
|
|
60
|
+
}
|
|
61
|
+
</div>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Triggers stamped from outside-declared templates
|
|
65
|
+
|
|
66
|
+
Angular resolves `ng-template` DI at the template's **declaration** site, not where it is stamped. A `[forContextMenuTrigger]` declared in a template outside the root throws the orphan error even when the template is rendered inside the root via `ngTemplateOutlet`. For that case the selector attribute accepts the root reference as a value, `routerLink`-style — grab it with `#root="forContextMenu"` and pass it through the outlet context. The bare valueless attribute keeps resolving via DI.
|
|
67
|
+
|
|
68
|
+
```html
|
|
69
|
+
<div forContextMenu #root="forContextMenu">
|
|
70
|
+
<ng-container *ngTemplateOutlet="chip; context: { root }" />
|
|
71
|
+
@if (root.open()) {
|
|
72
|
+
<div forMenuContent>…</div>
|
|
73
|
+
}
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<ng-template #chip let-root="root">
|
|
77
|
+
<span [forContextMenuTrigger]="root">Right-click here</span>
|
|
78
|
+
</ng-template>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Pieces
|
|
82
|
+
|
|
83
|
+
| Class | Selector | Role |
|
|
84
|
+
| ----------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
85
|
+
| `ForContextMenu` | `[forContextMenu]` | Root. Owns open state, the virtual anchor (pointer position), navigation, typeahead. |
|
|
86
|
+
| `ForContextMenuTrigger` | `[forContextMenuTrigger]` | The right-click region. Captures `contextmenu`, `Shift+F10`, and the `ContextMenu` key, prevents the native menu, and opens — anchored at the pointer (mouse) or the focused element (keyboard). |
|
|
87
|
+
|
|
88
|
+
The menu items themselves come from the [`menu/`](../menu/README.md) folder.
|
|
89
|
+
|
|
90
|
+
## Inputs (`ForContextMenu`)
|
|
91
|
+
|
|
92
|
+
| API | Default | Description |
|
|
93
|
+
| ------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
94
|
+
| `open` | `false` | Two-way bindable. Whether the menu is shown. |
|
|
95
|
+
| `side` | `'bottom'` | Anchor side relative to the pointer. |
|
|
96
|
+
| `align` | `'start'` | Alignment along `side` (`'start'` / `'center'` / `'end'`). |
|
|
97
|
+
| `sideOffset` | `0` | Gap (px) between the pointer and the menu along the main axis. |
|
|
98
|
+
| `alignOffset` | `0` | Gap (px) along the cross axis (parallel to `side`). |
|
|
99
|
+
| `loop` | `true` | Whether arrow navigation wraps. |
|
|
100
|
+
| `dir` | `'ltr'` | Writing direction. In RTL, ArrowLeft opens submenus and ArrowRight closes them — the swap is automatic. Inherited by every nested `[forMenuSub]` underneath unless overridden. |
|
|
101
|
+
| `disabled` | `false` | When `true`, the contextmenu event falls through to the native browser menu. |
|
|
102
|
+
| `dismissible` | `true` | When `false`, Escape and outside interactions don't close. |
|
|
103
|
+
| `returnFocus` | `true` | When `true`, focus returns to the trigger element on close. |
|
|
104
|
+
| `ariaLabel` | `null` | Manual `aria-label` on `[forMenuContent]`. |
|
|
105
|
+
|
|
106
|
+
## Outputs (`ForContextMenu`)
|
|
107
|
+
|
|
108
|
+
Same vetoable dismiss API as DropdownMenu — `(escapeKeyDown)`, `(pointerDownOutside)`, `(focusOutside)`, `(interactOutside)`. Each handler receives a `VetoableNativeEvent<E>` (the original DOM event lives on `.event`); call `preventDefault()` on the veto to keep the menu open.
|
|
109
|
+
|
|
110
|
+
`(autoFocusOnOpen)` / `(autoFocusOnClose)` fire just before the imperative focus move on mount / unmount. Each receives a `VetoableEvent`; call `preventDefault()` on the veto to skip the move while keeping the menu mounted. These are output-shape because ContextMenu always routes close transitions through `[(open)]` (via the implicit `openChange` emitter). See [CLAUDE.md › Auto-focus hook shape](../../../../../CLAUDE.md#auto-focus-hook-shape) for why Dialog uses callback-shape inputs instead.
|
|
111
|
+
|
|
112
|
+
## Styling
|
|
113
|
+
|
|
114
|
+
forty-cdk ships no styles. Add your own class to each piece — the for\* selectors are the behavior API, not a styling contract (see [Styling forty-cdk](../../../../../docs/styling.md)). Key your CSS off the reflected data-\* attributes below.
|
|
115
|
+
|
|
116
|
+
### Data attributes
|
|
117
|
+
|
|
118
|
+
| Piece | Attribute | Values |
|
|
119
|
+
| ------------------------- | --------------- | ------------------ |
|
|
120
|
+
| `[forContextMenu]` | `data-state` | `open` \| `closed` |
|
|
121
|
+
| `[forContextMenu]` | `data-disabled` | present \| absent |
|
|
122
|
+
| `[forContextMenuTrigger]` | `data-state` | `open` \| `closed` |
|
|
123
|
+
| `[forContextMenuTrigger]` | `data-disabled` | present \| absent |
|
|
124
|
+
|
|
125
|
+
> The menu content (`[forMenuContent]`, from the [`menu/`](../menu/README.md) folder) portals to `document.body`, so it sits outside the trigger's DOM subtree — descendant selectors won't reach it. Style it with **global CSS** or a class on the content element. The content host also exposes the shared positioner custom properties (`--for-anchor-width` / `--for-anchor-height`, `--for-available-width` / `--for-available-height`, `--for-content-transform-origin`); see [Styling floating content](../../../../../docs/styling-floating-content.md) for the full list and the animation rules.
|
|
126
|
+
|
|
127
|
+
```css
|
|
128
|
+
.context-menu-trigger[data-state='open'] {
|
|
129
|
+
outline: 2px solid hotpink;
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Behavior notes
|
|
134
|
+
|
|
135
|
+
- **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.
|
|
136
|
+
- **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.
|
|
137
|
+
- **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; use `tabindex="0"` if you want the region itself reachable via Tab.
|
|
138
|
+
- **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.
|
|
139
|
+
- **Mount equals open.** Same convention as the rest of the library — wrap `[forMenuContent]` in `@if (open())` and use `animate.enter` / `animate.leave` for transitions.
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# DateField
|
|
2
|
+
|
|
3
|
+
Headless, segmented, spin-editable date input — the keyboard-first counterpart to [Calendar](../calendar/README.md). There is **no single WAI-ARIA APG pattern** for a date field; it is a composition of [Spinbuttons](https://www.w3.org/WAI/ARIA/apg/patterns/spinbutton/) inside a labelled `role="group"`. Each day / month / year part is an independent `role="spinbutton"` segment, so entry is unambiguous and locale-correct — no free-text parsing, no `03/04`-is-it-March-4th guesswork. Segment **order** and separators follow the runtime locale (`MM/DD/YYYY` vs `DD.MM.YYYY` vs `YYYY/MM/DD`).
|
|
4
|
+
|
|
5
|
+
`ForDateField` implements `FormValueControl<D | null>` from `@angular/forms/signals`, so it auto-wires with `[formField]` and auto-associates inside a `[forField]` (label / description / error) with no extra markup. The value stays `null` until every segment is filled.
|
|
6
|
+
|
|
7
|
+
## Date adapter — pick one (required)
|
|
8
|
+
|
|
9
|
+
All date math goes through the same pluggable `DateAdapter<D>` as `ForCalendar`, so the library hard-depends on **no** date library. Provide exactly one adapter in your application (or component) providers:
|
|
10
|
+
|
|
11
|
+
| Provider | Date type `D` | Dependency |
|
|
12
|
+
| --------------------------------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------------------- |
|
|
13
|
+
| `provideInternationalizedDateAdapter()` | `CalendarDate` (`@internationalized/date`) | **Recommended.** From `forty-cdk/internationalized-date`; needs `@internationalized/date` (optional peer) |
|
|
14
|
+
| `provideNativeDateAdapter()` | `Date` | None (zero-dependency fallback) |
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { bootstrapApplication } from '@angular/platform-browser';
|
|
18
|
+
import { provideInternationalizedDateAdapter } from 'forty-cdk/internationalized-date';
|
|
19
|
+
|
|
20
|
+
bootstrapApplication(App, {
|
|
21
|
+
providers: [provideInternationalizedDateAdapter()],
|
|
22
|
+
});
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Pieces
|
|
26
|
+
|
|
27
|
+
| Class | Selector | Role |
|
|
28
|
+
| --------------------- | ----------------------- | -------------------------------------------------------------------------------------------------- |
|
|
29
|
+
| `ForDateField` | `[forDateField]` | Root (`role="group"`). Owns the entered parts, composes the value, and exposes `segments()`. |
|
|
30
|
+
| `ForDateFieldSegment` | `[forDateFieldSegment]` | One editable part (`role="spinbutton"`). Roving tab stop, ARIA value reflection, keyboard editing. |
|
|
31
|
+
| `ForDateFieldLiteral` | `[forDateFieldLiteral]` | A decorative separator (`/`, `.`, `-`). `aria-hidden`, out of the tab order. |
|
|
32
|
+
|
|
33
|
+
## Inputs / models — `ForDateField`
|
|
34
|
+
|
|
35
|
+
| API | Type | Description |
|
|
36
|
+
| ------------- | ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
|
37
|
+
| `value` | `model<D \| null>` | Two-way bindable entered date, or `null` while any segment is empty. The `FormValueControl` backing. Default `null`. |
|
|
38
|
+
| `minDate` | `input<D \| null>` | Minimum date (inclusive). A composed value below it is clamped up. Named `minDate` — see note below. Default `null`. |
|
|
39
|
+
| `maxDate` | `input<D \| null>` | Maximum date (inclusive). A composed value above it is clamped down. Default `null`. |
|
|
40
|
+
| `granularity` | `input<'day' \| 'hour' \| 'minute' \| 'second'>` | Date-time precision. `'day'` (default) is date-only; coarser-than-day off appends time segments. See below. |
|
|
41
|
+
| `hourCycle` | `input<12 \| 24 \| null>` | 12/24-hour cycle for the time segments. Default `null` → locale. 12-hour adds the AM/PM segment. |
|
|
42
|
+
| `locale` | `input<string \| null>` | BCP 47 locale driving segment order, separators, and month name. Default `null` → runtime locale. |
|
|
43
|
+
| `placeholder` | `input<Partial<Record<DateTimeSegmentType, string>>>` | Per-segment placeholder while empty. Unspecified parts fall back to `dd` / `mm` / `yyyy` / `hh` / `mm` / `ss` / `--`. Default `{}`. |
|
|
44
|
+
| `ariaLabel` | `input<string \| null>` | Accessible name for the group. Emits no `aria-label` while `null`. Default `null`. |
|
|
45
|
+
| `dir` | `input<'ltr' \| 'rtl' \| null>` | Writing direction. Default `null` resolves the ambient direction; mirrors ArrowLeft / ArrowRight segment navigation. |
|
|
46
|
+
|
|
47
|
+
Plus the shared `FormUiControl` members from `@angular/forms/signals`: `disabled`, `readonly`, `required`, `invalid`, `name`, `errors`, `touched` (bound automatically by `[formField]`).
|
|
48
|
+
|
|
49
|
+
> **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.
|
|
50
|
+
|
|
51
|
+
## Usage
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
|
|
55
|
+
import { CalendarDate } from '@internationalized/date';
|
|
56
|
+
import { ForDateField, ForDateFieldLiteral, ForDateFieldSegment } from 'forty-cdk/date-field';
|
|
57
|
+
|
|
58
|
+
@Component({
|
|
59
|
+
selector: 'app-dob',
|
|
60
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
61
|
+
imports: [ForDateField, ForDateFieldSegment, ForDateFieldLiteral],
|
|
62
|
+
template: `
|
|
63
|
+
<div
|
|
64
|
+
forDateField
|
|
65
|
+
class="date-field"
|
|
66
|
+
[(value)]="date"
|
|
67
|
+
[ariaLabel]="'Date of birth'"
|
|
68
|
+
#field="forDateField"
|
|
69
|
+
>
|
|
70
|
+
@for (seg of field.segments(); track seg.id) {
|
|
71
|
+
@if (seg.isLiteral) {
|
|
72
|
+
<span forDateFieldLiteral>{{ seg.text }}</span>
|
|
73
|
+
} @else {
|
|
74
|
+
<span forDateFieldSegment class="date-field-segment" [segment]="seg.type!">{{
|
|
75
|
+
seg.text
|
|
76
|
+
}}</span>
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
</div>
|
|
80
|
+
`,
|
|
81
|
+
})
|
|
82
|
+
export class DobField {
|
|
83
|
+
readonly date = signal<CalendarDate | null>(null);
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The library is styleless: style the boolean `data-*` hooks on the segments yourself — `[data-highlighted]` (the focused/roving segment), `[data-placeholder]` (empty), `[data-disabled]`, `[data-readonly]` — and `[data-empty]` / `[data-disabled]` / `[data-readonly]` on the root group.
|
|
88
|
+
|
|
89
|
+
## Keyboard (per segment)
|
|
90
|
+
|
|
91
|
+
Horizontal arrows mirror under `dir="rtl"`.
|
|
92
|
+
|
|
93
|
+
| Key | Behavior |
|
|
94
|
+
| -------------------------- | ------------------------------------------------------------------------ |
|
|
95
|
+
| **0–9** | Type the value; auto-advances to the next segment when full. |
|
|
96
|
+
| **ArrowUp / ArrowDown** | Step the value. Day and month wrap; year clamps. Empty seeds from today. |
|
|
97
|
+
| **ArrowLeft / ArrowRight** | Move to the previous / next segment (no wrap). |
|
|
98
|
+
| **Home / End** | Jump to the segment minimum / maximum. |
|
|
99
|
+
| **Backspace / Delete** | Clear the segment (the value becomes `null` until refilled). |
|
|
100
|
+
|
|
101
|
+
The day clamps to the current month's length (e.g. 31 → 28 in February), and a composed value is clamped into `[minDate, maxDate]`.
|
|
102
|
+
|
|
103
|
+
## Date-time field (`granularity > 'day'`)
|
|
104
|
+
|
|
105
|
+
Set `granularity` to `'hour'`, `'minute'`, or `'second'` to append time segments — hour / minute / second and, in 12-hour mode, an AM·PM `dayPeriod` — after the date segments in the same `role="group"`. The whole field stays a single tab stop with one roving cursor across **all** segments; `field.segments()` already returns the combined, locale-ordered list, so the same `@for` template renders it. This needs a **time-capable** adapter — `provideNativeDateAdapter()` (`Date`) or `provideInternationalizedDateTimeAdapter()` (`CalendarDateTime`); the day-only `provideInternationalizedDateAdapter()` (`CalendarDate`) throws.
|
|
106
|
+
|
|
107
|
+
```html
|
|
108
|
+
<div
|
|
109
|
+
forDateField
|
|
110
|
+
class="date-field"
|
|
111
|
+
[(value)]="when"
|
|
112
|
+
granularity="minute"
|
|
113
|
+
[hourCycle]="24"
|
|
114
|
+
#field="forDateField"
|
|
115
|
+
>
|
|
116
|
+
@for (seg of field.segments(); track seg.id) { @if (seg.isLiteral) {
|
|
117
|
+
<span forDateFieldLiteral>{{ seg.text }}</span>
|
|
118
|
+
} @else {
|
|
119
|
+
<span forDateFieldSegment class="date-field-segment" [segment]="seg.type!">{{ seg.text }}</span>
|
|
120
|
+
} }
|
|
121
|
+
</div>
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
On the AM/PM segment, `a` / `p` set the period and ArrowUp / ArrowDown toggle it; the period is derived from the entered hour, so clearing it is a no-op (clear or step the hour instead). The value stays `null` until every visible segment — date **and** time — is filled.
|
|
125
|
+
|
|
126
|
+
## Scope defaults
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
import { provideForDateFieldDefaults } from 'forty-cdk/date-field';
|
|
130
|
+
|
|
131
|
+
// app config or a component's providers — localize segment labels and the
|
|
132
|
+
// empty-segment announcement for every nested [forDateField].
|
|
133
|
+
providers: [
|
|
134
|
+
provideForDateFieldDefaults({
|
|
135
|
+
emptySegmentText: 'Vacío',
|
|
136
|
+
segmentLabels: { day: 'día', month: 'mes', year: 'año', dayPeriod: 'AM/PM' },
|
|
137
|
+
}),
|
|
138
|
+
];
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
`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.
|
|
142
|
+
|
|
143
|
+
## Styling
|
|
144
|
+
|
|
145
|
+
forty-cdk ships no styles. Add your own class to each piece — the for\* selectors are the behavior API, not a styling contract (see [Styling forty-cdk](../../../../../docs/styling.md)). Key your CSS off the reflected data-\* attributes below.
|
|
146
|
+
|
|
147
|
+
### Data attributes
|
|
148
|
+
|
|
149
|
+
| Piece | Attribute | Values |
|
|
150
|
+
| ----------------------- | ------------------ | ----------------- |
|
|
151
|
+
| `[forDateField]` | `data-disabled` | present \| absent |
|
|
152
|
+
| `[forDateField]` | `data-readonly` | present \| absent |
|
|
153
|
+
| `[forDateField]` | `data-empty` | present \| absent |
|
|
154
|
+
| `[forDateFieldSegment]` | `data-highlighted` | present \| absent |
|
|
155
|
+
| `[forDateFieldSegment]` | `data-placeholder` | present \| absent |
|
|
156
|
+
| `[forDateFieldSegment]` | `data-disabled` | present \| absent |
|
|
157
|
+
| `[forDateFieldSegment]` | `data-readonly` | present \| absent |
|
|
158
|
+
|
|
159
|
+
`data-empty` marks the whole field while any segment is still unfilled (the value is `null`); `data-placeholder` marks each individual segment that is still empty. `data-highlighted` is the current roving-tabindex segment — the only focus hook the consumer gets, shared with the other roving primitives. `[forDateFieldLiteral]` carries no data-\* attributes (it is `aria-hidden` and out of the tab order).
|
|
160
|
+
|
|
161
|
+
```css
|
|
162
|
+
.date-field-segment[data-placeholder] {
|
|
163
|
+
color: GrayText;
|
|
164
|
+
}
|
|
165
|
+
.date-field-segment[data-highlighted] {
|
|
166
|
+
background: Highlight;
|
|
167
|
+
color: HighlightText;
|
|
168
|
+
}
|
|
169
|
+
.date-field[data-disabled] {
|
|
170
|
+
opacity: 0.5;
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Accessibility notes
|
|
175
|
+
|
|
176
|
+
- **`role="group"`** on the root carries the field's accessible name (`ariaLabel`, or point native `aria-labelledby` at a visible label).
|
|
177
|
+
- **`role="spinbutton"`** per segment, with `aria-valuemin` / `aria-valuemax` / `aria-valuenow` reflected; the month segment also exposes a localized `aria-valuetext` ("March"), so screen readers read the name rather than the number.
|
|
178
|
+
- **Roving tabindex**: exactly one segment is tabbable, so `Tab` enters and leaves the whole field in one stop; arrows move between segments.
|
|
179
|
+
- **Literals are `aria-hidden`** and never focusable — assistive tech reads only the spinbutton segments.
|
|
180
|
+
- **Boolean `data-*`** on each segment — `data-highlighted` (focused/roving), `data-placeholder` (empty), `data-disabled`, `data-readonly` — present when true, absent when false.
|
|
181
|
+
|
|
182
|
+
## Wrapping in a design system
|
|
183
|
+
|
|
184
|
+
Both supported wrapper patterns — `hostDirectives` with the exported `FOR_DATE_FIELD_HOST_DIRECTIVE_INPUTS` / `FOR_DATE_FIELD_HOST_DIRECTIVE_OUTPUTS` name tuples, and subclassing — are documented in [Wrapping form primitives](../../../../../docs/wrapping-form-primitives.md).
|