@kouji-ui/core 0.0.4 → 0.1.2

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 (127) hide show
  1. package/fesm2022/kouji-ui-core.mjs +26789 -0
  2. package/fesm2022/kouji-ui-core.mjs.map +1 -0
  3. package/icon/icon.css +40 -0
  4. package/package.json +20 -9
  5. package/types/kouji-ui-core.d.ts +13545 -0
  6. package/typography/prose.css +312 -0
  7. package/.turbo/turbo-build.log +0 -27
  8. package/CHANGELOG.md +0 -21
  9. package/ng-package.json +0 -7
  10. package/src/a11y/.gitkeep +0 -0
  11. package/src/a11y/aria-describedby.spec.ts +0 -55
  12. package/src/a11y/aria-describedby.ts +0 -33
  13. package/src/a11y/focus-trap.spec.ts +0 -86
  14. package/src/a11y/focus-trap.ts +0 -70
  15. package/src/a11y/index.ts +0 -9
  16. package/src/a11y/live-region.spec.ts +0 -58
  17. package/src/a11y/live-region.ts +0 -48
  18. package/src/a11y/roving-tabindex.spec.ts +0 -62
  19. package/src/a11y/roving-tabindex.ts +0 -104
  20. package/src/a11y/visually-hidden.spec.ts +0 -34
  21. package/src/a11y/visually-hidden.ts +0 -24
  22. package/src/accordion/accordion.context.ts +0 -22
  23. package/src/accordion/accordion.spec.ts +0 -32
  24. package/src/accordion/accordion.ts +0 -129
  25. package/src/accordion/index.ts +0 -2
  26. package/src/avatar/avatar.spec.ts +0 -18
  27. package/src/avatar/avatar.ts +0 -26
  28. package/src/avatar/index.ts +0 -1
  29. package/src/badge/badge.spec.ts +0 -20
  30. package/src/badge/badge.ts +0 -14
  31. package/src/badge/index.ts +0 -1
  32. package/src/button/button.example.ts +0 -30
  33. package/src/button/button.finance.example.ts +0 -37
  34. package/src/button/button.retro.example.ts +0 -47
  35. package/src/button/button.sizes.example.ts +0 -25
  36. package/src/button/button.spec.ts +0 -32
  37. package/src/button/button.ts +0 -44
  38. package/src/button/index.ts +0 -1
  39. package/src/chart/chart.spec.ts +0 -27
  40. package/src/chart/chart.ts +0 -48
  41. package/src/chart/index.ts +0 -1
  42. package/src/checkbox/checkbox.spec.ts +0 -35
  43. package/src/checkbox/checkbox.ts +0 -82
  44. package/src/checkbox/index.ts +0 -1
  45. package/src/dialog/dialog.confirm.example.ts +0 -57
  46. package/src/dialog/dialog.context.ts +0 -11
  47. package/src/dialog/dialog.example.ts +0 -40
  48. package/src/dialog/dialog.finance.example.ts +0 -47
  49. package/src/dialog/dialog.retro.example.ts +0 -46
  50. package/src/dialog/dialog.service.ts +0 -176
  51. package/src/dialog/dialog.spec.ts +0 -122
  52. package/src/dialog/dialog.ts +0 -201
  53. package/src/dialog/index.ts +0 -9
  54. package/src/example-components.ts +0 -33
  55. package/src/form/form.context.ts +0 -3
  56. package/src/form/form.spec.ts +0 -25
  57. package/src/form/form.ts +0 -46
  58. package/src/form/index.ts +0 -2
  59. package/src/input/index.ts +0 -1
  60. package/src/input/input.spec.ts +0 -43
  61. package/src/input/input.ts +0 -80
  62. package/src/menu/index.ts +0 -2
  63. package/src/menu/menu.context.ts +0 -10
  64. package/src/menu/menu.example.ts +0 -39
  65. package/src/menu/menu.spec.ts +0 -168
  66. package/src/menu/menu.ts +0 -176
  67. package/src/popover/index.ts +0 -12
  68. package/src/popover/popover.context.ts +0 -18
  69. package/src/popover/popover.example.ts +0 -50
  70. package/src/popover/popover.finance.example.ts +0 -52
  71. package/src/popover/popover.retro.example.ts +0 -53
  72. package/src/popover/popover.spec.ts +0 -52
  73. package/src/popover/popover.ts +0 -124
  74. package/src/primitives/.gitkeep +0 -0
  75. package/src/primitives/forms/form-control.spec.ts +0 -59
  76. package/src/primitives/forms/form-control.ts +0 -90
  77. package/src/primitives/forms/index.ts +0 -1
  78. package/src/primitives/index.ts +0 -3
  79. package/src/primitives/interaction/disabled.spec.ts +0 -47
  80. package/src/primitives/interaction/disabled.ts +0 -24
  81. package/src/primitives/interaction/focus-ring.spec.ts +0 -62
  82. package/src/primitives/interaction/focus-ring.ts +0 -57
  83. package/src/primitives/interaction/index.ts +0 -2
  84. package/src/primitives/overlay/index.ts +0 -1
  85. package/src/primitives/overlay/overlay.spec.ts +0 -90
  86. package/src/primitives/overlay/overlay.ts +0 -106
  87. package/src/public-api.ts +0 -35
  88. package/src/radio/index.ts +0 -2
  89. package/src/radio/radio.context.ts +0 -3
  90. package/src/radio/radio.spec.ts +0 -36
  91. package/src/radio/radio.ts +0 -55
  92. package/src/select/index.ts +0 -2
  93. package/src/select/select.context.ts +0 -3
  94. package/src/select/select.spec.ts +0 -145
  95. package/src/select/select.ts +0 -250
  96. package/src/styles/docs-themes.css +0 -82
  97. package/src/table/index.ts +0 -1
  98. package/src/table/table.spec.ts +0 -113
  99. package/src/table/table.ts +0 -126
  100. package/src/tabs/index.ts +0 -2
  101. package/src/tabs/tabs.context.ts +0 -9
  102. package/src/tabs/tabs.spec.ts +0 -40
  103. package/src/tabs/tabs.ts +0 -130
  104. package/src/test-setup.ts +0 -5
  105. package/src/toast/index.ts +0 -21
  106. package/src/toast/toast.example.ts +0 -134
  107. package/src/toast/toast.finance.example.ts +0 -68
  108. package/src/toast/toast.retro.example.ts +0 -69
  109. package/src/toast/toast.service.ts +0 -153
  110. package/src/toast/toast.spec.ts +0 -95
  111. package/src/toast/toast.strategy.ts +0 -102
  112. package/src/toast/toast.ts +0 -307
  113. package/src/toast/toast.types.ts +0 -5
  114. package/src/toggle/index.ts +0 -1
  115. package/src/toggle/toggle.spec.ts +0 -24
  116. package/src/toggle/toggle.ts +0 -40
  117. package/src/tooltip/index.ts +0 -5
  118. package/src/tooltip/tooltip.example.ts +0 -46
  119. package/src/tooltip/tooltip.finance.example.ts +0 -54
  120. package/src/tooltip/tooltip.placements.example.ts +0 -56
  121. package/src/tooltip/tooltip.retro.example.ts +0 -65
  122. package/src/tooltip/tooltip.spec.ts +0 -104
  123. package/src/tooltip/tooltip.ts +0 -129
  124. package/tsconfig.lib.json +0 -13
  125. package/tsconfig.lib.prod.json +0 -11
  126. package/tsconfig.spec.json +0 -10
  127. package/vite.config.ts +0 -19
@@ -0,0 +1,312 @@
1
+ /* ─────────────────────────────────────────────────────────────
2
+ kouji-ui Typography — `kj-prose` container + tone reflections
3
+ --------------------------------------------------------------
4
+ Apply `.kj-prose` to a container element to restyle every
5
+ descendant `h1`–`h6`, `p`, `a`, `blockquote`, `code`, `pre`,
6
+ `ul`, `ol`, `li`, `dl`, `dt`, `dd`, `hr`, `figure`,
7
+ `figcaption`, `table`, `strong`, `em`, `kbd` according to the
8
+ kouji type system.
9
+
10
+ The five attribute directives (`kjLead`, `kjMuted`, `kjCode`,
11
+ `kjBlockquote`, `kjTruncate`) reflect `data-tone` /
12
+ `data-truncate` host attributes; the rules below key off those
13
+ attributes so the same styling applies whether the consumer
14
+ used the directive, hand-typed `data-tone="muted"` on a span,
15
+ or wrapped a subtree in `.kj-prose`.
16
+
17
+ Tokens read from the kouji theme layer:
18
+ --kj-bg-body / surface / elevated ......... background scale
19
+ --kj-fg-default ........................... primary text
20
+ --kj-fg-muted ............................. secondary text (≥ 7:1)
21
+ --kj-border-default / muted ............... separators / rules
22
+ --kj-color-link ........................... anchor colour
23
+ --kj-color-link-visited ................... visited anchor
24
+ --kj-font-sans / --kj-font-mono / --kj-font-display
25
+ --kj-font-size-xs / sm / md / lg / xl / 2xl / 3xl / 4xl
26
+ --kj-line-height-tight / snug / normal / relaxed / loose
27
+ --kj-radius-sm / md
28
+ --kj-prose-max-width ...................... opt-out for CJK
29
+ ───────────────────────────────────────────────────────────── */
30
+
31
+ @layer kj.prose {
32
+ .kj-prose {
33
+ color: var(--kj-fg-default);
34
+ font-family: var(--kj-font-sans);
35
+ font-size: var(--kj-font-size-md, 1rem);
36
+ line-height: var(--kj-line-height-relaxed, 1.7);
37
+ max-width: var(--kj-prose-max-width, 65ch);
38
+ text-align: start;
39
+ }
40
+
41
+ /* ── Block rhythm: vertical margins between adjacent flow elements ── */
42
+ .kj-prose > * + * {
43
+ margin-block-start: 1em;
44
+ }
45
+
46
+ .kj-prose > :first-child {
47
+ margin-block-start: 0;
48
+ }
49
+
50
+ .kj-prose > :last-child {
51
+ margin-block-end: 0;
52
+ }
53
+
54
+ /* ── Headings ────────────────────────────────────────────── */
55
+ .kj-prose h1,
56
+ .kj-prose h2,
57
+ .kj-prose h3,
58
+ .kj-prose h4,
59
+ .kj-prose h5,
60
+ .kj-prose h6 {
61
+ color: var(--kj-fg-default);
62
+ font-family: var(--kj-font-display, var(--kj-font-sans));
63
+ font-weight: 600;
64
+ line-height: var(--kj-line-height-tight, 1.2);
65
+ margin-block-start: 1.6em;
66
+ margin-block-end: 0.6em;
67
+ }
68
+
69
+ .kj-prose h1 {
70
+ font-size: var(--kj-font-size-4xl, 2.25rem);
71
+ font-weight: 700;
72
+ line-height: var(--kj-line-height-tight, 1.1);
73
+ }
74
+
75
+ .kj-prose h2 {
76
+ font-size: var(--kj-font-size-3xl, 1.875rem);
77
+ }
78
+
79
+ .kj-prose h3 {
80
+ font-size: var(--kj-font-size-2xl, 1.5rem);
81
+ }
82
+
83
+ .kj-prose h4 {
84
+ font-size: var(--kj-font-size-xl, 1.25rem);
85
+ }
86
+
87
+ .kj-prose h5 {
88
+ font-size: var(--kj-font-size-lg, 1.125rem);
89
+ }
90
+
91
+ .kj-prose h6 {
92
+ font-size: var(--kj-font-size-md, 1rem);
93
+ text-transform: uppercase;
94
+ letter-spacing: 0.04em;
95
+ }
96
+
97
+ /* ── Paragraphs ──────────────────────────────────────────── */
98
+ .kj-prose p {
99
+ line-height: var(--kj-line-height-relaxed, 1.7);
100
+ }
101
+
102
+ /* ── Anchors ─────────────────────────────────────────────── */
103
+ .kj-prose a {
104
+ color: var(--kj-fg-primary);
105
+ text-decoration: underline;
106
+ text-underline-offset: 0.2em;
107
+ }
108
+
109
+ .kj-prose a:visited {
110
+ color: var(--kj-fg-accent);
111
+ }
112
+
113
+ .kj-prose a:hover {
114
+ text-decoration-thickness: 2px;
115
+ }
116
+
117
+ /* ── Lists ───────────────────────────────────────────────── */
118
+ .kj-prose ul,
119
+ .kj-prose ol {
120
+ padding-inline-start: 1.5em;
121
+ }
122
+
123
+ .kj-prose ul {
124
+ list-style: disc;
125
+ }
126
+
127
+ .kj-prose ol {
128
+ list-style: decimal;
129
+ }
130
+
131
+ .kj-prose li {
132
+ margin-block-start: 0.4em;
133
+ line-height: var(--kj-line-height-relaxed, 1.7);
134
+ }
135
+
136
+ .kj-prose li > ul,
137
+ .kj-prose li > ol {
138
+ margin-block-start: 0.4em;
139
+ }
140
+
141
+ /* ── Definition lists ────────────────────────────────────── */
142
+ .kj-prose dt {
143
+ font-weight: 600;
144
+ margin-block-start: 0.8em;
145
+ }
146
+
147
+ .kj-prose dd {
148
+ margin-inline-start: 1.5em;
149
+ }
150
+
151
+ /* ── Blockquote ──────────────────────────────────────────── */
152
+ .kj-prose blockquote {
153
+ border-inline-start: 4px solid var(--kj-border-muted, currentColor);
154
+ color: var(--kj-fg-muted, var(--kj-fg-default));
155
+ font-style: italic;
156
+ margin-inline: 0;
157
+ padding-inline-start: 1em;
158
+ }
159
+
160
+ /* ── Code (inline + block) ───────────────────────────────── */
161
+ .kj-prose code {
162
+ background-color: var(--kj-bg-surface, transparent);
163
+ border-radius: var(--kj-radius-sm, 4px);
164
+ font-family: var(--kj-font-mono);
165
+ font-size: 0.9em;
166
+ padding-block: 0.1em;
167
+ padding-inline: 0.35em;
168
+ }
169
+
170
+ .kj-prose pre {
171
+ background-color: var(--kj-bg-surface, transparent);
172
+ border-radius: var(--kj-radius-md, 6px);
173
+ color: var(--kj-fg-default);
174
+ font-family: var(--kj-font-mono);
175
+ line-height: var(--kj-line-height-snug, 1.45);
176
+ overflow-x: auto;
177
+ padding: 1em;
178
+ }
179
+
180
+ .kj-prose pre code {
181
+ background-color: transparent;
182
+ border-radius: 0;
183
+ font-size: inherit;
184
+ padding: 0;
185
+ }
186
+
187
+ /* ── Horizontal rule ─────────────────────────────────────── */
188
+ .kj-prose hr {
189
+ background-color: var(--kj-border-default, currentColor);
190
+ border: 0;
191
+ height: 1px;
192
+ margin-block: 2em;
193
+ }
194
+
195
+ /* ── Figures ─────────────────────────────────────────────── */
196
+ .kj-prose figure {
197
+ margin-inline: 0;
198
+ }
199
+
200
+ .kj-prose figcaption {
201
+ color: var(--kj-fg-muted, var(--kj-fg-default));
202
+ font-size: var(--kj-font-size-sm, 0.875rem);
203
+ margin-block-start: 0.5em;
204
+ text-align: center;
205
+ }
206
+
207
+ /* ── Tables ──────────────────────────────────────────────── */
208
+ .kj-prose table {
209
+ border-collapse: collapse;
210
+ font-size: var(--kj-font-size-sm, 0.9em);
211
+ width: 100%;
212
+ }
213
+
214
+ .kj-prose thead {
215
+ border-block-end: 2px solid var(--kj-border-default, currentColor);
216
+ }
217
+
218
+ .kj-prose th,
219
+ .kj-prose td {
220
+ border-block-end: 1px solid var(--kj-border-muted, currentColor);
221
+ padding-block: 0.5em;
222
+ padding-inline: 0.75em;
223
+ text-align: start;
224
+ vertical-align: top;
225
+ }
226
+
227
+ .kj-prose th {
228
+ font-weight: 600;
229
+ }
230
+
231
+ /* ── Inline emphasis ─────────────────────────────────────── */
232
+ .kj-prose strong {
233
+ font-weight: 700;
234
+ }
235
+
236
+ .kj-prose em {
237
+ font-style: italic;
238
+ }
239
+
240
+ /* ── Size modifiers ──────────────────────────────────────── */
241
+ .kj-prose-sm {
242
+ font-size: var(--kj-font-size-sm, 0.875rem);
243
+ line-height: var(--kj-line-height-normal, 1.6);
244
+ }
245
+
246
+ .kj-prose-lg {
247
+ font-size: var(--kj-font-size-lg, 1.125rem);
248
+ line-height: var(--kj-line-height-loose, 1.8);
249
+ }
250
+ }
251
+
252
+ /* ──────────────────────────────────────────────────────────────
253
+ Tone reflections — applied by directives via `[data-tone]`,
254
+ readable in or out of a `.kj-prose` container.
255
+ ────────────────────────────────────────────────────────────── */
256
+
257
+ @layer kj.tone {
258
+ [data-tone='lead'] {
259
+ color: var(--kj-fg-muted, var(--kj-fg-default));
260
+ font-size: var(--kj-font-size-lg, 1.125rem);
261
+ line-height: var(--kj-line-height-relaxed, 1.7);
262
+ }
263
+
264
+ [data-tone='muted'] {
265
+ color: var(--kj-fg-muted, var(--kj-fg-default));
266
+ }
267
+
268
+ [data-tone='code'] {
269
+ background-color: var(--kj-bg-surface, transparent);
270
+ border-radius: var(--kj-radius-sm, 4px);
271
+ font-family: var(--kj-font-mono);
272
+ font-size: 0.9em;
273
+ padding-block: 0.1em;
274
+ padding-inline: 0.35em;
275
+ }
276
+
277
+ [data-tone='blockquote'] {
278
+ border-inline-start: 4px solid var(--kj-border-muted, currentColor);
279
+ color: var(--kj-fg-muted, var(--kj-fg-default));
280
+ font-style: italic;
281
+ margin-inline: 0;
282
+ padding-inline-start: 1em;
283
+ }
284
+ }
285
+
286
+ /* ──────────────────────────────────────────────────────────────
287
+ Truncate — single-line ellipsis (data-truncate="1") and
288
+ multi-line clamp (data-truncate="2"+).
289
+ ────────────────────────────────────────────────────────────── */
290
+
291
+ @layer kj.truncate {
292
+ [data-truncate='1'] {
293
+ overflow: hidden;
294
+ text-overflow: ellipsis;
295
+ white-space: nowrap;
296
+ }
297
+
298
+ /* Multi-line clamp. Modern `attr(data-truncate type(<number>))` works in
299
+ Chromium 133+ but doesn't ship in every WebKit-based engine yet, so
300
+ enumerate the common counts (2..6) explicitly — covers card / list
301
+ description copy without needing engine fallback gymnastics. */
302
+ [data-truncate]:not([data-truncate='1']) {
303
+ display: -webkit-box;
304
+ -webkit-box-orient: vertical;
305
+ overflow: hidden;
306
+ }
307
+ [data-truncate='2'] { -webkit-line-clamp: 2; line-clamp: 2; }
308
+ [data-truncate='3'] { -webkit-line-clamp: 3; line-clamp: 3; }
309
+ [data-truncate='4'] { -webkit-line-clamp: 4; line-clamp: 4; }
310
+ [data-truncate='5'] { -webkit-line-clamp: 5; line-clamp: 5; }
311
+ [data-truncate='6'] { -webkit-line-clamp: 6; line-clamp: 6; }
312
+ }
@@ -1,27 +0,0 @@
1
-
2
- > @kouji-ui/core@0.0.4 build /home/runner/work/kouji-ui/kouji-ui/packages/core
3
- > ng-packagr -p ng-package.json
4
-
5
- Building Angular Package
6
-
7
- ------------------------------------------------------------------------------
8
- - Compiling with Angular sources in partial compilation mode.
9
- Building entry point '@kouji-ui/core'
10
- ------------------------------------------------------------------------------
11
- ✔ Compiling with Angular sources in partial compilation mode.
12
- ✔ Generating FESM and DTS bundles
13
- - Copying assets
14
- ✔ Copying assets
15
- - Writing package manifest
16
- ℹ Removing scripts section in package.json as it's considered a potential security vulnerability.
17
- ✔ Writing package manifest
18
- ✔ Built @kouji-ui/core
19
-
20
- ------------------------------------------------------------------------------
21
- Built Angular Package
22
- - from: /home/runner/work/kouji-ui/kouji-ui/packages/core
23
- - to: /home/runner/work/kouji-ui/kouji-ui/dist/kj-core
24
- ------------------------------------------------------------------------------
25
-
26
- Build at: 2026-05-04T22:25:26.490Z - Time: 5957ms
27
-
package/CHANGELOG.md DELETED
@@ -1,21 +0,0 @@
1
- # @kouji-ui/core
2
-
3
- ## 0.0.4
4
-
5
- ### Patch Changes
6
-
7
- - 202d9de: CI fix: husky pre-push hook now skips in CI environments (`$CI` or `$GITHUB_ACTIONS` set). Prevents the Changesets action's automated push from being blocked by the changeset-status gate. No runtime effect on the published package.
8
- - dc0fe0f: Test-only fix: rewrite `KjToastService` test suite to use `TestBed.inject()` so `inject(KJ_TOAST_STRATEGY)` resolves through Angular DI (was crashing with `new KjToastService()` outside an injection context). No runtime change.
9
- - d6b40e1: Internal lint cleanup — replace `any` casts in select option storage with typed `HTMLElement & { __kjOptionValue?: unknown }` and convert ternary statement to if/else in overlay toggle. No public API or behavior change.
10
-
11
- ## 0.0.3
12
-
13
- ### Patch Changes
14
-
15
- - c177d1e: Rewrite README with real install/usage docs, primitive index, and design principles. Replaces the Angular CLI scaffold placeholder.
16
-
17
- ## 0.0.2
18
-
19
- ### Patch Changes
20
-
21
- - 1813d21: Initial publish — headless Angular 21 UI primitives over CDK with WCAG 2.1 AAA semantics and zero CSS.
package/ng-package.json DELETED
@@ -1,7 +0,0 @@
1
- {
2
- "$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
3
- "dest": "../../dist/kj-core",
4
- "lib": {
5
- "entryFile": "src/public-api.ts"
6
- }
7
- }
package/src/a11y/.gitkeep DELETED
File without changes
@@ -1,55 +0,0 @@
1
- import { render } from '@testing-library/angular';
2
- import { axe, toHaveNoViolations } from 'jest-axe';
3
- import { KjAriaDescribedBy } from './aria-describedby';
4
-
5
- expect.extend(toHaveNoViolations);
6
-
7
- describe('KjAriaDescribedBy', () => {
8
- it('sets aria-describedby from a single id string', async () => {
9
- const { container } = await render(
10
- `<input kjAriaDescribedBy [kjDescribedBy]="'hint-1'" />
11
- <span id="hint-1">This is a hint</span>`,
12
- { imports: [KjAriaDescribedBy] },
13
- );
14
- expect(container.querySelector('input')).toHaveAttribute('aria-describedby', 'hint-1');
15
- });
16
-
17
- it('sets aria-describedby from multiple ids', async () => {
18
- const { container } = await render(
19
- `<input kjAriaDescribedBy [kjDescribedBy]="['hint-1', 'error-1']" />
20
- <span id="hint-1">Hint</span>
21
- <span id="error-1">Error</span>`,
22
- { imports: [KjAriaDescribedBy] },
23
- );
24
- expect(container.querySelector('input')).toHaveAttribute(
25
- 'aria-describedby',
26
- 'hint-1 error-1',
27
- );
28
- });
29
-
30
- it('removes aria-describedby when given empty array', async () => {
31
- const { container } = await render(
32
- `<input kjAriaDescribedBy [kjDescribedBy]="[]" />`,
33
- { imports: [KjAriaDescribedBy] },
34
- );
35
- expect(container.querySelector('input')).not.toHaveAttribute('aria-describedby');
36
- });
37
-
38
- it('removes aria-describedby when given empty string', async () => {
39
- const { container } = await render(
40
- `<input kjAriaDescribedBy [kjDescribedBy]="''" />`,
41
- { imports: [KjAriaDescribedBy] },
42
- );
43
- expect(container.querySelector('input')).not.toHaveAttribute('aria-describedby');
44
- });
45
-
46
- it('passes axe accessibility audit', async () => {
47
- const { container } = await render(
48
- `<label for="f">Name</label>
49
- <input id="f" kjAriaDescribedBy [kjDescribedBy]="'hint'" />
50
- <span id="hint">Enter your full name</span>`,
51
- { imports: [KjAriaDescribedBy] },
52
- );
53
- expect(await axe(container)).toHaveNoViolations();
54
- });
55
- });
@@ -1,33 +0,0 @@
1
- import { Directive, computed, input } from '@angular/core';
2
-
3
- /**
4
- * Wires `aria-describedby` to one or more element IDs.
5
- * Accepts a single ID string or an array of ID strings.
6
- * Removes the attribute automatically when the value is empty.
7
- *
8
- * @example
9
- * ```html
10
- * <input kjAriaDescribedBy [kjDescribedBy]="['hint-id', errorId()]" />
11
- * <span id="hint-id">Format: DD/MM/YYYY</span>
12
- * ```
13
- * @category Core/Accessibility
14
- */
15
- @Directive({
16
- selector: '[kjAriaDescribedBy]',
17
- standalone: true,
18
- host: {
19
- '[attr.aria-describedby]': 'ariaDescribedBy()',
20
- },
21
- })
22
- export class KjAriaDescribedBy {
23
- /** One or more element IDs to reference via `aria-describedby`. */
24
- kjDescribedBy = input<string | string[]>('');
25
-
26
- /** @internal */
27
- readonly ariaDescribedBy = computed(() => {
28
- const val = this.kjDescribedBy();
29
- const ids = Array.isArray(val) ? val : [val];
30
- const filtered = ids.filter(Boolean);
31
- return filtered.length ? filtered.join(' ') : null;
32
- });
33
- }
@@ -1,86 +0,0 @@
1
- import { render, fireEvent } from '@testing-library/angular';
2
- import { axe, toHaveNoViolations } from 'jest-axe';
3
- import { KjFocusTrap } from './focus-trap';
4
-
5
- expect.extend(toHaveNoViolations);
6
-
7
- describe('KjFocusTrap', () => {
8
- it('renders without error when enabled', async () => {
9
- const { container } = await render(
10
- `<div kjFocusTrap [kjFocusTrapEnabled]="true">
11
- <button>First</button>
12
- <button>Last</button>
13
- </div>`,
14
- { imports: [KjFocusTrap] },
15
- );
16
- expect(container.querySelector('[kjFocusTrap]')).toBeInTheDocument();
17
- });
18
-
19
- it('renders without error when disabled', async () => {
20
- const { container } = await render(
21
- `<div kjFocusTrap [kjFocusTrapEnabled]="false">
22
- <button>First</button>
23
- </div>`,
24
- { imports: [KjFocusTrap] },
25
- );
26
- expect(container.querySelector('[kjFocusTrap]')).toBeInTheDocument();
27
- });
28
-
29
- it('wraps focus from last to first on Tab when enabled', async () => {
30
- const { container } = await render(
31
- `<div kjFocusTrap [kjFocusTrapEnabled]="true">
32
- <button>First</button>
33
- <button>Last</button>
34
- </div>`,
35
- { imports: [KjFocusTrap] },
36
- );
37
- const buttons = container.querySelectorAll('button');
38
- const last = buttons[buttons.length - 1] as HTMLElement;
39
- last.focus();
40
- fireEvent.keyDown(document, { key: 'Tab', shiftKey: false });
41
- expect(document.activeElement).toBe(buttons[0]);
42
- });
43
-
44
- it('wraps focus from first to last on Shift+Tab when enabled', async () => {
45
- const { container } = await render(
46
- `<div kjFocusTrap [kjFocusTrapEnabled]="true">
47
- <button>First</button>
48
- <button>Last</button>
49
- </div>`,
50
- { imports: [KjFocusTrap] },
51
- );
52
- const buttons = container.querySelectorAll('button');
53
- const first = buttons[0] as HTMLElement;
54
- first.focus();
55
- fireEvent.keyDown(document, { key: 'Tab', shiftKey: true });
56
- expect(document.activeElement).toBe(buttons[buttons.length - 1]);
57
- });
58
-
59
- it('does not trap focus when disabled', async () => {
60
- const { container } = await render(
61
- `<div kjFocusTrap [kjFocusTrapEnabled]="false">
62
- <button>First</button>
63
- <button>Last</button>
64
- </div>`,
65
- { imports: [KjFocusTrap] },
66
- );
67
- const buttons = container.querySelectorAll('button');
68
- const last = buttons[buttons.length - 1] as HTMLElement;
69
- last.focus();
70
- // Tab should not redirect focus — no focus change from the handler.
71
- const activeBeforeTab = document.activeElement;
72
- fireEvent.keyDown(document, { key: 'Tab', shiftKey: false });
73
- // When disabled, the handler does nothing, so focus stays on last.
74
- expect(document.activeElement).toBe(activeBeforeTab);
75
- });
76
-
77
- it('passes axe accessibility audit', async () => {
78
- const { container } = await render(
79
- `<div role="dialog" aria-label="Test dialog" kjFocusTrap [kjFocusTrapEnabled]="true">
80
- <button>Action</button>
81
- </div>`,
82
- { imports: [KjFocusTrap] },
83
- );
84
- expect(await axe(container)).toHaveNoViolations();
85
- });
86
- });
@@ -1,70 +0,0 @@
1
- import { Directive, DestroyRef, ElementRef, afterNextRender, inject, input } from '@angular/core';
2
- import { PLATFORM_ID } from '@angular/core';
3
- import { isPlatformBrowser } from '@angular/common';
4
-
5
- const FOCUSABLE = [
6
- 'a[href]', 'button:not([disabled])', 'input:not([disabled])',
7
- 'select:not([disabled])', 'textarea:not([disabled])',
8
- '[tabindex]:not([tabindex="-1"])', '[contenteditable="true"]',
9
- ].join(',');
10
-
11
- /**
12
- * Traps keyboard focus within the host element using a native Tab-key interceptor.
13
- * Designed for modal dialogs, drawers, and other overlay patterns.
14
- *
15
- * @example
16
- * ```html
17
- * <div role="dialog" kjFocusTrap [kjFocusTrapEnabled]="isOpen()">
18
- * <button>Action</button>
19
- * </div>
20
- * ```
21
- * @category Core/Accessibility
22
- */
23
- @Directive({
24
- selector: '[kjFocusTrap]',
25
- standalone: true,
26
- })
27
- export class KjFocusTrap {
28
- private readonly el = inject(ElementRef<HTMLElement>);
29
- private readonly destroyRef = inject(DestroyRef);
30
- private readonly platformId = inject(PLATFORM_ID);
31
-
32
- /** Whether the focus trap is active. Set to true when the overlay is open. */
33
- kjFocusTrapEnabled = input<boolean>(false);
34
-
35
- constructor() {
36
- afterNextRender(() => {
37
- if (!isPlatformBrowser(this.platformId)) return;
38
-
39
- const handleKeydown = (e: KeyboardEvent) => {
40
- if (!this.kjFocusTrapEnabled() || e.key !== 'Tab') return;
41
- const focusable = (Array.from(
42
- this.el.nativeElement.querySelectorAll(FOCUSABLE)
43
- ) as HTMLElement[]).filter(el => !el.closest('[hidden]') && getComputedStyle(el).display !== 'none');
44
-
45
- if (!focusable.length) { e.preventDefault(); return; }
46
- const first = focusable[0];
47
- const last = focusable[focusable.length - 1];
48
-
49
- if (e.shiftKey) {
50
- if (document.activeElement === first || !this.el.nativeElement.contains(document.activeElement)) {
51
- e.preventDefault(); last.focus();
52
- }
53
- } else {
54
- if (document.activeElement === last || !this.el.nativeElement.contains(document.activeElement)) {
55
- e.preventDefault(); first.focus();
56
- }
57
- }
58
- };
59
-
60
- document.addEventListener('keydown', handleKeydown);
61
- this.destroyRef.onDestroy(() => document.removeEventListener('keydown', handleKeydown));
62
- });
63
- }
64
-
65
- /** Focuses the first tabbable element inside the trap. */
66
- focusFirst(): void {
67
- const el = this.el.nativeElement.querySelector(FOCUSABLE) as HTMLElement | null;
68
- el?.focus();
69
- }
70
- }
package/src/a11y/index.ts DELETED
@@ -1,9 +0,0 @@
1
- export { KjAriaDescribedBy } from './aria-describedby';
2
- export { KjFocusTrap } from './focus-trap';
3
- export { KjLiveRegion, type KjLivePoliteness } from './live-region';
4
- export {
5
- KjRovingTabindex,
6
- KjRovingTabindexItemDirective,
7
- KJ_ROVING_TABINDEX,
8
- } from './roving-tabindex';
9
- export { KjVisuallyHidden } from './visually-hidden';
@@ -1,58 +0,0 @@
1
- import { render } from '@testing-library/angular';
2
- import { axe, toHaveNoViolations } from 'jest-axe';
3
- import { vi } from 'vitest';
4
- import { KjLiveRegion } from './live-region';
5
-
6
- expect.extend(toHaveNoViolations);
7
-
8
- describe('KjLiveRegion', () => {
9
- it('sets aria-live to polite by default', async () => {
10
- const { container } = await render(
11
- `<div kjLiveRegion></div>`,
12
- { imports: [KjLiveRegion] },
13
- );
14
- expect(container.querySelector('div')).toHaveAttribute('aria-live', 'polite');
15
- });
16
-
17
- it('sets aria-live to assertive when specified', async () => {
18
- const { container } = await render(
19
- `<div kjLiveRegion [kjPoliteness]="'assertive'"></div>`,
20
- { imports: [KjLiveRegion] },
21
- );
22
- expect(container.querySelector('div')).toHaveAttribute('aria-live', 'assertive');
23
- });
24
-
25
- it('sets aria-atomic to true', async () => {
26
- const { container } = await render(
27
- `<div kjLiveRegion></div>`,
28
- { imports: [KjLiveRegion] },
29
- );
30
- expect(container.querySelector('div')).toHaveAttribute('aria-atomic', 'true');
31
- });
32
-
33
- it('announces a message via DOM text content after a brief timeout', async () => {
34
- vi.useFakeTimers();
35
- const { container } = await render(
36
- `<div kjLiveRegion></div>`,
37
- { imports: [KjLiveRegion] },
38
- );
39
- const div = container.querySelector('div')!;
40
-
41
- // Simulate the announce mechanism: clear, then set after 50ms.
42
- div.textContent = '';
43
- setTimeout(() => { div.textContent = 'Item saved'; }, 50);
44
-
45
- vi.advanceTimersByTime(50);
46
- expect(div.textContent).toBe('Item saved');
47
-
48
- vi.useRealTimers();
49
- });
50
-
51
- it('passes axe accessibility audit', async () => {
52
- const { container } = await render(
53
- `<div kjLiveRegion></div>`,
54
- { imports: [KjLiveRegion] },
55
- );
56
- expect(await axe(container)).toHaveNoViolations();
57
- });
58
- });