forty-cdk 0.25.1 → 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.
Files changed (95) hide show
  1. package/accordion/README.md +65 -17
  2. package/aspect-ratio/README.md +12 -29
  3. package/avatar/README.md +31 -33
  4. package/breadcrumbs/README.md +54 -11
  5. package/breakpoints/README.md +68 -12
  6. package/button/README.md +38 -11
  7. package/calendar/README.md +141 -17
  8. package/carousel/README.md +184 -40
  9. package/checkbox/README.md +19 -10
  10. package/combobox/README.md +176 -38
  11. package/context-menu/README.md +77 -69
  12. package/date-field/README.md +42 -14
  13. package/date-picker/README.md +81 -31
  14. package/dialog/README.md +235 -186
  15. package/disclosure/README.md +29 -13
  16. package/drag-drop/README.md +129 -31
  17. package/drawer/README.md +149 -30
  18. package/dropdown-menu/README.md +80 -83
  19. package/fesm2022/forty-cdk-checkbox.mjs.map +1 -1
  20. package/fesm2022/forty-cdk-combobox.mjs +23 -3
  21. package/fesm2022/forty-cdk-combobox.mjs.map +1 -1
  22. package/fesm2022/forty-cdk-core-overlay.mjs +86 -46
  23. package/fesm2022/forty-cdk-core-overlay.mjs.map +1 -1
  24. package/fesm2022/forty-cdk-core.mjs +107 -3
  25. package/fesm2022/forty-cdk-core.mjs.map +1 -1
  26. package/fesm2022/forty-cdk-date-picker.mjs +5 -3
  27. package/fesm2022/forty-cdk-date-picker.mjs.map +1 -1
  28. package/fesm2022/forty-cdk-drag-drop.mjs +14 -6
  29. package/fesm2022/forty-cdk-drag-drop.mjs.map +1 -1
  30. package/fesm2022/forty-cdk-listbox.mjs +23 -12
  31. package/fesm2022/forty-cdk-listbox.mjs.map +1 -1
  32. package/fesm2022/forty-cdk-menu.mjs +4 -2
  33. package/fesm2022/forty-cdk-menu.mjs.map +1 -1
  34. package/fesm2022/forty-cdk-menubar.mjs +18 -4
  35. package/fesm2022/forty-cdk-menubar.mjs.map +1 -1
  36. package/fesm2022/forty-cdk-popover.mjs +5 -3
  37. package/fesm2022/forty-cdk-popover.mjs.map +1 -1
  38. package/fesm2022/forty-cdk-radio-group.mjs.map +1 -1
  39. package/fesm2022/forty-cdk-select.mjs +11 -12
  40. package/fesm2022/forty-cdk-select.mjs.map +1 -1
  41. package/fesm2022/forty-cdk-table.mjs +109 -4
  42. package/fesm2022/forty-cdk-table.mjs.map +1 -1
  43. package/fesm2022/forty-cdk-tree.mjs +195 -59
  44. package/fesm2022/forty-cdk-tree.mjs.map +1 -1
  45. package/field/README.md +39 -23
  46. package/fieldset/README.md +30 -16
  47. package/file-upload/README.md +78 -14
  48. package/hover-card/README.md +64 -39
  49. package/input/README.md +59 -24
  50. package/internationalized-date/README.md +2 -0
  51. package/listbox/README.md +87 -31
  52. package/menu/README.md +196 -24
  53. package/menubar/README.md +54 -32
  54. package/meter/README.md +32 -34
  55. package/navigation-menu/README.md +125 -32
  56. package/number-input/README.md +18 -16
  57. package/otp-input/README.md +66 -62
  58. package/package.json +1 -1
  59. package/pagination/README.md +75 -11
  60. package/pane-resizer/README.md +28 -38
  61. package/popover/README.md +66 -41
  62. package/progress/README.md +25 -41
  63. package/radio-group/README.md +40 -10
  64. package/scroll-area/README.md +62 -87
  65. package/search/README.md +108 -41
  66. package/select/README.md +189 -35
  67. package/separator/README.md +19 -20
  68. package/shared/README.md +7 -1
  69. package/slider/README.md +39 -11
  70. package/stepper/README.md +184 -105
  71. package/switch/README.md +19 -13
  72. package/table/README.md +338 -142
  73. package/table-virtualization/README.md +21 -19
  74. package/tabs/README.md +62 -23
  75. package/time-field/README.md +40 -7
  76. package/time-picker/README.md +96 -35
  77. package/toast/README.md +130 -54
  78. package/toggle/README.md +46 -35
  79. package/toolbar/README.md +75 -16
  80. package/tooltip/README.md +76 -56
  81. package/tree/README.md +214 -113
  82. package/types/forty-cdk-checkbox.d.ts +1 -1
  83. package/types/forty-cdk-combobox.d.ts +10 -3
  84. package/types/forty-cdk-core-overlay.d.ts +35 -13
  85. package/types/forty-cdk-core.d.ts +73 -5
  86. package/types/forty-cdk-listbox.d.ts +8 -1
  87. package/types/forty-cdk-menu.d.ts +12 -10
  88. package/types/forty-cdk-menubar.d.ts +21 -3
  89. package/types/forty-cdk-radio-group.d.ts +1 -1
  90. package/types/forty-cdk-select.d.ts +1 -1
  91. package/types/forty-cdk-table.d.ts +64 -21
  92. package/types/forty-cdk-tree.d.ts +57 -10
  93. package/virtual-reorder/README.md +22 -20
  94. package/virtualization/README.md +127 -38
  95. package/visually-hidden/README.md +58 -21
@@ -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: 'demo-faq',
52
+ selector: 'app-accordion-default-example',
53
+ changeDetection: ChangeDetectionStrategy.OnPush,
39
54
  imports: [ForAccordion, ForAccordionItem, ForAccordionTrigger, ForAccordionContent],
40
55
  template: `
41
- <div forAccordion [(value)]="open" collapsible>
42
- <div forAccordionItem value="shipping">
43
- <h3>
44
- <button type="button" forAccordionTrigger class="accordion-trigger">Shipping</button>
45
- </h3>
46
- <section forAccordionContent>Ships in 24h.</section>
47
- </div>
48
- <div forAccordionItem value="returns">
49
- <h3>
50
- <button type="button" forAccordionTrigger class="accordion-trigger">Returns</button>
51
- </h3>
52
- <section forAccordionContent>Free 30-day returns.</section>
53
- </div>
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 DemoFaq {
58
- readonly open = signal<readonly string[]>([]);
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`
@@ -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: 'demo-aspect-ratio',
49
+ selector: 'app-aspect-ratio-default-example',
50
+ changeDetection: ChangeDetectionStrategy.OnPush,
48
51
  imports: [ForAspectRatio],
49
52
  template: `
50
- <div forAspectRatio [ratio]="16 / 9" class="card-cover">
51
- <img src="cover.jpg" alt="" />
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 DemoAspectRatio {}
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 { Component, signal } from '@angular/core';
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: 'demo-avatar',
48
+ selector: 'app-avatar-default-example',
49
+ changeDetection: ChangeDetectionStrategy.OnPush,
31
50
  imports: [ForAvatar, ForAvatarImage, ForAvatarFallback],
32
51
  template: `
33
- <span forAvatar #a="forAvatar" class="avatar" fallbackDelayMs="500">
34
- <img forAvatarImage class="avatar-image" [src]="user.avatarUrl" [alt]="user.name" />
35
- @if (a.shouldShowFallback()) {
36
- <span forAvatarFallback class="avatar-fallback">{{ initials() }}</span>
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 DemoAvatar {
66
- readonly user = { name: 'Ada Lovelace', avatarUrl: '/api/avatar/ada.jpg' };
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`
@@ -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: 'demo-breadcrumbs',
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
- <li><a forBreadcrumbItem href="/">Home</a></li>
37
- <li forBreadcrumbSeparator>/</li>
38
- <li><a forBreadcrumbItem href="/library">Library</a></li>
39
- <li forBreadcrumbSeparator>/</li>
40
- <li><a forBreadcrumbItem href="/library/data" current>Data</a></li>
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 DemoBreadcrumbs {}
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
- ### Localizing the label
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
 
@@ -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 { Component, inject } from '@angular/core';
33
- import { injectBreakpoints } from 'forty-cdk/breakpoints';
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-layout',
48
+ selector: 'app-breakpoints-active-example',
49
+ changeDetection: ChangeDetectionStrategy.OnPush,
37
50
  template: `
38
- @if (isDesktop()) {
39
- <aside>Sidebar</aside>
40
- }
41
- <main>Active breakpoint: {{ active() }}</main>
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 Layout {
45
- private bp = injectBreakpoints();
46
-
47
- protected isDesktop = this.bp.up('lg'); // (min-width: 1024px) and wider
48
- protected active = this.bp.active; // 'sm' | 'md' | … | null
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
- ### Basic usage
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 &lt;button&gt;</button>
39
+ <span forButton class="btn">Custom &lt;span&gt;</span>
40
+ </div>
41
+ `,
42
+ })
43
+ export class ButtonDefaultExample {}
44
+ ```
27
45
 
28
- ```html
29
- <!-- Native button — platform handles Enter/Space → click synthesis -->
30
- <button forButton (activate)="save()">Save</button>
46
+ ### Disabled stays focusable
31
47
 
32
- <!-- Non-button host — role="button", tabindex="0", and keyboard activation added automatically -->
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
- ### Disabled
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
- ### Preserve consumer `type`
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, Enter/Space activation, and `type` handling all apply.
86
- - **Non-button hosts get `role="button"` and `tabindex="0"`** plus keyboard activation (Enter/Space), matching the native button contract.
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