@malamute/ai-rules 1.0.0 → 1.2.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/README.md +270 -121
- package/bin/cli.js +5 -2
- package/configs/_shared/.claude/rules/conventions/documentation.md +324 -0
- package/configs/_shared/.claude/rules/conventions/git.md +265 -0
- package/configs/_shared/.claude/rules/{performance.md → conventions/performance.md} +1 -1
- package/configs/_shared/.claude/rules/conventions/principles.md +334 -0
- package/configs/_shared/.claude/rules/devops/ci-cd.md +262 -0
- package/configs/_shared/.claude/rules/devops/docker.md +275 -0
- package/configs/_shared/.claude/rules/devops/nx.md +194 -0
- package/configs/_shared/.claude/rules/domain/backend/api-design.md +203 -0
- package/configs/_shared/.claude/rules/lang/csharp/async.md +220 -0
- package/configs/_shared/.claude/rules/lang/csharp/csharp.md +314 -0
- package/configs/_shared/.claude/rules/lang/csharp/linq.md +210 -0
- package/configs/_shared/.claude/rules/lang/python/async.md +337 -0
- package/configs/_shared/.claude/rules/lang/python/celery.md +476 -0
- package/configs/_shared/.claude/rules/lang/python/config.md +339 -0
- package/configs/{python/.claude/rules → _shared/.claude/rules/lang/python}/database/sqlalchemy.md +6 -1
- package/configs/_shared/.claude/rules/lang/python/deployment.md +523 -0
- package/configs/_shared/.claude/rules/lang/python/error-handling.md +330 -0
- package/configs/_shared/.claude/rules/lang/python/migrations.md +421 -0
- package/configs/_shared/.claude/rules/lang/python/python.md +172 -0
- package/configs/_shared/.claude/rules/lang/python/repository.md +383 -0
- package/configs/{python/.claude/rules → _shared/.claude/rules/lang/python}/testing.md +2 -69
- package/configs/_shared/.claude/rules/lang/typescript/async.md +447 -0
- package/configs/_shared/.claude/rules/lang/typescript/generics.md +356 -0
- package/configs/_shared/.claude/rules/lang/typescript/typescript.md +212 -0
- package/configs/_shared/.claude/rules/quality/error-handling.md +48 -0
- package/configs/_shared/.claude/rules/quality/logging.md +45 -0
- package/configs/_shared/.claude/rules/quality/observability.md +240 -0
- package/configs/_shared/.claude/rules/quality/testing-patterns.md +65 -0
- package/configs/_shared/.claude/rules/security/secrets-management.md +222 -0
- package/configs/_shared/.claude/skills/analysis/explore/SKILL.md +257 -0
- package/configs/_shared/.claude/skills/analysis/security-audit/SKILL.md +184 -0
- package/configs/_shared/.claude/skills/dev/api-endpoint/SKILL.md +126 -0
- package/configs/_shared/.claude/{commands/generate-tests.md → skills/dev/generate-tests/SKILL.md} +6 -0
- package/configs/_shared/.claude/{commands/fix-issue.md → skills/git/fix-issue/SKILL.md} +6 -0
- package/configs/_shared/.claude/{commands/review-pr.md → skills/git/review-pr/SKILL.md} +6 -0
- package/configs/_shared/.claude/skills/infra/deploy/SKILL.md +139 -0
- package/configs/_shared/.claude/skills/infra/docker/SKILL.md +95 -0
- package/configs/_shared/.claude/skills/infra/migration/SKILL.md +158 -0
- package/configs/_shared/.claude/skills/nx/nx-affected/SKILL.md +72 -0
- package/configs/_shared/.claude/skills/nx/nx-lib/SKILL.md +375 -0
- package/configs/_shared/CLAUDE.md +52 -149
- package/configs/angular/.claude/rules/{components.md → core/components.md} +69 -15
- package/configs/angular/.claude/rules/core/resource.md +285 -0
- package/configs/angular/.claude/rules/core/signals.md +323 -0
- package/configs/angular/.claude/rules/http.md +338 -0
- package/configs/angular/.claude/rules/routing.md +291 -0
- package/configs/angular/.claude/rules/ssr.md +312 -0
- package/configs/angular/.claude/rules/state/signal-store.md +408 -0
- package/configs/angular/.claude/rules/{state.md → state/state.md} +2 -2
- package/configs/angular/.claude/rules/testing.md +7 -7
- package/configs/angular/.claude/rules/ui/aria.md +422 -0
- package/configs/angular/.claude/rules/ui/forms.md +424 -0
- package/configs/angular/.claude/rules/ui/pipes-directives.md +335 -0
- package/configs/angular/.claude/settings.json +1 -0
- package/configs/angular/.claude/skills/ngrx-slice/SKILL.md +362 -0
- package/configs/angular/.claude/skills/signal-store/SKILL.md +445 -0
- package/configs/angular/CLAUDE.md +24 -216
- package/configs/dotnet/.claude/rules/background-services.md +552 -0
- package/configs/dotnet/.claude/rules/configuration.md +426 -0
- package/configs/dotnet/.claude/rules/ddd.md +447 -0
- package/configs/dotnet/.claude/rules/dependency-injection.md +343 -0
- package/configs/dotnet/.claude/rules/mediatr.md +320 -0
- package/configs/dotnet/.claude/rules/middleware.md +489 -0
- package/configs/dotnet/.claude/rules/result-pattern.md +363 -0
- package/configs/dotnet/.claude/rules/validation.md +388 -0
- package/configs/dotnet/.claude/settings.json +21 -3
- package/configs/dotnet/CLAUDE.md +53 -286
- package/configs/fastapi/.claude/rules/background-tasks.md +254 -0
- package/configs/fastapi/.claude/rules/dependencies.md +170 -0
- package/configs/{python → fastapi}/.claude/rules/fastapi.md +61 -1
- package/configs/fastapi/.claude/rules/lifespan.md +274 -0
- package/configs/fastapi/.claude/rules/middleware.md +229 -0
- package/configs/fastapi/.claude/rules/pydantic.md +433 -0
- package/configs/fastapi/.claude/rules/responses.md +251 -0
- package/configs/fastapi/.claude/rules/routers.md +202 -0
- package/configs/fastapi/.claude/rules/security.md +222 -0
- package/configs/fastapi/.claude/rules/testing.md +251 -0
- package/configs/fastapi/.claude/rules/websockets.md +298 -0
- package/configs/fastapi/.claude/settings.json +33 -0
- package/configs/fastapi/CLAUDE.md +144 -0
- package/configs/flask/.claude/rules/blueprints.md +208 -0
- package/configs/flask/.claude/rules/cli.md +285 -0
- package/configs/flask/.claude/rules/configuration.md +281 -0
- package/configs/flask/.claude/rules/context.md +238 -0
- package/configs/flask/.claude/rules/error-handlers.md +278 -0
- package/configs/flask/.claude/rules/extensions.md +278 -0
- package/configs/flask/.claude/rules/flask.md +171 -0
- package/configs/flask/.claude/rules/marshmallow.md +206 -0
- package/configs/flask/.claude/rules/security.md +267 -0
- package/configs/flask/.claude/rules/testing.md +284 -0
- package/configs/flask/.claude/settings.json +33 -0
- package/configs/flask/CLAUDE.md +166 -0
- package/configs/nestjs/.claude/rules/common-patterns.md +300 -0
- package/configs/nestjs/.claude/rules/filters.md +376 -0
- package/configs/nestjs/.claude/rules/interceptors.md +317 -0
- package/configs/nestjs/.claude/rules/middleware.md +321 -0
- package/configs/nestjs/.claude/rules/modules.md +26 -0
- package/configs/nestjs/.claude/rules/pipes.md +351 -0
- package/configs/nestjs/.claude/rules/websockets.md +451 -0
- package/configs/nestjs/.claude/settings.json +16 -2
- package/configs/nestjs/CLAUDE.md +57 -215
- package/configs/nextjs/.claude/rules/api-routes.md +358 -0
- package/configs/nextjs/.claude/rules/authentication.md +355 -0
- package/configs/nextjs/.claude/rules/components.md +52 -0
- package/configs/nextjs/.claude/rules/data-fetching.md +249 -0
- package/configs/nextjs/.claude/rules/database.md +400 -0
- package/configs/nextjs/.claude/rules/middleware.md +303 -0
- package/configs/nextjs/.claude/rules/routing.md +324 -0
- package/configs/nextjs/.claude/rules/seo.md +350 -0
- package/configs/nextjs/.claude/rules/server-actions.md +353 -0
- package/configs/nextjs/.claude/rules/state/zustand.md +6 -6
- package/configs/nextjs/.claude/settings.json +5 -0
- package/configs/nextjs/CLAUDE.md +69 -331
- package/package.json +23 -9
- package/src/cli.js +220 -0
- package/src/config.js +29 -0
- package/src/index.js +13 -0
- package/src/installer.js +361 -0
- package/src/merge.js +116 -0
- package/src/tech-config.json +29 -0
- package/src/utils.js +96 -0
- package/configs/python/.claude/rules/flask.md +0 -332
- package/configs/python/.claude/settings.json +0 -18
- package/configs/python/CLAUDE.md +0 -273
- package/src/install.js +0 -315
- /package/configs/_shared/.claude/rules/{accessibility.md → domain/frontend/accessibility.md} +0 -0
- /package/configs/_shared/.claude/rules/{security.md → security/security.md} +0 -0
- /package/configs/_shared/.claude/skills/{debug → dev/debug}/SKILL.md +0 -0
- /package/configs/_shared/.claude/skills/{learning → dev/learning}/SKILL.md +0 -0
- /package/configs/_shared/.claude/skills/{spec → dev/spec}/SKILL.md +0 -0
- /package/configs/_shared/.claude/skills/{review → git/review}/SKILL.md +0 -0
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "**/ui/**/*.component.ts"
|
|
4
|
+
- "**/ui/**/*.component.html"
|
|
5
|
+
- "**/shared/ui/**/*.ts"
|
|
6
|
+
- "**/*-dialog.component.ts"
|
|
7
|
+
- "**/*-modal.component.ts"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Angular ARIA (Accessibility)
|
|
11
|
+
|
|
12
|
+
The `@angular/cdk-experimental/ui` package provides headless, accessible components.
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @angular/cdk-experimental
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Listbox
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import { CdkListbox, CdkOption } from '@angular/cdk-experimental/ui';
|
|
24
|
+
|
|
25
|
+
@Component({
|
|
26
|
+
selector: 'app-color-picker',
|
|
27
|
+
imports: [CdkListbox, CdkOption],
|
|
28
|
+
template: `
|
|
29
|
+
<ul cdkListbox [(value)]="selectedColor" aria-label="Choose a color">
|
|
30
|
+
@for (color of colors; track color.value) {
|
|
31
|
+
<li [cdkOption]="color.value">
|
|
32
|
+
{{ color.label }}
|
|
33
|
+
</li>
|
|
34
|
+
}
|
|
35
|
+
</ul>
|
|
36
|
+
`,
|
|
37
|
+
})
|
|
38
|
+
export class ColorPickerComponent {
|
|
39
|
+
protected readonly colors = [
|
|
40
|
+
{ value: 'red', label: 'Red' },
|
|
41
|
+
{ value: 'green', label: 'Green' },
|
|
42
|
+
{ value: 'blue', label: 'Blue' },
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
protected readonly selectedColor = model<string>('red');
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Tabs
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
import { CdkTabs, CdkTabList, CdkTab, CdkTabPanel } from '@angular/cdk-experimental/ui';
|
|
53
|
+
|
|
54
|
+
@Component({
|
|
55
|
+
selector: 'app-settings-tabs',
|
|
56
|
+
imports: [CdkTabs, CdkTabList, CdkTab, CdkTabPanel],
|
|
57
|
+
template: `
|
|
58
|
+
<div cdkTabs>
|
|
59
|
+
<div cdkTabList aria-label="Settings sections">
|
|
60
|
+
<button cdkTab="profile">Profile</button>
|
|
61
|
+
<button cdkTab="security">Security</button>
|
|
62
|
+
<button cdkTab="notifications">Notifications</button>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<div cdkTabPanel="profile">
|
|
66
|
+
<app-profile-settings />
|
|
67
|
+
</div>
|
|
68
|
+
<div cdkTabPanel="security">
|
|
69
|
+
<app-security-settings />
|
|
70
|
+
</div>
|
|
71
|
+
<div cdkTabPanel="notifications">
|
|
72
|
+
<app-notification-settings />
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
`,
|
|
76
|
+
})
|
|
77
|
+
export class SettingsTabsComponent { }
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Disclosure (Accordion)
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
import { CdkDisclosure, CdkDisclosureTrigger, CdkDisclosureContent } from '@angular/cdk-experimental/ui';
|
|
84
|
+
|
|
85
|
+
@Component({
|
|
86
|
+
selector: 'app-faq',
|
|
87
|
+
imports: [CdkDisclosure, CdkDisclosureTrigger, CdkDisclosureContent],
|
|
88
|
+
template: `
|
|
89
|
+
@for (item of faqItems; track item.id) {
|
|
90
|
+
<div cdkDisclosure>
|
|
91
|
+
<button cdkDisclosureTrigger>
|
|
92
|
+
{{ item.question }}
|
|
93
|
+
</button>
|
|
94
|
+
<div cdkDisclosureContent>
|
|
95
|
+
{{ item.answer }}
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
}
|
|
99
|
+
`,
|
|
100
|
+
})
|
|
101
|
+
export class FaqComponent {
|
|
102
|
+
protected readonly faqItems = [
|
|
103
|
+
{ id: 1, question: 'How do I reset my password?', answer: '...' },
|
|
104
|
+
{ id: 2, question: 'Where can I find my invoices?', answer: '...' },
|
|
105
|
+
];
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Dialog
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
import { CdkDialog, CdkDialogTrigger, CdkDialogContent } from '@angular/cdk-experimental/ui';
|
|
113
|
+
|
|
114
|
+
@Component({
|
|
115
|
+
selector: 'app-confirm-dialog',
|
|
116
|
+
imports: [CdkDialog, CdkDialogTrigger, CdkDialogContent],
|
|
117
|
+
template: `
|
|
118
|
+
<div cdkDialog #dialog>
|
|
119
|
+
<button cdkDialogTrigger>Delete Item</button>
|
|
120
|
+
|
|
121
|
+
<div cdkDialogContent role="alertdialog" aria-labelledby="dialog-title">
|
|
122
|
+
<h2 id="dialog-title">Confirm Deletion</h2>
|
|
123
|
+
<p>Are you sure you want to delete this item?</p>
|
|
124
|
+
<div class="actions">
|
|
125
|
+
<button (click)="dialog.close()">Cancel</button>
|
|
126
|
+
<button (click)="onConfirm(); dialog.close()">Delete</button>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
`,
|
|
131
|
+
})
|
|
132
|
+
export class ConfirmDialogComponent {
|
|
133
|
+
public readonly confirmed = output<void>();
|
|
134
|
+
|
|
135
|
+
protected onConfirm(): void {
|
|
136
|
+
this.confirmed.emit();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Custom Accessible Components
|
|
142
|
+
|
|
143
|
+
### Focus Management
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
import { FocusMonitor, FocusTrap, FocusTrapFactory } from '@angular/cdk/a11y';
|
|
147
|
+
|
|
148
|
+
@Component({
|
|
149
|
+
selector: 'app-modal',
|
|
150
|
+
template: `
|
|
151
|
+
<div class="modal-backdrop" (click)="close()"></div>
|
|
152
|
+
<div
|
|
153
|
+
class="modal"
|
|
154
|
+
role="dialog"
|
|
155
|
+
aria-modal="true"
|
|
156
|
+
[attr.aria-labelledby]="titleId"
|
|
157
|
+
#modalElement
|
|
158
|
+
>
|
|
159
|
+
<h2 [id]="titleId">{{ title() }}</h2>
|
|
160
|
+
<ng-content />
|
|
161
|
+
</div>
|
|
162
|
+
`,
|
|
163
|
+
})
|
|
164
|
+
export class ModalComponent implements AfterViewInit, OnDestroy {
|
|
165
|
+
private readonly focusTrapFactory = inject(FocusTrapFactory);
|
|
166
|
+
private readonly elementRef = inject(ElementRef);
|
|
167
|
+
|
|
168
|
+
public readonly title = input.required<string>();
|
|
169
|
+
public readonly closed = output<void>();
|
|
170
|
+
|
|
171
|
+
protected readonly titleId = `modal-title-${crypto.randomUUID()}`;
|
|
172
|
+
|
|
173
|
+
private focusTrap?: FocusTrap;
|
|
174
|
+
private previouslyFocusedElement?: HTMLElement;
|
|
175
|
+
|
|
176
|
+
constructor() {
|
|
177
|
+
afterNextRender(() => {
|
|
178
|
+
this.previouslyFocusedElement = document.activeElement as HTMLElement;
|
|
179
|
+
this.focusTrap = this.focusTrapFactory.create(this.elementRef.nativeElement);
|
|
180
|
+
this.focusTrap.focusInitialElement();
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
ngOnDestroy(): void {
|
|
185
|
+
this.focusTrap?.destroy();
|
|
186
|
+
this.previouslyFocusedElement?.focus();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
protected close(): void {
|
|
190
|
+
this.closed.emit();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Live Announcements
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
import { LiveAnnouncer } from '@angular/cdk/a11y';
|
|
199
|
+
|
|
200
|
+
@Component({ ... })
|
|
201
|
+
export class NotificationComponent {
|
|
202
|
+
private readonly liveAnnouncer = inject(LiveAnnouncer);
|
|
203
|
+
|
|
204
|
+
public async showSuccess(message: string): Promise<void> {
|
|
205
|
+
// Announces to screen readers
|
|
206
|
+
await this.liveAnnouncer.announce(message, 'polite');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
public async showError(message: string): Promise<void> {
|
|
210
|
+
// 'assertive' interrupts current speech
|
|
211
|
+
await this.liveAnnouncer.announce(message, 'assertive');
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Keyboard Navigation
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
import { ListKeyManager } from '@angular/cdk/a11y';
|
|
220
|
+
|
|
221
|
+
@Component({
|
|
222
|
+
selector: 'app-menu',
|
|
223
|
+
template: `
|
|
224
|
+
<ul
|
|
225
|
+
role="menu"
|
|
226
|
+
(keydown)="onKeydown($event)"
|
|
227
|
+
#menuElement
|
|
228
|
+
>
|
|
229
|
+
@for (item of items(); track item.id) {
|
|
230
|
+
<li
|
|
231
|
+
role="menuitem"
|
|
232
|
+
[tabindex]="keyManager?.activeItem === item ? 0 : -1"
|
|
233
|
+
(click)="selectItem(item)"
|
|
234
|
+
>
|
|
235
|
+
{{ item.label }}
|
|
236
|
+
</li>
|
|
237
|
+
}
|
|
238
|
+
</ul>
|
|
239
|
+
`,
|
|
240
|
+
})
|
|
241
|
+
export class MenuComponent implements AfterViewInit {
|
|
242
|
+
public readonly items = input.required<MenuItem[]>();
|
|
243
|
+
|
|
244
|
+
@ViewChildren('menuItem') menuItems!: QueryList<ElementRef>;
|
|
245
|
+
|
|
246
|
+
protected keyManager?: ListKeyManager<MenuItem>;
|
|
247
|
+
|
|
248
|
+
ngAfterViewInit(): void {
|
|
249
|
+
this.keyManager = new ListKeyManager(this.items())
|
|
250
|
+
.withWrap()
|
|
251
|
+
.withHomeAndEnd()
|
|
252
|
+
.withTypeAhead();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
protected onKeydown(event: KeyboardEvent): void {
|
|
256
|
+
this.keyManager?.onKeydown(event);
|
|
257
|
+
|
|
258
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
259
|
+
event.preventDefault();
|
|
260
|
+
const activeItem = this.keyManager?.activeItem;
|
|
261
|
+
if (activeItem) {
|
|
262
|
+
this.selectItem(activeItem);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
## ARIA Attributes
|
|
270
|
+
|
|
271
|
+
### Common Patterns
|
|
272
|
+
|
|
273
|
+
```html
|
|
274
|
+
<!-- Buttons -->
|
|
275
|
+
<button aria-label="Close dialog" aria-describedby="close-hint">
|
|
276
|
+
<app-icon name="close" />
|
|
277
|
+
</button>
|
|
278
|
+
<span id="close-hint" class="sr-only">Press Escape to close</span>
|
|
279
|
+
|
|
280
|
+
<!-- Loading states -->
|
|
281
|
+
<button [attr.aria-busy]="isLoading()" [attr.aria-disabled]="isLoading()">
|
|
282
|
+
@if (isLoading()) {
|
|
283
|
+
<app-spinner aria-hidden="true" />
|
|
284
|
+
<span class="sr-only">Loading...</span>
|
|
285
|
+
} @else {
|
|
286
|
+
Submit
|
|
287
|
+
}
|
|
288
|
+
</button>
|
|
289
|
+
|
|
290
|
+
<!-- Expandable -->
|
|
291
|
+
<button
|
|
292
|
+
[attr.aria-expanded]="isExpanded()"
|
|
293
|
+
[attr.aria-controls]="contentId"
|
|
294
|
+
>
|
|
295
|
+
Show Details
|
|
296
|
+
</button>
|
|
297
|
+
<div [id]="contentId" [hidden]="!isExpanded()">
|
|
298
|
+
<!-- Content -->
|
|
299
|
+
</div>
|
|
300
|
+
|
|
301
|
+
<!-- Form errors -->
|
|
302
|
+
<input
|
|
303
|
+
[attr.aria-invalid]="hasError()"
|
|
304
|
+
[attr.aria-describedby]="hasError() ? errorId : null"
|
|
305
|
+
/>
|
|
306
|
+
@if (hasError()) {
|
|
307
|
+
<span [id]="errorId" role="alert">
|
|
308
|
+
{{ errorMessage() }}
|
|
309
|
+
</span>
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
<!-- Progress -->
|
|
313
|
+
<div
|
|
314
|
+
role="progressbar"
|
|
315
|
+
[attr.aria-valuenow]="progress()"
|
|
316
|
+
aria-valuemin="0"
|
|
317
|
+
aria-valuemax="100"
|
|
318
|
+
[attr.aria-label]="'Upload progress: ' + progress() + '%'"
|
|
319
|
+
>
|
|
320
|
+
</div>
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### Screen Reader Only Content
|
|
324
|
+
|
|
325
|
+
```scss
|
|
326
|
+
// styles.scss
|
|
327
|
+
.sr-only {
|
|
328
|
+
position: absolute;
|
|
329
|
+
width: 1px;
|
|
330
|
+
height: 1px;
|
|
331
|
+
padding: 0;
|
|
332
|
+
margin: -1px;
|
|
333
|
+
overflow: hidden;
|
|
334
|
+
clip: rect(0, 0, 0, 0);
|
|
335
|
+
white-space: nowrap;
|
|
336
|
+
border: 0;
|
|
337
|
+
}
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
```html
|
|
341
|
+
<button>
|
|
342
|
+
<app-icon name="delete" aria-hidden="true" />
|
|
343
|
+
<span class="sr-only">Delete item</span>
|
|
344
|
+
</button>
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
## Anti-patterns
|
|
348
|
+
|
|
349
|
+
```html
|
|
350
|
+
<!-- BAD: No accessible name -->
|
|
351
|
+
<button><app-icon name="close" /></button>
|
|
352
|
+
|
|
353
|
+
<!-- GOOD: Add aria-label -->
|
|
354
|
+
<button aria-label="Close">
|
|
355
|
+
<app-icon name="close" aria-hidden="true" />
|
|
356
|
+
</button>
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
<!-- BAD: Using div as button -->
|
|
360
|
+
<div (click)="submit()">Submit</div>
|
|
361
|
+
|
|
362
|
+
<!-- GOOD: Use semantic elements -->
|
|
363
|
+
<button type="submit" (click)="submit()">Submit</button>
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
<!-- BAD: Missing form labels -->
|
|
367
|
+
<input type="email" placeholder="Email" />
|
|
368
|
+
|
|
369
|
+
<!-- GOOD: Proper labeling -->
|
|
370
|
+
<label for="email">Email</label>
|
|
371
|
+
<input id="email" type="email" />
|
|
372
|
+
|
|
373
|
+
<!-- Or with aria-label -->
|
|
374
|
+
<input type="email" aria-label="Email address" placeholder="email@example.com" />
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
<!-- BAD: Non-descriptive link text -->
|
|
378
|
+
<a href="/docs">Click here</a>
|
|
379
|
+
|
|
380
|
+
<!-- GOOD: Descriptive link text -->
|
|
381
|
+
<a href="/docs">View documentation</a>
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
<!-- BAD: Auto-playing content -->
|
|
385
|
+
<video autoplay>...</video>
|
|
386
|
+
|
|
387
|
+
<!-- GOOD: User-controlled -->
|
|
388
|
+
<video controls>...</video>
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
## Testing Accessibility
|
|
392
|
+
|
|
393
|
+
```typescript
|
|
394
|
+
// Using @angular-eslint for static analysis
|
|
395
|
+
// eslint.config.js
|
|
396
|
+
{
|
|
397
|
+
rules: {
|
|
398
|
+
'@angular-eslint/template/accessibility-alt-text': 'error',
|
|
399
|
+
'@angular-eslint/template/accessibility-elements-content': 'error',
|
|
400
|
+
'@angular-eslint/template/accessibility-label-has-associated-control': 'error',
|
|
401
|
+
'@angular-eslint/template/accessibility-valid-aria': 'error',
|
|
402
|
+
'@angular-eslint/template/click-events-have-key-events': 'error',
|
|
403
|
+
'@angular-eslint/template/mouse-events-have-key-events': 'error',
|
|
404
|
+
'@angular-eslint/template/no-autofocus': 'error',
|
|
405
|
+
'@angular-eslint/template/no-positive-tabindex': 'error',
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
```typescript
|
|
411
|
+
// E2E with Playwright accessibility testing
|
|
412
|
+
import { test, expect } from '@playwright/test';
|
|
413
|
+
import AxeBuilder from '@axe-core/playwright';
|
|
414
|
+
|
|
415
|
+
test('should have no accessibility violations', async ({ page }) => {
|
|
416
|
+
await page.goto('/');
|
|
417
|
+
|
|
418
|
+
const accessibilityScanResults = await new AxeBuilder({ page }).analyze();
|
|
419
|
+
|
|
420
|
+
expect(accessibilityScanResults.violations).toEqual([]);
|
|
421
|
+
});
|
|
422
|
+
```
|