forty-cdk 0.25.2 → 0.26.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 +65 -17
- package/aspect-ratio/README.md +12 -29
- package/avatar/README.md +31 -33
- package/breadcrumbs/README.md +54 -11
- package/breakpoints/README.md +68 -12
- package/button/README.md +38 -11
- package/calendar/README.md +141 -17
- package/carousel/README.md +184 -40
- package/checkbox/README.md +19 -10
- package/combobox/README.md +176 -38
- package/context-menu/README.md +77 -69
- package/date-field/README.md +42 -14
- package/date-picker/README.md +81 -31
- package/dialog/README.md +235 -186
- package/disclosure/README.md +29 -13
- package/drag-drop/README.md +129 -31
- package/drawer/README.md +149 -30
- package/dropdown-menu/README.md +80 -83
- package/fesm2022/forty-cdk-checkbox.mjs.map +1 -1
- package/fesm2022/forty-cdk-combobox.mjs +23 -3
- package/fesm2022/forty-cdk-combobox.mjs.map +1 -1
- package/fesm2022/forty-cdk-core-overlay.mjs +86 -46
- package/fesm2022/forty-cdk-core-overlay.mjs.map +1 -1
- package/fesm2022/forty-cdk-core.mjs +52 -3
- package/fesm2022/forty-cdk-core.mjs.map +1 -1
- package/fesm2022/forty-cdk-date-picker.mjs +5 -3
- package/fesm2022/forty-cdk-date-picker.mjs.map +1 -1
- package/fesm2022/forty-cdk-drag-drop.mjs +14 -6
- package/fesm2022/forty-cdk-drag-drop.mjs.map +1 -1
- package/fesm2022/forty-cdk-listbox.mjs +23 -12
- package/fesm2022/forty-cdk-listbox.mjs.map +1 -1
- package/fesm2022/forty-cdk-menu.mjs +4 -2
- package/fesm2022/forty-cdk-menu.mjs.map +1 -1
- package/fesm2022/forty-cdk-menubar.mjs +18 -4
- package/fesm2022/forty-cdk-menubar.mjs.map +1 -1
- package/fesm2022/forty-cdk-popover.mjs +5 -3
- package/fesm2022/forty-cdk-popover.mjs.map +1 -1
- package/fesm2022/forty-cdk-radio-group.mjs.map +1 -1
- package/fesm2022/forty-cdk-select.mjs +11 -12
- package/fesm2022/forty-cdk-select.mjs.map +1 -1
- package/fesm2022/forty-cdk-table.mjs.map +1 -1
- package/fesm2022/forty-cdk-tree.mjs +195 -59
- package/fesm2022/forty-cdk-tree.mjs.map +1 -1
- package/field/README.md +39 -23
- package/fieldset/README.md +30 -16
- package/file-upload/README.md +78 -14
- package/hover-card/README.md +64 -39
- package/input/README.md +59 -24
- package/internationalized-date/README.md +2 -0
- package/listbox/README.md +87 -31
- package/menu/README.md +196 -24
- package/menubar/README.md +54 -32
- package/meter/README.md +32 -34
- package/navigation-menu/README.md +125 -32
- package/number-input/README.md +18 -16
- package/otp-input/README.md +66 -62
- package/package.json +1 -1
- package/pagination/README.md +75 -11
- package/pane-resizer/README.md +28 -38
- package/popover/README.md +66 -41
- package/progress/README.md +25 -41
- package/radio-group/README.md +40 -10
- package/scroll-area/README.md +62 -87
- package/search/README.md +108 -41
- package/select/README.md +189 -35
- package/separator/README.md +19 -20
- package/shared/README.md +7 -1
- package/slider/README.md +39 -11
- package/stepper/README.md +184 -105
- package/switch/README.md +19 -13
- package/table/README.md +338 -143
- package/table-virtualization/README.md +21 -19
- package/tabs/README.md +62 -23
- package/time-field/README.md +40 -7
- package/time-picker/README.md +96 -35
- package/toast/README.md +130 -54
- package/toggle/README.md +46 -35
- package/toolbar/README.md +75 -16
- package/tooltip/README.md +76 -56
- package/tree/README.md +214 -113
- package/types/forty-cdk-checkbox.d.ts +1 -1
- package/types/forty-cdk-combobox.d.ts +10 -3
- package/types/forty-cdk-core-overlay.d.ts +35 -13
- package/types/forty-cdk-core.d.ts +47 -5
- package/types/forty-cdk-listbox.d.ts +8 -1
- package/types/forty-cdk-menu.d.ts +12 -10
- package/types/forty-cdk-menubar.d.ts +21 -3
- package/types/forty-cdk-radio-group.d.ts +1 -1
- package/types/forty-cdk-select.d.ts +1 -1
- package/types/forty-cdk-table.d.ts +1 -1
- package/types/forty-cdk-tree.d.ts +57 -10
- package/virtual-reorder/README.md +22 -20
- package/virtualization/README.md +127 -38
- package/visually-hidden/README.md +58 -21
package/accordion/README.md
CHANGED
|
@@ -9,6 +9,12 @@ apgUrl: https://www.w3.org/WAI/ARIA/apg/patterns/accordion/
|
|
|
9
9
|
|
|
10
10
|
A stack of collapsible sections, optionally allowing multiple panels open at once.
|
|
11
11
|
|
|
12
|
+
## When to choose
|
|
13
|
+
|
|
14
|
+
- **Accordion** — a group of collapsible items under one root. `[(value)]` holds which are open, `multiple` decides whether more than one may be, and ArrowUp / ArrowDown / Home / End move focus across the triggers.
|
|
15
|
+
- **[Disclosure](../disclosure/README.md)** — a single trigger and its region, with no shared state and no arrow-key navigation. Stacking several of them is not an accordion, and that is the right shape when the panels are unrelated.
|
|
16
|
+
- **[Tabs](../tabs/README.md)** — when exactly one panel is ever visible and the panels are alternatives rather than sections the reader may open together.
|
|
17
|
+
|
|
12
18
|
## Anatomy
|
|
13
19
|
|
|
14
20
|
```html
|
|
@@ -25,8 +31,10 @@ A stack of collapsible sections, optionally allowing multiple panels open at onc
|
|
|
25
31
|
|
|
26
32
|
## Examples
|
|
27
33
|
|
|
34
|
+
Open a panel with the pointer or `Enter`, move between headers with the arrow keys, and watch `data-state` flip on the item, its trigger and its content together.
|
|
35
|
+
|
|
28
36
|
```ts
|
|
29
|
-
import { Component, signal } from '@angular/core';
|
|
37
|
+
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
|
|
30
38
|
import {
|
|
31
39
|
ForAccordion,
|
|
32
40
|
ForAccordionContent,
|
|
@@ -34,31 +42,71 @@ import {
|
|
|
34
42
|
ForAccordionTrigger,
|
|
35
43
|
} from 'forty-cdk/accordion';
|
|
36
44
|
|
|
45
|
+
interface AccordionEntry {
|
|
46
|
+
readonly value: string;
|
|
47
|
+
readonly title: string;
|
|
48
|
+
readonly body: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
37
51
|
@Component({
|
|
38
|
-
selector: '
|
|
52
|
+
selector: 'app-accordion-default-example',
|
|
53
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
39
54
|
imports: [ForAccordion, ForAccordionItem, ForAccordionTrigger, ForAccordionContent],
|
|
40
55
|
template: `
|
|
41
|
-
<div forAccordion [(value)]="
|
|
42
|
-
|
|
43
|
-
<
|
|
44
|
-
<
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
<
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
56
|
+
<div forAccordion class="acc-root" [(value)]="value" collapsible>
|
|
57
|
+
@for (item of items; track item.value) {
|
|
58
|
+
<div forAccordionItem class="acc-item" [value]="item.value">
|
|
59
|
+
<h3 class="acc-heading">
|
|
60
|
+
<button type="button" forAccordionTrigger class="acc-trigger">
|
|
61
|
+
<span>{{ item.title }}</span>
|
|
62
|
+
<span class="chevron" aria-hidden="true"></span>
|
|
63
|
+
</button>
|
|
64
|
+
</h3>
|
|
65
|
+
<section forAccordionContent class="acc-content">
|
|
66
|
+
<div class="acc-inner">
|
|
67
|
+
<p>{{ item.body }}</p>
|
|
68
|
+
</div>
|
|
69
|
+
</section>
|
|
70
|
+
</div>
|
|
71
|
+
}
|
|
54
72
|
</div>
|
|
55
73
|
`,
|
|
56
74
|
})
|
|
57
|
-
export class
|
|
58
|
-
readonly
|
|
75
|
+
export class AccordionDefaultExample {
|
|
76
|
+
protected readonly items: readonly AccordionEntry[] = [
|
|
77
|
+
{
|
|
78
|
+
value: 'a',
|
|
79
|
+
title: 'What is forty-cdk?',
|
|
80
|
+
body: 'A library of headless UI primitives with built-in WAI-ARIA accessibility.',
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
value: 'b',
|
|
84
|
+
title: 'Does it ship styles?',
|
|
85
|
+
body: 'No. It exposes state, behavior, focus and ARIA; you apply the styles yourself.',
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
value: 'c',
|
|
89
|
+
title: 'Does it work without Zone.js?',
|
|
90
|
+
body: 'Yes, it is designed to run under provideZonelessChangeDetection().',
|
|
91
|
+
},
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
protected readonly value = signal<readonly string[]>(['a']);
|
|
59
95
|
}
|
|
60
96
|
```
|
|
61
97
|
|
|
98
|
+
### Multiple
|
|
99
|
+
|
|
100
|
+
`multiple` lets several sections stay open at once, so `value` holds an array of every open item.
|
|
101
|
+
|
|
102
|
+
### Horizontal
|
|
103
|
+
|
|
104
|
+
`orientation='horizontal'` lays the sections out in a row and switches roving navigation to `ArrowLeft` / `ArrowRight`. It is reflected as `data-orientation` for styling.
|
|
105
|
+
|
|
106
|
+
### Disabled item
|
|
107
|
+
|
|
108
|
+
A disabled item cannot be toggled and is skipped by the arrow keys, while staying in the DOM for screen readers.
|
|
109
|
+
|
|
62
110
|
## API
|
|
63
111
|
|
|
64
112
|
### `ForAccordion`
|
package/aspect-ratio/README.md
CHANGED
|
@@ -39,46 +39,29 @@ If your ratio is a literal constant, prefer the CSS property directly and keep t
|
|
|
39
39
|
|
|
40
40
|
## Examples
|
|
41
41
|
|
|
42
|
+
Resize the preview and watch the frame hold its 16 / 9 ratio — the primitive writes the ratio and nothing else, so every border, colour and inset below is your own CSS.
|
|
43
|
+
|
|
42
44
|
```ts
|
|
43
|
-
import { Component } from '@angular/core';
|
|
45
|
+
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
|
44
46
|
import { ForAspectRatio } from 'forty-cdk/aspect-ratio';
|
|
45
47
|
|
|
46
48
|
@Component({
|
|
47
|
-
selector: '
|
|
49
|
+
selector: 'app-aspect-ratio-default-example',
|
|
50
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
48
51
|
imports: [ForAspectRatio],
|
|
49
52
|
template: `
|
|
50
|
-
<div forAspectRatio [ratio]="16 / 9"
|
|
51
|
-
<
|
|
52
|
-
</div>
|
|
53
|
-
|
|
54
|
-
<div forAspectRatio ratio="1" class="avatar">
|
|
55
|
-
<img src="me.jpg" alt="Me" />
|
|
56
|
-
</div>
|
|
57
|
-
|
|
58
|
-
<div forAspectRatio [ratio]="21 / 9" class="hero">
|
|
59
|
-
<video src="hero.mp4" autoplay loop muted></video>
|
|
53
|
+
<div forAspectRatio class="box" [ratio]="16 / 9">
|
|
54
|
+
<span class="label">16 / 9</span>
|
|
60
55
|
</div>
|
|
61
56
|
`,
|
|
62
|
-
styles: [
|
|
63
|
-
`
|
|
64
|
-
.card-cover,
|
|
65
|
-
.avatar,
|
|
66
|
-
.hero {
|
|
67
|
-
width: 100%;
|
|
68
|
-
}
|
|
69
|
-
.card-cover img,
|
|
70
|
-
.avatar img,
|
|
71
|
-
.hero video {
|
|
72
|
-
width: 100%;
|
|
73
|
-
height: 100%;
|
|
74
|
-
object-fit: cover;
|
|
75
|
-
}
|
|
76
|
-
`,
|
|
77
|
-
],
|
|
78
57
|
})
|
|
79
|
-
export class
|
|
58
|
+
export class AspectRatioDefaultExample {}
|
|
80
59
|
```
|
|
81
60
|
|
|
61
|
+
### Square (1 / 1)
|
|
62
|
+
|
|
63
|
+
Set `ratio` to `1` to keep a box perfectly square at any width — handy for avatars, thumbnails, or uniform grid cards.
|
|
64
|
+
|
|
82
65
|
## API
|
|
83
66
|
|
|
84
67
|
### `ForAspectRatio`
|
package/avatar/README.md
CHANGED
|
@@ -22,52 +22,50 @@ Headless and presentational — it tracks the load lifecycle of an `<img>` and l
|
|
|
22
22
|
|
|
23
23
|
## Examples
|
|
24
24
|
|
|
25
|
+
Let the image load, then break its URL: `data-status` moves between `loading`, `loaded` and `error`, and the fallback only appears once the delay has passed without an image.
|
|
26
|
+
|
|
25
27
|
```ts
|
|
26
|
-
import {
|
|
28
|
+
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
|
27
29
|
import { ForAvatar, ForAvatarFallback, ForAvatarImage } from 'forty-cdk/avatar';
|
|
28
30
|
|
|
31
|
+
const AVATAR_SRC =
|
|
32
|
+
'data:image/svg+xml;utf8,' +
|
|
33
|
+
encodeURIComponent(
|
|
34
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="72" height="72" viewBox="0 0 72 72">
|
|
35
|
+
<defs>
|
|
36
|
+
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
|
|
37
|
+
<stop offset="0" stop-color="#6366f1" />
|
|
38
|
+
<stop offset="1" stop-color="#ec4899" />
|
|
39
|
+
</linearGradient>
|
|
40
|
+
</defs>
|
|
41
|
+
<rect width="72" height="72" fill="url(#g)" />
|
|
42
|
+
<circle cx="36" cy="28" r="14" fill="#fff" opacity="0.92" />
|
|
43
|
+
<path d="M14 64c0-12 9.8-20 22-20s22 8 22 20Z" fill="#fff" opacity="0.92" />
|
|
44
|
+
</svg>`,
|
|
45
|
+
);
|
|
46
|
+
|
|
29
47
|
@Component({
|
|
30
|
-
selector: '
|
|
48
|
+
selector: 'app-avatar-default-example',
|
|
49
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
31
50
|
imports: [ForAvatar, ForAvatarImage, ForAvatarFallback],
|
|
32
51
|
template: `
|
|
33
|
-
<span forAvatar #
|
|
34
|
-
<img forAvatarImage class="avatar-image" [src]="
|
|
35
|
-
@if (
|
|
36
|
-
<span forAvatarFallback class="avatar-fallback">
|
|
52
|
+
<span forAvatar #avatar="forAvatar" class="avatar" [fallbackDelayMs]="500">
|
|
53
|
+
<img forAvatarImage class="avatar-image" [src]="src" alt="Ada Lovelace" />
|
|
54
|
+
@if (avatar.shouldShowFallback()) {
|
|
55
|
+
<span forAvatarFallback class="avatar-fallback">AL</span>
|
|
37
56
|
}
|
|
38
57
|
</span>
|
|
39
58
|
`,
|
|
40
|
-
styles: [
|
|
41
|
-
`
|
|
42
|
-
.avatar {
|
|
43
|
-
display: inline-flex;
|
|
44
|
-
width: 40px;
|
|
45
|
-
height: 40px;
|
|
46
|
-
border-radius: 999px;
|
|
47
|
-
overflow: hidden;
|
|
48
|
-
background: #eee;
|
|
49
|
-
font: 600 14px/40px system-ui;
|
|
50
|
-
align-items: center;
|
|
51
|
-
justify-content: center;
|
|
52
|
-
}
|
|
53
|
-
.avatar-image {
|
|
54
|
-
width: 100%;
|
|
55
|
-
height: 100%;
|
|
56
|
-
object-fit: cover;
|
|
57
|
-
}
|
|
58
|
-
.avatar-image[data-status='loading'],
|
|
59
|
-
.avatar-image[data-status='error'] {
|
|
60
|
-
display: none;
|
|
61
|
-
}
|
|
62
|
-
`,
|
|
63
|
-
],
|
|
64
59
|
})
|
|
65
|
-
export class
|
|
66
|
-
readonly
|
|
67
|
-
readonly initials = signal('AL');
|
|
60
|
+
export class AvatarDefaultExample {
|
|
61
|
+
protected readonly src = AVATAR_SRC;
|
|
68
62
|
}
|
|
69
63
|
```
|
|
70
64
|
|
|
65
|
+
### Failed load
|
|
66
|
+
|
|
67
|
+
When the image errors, the directive flips `shouldShowFallback()` and the initials render in its place — an error shows the fallback at once, skipping the `fallbackDelayMs` wait.
|
|
68
|
+
|
|
71
69
|
## API
|
|
72
70
|
|
|
73
71
|
### `ForAvatar`
|
package/breadcrumbs/README.md
CHANGED
|
@@ -23,34 +23,77 @@ A labelled navigation landmark for a breadcrumb trail: links with aria-current='
|
|
|
23
23
|
|
|
24
24
|
## Examples
|
|
25
25
|
|
|
26
|
+
Walk the trail with `Tab` — the last crumb is the page you are on, so it carries `aria-current="page"` and is not a link back to itself.
|
|
27
|
+
|
|
26
28
|
```ts
|
|
27
|
-
import { Component } from '@angular/core';
|
|
29
|
+
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
|
|
28
30
|
import { ForBreadcrumbItem, ForBreadcrumbSeparator, ForBreadcrumbs } from 'forty-cdk/breadcrumbs';
|
|
29
31
|
|
|
32
|
+
interface Crumb {
|
|
33
|
+
readonly label: string;
|
|
34
|
+
readonly href: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
30
37
|
@Component({
|
|
31
|
-
selector: '
|
|
38
|
+
selector: 'app-breadcrumbs-default-example',
|
|
39
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
32
40
|
imports: [ForBreadcrumbs, ForBreadcrumbItem, ForBreadcrumbSeparator],
|
|
33
41
|
template: `
|
|
34
|
-
<nav forBreadcrumbs>
|
|
35
|
-
<ol>
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
42
|
+
<nav forBreadcrumbs class="bc">
|
|
43
|
+
<ol class="bc-list">
|
|
44
|
+
@for (crumb of crumbs(); track crumb.href; let last = $last) {
|
|
45
|
+
<li class="bc-li">
|
|
46
|
+
<a
|
|
47
|
+
forBreadcrumbItem
|
|
48
|
+
class="bc-link"
|
|
49
|
+
[href]="crumb.href"
|
|
50
|
+
[current]="last"
|
|
51
|
+
(click)="$event.preventDefault()"
|
|
52
|
+
>
|
|
53
|
+
{{ crumb.label }}
|
|
54
|
+
</a>
|
|
55
|
+
</li>
|
|
56
|
+
@if (!last) {
|
|
57
|
+
<li forBreadcrumbSeparator class="bc-sep">
|
|
58
|
+
<svg viewBox="0 0 24 24" aria-hidden="true">
|
|
59
|
+
<path
|
|
60
|
+
d="m8.25 4.5 7.5 7.5-7.5 7.5"
|
|
61
|
+
fill="none"
|
|
62
|
+
stroke="currentColor"
|
|
63
|
+
stroke-width="1.75"
|
|
64
|
+
stroke-linecap="round"
|
|
65
|
+
stroke-linejoin="round"
|
|
66
|
+
/>
|
|
67
|
+
</svg>
|
|
68
|
+
</li>
|
|
69
|
+
}
|
|
70
|
+
}
|
|
41
71
|
</ol>
|
|
42
72
|
</nav>
|
|
43
73
|
`,
|
|
44
74
|
})
|
|
45
|
-
export class
|
|
75
|
+
export class BreadcrumbsDefaultExample {
|
|
76
|
+
protected readonly crumbs = signal<readonly Crumb[]>([
|
|
77
|
+
{ label: 'Home', href: '/' },
|
|
78
|
+
{ label: 'Components', href: '/components' },
|
|
79
|
+
{ label: 'Navigation', href: '/components/navigation' },
|
|
80
|
+
{ label: 'Breadcrumbs', href: '/components/navigation/breadcrumbs' },
|
|
81
|
+
]);
|
|
82
|
+
}
|
|
46
83
|
```
|
|
47
84
|
|
|
48
85
|
The root defaults its label to `Breadcrumb`. Override it with `ariaLabel="…"` (or point a native `aria-labelledby` at a visible heading) when a page hosts more than one breadcrumb trail.
|
|
49
86
|
|
|
50
|
-
###
|
|
87
|
+
### Collapsing a long trail
|
|
88
|
+
|
|
89
|
+
The primitive renders whatever items you give it, so collapsing a deep path is a consumer decision. Here the middle is folded into an expandable ellipsis button that reveals the hidden crumbs — the trail stays a single accessible navigation landmark either way.
|
|
90
|
+
|
|
91
|
+
## Localizing the label
|
|
51
92
|
|
|
52
93
|
`Breadcrumb` is verbalized by screen readers, so translate it per injector scope with `provideForBreadcrumbsDefaults`. Configure it at the application root, or in any component's `providers` to scope the translation to a subtree. A per-instance `[ariaLabel]` still wins over the scope default.
|
|
53
94
|
|
|
95
|
+
<!-- snippet: fragment -->
|
|
96
|
+
|
|
54
97
|
```ts
|
|
55
98
|
import { provideForBreadcrumbsDefaults } from 'forty-cdk/breadcrumbs';
|
|
56
99
|
|
package/breakpoints/README.md
CHANGED
|
@@ -15,6 +15,7 @@ It is a headless reactive utility, not a UI primitive: no DOM, no ARIA, no templ
|
|
|
15
15
|
Configuring is optional — without a provider the Tailwind scale (`sm` 640, `md` 768, `lg` 1024, `xl` 1280, `2xl` 1536) is used. To define your own:
|
|
16
16
|
|
|
17
17
|
```ts
|
|
18
|
+
import { ApplicationConfig } from '@angular/core';
|
|
18
19
|
import { provideForBreakpointsDefaults } from 'forty-cdk/breakpoints';
|
|
19
20
|
|
|
20
21
|
export const appConfig: ApplicationConfig = {
|
|
@@ -28,33 +29,88 @@ Providing it again on a component injector replaces the map for that subtree onl
|
|
|
28
29
|
|
|
29
30
|
## Examples
|
|
30
31
|
|
|
32
|
+
Resize the preview and watch the matched breakpoint change: `injectBreakpoints()` hands back signals, so the template re-renders without a listener of your own.
|
|
33
|
+
|
|
31
34
|
```ts
|
|
32
|
-
import {
|
|
33
|
-
|
|
35
|
+
import {
|
|
36
|
+
afterNextRender,
|
|
37
|
+
ChangeDetectionStrategy,
|
|
38
|
+
Component,
|
|
39
|
+
DestroyRef,
|
|
40
|
+
inject,
|
|
41
|
+
signal,
|
|
42
|
+
} from '@angular/core';
|
|
43
|
+
import { forBreakpointsTailwind, injectBreakpoints } from 'forty-cdk/breakpoints';
|
|
44
|
+
|
|
45
|
+
type TailwindName = keyof typeof forBreakpointsTailwind;
|
|
34
46
|
|
|
35
47
|
@Component({
|
|
36
|
-
selector: 'app-
|
|
48
|
+
selector: 'app-breakpoints-active-example',
|
|
49
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
37
50
|
template: `
|
|
38
|
-
|
|
39
|
-
<
|
|
40
|
-
|
|
41
|
-
|
|
51
|
+
<div class="bp-demo">
|
|
52
|
+
<div class="bp-readout">
|
|
53
|
+
<span class="bp-readout-label">active breakpoint</span>
|
|
54
|
+
<span class="bp-active">{{ active() ?? 'below sm' }}</span>
|
|
55
|
+
<span class="bp-width">viewport ≈ {{ width() }}px</span>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<ul class="bp-grid">
|
|
59
|
+
@for (row of rows; track row.name) {
|
|
60
|
+
<li class="bp-cell" [class.bp-cell--on]="row.up()">
|
|
61
|
+
<span class="bp-name">{{ row.name }}</span>
|
|
62
|
+
<span class="bp-min">≥ {{ row.min }}px</span>
|
|
63
|
+
<span class="bp-flags">
|
|
64
|
+
<span class="bp-flag" [class.bp-flag--on]="row.up()">up</span>
|
|
65
|
+
<span class="bp-flag" [class.bp-flag--on]="row.only()">only</span>
|
|
66
|
+
</span>
|
|
67
|
+
</li>
|
|
68
|
+
}
|
|
69
|
+
</ul>
|
|
70
|
+
</div>
|
|
42
71
|
`,
|
|
43
72
|
})
|
|
44
|
-
export class
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
protected
|
|
73
|
+
export class BreakpointsActiveExample {
|
|
74
|
+
protected readonly bp = injectBreakpoints();
|
|
75
|
+
protected readonly active = this.bp.active;
|
|
76
|
+
|
|
77
|
+
protected readonly rows = (Object.keys(forBreakpointsTailwind) as TailwindName[]).map((name) => ({
|
|
78
|
+
name,
|
|
79
|
+
min: forBreakpointsTailwind[name],
|
|
80
|
+
up: this.bp.up(name),
|
|
81
|
+
only: this.bp.only(name),
|
|
82
|
+
}));
|
|
83
|
+
|
|
84
|
+
protected readonly width = signal(0);
|
|
85
|
+
|
|
86
|
+
constructor() {
|
|
87
|
+
const destroyRef = inject(DestroyRef);
|
|
88
|
+
afterNextRender(() => {
|
|
89
|
+
const onResize = (): void => this.width.set(globalThis.innerWidth);
|
|
90
|
+
onResize();
|
|
91
|
+
globalThis.addEventListener('resize', onResize, { passive: true });
|
|
92
|
+
destroyRef.onDestroy(() => globalThis.removeEventListener('resize', onResize));
|
|
93
|
+
});
|
|
94
|
+
}
|
|
49
95
|
}
|
|
50
96
|
```
|
|
51
97
|
|
|
52
98
|
The returned handle captures its injection context, so the query methods can be called lazily from a `computed()` or a template, not only during construction:
|
|
53
99
|
|
|
100
|
+
<!-- snippet: fragment -->
|
|
101
|
+
|
|
54
102
|
```ts
|
|
55
103
|
protected columns = computed(() => (this.bp.up('xl')() ? 4 : this.bp.up('md')() ? 2 : 1));
|
|
56
104
|
```
|
|
57
105
|
|
|
106
|
+
### Responsive layout
|
|
107
|
+
|
|
108
|
+
Derive UI from the breakpoint inside `computed()` and `@if` instead of repeating media queries in the template. The card grid picks its column count from `up('md')` / `up('lg')` / `up('xl')`, and the sidebar is only mounted at `lg` and wider.
|
|
109
|
+
|
|
110
|
+
### Arbitrary media queries
|
|
111
|
+
|
|
112
|
+
`matches(query)` is the escape hatch for any media feature the named width helpers don't cover — orientation, pointer, hover, and the `prefers-*` user settings. Each call returns a live `Signal<boolean>` from the same cached `MediaQueryList` layer.
|
|
113
|
+
|
|
58
114
|
## Typed custom names
|
|
59
115
|
|
|
60
116
|
The default map gives you fully-typed names out of the box (`up('md')` autocompletes; `up('foo')` is a type error). When you provide a custom map, recover the same typing by augmenting `BreakpointRegistry` once — derive the keys from your map so you never write them twice:
|
package/button/README.md
CHANGED
|
@@ -23,17 +23,31 @@ A single `[forButton]` directive does all of this. On a native `<button>` host t
|
|
|
23
23
|
|
|
24
24
|
## Examples
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
Press and hold either control — a native `<button>` and a `<span>` — and watch `data-pressed`, `data-hovered` and `data-focus-visible` appear on both, so one rule styles the pair.
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
|
30
|
+
import { ForButton } from 'forty-cdk/button';
|
|
31
|
+
|
|
32
|
+
@Component({
|
|
33
|
+
selector: 'app-button-default-example',
|
|
34
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
35
|
+
imports: [ForButton],
|
|
36
|
+
template: `
|
|
37
|
+
<div class="stage">
|
|
38
|
+
<button forButton class="btn btn--primary">Native <button></button>
|
|
39
|
+
<span forButton class="btn">Custom <span></span>
|
|
40
|
+
</div>
|
|
41
|
+
`,
|
|
42
|
+
})
|
|
43
|
+
export class ButtonDefaultExample {}
|
|
44
|
+
```
|
|
27
45
|
|
|
28
|
-
|
|
29
|
-
<!-- Native button — platform handles Enter/Space → click synthesis -->
|
|
30
|
-
<button forButton (activate)="save()">Save</button>
|
|
46
|
+
### Disabled stays focusable
|
|
31
47
|
|
|
32
|
-
|
|
33
|
-
<div forButton (activate)="save()">Save</div>
|
|
34
|
-
```
|
|
48
|
+
Per the APG, a disabled button must stay reachable so assistive tech can announce it. `forButton` never sets the native `disabled` attribute — it reflects `aria-disabled='true'` + `data-disabled` and makes activation a no-op. The native disabled button is skipped entirely.
|
|
35
49
|
|
|
36
|
-
|
|
50
|
+
## Disabled
|
|
37
51
|
|
|
38
52
|
Disabled buttons stay focusable so assistive technology can announce them. The native `disabled` attribute is never set; instead `aria-disabled="true"` is reflected.
|
|
39
53
|
|
|
@@ -50,7 +64,7 @@ A surrounding disabled `[forFieldset]` disables the button too — its `disabled
|
|
|
50
64
|
</fieldset>
|
|
51
65
|
```
|
|
52
66
|
|
|
53
|
-
|
|
67
|
+
## Preserve consumer `type`
|
|
54
68
|
|
|
55
69
|
A native `<button>` without an explicit `type` attribute defaults to `type="button"`. A consumer-set `type="submit"` is preserved:
|
|
56
70
|
|
|
@@ -78,12 +92,25 @@ The directive reflects boolean `data-*` attributes (present with an empty-string
|
|
|
78
92
|
|
|
79
93
|
`data-pressed` is present while the primary pointer is held down or Enter/Space is held. `data-hovered` is present while a mouse/pen pointer is over the element. `data-focus-visible` is present when focused via keyboard (keyboard modality active).
|
|
80
94
|
|
|
95
|
+
## Keyboard
|
|
96
|
+
|
|
97
|
+
On a native `<button>` host the platform owns activation: every key handler the directive binds returns immediately, and nothing in this table is its doing. On any other host (`<div forButton>`, `<span forButton>`) it synthesizes the activation itself, through the same `(click)` path a pointer takes — on a deliberately asymmetric split, because that is what a native button does.
|
|
98
|
+
|
|
99
|
+
| Key | Action |
|
|
100
|
+
| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
101
|
+
| `Enter` — native `<button>` | The platform synthesizes the click; the directive adds nothing. |
|
|
102
|
+
| `Enter` — any other host | Activates on `keydown`, so `(activate)` fires while the key is still held. While disabled the event is left alone entirely — not even its default is prevented. |
|
|
103
|
+
| `Space` — native `<button>` | The platform synthesizes the click on release and suppresses the page scroll itself. |
|
|
104
|
+
| `Space` — any other host | Activates on `keyup`, and only when the matching `keydown` reached the same host — focus leaving mid-press drops the press. Its `keydown` always calls `preventDefault()` to stop the page scrolling, **even while the button is disabled**. |
|
|
105
|
+
|
|
106
|
+
Both keys drive `data-pressed` on every host: present from `keydown` until `keyup`, until focus leaves, or until the pointer is released. `data-focus-visible` instead follows the keyboard modality, so a `keydown` carrying `Meta` / `Control` / `Alt` is read as a shortcut and does not turn it on, while `Shift` does.
|
|
107
|
+
|
|
81
108
|
## Accessibility
|
|
82
109
|
|
|
83
110
|
Implements the [WAI-ARIA Button pattern](https://www.w3.org/WAI/ARIA/apg/patterns/button/).
|
|
84
111
|
|
|
85
|
-
- **Native `<button>` semantics are preserved.** On a native host, no extra ARIA is added; the browser's built-in button role,
|
|
86
|
-
- **Non-button hosts get `role="button"` and `tabindex="0"
|
|
112
|
+
- **Native `<button>` semantics are preserved.** On a native host, no extra ARIA is added; the browser's built-in button role, keyboard activation, and `type` handling all apply.
|
|
113
|
+
- **Non-button hosts get `role="button"` and `tabindex="0"`**, and the directive synthesizes the activation the platform would have — see [Keyboard](#keyboard).
|
|
87
114
|
- **Disabled buttons stay focusable.** `aria-disabled="true"` is used instead of the native `disabled` attribute so assistive technology can still announce the control's purpose.
|
|
88
115
|
|
|
89
116
|
## Styling
|