Skip to content

Commit b01dfbd

Browse files
committed
docs: update Angular Aria guides
1 parent d89b707 commit b01dfbd

1 file changed

Lines changed: 166 additions & 36 deletions

File tree

skills/dev-skills/angular-developer/references/angular-aria.md

Lines changed: 166 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,11 @@ export class App {
4444
```html
4545
<div ngAccordionGroup [multiExpandable]="false">
4646
<div class="accordion-item">
47-
<button ngAccordionTrigger panelId="panel-1" class="accordion-header">
47+
<button ngAccordionTrigger [panel]="panel1" class="accordion-header">
4848
Section 1
4949
<span class="icon">▼</span>
5050
</button>
51-
<div ngAccordionPanel panelId="panel-1" class="accordion-panel">
51+
<div ngAccordionPanel #panel1="ngAccordionPanel" class="accordion-panel">
5252
<ng-template ngAccordionContent>
5353
<p>Lazy loaded content here.</p>
5454
</ng-template>
@@ -98,7 +98,7 @@ export class App {
9898

9999
```html
100100
<!-- horizontal or vertical orientation -->
101-
<ul ngListbox [(values)]="selectedItems" orientation="horizontal" [multi]="true">
101+
<ul ngListbox [(value)]="selectedItems" orientation="horizontal" [multi]="true">
102102
<li ngOption value="apple" class="option">Apple</li>
103103
<li ngOption value="banana" class="option">Banana</li>
104104
</ul>
@@ -132,28 +132,40 @@ These patterns combine `ngCombobox` with a popup containing an `ngListbox`.
132132
- **Select**: Readonly Combobox + single-select Listbox.
133133
- **Multiselect**: Readonly Combobox + multi-select Listbox.
134134

135-
**Usage:** The Combobox is a low-level primitive directive that synchronizes a text input with a popup, serving as the foundational logic for autocomplete, select, and multiselect patterns. Use it specifically for building custom filtering, unique selection requirements, or specialized input-to-popup coordination that deviates from standard, documented components.
135+
**Usage:** The Combobox is a low-level primitive directive that synchronizes a text input with a popup, serving as the foundational logic for autocomplete, select, and multiselect patterns. Use it specifically for building custom filtering, unique selection requirements, or specialized input-to-popup coordination.
136136

137137
**Imports:**
138138

139-
```
140-
import {Combobox, ComboboxInput, ComboboxPopupContainer} from '@angular/aria/combobox';
141-
import {Listbox, Option} from '@angular/aria/listbox';
139+
```ts
140+
import {Combobox, ComboboxPopup, ComboboxWidget} from '@angular/aria/combobox';
141+
import {Listbox, Option} from '@angular/aria/listbox';
142142
```
143143

144-
**Directives:** `ngCombobox`, `ngComboboxInput`, `ngComboboxPopupContainer`, `ngListbox`, `ngOption`.
144+
**Directives:** `ngCombobox`, `ngComboboxPopup`, `ngComboboxWidget`, `ngListbox`, `ngOption`.
145145

146146
```html
147-
<!-- Example: Standard Select -->
148-
<div ngCombobox [readonly]="true">
149-
<button ngComboboxInput class="select-trigger">
150-
{{ selectedValue() || 'Choose an option' }}
151-
</button>
152-
153-
<ng-template ngComboboxPopupContainer>
154-
<ul ngListbox [(values)]="selectedValue" class="dropdown-menu">
155-
<li ngOption value="option1">Option 1</li>
156-
<li ngOption value="option2">Option 2</li>
147+
<!-- Example: Standard Autocomplete / Select -->
148+
<div>
149+
<input
150+
ngCombobox
151+
#combobox="ngCombobox"
152+
[(value)]="searchString"
153+
[(expanded)]="isExpanded"
154+
placeholder="Choose an option"
155+
class="select-trigger"
156+
/>
157+
158+
<ng-template ngComboboxPopup [combobox]="combobox">
159+
<ul
160+
ngComboboxWidget
161+
ngListbox
162+
#listbox="ngListbox"
163+
[(value)]="selectedValue"
164+
[activeDescendant]="listbox.activeDescendant()"
165+
class="dropdown-menu"
166+
>
167+
<li ngOption value="option1" label="Option 1" class="option">Option 1</li>
168+
<li ngOption value="option2" label="Option 2" class="option">Option 2</li>
157169
</ul>
158170
</ng-template>
159171
</div>
@@ -186,22 +198,30 @@ For actions, commands, and context menus (not for form selection).
186198

187199
**Usage:** The Menubar is a high-level navigation pattern designed for building desktop-style application command bars (e.g., File, Edit, View) that stay persistent across an interface. It is best utilized for organizing complex commands into logical top-level categories with full horizontal keyboard support, but it should be avoided for simple standalone action lists or mobile-first layouts where horizontal space is constrained.
188200

189-
**Imports:** `import {MenuBar, Menu, MenuContent, MenuItem} from '@angular/aria/menu';`
201+
**Imports:** `import {MenuBar, Menu, MenuContent, MenuItem, MenuTrigger} from '@angular/aria/menu';`
190202

191-
**Directives:** `ngMenuBar`, `ngMenu`, `ngMenuItem`, `ngMenuTrigger`.
203+
**Directives:** `ngMenuBar`, `ngMenu`, `ngMenuItem`, `ngMenuTrigger`, `ngMenuContent`.
192204

193205
```html
194206
<!-- Menubar Example -->
195-
<ul ngMenuBar class="menubar">
196-
<li ngMenuItem value="file">
197-
<button ngMenuTrigger [menu]="fileMenu">File</button>
198-
</li>
199-
</ul>
207+
<div ngMenuBar class="menubar">
208+
<div ngMenuItem value="file" [submenu]="fileMenu" class="menubar-item">File</div>
209+
<div ngMenuItem value="edit" [submenu]="editMenu" class="menubar-item">Edit</div>
210+
</div>
200211

201-
<ul ngMenu #fileMenu="ngMenu" class="menu">
202-
<li ngMenuItem value="new">New</li>
203-
<li ngMenuItem value="open">Open</li>
204-
</ul>
212+
<div ngMenu #fileMenu="ngMenu" class="menu">
213+
<ng-template ngMenuContent>
214+
<div ngMenuItem value="new">New</div>
215+
<div ngMenuItem value="open">Open</div>
216+
</ng-template>
217+
</div>
218+
219+
<div ngMenu #editMenu="ngMenu" class="menu">
220+
<ng-template ngMenuContent>
221+
<div ngMenuItem value="cut">Cut</div>
222+
<div ngMenuItem value="copy">Copy</div>
223+
</ng-template>
224+
</div>
205225
```
206226

207227
**Styling Strategy:**
@@ -239,7 +259,7 @@ Layered content sections where only one panel is visible.
239259

240260
```html
241261
<div ngTabs>
242-
<ul ngTabList class="tab-list">
262+
<ul ngTabList [(selectedTab)]="selectedTabValue" class="tab-list">
243263
<li ngTab value="profile" class="tab-btn">Profile</li>
244264
<li ngTab value="security" class="tab-btn">Security</li>
245265
</ul>
@@ -328,14 +348,17 @@ Displays hierarchical data (file systems, nested nav).
328348

329349
**Imports:** `import {Tree, TreeItem, TreeItemGroup} from '@angular/aria/tree';`
330350

331-
**Directives:** `ngTree`, `ngTreeItem`, `ngTreeGroup`.
351+
**Directives:** `ngTree`, `ngTreeItem`, `ngTreeItemGroup`.
332352

333353
```html
334-
<ul ngTree class="tree">
335-
<li ngTreeItem value="documents">
354+
<ul ngTree #tree="ngTree" [(value)]="selectedValues" class="tree">
355+
<li ngTreeItem [parent]="tree" value="documents" #docsItem="ngTreeItem">
336356
<span class="tree-label">Documents</span>
337-
<ul ngTreeGroup class="tree-group">
338-
<li ngTreeItem value="resume">Resume.pdf</li>
357+
<ul role="group">
358+
<ng-template ngTreeItemGroup [ownedBy]="docsItem" #docsGroup="ngTreeItemGroup">
359+
<li ngTreeItem [parent]="docsGroup" value="resume">Resume.pdf</li>
360+
<li ngTreeItem [parent]="docsGroup" value="cover-letter">CoverLetter.pdf</li>
361+
</ng-template>
339362
</ul>
340363
</li>
341364
</ul>
@@ -403,8 +426,115 @@ Target `[aria-selected="true"]` for selected cells and `:focus-visible` for the
403426
}
404427
```
405428

429+
## 9. Testing with Component Harnesses
430+
431+
Angular Aria provides standard Component Harnesses (based on `@angular/cdk/testing`) to make unit testing clean, robust, and decoupled from DOM structural details.
432+
433+
**Imports:**
434+
435+
```ts
436+
import {HarnessLoader} from '@angular/cdk/testing';
437+
import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed';
438+
import {AccordionGroupHarness, AccordionHarness} from '@angular/aria/accordion/testing';
439+
import {ListboxHarness, ListboxOptionHarness} from '@angular/aria/listbox/testing';
440+
```
441+
442+
### Example: Testing an Accordion with Harnesses
443+
444+
```ts
445+
describe('MyAccordionComponent', () => {
446+
let fixture: ComponentFixture<MyAccordionComponent>;
447+
let loader: HarnessLoader;
448+
449+
beforeEach(async () => {
450+
fixture = TestBed.createComponent(MyAccordionComponent);
451+
fixture.detectChanges();
452+
loader = TestbedHarnessEnvironment.loader(fixture);
453+
});
454+
455+
it('should expand accordion on toggle', async () => {
456+
// Get the harness by its trigger title
457+
const accordion = await loader.getHarness(AccordionHarness.with({title: 'Section 1'}));
458+
459+
expect(await accordion.isExpanded()).toBeFalse();
460+
461+
// Expand the accordion
462+
await accordion.expand();
463+
464+
expect(await accordion.isExpanded()).toBeTrue();
465+
});
466+
});
467+
```
468+
469+
## 10. Integration with Signal Forms
470+
471+
Because Angular Aria directives leverage Angular's modern `model()` signals for managing interactive values, they integrate **out-of-the-box** with Angular's new Signal Forms (`@angular/forms/signals`).
472+
473+
The `[formField]` directive automatically detects directives like `ngCombobox` or `ngListbox` as custom form controls because they expose a `value` model.
474+
475+
**Imports:**
476+
477+
```ts
478+
import {form, schema, required} from '@angular/forms/signals';
479+
import {Combobox, ComboboxPopup, ComboboxWidget} from '@angular/aria/combobox';
480+
import {Listbox, Option} from '@angular/aria/listbox';
481+
```
482+
483+
### Example 1: Autocomplete Combobox inside a Form
484+
485+
Given a form model defined in your component:
486+
487+
```ts
488+
protected readonly citySignal = signal({name: '', city: ''});
489+
protected readonly myForm = form(this.citySignal, schema(f => {
490+
required(f.city);
491+
}));
492+
```
493+
494+
You bind it directly using `[formField]`:
495+
496+
```html
497+
<div>
498+
<label for="city-input">Choose your city:</label>
499+
<input
500+
id="city-input"
501+
ngCombobox
502+
#combobox="ngCombobox"
503+
[formField]="myForm.city"
504+
[(expanded)]="isExpanded"
505+
placeholder="Search cities..."
506+
/>
507+
508+
<ng-template ngComboboxPopup [combobox]="combobox">
509+
<ul
510+
ngComboboxWidget
511+
ngListbox
512+
#listbox="ngListbox"
513+
[(value)]="selectedValue"
514+
[activeDescendant]="listbox.activeDescendant()"
515+
class="dropdown-menu"
516+
>
517+
<li ngOption value="sfo" label="San Francisco">San Francisco</li>
518+
<li ngOption value="nyc" label="New York">New York</li>
519+
</ul>
520+
</ng-template>
521+
</div>
522+
```
523+
524+
### Example 2: Standalone Listbox (Multi-select) inside a Form
525+
526+
You can bind a multi-selectable Listbox directly to a form array:
527+
528+
```html
529+
<ul ngListbox [formField]="myForm.interests" [multi]="true" class="interest-list">
530+
<li ngOption value="sports">Sports</li>
531+
<li ngOption value="music">Music</li>
532+
<li ngOption value="tech">Technology</li>
533+
</ul>
534+
```
535+
406536
## General Rules for Agents
407537

408538
1. **Never use native HTML elements like `<select>`** when asked to implement these specific Aria patterns. Use the `ng*` directives.
409539
2. **Handle CSS manually**: Remember that `Angular Aria` does NOT provide styles. You must write the CSS, targeting the native ARIA attributes (`aria-expanded`, `aria-selected`, etc.) that the directives automatically toggle.
410-
3. **Lazy Loading**: Always use the provided structural directives (`ngAccordionContent`, `ngTabContent`) inside `ng-template` for heavy content panels to ensure they are lazily rendered.
540+
3. **Lazy Loading**: Always use the provided structural directives (`ngAccordionContent`, `ngTabContent`, `ngMenuContent`, `ngComboboxPopup`, `ngTreeItemGroup`) inside `ng-template` for heavy content panels or nested groups to ensure they are lazily rendered.

0 commit comments

Comments
 (0)