Skip to content

Commit 2998a6a

Browse files
committed
docs(cdk-experimental/listbox): add docs & examples
1 parent 1c7b940 commit 2998a6a

File tree

44 files changed

+1251
-176
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+1251
-176
lines changed
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
The `@angular/cdk/listbox` module provides directives to help create custom listbox interactions
2+
based on the [WAI ARIA listbox pattern][aria].
3+
4+
By using `@angular/cdk/listbox` you get all the expected behaviors for an accessible experience,
5+
including bidi layout support, keyboard interaction, and focus management. All directives apply
6+
their associated ARIA roles to their host element.
7+
8+
### Supported ARIA Roles
9+
10+
The directives in `@angular/cdk/listbox` set the appropriate roles on their host element.
11+
12+
| Directive | ARIA Role |
13+
|------------|-----------|
14+
| cdkOption | option |
15+
| cdkListbox | listbox |
16+
17+
### CSS Styles and Classes
18+
19+
The `@angular/cdk/listbox` is designed to be highly customizable to your needs. It therefore does not
20+
make any assumptions about how elements should be styled. You are expected to apply any required
21+
CSS styles, but the directives do apply CSS classes to make it easier for you to add custom styles.
22+
The available CSS classes are listed below, by directive.
23+
24+
| Directive | CSS Class | Applied... |
25+
|:---------------|--------------------|-------------------------|
26+
| cdkOption | .cdk-option | Always |
27+
| cdkOption | .cdk-option-active | If the option is active |
28+
| cdkListbox | .cdk-listbox | Always |
29+
30+
In addition to CSS classes, these directives add aria attributes that can be targeted in CSS.
31+
32+
| Directive | Attribute Selector | Applied... |
33+
|:-----------|----------------------------------|------------------------------------------|
34+
| cdkOption | \[aria-disabled="true"] | If the option is disabled |
35+
| cdkOption | \[aria-selected="true"] | If the option is selected |
36+
| cdkListbox | \[aria-disabled="true"] | If the listbox is selected |
37+
| cdkListbox | \[aria-multiselectable="true"] | If the listbox allows multiple selection |
38+
| cdkListbox | \[aria-orientation="horizontal"] | If the listbox is oriented horizontally |
39+
| cdkListbox | \[aria-orientation="vertical"] | If the listbox is oriented vertically |
40+
41+
### Getting started
42+
43+
Import the `CdkListboxModule` into the `NgModule` in which you want to create a listbox. You can
44+
then apply listbox directives to build your custom listbox. A typical listbox consists of the
45+
following directives:
46+
47+
- `cdkListbox` - Added to the container element containing the options to be selected
48+
- `cdkOption` - Added to each selectable option in the listbox
49+
50+
<!-- example({
51+
"example": "cdk-listbox-overview",
52+
"file": "cdk-listbox-overview-example.html",
53+
"region": "listbox"
54+
}) -->
55+
56+
### Option values
57+
58+
Each option in a listbox is bound to the value it represents when selected, e.g.
59+
`<li cdkOption="red">Red</li>`. Within a single listbox, each option must have a unique value. If
60+
an option is not explicitly given a value, its value is considered to be `''` (empty string), e.g.
61+
`<li cdkOption>No color preference</li>`.
62+
63+
<!-- example({
64+
"example": "cdk-listbox-overview",
65+
"file": "cdk-listbox-overview-example.html",
66+
"region": "option"
67+
}) -->
68+
69+
### Single vs multiple selection
70+
71+
Listboxes only support a single selected option at a time by default, but adding
72+
`cdkListboxMultiple` will enable selecting more than one option.
73+
74+
<!-- example({
75+
"example": "cdk-listbox-multiple",
76+
"file": "cdk-listbox-multiple-example.html",
77+
"region": "listbox"
78+
}) -->
79+
80+
### Listbox value
81+
82+
The listbox's value is an array containing the values of the selected option(s). The listbox's value
83+
can be bound using `[cdkListboxValue]` and `(cdkListboxValueChange)`.
84+
85+
<!-- example({
86+
"example": "cdk-listbox-value-binding",
87+
"file": "cdk-listbox-value-binding-example.html",
88+
"region": "listbox"
89+
}) -->
90+
91+
Internally the listbox compares the listbox value against the individual option values using
92+
`Object.is` to determine which options should appear selected. If your option values are complex
93+
objects, you should provide a custom comparison function instead. This can be set via the
94+
`cdkListboxCompareWith` input on the listbox.
95+
96+
<!-- example({
97+
"example": "cdk-listbox-compare-with",
98+
"file": "cdk-listbox-compare-with-example.html",
99+
"region": "listbox"
100+
}) -->
101+
102+
### Angular Forms support
103+
104+
The CDK Listbox has out of the box support for both template driven forms and reactive forms.
105+
106+
<!-- example({
107+
"example": "cdk-listbox-template-forms",
108+
"file": "cdk-listbox-template-forms-example.html",
109+
"region": "listbox"
110+
}) -->
111+
112+
<!-- example({
113+
"example": "cdk-listbox-reactive-forms",
114+
"file": "cdk-listbox-reactive-forms-example.html",
115+
"region": "listbox"
116+
}) -->
117+
118+
#### Forms validation
119+
120+
The CDK listbox integrates with Angular's form validation API and has the following built-in
121+
validation errors:
122+
123+
- `cdkListboxUnexpectedOptionValues` - Raised when the bound value contains values that do not
124+
appear as option value in the listbox. The validation error contains a `values` property that
125+
lists the invalid values
126+
- `cdkListboxUnexpectedMultipleValues` - Raised when a single-selection listbox is bound to a value
127+
containing multiple selected options.
128+
129+
<!-- example({
130+
"example": "cdk-listbox-forms-validation",
131+
"file": "cdk-listbox-forms-validation-example.ts",
132+
"region": "errors"
133+
}) -->
134+
135+
### Disabling options
136+
137+
Individual options can be disabled for selection by setting `cdkOptionDisabled` on them.
138+
In addition, the entire listbox control can be disabled by setting `cdkListboxDisabled` on the
139+
listbox element.
140+
141+
<!-- example({
142+
"example": "cdk-listbox-disabled",
143+
"file": "cdk-listbox-disabled-example.html",
144+
"region": "listbox"
145+
}) -->
146+
147+
### Accessibility
148+
149+
The directives defined in `@angular/cdk/listbox` follow accessibility best practices as defined
150+
in the [ARIA spec][aria]. Keyboard interaction is supported as defined in the
151+
[ARIA listbox keyboard interaction spec][keyboard] _without_ the optional selection follows focus
152+
logic (TODO: should we make this an option?).
153+
154+
#### Listbox label
155+
156+
Always give the listbox a meaningful label for screen readers. If your listbox has a visual label,
157+
you can associate it with the listbox using `aria-labelledby`, otherwise you should provide a
158+
screen-reader-only label with `aria-label`.
159+
160+
#### Roving tabindex vs active descendant
161+
162+
By default, the CDK listbox uses the [roving tabindex][roving-tabindex] strategy to manage focus.
163+
If you prefer to use the [aria-activedescendant][activedescendant] strategy instead, set
164+
`useActiveDescendant=true` on the listbox.
165+
166+
<!-- example({
167+
"example": "cdk-listbox-activedescendant",
168+
"file": "cdk-listbox-activedescendant-example.html",
169+
"region": "listbox"
170+
}) -->
171+
172+
#### Orientation
173+
174+
Listboxes assume a vertical orientation by default, but can be customized by setting the
175+
`cdkListboxOrientation` input oe listbox. Note that this only affects the keyboard navigation. You
176+
will still need to adjust your CSS styles to change the visual appearance.
177+
178+
<!-- example({
179+
"example": "cdk-listbox-horizontal",
180+
"file": "cdk-listbox-horizontal-example.html",
181+
"region": "listbox"
182+
}) -->
183+
184+
#### Option typeahead
185+
186+
The CDK listbox supports typeahead based on the option text. If the typeahead text for your options
187+
needs to be different than the display text (e.g. to exclude emoji), this can be accomplished by
188+
setting the `cdkOptionTypeaheadLabel` on the option.
189+
190+
<!-- example({
191+
"example": "cdk-listbox-custom-typeahead",
192+
"file": "cdk-listbox-custom-typeahead-example.html",
193+
"region": "listbox"
194+
}) -->
195+
196+
#### Keyboard navigation options
197+
198+
When using keyboard navigation to navigate through the options, the navigation wraps when attempting
199+
to navigate past the start or end of the options. To change this, set
200+
`cdkListboxNavigationWraps=false` on the listbox.
201+
202+
Keyboard navigation skips disabled options by default. To change this set
203+
`cdkListboxNavigationSkipsDisabled=false` on the listbox.
204+
205+
<!-- example({
206+
"example": "cdk-listbox-custom-navigation",
207+
"file": "cdk-listbox-custom-navigation-example.html",
208+
"region": "listbox"
209+
}) -->
210+
211+
<!-- links -->
212+
213+
[aria]: https://www.w3.org/WAI/ARIA/apg/patterns/listbox/ 'WAI ARIA Listbox Pattern'
214+
[keyboard]: https://www.w3.org/WAI/ARIA/apg/patterns/listbox/#keyboard-interaction-11
215+
[roving-tabindex]: https://developer.mozilla.org/en-US/docs/Web/Accessibility/Keyboard-navigable_JavaScript_widgets#technique_1_roving_tabindex
216+
[activedescendant]: https://developer.mozilla.org/en-US/docs/Web/Accessibility/Keyboard-navigable_JavaScript_widgets#technique_2_aria-activedescendant

src/cdk-experimental/listbox/listbox.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,8 @@ class ListboxSelectionModel<T> extends SelectionModel<T> {
9595
'[id]': 'id',
9696
'[attr.aria-selected]': 'isSelected() || null',
9797
'[attr.tabindex]': '_getTabIndex()',
98-
'[attr.aria-disabled]': 'disabled',
99-
'[class.cdk-option-disabled]': 'disabled',
98+
'[attr.aria-disabled]': 'disabled || null',
10099
'[class.cdk-option-active]': 'isActive()',
101-
'[class.cdk-option-selected]': 'isSelected()',
102100
'(click)': '_clicked.next($event)',
103101
'(focus)': '_handleFocus()',
104102
},
@@ -245,8 +243,8 @@ export class CdkOption<T = unknown> implements ListKeyManagerOption, Highlightab
245243
'class': 'cdk-listbox',
246244
'[id]': 'id',
247245
'[attr.tabindex]': '_getTabIndex()',
248-
'[attr.aria-disabled]': 'disabled',
249-
'[attr.aria-multiselectable]': 'multiple',
246+
'[attr.aria-disabled]': 'disabled || null',
247+
'[attr.aria-multiselectable]': 'multiple || null',
250248
'[attr.aria-activedescendant]': '_getAriaActiveDescendant()',
251249
'[attr.aria-orientation]': 'orientation',
252250
'(focus)': '_handleFocus()',
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
load("//tools:defaults.bzl", "ng_module")
2+
3+
package(default_visibility = ["//visibility:public"])
4+
5+
ng_module(
6+
name = "listbox",
7+
srcs = glob(["**/*.ts"]),
8+
assets = glob([
9+
"**/*.html",
10+
"**/*.css",
11+
]),
12+
deps = [
13+
"//src/cdk-experimental/listbox",
14+
"@npm//@angular/common",
15+
"@npm//@angular/forms",
16+
],
17+
)
18+
19+
filegroup(
20+
name = "source-files",
21+
srcs = glob([
22+
"**/*.html",
23+
"**/*.css",
24+
"**/*.ts",
25+
]),
26+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
.example-listbox-container {
2+
display: block;
3+
width: 250px;
4+
border: 1px solid black;
5+
}
6+
7+
.example-listbox-label {
8+
display: block;
9+
padding: 5px;
10+
}
11+
12+
.example-listbox {
13+
list-style: none;
14+
padding: 0;
15+
margin: 0;
16+
}
17+
18+
.example-option {
19+
position: relative;
20+
padding: 5px 5px 5px 25px;
21+
}
22+
23+
.example-option[aria-selected]::before {
24+
content: '✓';
25+
position: absolute;
26+
left: 5px;
27+
}
28+
29+
.example-option.cdk-option-active {
30+
background: rgba(0, 0, 0, 0.2);
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<div class="example-listbox-container">
2+
<!-- #docregion listbox -->
3+
<label class="example-listbox-label" id="example-spatula-label">
4+
Spatula Features
5+
</label>
6+
<ul cdkListbox
7+
cdkListboxMultiple
8+
cdkListboxUseActiveDescendant
9+
aria-labelledby="example-spatula-label"
10+
class="example-listbox">
11+
<li *ngFor="let feature of features"
12+
[cdkOption]="feature"
13+
class="example-option">
14+
{{feature}}
15+
</li>
16+
</ul>
17+
<!-- #enddocregion listbox -->
18+
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import {Component} from '@angular/core';
2+
3+
/** @title Listbox with aria-activedescendant. */
4+
@Component({
5+
selector: 'cdk-listbox-activedescendant-example',
6+
exportAs: 'cdkListboxActivedescendantExample',
7+
templateUrl: 'cdk-listbox-activedescendant-example.html',
8+
styleUrls: ['cdk-listbox-activedescendant-example.css'],
9+
})
10+
export class CdkListboxActivedescendantExample {
11+
features = ['Hydrodynamic', 'Port & Starboard Attachments', 'Turbo Drive'];
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
.example-listbox-container {
2+
display: block;
3+
width: 250px;
4+
border: 1px solid black;
5+
}
6+
7+
.example-listbox-label {
8+
display: block;
9+
padding: 5px;
10+
}
11+
12+
.example-listbox {
13+
list-style: none;
14+
padding: 0;
15+
margin: 0;
16+
}
17+
18+
.example-option {
19+
position: relative;
20+
padding: 5px 5px 5px 25px;
21+
}
22+
23+
.example-option[aria-selected]::before {
24+
content: '✓';
25+
position: absolute;
26+
left: 5px;
27+
}
28+
29+
.example-option.cdk-option-active {
30+
background: rgba(0, 0, 0, 0.2);
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<div class="example-listbox-container">
2+
<!-- #docregion listbox -->
3+
<label class="example-listbox-label" id="example-appointment-label">
4+
Appointment Time
5+
</label>
6+
<ul cdkListbox
7+
[cdkListboxValue]="appointment"
8+
[cdkListboxCompareWith]="compareDate"
9+
(cdkListboxValueChange)="appointment = $event.value"
10+
aria-labelledby="example-appointment-label"
11+
class="example-listbox">
12+
<li *ngFor="let time of slots"
13+
[cdkOption]="time"
14+
class="example-option">
15+
{{formatTime(time)}}
16+
</li>
17+
</ul>
18+
<!-- #enddocregion listbox -->
19+
</div>
20+
<p *ngIf="appointment[0]">
21+
Your appointment is scheduled for <b>{{formatAppointment() | json}}</b>&nbsp;
22+
</p>

0 commit comments

Comments
 (0)