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,129 @@
|
|
|
1
|
+
# Radio Group
|
|
2
|
+
|
|
3
|
+
Headless implementation of the [WAI-ARIA Radio Group pattern](https://www.w3.org/WAI/ARIA/apg/patterns/radio/) with selection-on-focus, wrap-around arrow navigation, and `FormValueControl<string>` integration for Angular Signal Forms.
|
|
4
|
+
|
|
5
|
+
## Pieces
|
|
6
|
+
|
|
7
|
+
| Class | Selector | Role |
|
|
8
|
+
| --------------- | ----------------- | --------------------------------------------------------------------------------------------------------------- |
|
|
9
|
+
| `ForRadioGroup` | `[forRadioGroup]` | Container. Owns the selected value, orientation, disabled / readonly / form state. Provides the shared context. |
|
|
10
|
+
| `ForRadio` | `[forRadio]` | One radio. Apply on a `<button type="button">`. |
|
|
11
|
+
|
|
12
|
+
## Inputs / models
|
|
13
|
+
|
|
14
|
+
### `ForRadioGroup`
|
|
15
|
+
|
|
16
|
+
| API | Type | Description |
|
|
17
|
+
| ------------------------------------------------------------ | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
18
|
+
| `value` | `model<string>` | Two-way bindable. The selected radio's value. Empty string = none selected (matches HTML form semantics). Required by `FormValueControl<string>`. |
|
|
19
|
+
| `orientation` | `input<'horizontal' \| 'vertical'>` | Default `'vertical'`. Drives keyboard navigation and `aria-orientation`. |
|
|
20
|
+
| `dir` | `input<'ltr' \| 'rtl'>` | Default `'ltr'`. Swaps ArrowLeft / ArrowRight in horizontal layouts. |
|
|
21
|
+
| `disabled` / `readonly` / `required` / `invalid` / `pending` | `input<boolean>` | Reflected as `aria-*` / `data-*`. `disabled` and `readonly` block all selection. |
|
|
22
|
+
| `loop` | `input<boolean>` | When true (default), arrow nav wraps around past the first / last enabled radio. Set to `false` for a non-wrapping group. |
|
|
23
|
+
| `name` | `input<string>` | For form association. |
|
|
24
|
+
| `errors` | `input<readonly ValidationError.WithOptionalFieldTree[]>` | Wired by `[formField]`. |
|
|
25
|
+
| `touched` | `model<boolean>` | Set to `true` when focus leaves the group entirely. |
|
|
26
|
+
|
|
27
|
+
The group host gets `data-orientation`, `data-disabled`, and `data-readonly` for CSS hooks.
|
|
28
|
+
|
|
29
|
+
### `ForRadio`
|
|
30
|
+
|
|
31
|
+
| API | Type | Description |
|
|
32
|
+
| ---------- | ------------------------ | ---------------------------------------------------------------------------------------------------- |
|
|
33
|
+
| `value` | `input.required<string>` | This radio's identifier. Must be unique within the group and non-empty. |
|
|
34
|
+
| `disabled` | `input<boolean>` | Disables this radio independently of the group. Disabled radios are skipped during arrow navigation. |
|
|
35
|
+
|
|
36
|
+
The radio host gets `aria-checked`, `aria-disabled`, `tabindex`, `data-state`, and `data-disabled`. A disabled radio reflects `aria-disabled="true"` + `data-disabled=""` (no native `disabled`, per APG) — announced but non-selectable, and skipped during arrow nav. Tabindex is `0` for the selected radio (or, when no radio is selected, the first enabled one) and `-1` for the rest.
|
|
37
|
+
|
|
38
|
+
## Stand-alone usage
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { Component, signal } from '@angular/core';
|
|
42
|
+
import { ForRadio, ForRadioGroup } from 'forty-cdk/radio-group';
|
|
43
|
+
|
|
44
|
+
@Component({
|
|
45
|
+
selector: 'demo-color',
|
|
46
|
+
imports: [ForRadioGroup, ForRadio],
|
|
47
|
+
template: `
|
|
48
|
+
<div forRadioGroup [(value)]="color" aria-labelledby="color-label">
|
|
49
|
+
<span id="color-label">Color</span>
|
|
50
|
+
<button type="button" forRadio class="radio-group-item" value="red">Red</button>
|
|
51
|
+
<button type="button" forRadio class="radio-group-item" value="green">Green</button>
|
|
52
|
+
<button type="button" forRadio class="radio-group-item" value="blue" disabled>Blue</button>
|
|
53
|
+
</div>
|
|
54
|
+
`,
|
|
55
|
+
})
|
|
56
|
+
export class DemoColor {
|
|
57
|
+
readonly color = signal('red');
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Signal Forms usage
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import { Component, signal } from '@angular/core';
|
|
65
|
+
import { form, required } from '@angular/forms/signals';
|
|
66
|
+
import { ForRadio, ForRadioGroup } from 'forty-cdk/radio-group';
|
|
67
|
+
|
|
68
|
+
@Component({
|
|
69
|
+
selector: 'demo-shipping',
|
|
70
|
+
imports: [ForRadioGroup, ForRadio /* , FormField from @angular/forms */],
|
|
71
|
+
template: `
|
|
72
|
+
<div forRadioGroup [formField]="checkout.shipping" aria-labelledby="ship-label">
|
|
73
|
+
<span id="ship-label">Shipping</span>
|
|
74
|
+
<button type="button" forRadio class="radio-group-item" value="standard">Standard</button>
|
|
75
|
+
<button type="button" forRadio class="radio-group-item" value="express">Express</button>
|
|
76
|
+
<button type="button" forRadio class="radio-group-item" value="overnight">Overnight</button>
|
|
77
|
+
</div>
|
|
78
|
+
`,
|
|
79
|
+
})
|
|
80
|
+
export class DemoShipping {
|
|
81
|
+
readonly model = signal({ shipping: '' });
|
|
82
|
+
readonly checkout = form(this.model, (s) => required(s.shipping));
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Styling
|
|
87
|
+
|
|
88
|
+
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.
|
|
89
|
+
|
|
90
|
+
### Data attributes
|
|
91
|
+
|
|
92
|
+
| Piece | Attribute | Values |
|
|
93
|
+
| --------------------- | ------------------ | -------------------------- |
|
|
94
|
+
| `[forRadioGroup]` | `data-orientation` | `horizontal` \| `vertical` |
|
|
95
|
+
| `[forRadioGroup]` | `data-disabled` | present \| absent |
|
|
96
|
+
| `[forRadioGroup]` | `data-readonly` | present \| absent |
|
|
97
|
+
| `[forRadio]` | `data-state` | `checked` \| `unchecked` |
|
|
98
|
+
| `[forRadio]` | `data-disabled` | present \| absent |
|
|
99
|
+
| `[forRadio]` | `data-orientation` | `horizontal` \| `vertical` |
|
|
100
|
+
| `[forRadioIndicator]` | `data-state` | `checked` \| `unchecked` |
|
|
101
|
+
| `[forRadioIndicator]` | `data-orientation` | `horizontal` \| `vertical` |
|
|
102
|
+
|
|
103
|
+
```css
|
|
104
|
+
.radio-group-indicator[data-state='unchecked'] {
|
|
105
|
+
display: none;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.radio-group-item:not([data-disabled]):hover {
|
|
109
|
+
cursor: pointer;
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Keyboard
|
|
114
|
+
|
|
115
|
+
- **Tab** moves focus into / out of the group; lands on the selected radio (or the first enabled one if nothing is selected).
|
|
116
|
+
- **Space** / **Enter** select the focused radio (Space is APG; Enter comes from the underlying `<button>` and is harmless).
|
|
117
|
+
- **ArrowDown** / **ArrowUp** in a vertical group, **ArrowRight** / **ArrowLeft** in a horizontal group: move focus AND change selection ("selection on focus"), wrapping at the ends. RTL swaps Left/Right.
|
|
118
|
+
- **Home** / **End** jump to the first / last enabled radio (and select it).
|
|
119
|
+
- Disabled radios are skipped.
|
|
120
|
+
|
|
121
|
+
## Accessibility notes
|
|
122
|
+
|
|
123
|
+
- **Provide a group label.** Use `aria-labelledby` (pointing to a heading or `<span>`) or `aria-label`. Without one, screen readers cannot announce the group's purpose.
|
|
124
|
+
- **Selection-on-focus** is the APG-mandated behavior for standard radio groups (toolbars use a different model). Be aware that arrow navigation immediately changes the form value.
|
|
125
|
+
- **`role="radio"`** on a `<button>` is the most accessible host: it gets keyboard activation and SR-friendly semantics. Other host elements lose those defaults.
|
|
126
|
+
|
|
127
|
+
## Wrapping in a design system
|
|
128
|
+
|
|
129
|
+
Both supported wrapper patterns — `hostDirectives` with the exported `FOR_RADIO_GROUP_HOST_DIRECTIVE_INPUTS` / `FOR_RADIO_GROUP_HOST_DIRECTIVE_OUTPUTS` name tuples, and subclassing — are documented in [Wrapping form primitives](../../../../../docs/wrapping-form-primitives.md).
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# ScrollArea
|
|
2
|
+
|
|
3
|
+
Headless custom-scrollbar primitive. Hides native scrollbars on the inner viewport and exposes synthetic `scrollbar` + `thumb` + `corner` directives that the consumer styles freely.
|
|
4
|
+
|
|
5
|
+
This is the **only** primitive in forty-cdk that ships CSS — a single `<style>` tag (id `for-scroll-area-hide-native`) is injected into `document.head` the first time a viewport mounts. It hides webkit / Firefox / IE native scrollbars on `[forScrollAreaViewport]` only, leaving the rest of your CSS untouched.
|
|
6
|
+
|
|
7
|
+
## Pieces
|
|
8
|
+
|
|
9
|
+
| Class | Selector | Role |
|
|
10
|
+
| ------------------------ | -------------------------- | ------------------------------------------------------------------------------------------------ |
|
|
11
|
+
| `ForScrollArea` | `[forScrollArea]` | Root. Owns `type`, `scrollHideDelay`, hover / scrolling state. |
|
|
12
|
+
| `ForScrollAreaViewport` | `[forScrollAreaViewport]` | The actual scrolling element. |
|
|
13
|
+
| `ForScrollAreaContent` | `[forScrollAreaContent]` | Marks the content element inside the viewport so its size changes drive the synthetic scrollbar. |
|
|
14
|
+
| `ForScrollAreaScrollbar` | `[forScrollAreaScrollbar]` | Synthetic track. Required `orientation`. |
|
|
15
|
+
| `ForScrollAreaThumb` | `[forScrollAreaThumb]` | Draggable thumb sized & translated automatically. |
|
|
16
|
+
| `ForScrollAreaCorner` | `[forScrollAreaCorner]` | Only shows when both scrollbars are visible. |
|
|
17
|
+
|
|
18
|
+
## Inputs (root)
|
|
19
|
+
|
|
20
|
+
| API | Type | Description |
|
|
21
|
+
| ----------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
22
|
+
| `type` | `input<'auto' \| 'always' \| 'scroll' \| 'hover'>` | Visibility behavior. Default `'hover'`. `'always'` keeps the track painted even with no overflow; `'auto'` self-hides — see [Notes](#notes) and the grid example for reserving the gutter. |
|
|
23
|
+
| `scrollHideDelay` | `input<number>` | ms after the most recent scroll before scrollbars fade (`'scroll'` and `'hover'`). Default `600`. |
|
|
24
|
+
| `dir` | `input<WritingDirection>` | Reflected as `dir`. |
|
|
25
|
+
|
|
26
|
+
The scrollbar reflects `data-orientation`, `data-state` (`'visible'` / `'hidden'`); the thumb reflects `data-orientation` and `data-state`. Position is driven by inline `transform: translate{X,Y}(…)` on the thumb.
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { Component } from '@angular/core';
|
|
32
|
+
import {
|
|
33
|
+
ForScrollArea,
|
|
34
|
+
ForScrollAreaContent,
|
|
35
|
+
ForScrollAreaCorner,
|
|
36
|
+
ForScrollAreaScrollbar,
|
|
37
|
+
ForScrollAreaThumb,
|
|
38
|
+
ForScrollAreaViewport,
|
|
39
|
+
} from 'forty-cdk/scroll-area';
|
|
40
|
+
|
|
41
|
+
@Component({
|
|
42
|
+
selector: 'demo-scroll',
|
|
43
|
+
imports: [
|
|
44
|
+
ForScrollArea,
|
|
45
|
+
ForScrollAreaViewport,
|
|
46
|
+
ForScrollAreaContent,
|
|
47
|
+
ForScrollAreaScrollbar,
|
|
48
|
+
ForScrollAreaThumb,
|
|
49
|
+
ForScrollAreaCorner,
|
|
50
|
+
],
|
|
51
|
+
template: `
|
|
52
|
+
<div forScrollArea class="scroll-area">
|
|
53
|
+
<div forScrollAreaViewport class="scroll-area-viewport">
|
|
54
|
+
<div forScrollAreaContent class="content">…lots of stuff…</div>
|
|
55
|
+
</div>
|
|
56
|
+
<div forScrollAreaScrollbar class="scroll-area-scrollbar" orientation="vertical">
|
|
57
|
+
<div forScrollAreaThumb class="scroll-area-thumb"></div>
|
|
58
|
+
</div>
|
|
59
|
+
<div forScrollAreaScrollbar class="scroll-area-scrollbar" orientation="horizontal">
|
|
60
|
+
<div forScrollAreaThumb class="scroll-area-thumb"></div>
|
|
61
|
+
</div>
|
|
62
|
+
<div forScrollAreaCorner class="scroll-area-corner"></div>
|
|
63
|
+
</div>
|
|
64
|
+
`,
|
|
65
|
+
styles: [
|
|
66
|
+
`
|
|
67
|
+
.scroll-area {
|
|
68
|
+
position: relative;
|
|
69
|
+
width: 240px;
|
|
70
|
+
height: 240px;
|
|
71
|
+
}
|
|
72
|
+
.scroll-area-viewport {
|
|
73
|
+
position: absolute;
|
|
74
|
+
inset: 0;
|
|
75
|
+
}
|
|
76
|
+
.scroll-area-scrollbar[orientation='vertical'] {
|
|
77
|
+
position: absolute;
|
|
78
|
+
top: 0;
|
|
79
|
+
right: 0;
|
|
80
|
+
width: 8px;
|
|
81
|
+
height: 100%;
|
|
82
|
+
background: rgba(0, 0, 0, 0.04);
|
|
83
|
+
transition: opacity 0.2s;
|
|
84
|
+
}
|
|
85
|
+
.scroll-area-scrollbar[orientation='horizontal'] {
|
|
86
|
+
position: absolute;
|
|
87
|
+
bottom: 0;
|
|
88
|
+
left: 0;
|
|
89
|
+
height: 8px;
|
|
90
|
+
width: 100%;
|
|
91
|
+
background: rgba(0, 0, 0, 0.04);
|
|
92
|
+
transition: opacity 0.2s;
|
|
93
|
+
}
|
|
94
|
+
.scroll-area-scrollbar[data-state='hidden'] {
|
|
95
|
+
opacity: 0;
|
|
96
|
+
pointer-events: none;
|
|
97
|
+
}
|
|
98
|
+
.scroll-area-thumb {
|
|
99
|
+
background: rgba(0, 0, 0, 0.4);
|
|
100
|
+
border-radius: 4px;
|
|
101
|
+
}
|
|
102
|
+
.scroll-area-corner {
|
|
103
|
+
position: absolute;
|
|
104
|
+
right: 0;
|
|
105
|
+
bottom: 0;
|
|
106
|
+
width: 8px;
|
|
107
|
+
height: 8px;
|
|
108
|
+
background: rgba(0, 0, 0, 0.04);
|
|
109
|
+
}
|
|
110
|
+
`,
|
|
111
|
+
],
|
|
112
|
+
})
|
|
113
|
+
export class DemoScroll {}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Notes
|
|
117
|
+
|
|
118
|
+
- **`type="always"` keeps a stable, always-painted track.** Unlike `auto` / `scroll` / `hover` — which render a scrollbar only for an axis that actually overflows and self-hide otherwise — `always` keeps both scrollbars (and the corner) mounted and `data-state="visible"` regardless of overflow. When the axis does not overflow the thumb fills the full track and dragging it is a no-op, so the track never appears/disappears as content crosses the overflow boundary.
|
|
119
|
+
- **Reserving the gutter with `type="always"` is the consumer's layout job.** forty-cdk is headless and does not own layout, so it cannot literally reserve a gutter — `always` only guarantees the track stays painted. To get "no content shift" behavior, lay the scrollbar out _in flow_ (a grid column) rather than `position: absolute`, so the always-present track occupies real space:
|
|
120
|
+
|
|
121
|
+
```css
|
|
122
|
+
.scroll-area {
|
|
123
|
+
display: grid;
|
|
124
|
+
grid-template-columns: 1fr auto; /* content | reserved scrollbar gutter */
|
|
125
|
+
grid-template-rows: 1fr auto;
|
|
126
|
+
width: 240px;
|
|
127
|
+
height: 240px;
|
|
128
|
+
}
|
|
129
|
+
.scroll-area-viewport {
|
|
130
|
+
grid-column: 1;
|
|
131
|
+
grid-row: 1;
|
|
132
|
+
}
|
|
133
|
+
.scroll-area-scrollbar[orientation='vertical'] {
|
|
134
|
+
grid-column: 2;
|
|
135
|
+
grid-row: 1;
|
|
136
|
+
width: 8px;
|
|
137
|
+
}
|
|
138
|
+
.scroll-area-scrollbar[orientation='horizontal'] {
|
|
139
|
+
grid-column: 1;
|
|
140
|
+
grid-row: 2;
|
|
141
|
+
height: 8px;
|
|
142
|
+
}
|
|
143
|
+
.scroll-area-corner {
|
|
144
|
+
grid-column: 2;
|
|
145
|
+
grid-row: 2;
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
With `type="always"` the vertical track's grid column is always filled, so the viewport width stays constant whether or not the content overflows — no reflow when it crosses the boundary. The `position: absolute` layout in the [Usage](#usage) example above is the right default for `auto` / `hover` / `scroll`, where an overlaid self-hiding scrollbar is the desired look.
|
|
150
|
+
|
|
151
|
+
- **Native scrollbars are hidden globally on `[forScrollAreaViewport]`** via an injected `<style>` tag. If you need to opt out (e.g. for a debug build), remove `#for-scroll-area-hide-native` from the head — but that defeats the primitive's purpose.
|
|
152
|
+
- **Keyboard scrolling stays native.** PageUp / PageDown / arrows / Tab still scroll the viewport because the underlying element keeps `overflow: scroll`. The thumb is just a visual + drag affordance.
|
|
153
|
+
- **Drag uses pointer-capture** so the cursor doesn't lose the thumb if it briefly leaves the track. The `pointermove` / `pointerup` listeners are attached to the owner document (capture still routes them there), and an in-flight drag pins the track `data-state="visible"` and painted — so a drag is never silently aborted if the scrollbar would otherwise self-hide mid-gesture (e.g. a `type="scroll"` fade, or a consumer `display: none` on `[data-state="hidden"]`).
|
|
154
|
+
- **RTL is handled on the horizontal axis.** When the root resolves to `dir="rtl"`, the horizontal thumb starts pinned to the right edge of the track and a leftward drag scrolls the content forward (the browser's native negative-`scrollLeft` model). Set the direction the standard way — `[dir]` on the root or an ancestor `dir` attribute.
|
|
155
|
+
- **Minimum thumb size is 8px**, matching common UI conventions for very long content.
|
|
156
|
+
- **The corner only shows when both scrollbars are visible** (or always, under `type="always"`, where both tracks are permanently present). Otherwise the directive hides it with an inline `display: none` in addition to the `hidden` attribute that removes it from the accessibility tree (the only place the rule "primitives never apply `[hidden]`" doesn't apply — the corner has no logical presence without two scrollbars). Because the inline style beats any author selector rule, you can give `[forScrollAreaCorner]` a custom `display` without a `.x[hidden] { display: none }` workaround — the directive's `display: none` still wins while the corner is hidden, and your `display` applies once both scrollbars show.
|
|
157
|
+
- **Content observation is opt-in**: the viewport observes its own size automatically, but only observes the content element when the consumer tags it with `[forScrollAreaContent]`. Skipping the directive is allowed (the viewport still scrolls and the scrollbars still render); the scrollbars just won't react to content reflows. The library never guesses `firstElementChild`, since that silently breaks when content is wrapped in a layer or split across siblings.
|
|
158
|
+
|
|
159
|
+
## Styling
|
|
160
|
+
|
|
161
|
+
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.
|
|
162
|
+
|
|
163
|
+
### Data attributes
|
|
164
|
+
|
|
165
|
+
| Piece | Attribute | Values |
|
|
166
|
+
| -------------------------- | ------------------ | ----------------------------------------- |
|
|
167
|
+
| `[forScrollArea]` | `data-type` | `auto` \| `always` \| `scroll` \| `hover` |
|
|
168
|
+
| `[forScrollAreaScrollbar]` | `data-orientation` | `horizontal` \| `vertical` |
|
|
169
|
+
| `[forScrollAreaScrollbar]` | `data-state` | `visible` \| `hidden` |
|
|
170
|
+
| `[forScrollAreaThumb]` | `data-orientation` | `horizontal` \| `vertical` |
|
|
171
|
+
| `[forScrollAreaThumb]` | `data-state` | `visible` \| `hidden` |
|
|
172
|
+
|
|
173
|
+
The resolved writing direction is reflected on the root via the native `dir` attribute (`ltr` / `rtl`), not a `data-*` hook — select on `[dir='rtl']` / `:dir(rtl)` to flip layout. `[forScrollAreaViewport]`, `[forScrollAreaContent]`, and `[forScrollAreaCorner]` carry no `data-*` attributes; the scrollbar and corner additionally self-remove via the `hidden` attribute plus an inline `display: none` when their axis has no overflow (except under `type="always"`).
|
|
174
|
+
|
|
175
|
+
```css
|
|
176
|
+
.scroll-area-scrollbar[data-state='hidden'] {
|
|
177
|
+
opacity: 0;
|
|
178
|
+
pointer-events: none;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.scroll-area-scrollbar[data-orientation='vertical'] {
|
|
182
|
+
width: 8px;
|
|
183
|
+
}
|
|
184
|
+
```
|
package/search/README.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Search
|
|
2
|
+
|
|
3
|
+
Headless `role="searchbox"` text field. `[forSearch]` applies on a native
|
|
4
|
+
`<input>` and wires its value to a signal, reflects WAI-ARIA validation state,
|
|
5
|
+
and exposes a `clear()` / `focusInput()` API for the companion `[forSearchClear]`
|
|
6
|
+
clear button.
|
|
7
|
+
|
|
8
|
+
There is no dedicated WAI-ARIA APG pattern page for "search" — the primitive
|
|
9
|
+
is a thin `role="searchbox"` text input (https://www.w3.org/TR/wai-aria-1.2/#searchbox)
|
|
10
|
+
that reuses `[forInput]`'s form-value wiring wholesale.
|
|
11
|
+
|
|
12
|
+
## Basic usage
|
|
13
|
+
|
|
14
|
+
```html
|
|
15
|
+
<input forSearch #s="forSearch" [(value)]="query" placeholder="Search…" />
|
|
16
|
+
<button [forSearchClear]="s" aria-label="Clear search">×</button>
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`[forSearchClear]` self-hides while the value is empty and refocuses the input
|
|
20
|
+
on activation. Pass the exported `#s="forSearch"` reference through the
|
|
21
|
+
selector input — no wrapping element is required.
|
|
22
|
+
|
|
23
|
+
## With Signal Forms and Field
|
|
24
|
+
|
|
25
|
+
```html
|
|
26
|
+
<div forField>
|
|
27
|
+
<label forLabel>Search</label>
|
|
28
|
+
<input forSearch [formField]="searchForm.query" />
|
|
29
|
+
</div>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`[formField]` auto-wires the `FormValueControl<string>` contract — `required`,
|
|
33
|
+
`invalid`, `touched`, and the value itself flow in and out without extra glue.
|
|
34
|
+
|
|
35
|
+
## Accessibility notes
|
|
36
|
+
|
|
37
|
+
- The `role="searchbox"` attribute is set statically by the directive.
|
|
38
|
+
- Validation state (`aria-required`, `aria-invalid`, `aria-readonly`,
|
|
39
|
+
`aria-disabled`) is reflected as truthy-only attributes (absent when `false`).
|
|
40
|
+
- `[forSearchClear]` carries `aria-label="Clear"` by default; override it with
|
|
41
|
+
a consumer-set `aria-label` on the same element if the default label is not
|
|
42
|
+
appropriate for the context.
|