Skip to content

Commit 083f437

Browse files
authored
docs(cdk-experimental/listbox): add docs & examples (#25317)
* docs(cdk-experimental/listbox): add docs & examples * fixup! docs(cdk-experimental/listbox): add docs & examples * fixup! docs(cdk-experimental/listbox): add docs & examples
1 parent ede9cf6 commit 083f437

File tree

45 files changed

+1325
-202
lines changed

Some content is hidden

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

45 files changed

+1325
-202
lines changed
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
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). This is true even
83+
for the single selection listbox, whose value is an array containing a single element. The listbox's
84+
value can be bound using `[cdkListboxValue]` and `(cdkListboxValueChange)`.
85+
86+
<!-- example({
87+
"example": "cdk-listbox-value-binding",
88+
"file": "cdk-listbox-value-binding-example.html",
89+
"region": "listbox"
90+
}) -->
91+
92+
Internally the listbox compares the listbox value against the individual option values using
93+
`Object.is` to determine which options should appear selected. If your option values are complex
94+
objects, you should provide a custom comparison function instead. This can be set via the
95+
`cdkListboxCompareWith` input on the listbox.
96+
97+
<!-- example({
98+
"example": "cdk-listbox-compare-with",
99+
"file": "cdk-listbox-compare-with-example.html",
100+
"region": "listbox"
101+
}) -->
102+
103+
### Angular Forms support
104+
105+
The CDK Listbox supports both template driven forms and reactive forms.
106+
107+
<!-- example({
108+
"example": "cdk-listbox-template-forms",
109+
"file": "cdk-listbox-template-forms-example.html",
110+
"region": "listbox"
111+
}) -->
112+
113+
<!-- example({
114+
"example": "cdk-listbox-reactive-forms",
115+
"file": "cdk-listbox-reactive-forms-example.html",
116+
"region": "listbox"
117+
}) -->
118+
119+
#### Forms validation
120+
121+
The CDK listbox integrates with Angular's form validation API and has the following built-in
122+
validation errors:
123+
124+
- `cdkListboxUnexpectedOptionValues` - Raised when the bound value contains values that do not
125+
appear as option value in the listbox. The validation error contains a `values` property that
126+
lists the invalid values
127+
- `cdkListboxUnexpectedMultipleValues` - Raised when a single-selection listbox is bound to a value
128+
containing multiple selected options.
129+
130+
<!-- example({
131+
"example": "cdk-listbox-forms-validation",
132+
"file": "cdk-listbox-forms-validation-example.ts",
133+
"region": "errors"
134+
}) -->
135+
136+
### Disabling options
137+
138+
You can disable options for selection by setting `cdkOptionDisabled`.
139+
In addition, the entire listbox control can be disabled by setting `cdkListboxDisabled` on the
140+
listbox element.
141+
142+
<!-- example({
143+
"example": "cdk-listbox-disabled",
144+
"file": "cdk-listbox-disabled-example.html",
145+
"region": "listbox"
146+
}) -->
147+
148+
### Accessibility
149+
150+
The directives defined in `@angular/cdk/listbox` follow accessibility best practices as defined
151+
in the [ARIA spec][aria]. Keyboard interaction is supported as defined in the
152+
[ARIA listbox keyboard interaction spec][keyboard] _without_ the optional selection follows focus
153+
logic (TODO: should we make this an option?).
154+
155+
#### Listbox label
156+
157+
Always give the listbox a meaningful label for screen readers. If your listbox has a visual label,
158+
you can associate it with the listbox using `aria-labelledby`, otherwise you should provide a
159+
screen-reader-only label with `aria-label`.
160+
161+
#### Roving tabindex vs active descendant
162+
163+
By default, the CDK listbox uses the [roving tabindex][roving-tabindex] strategy to manage focus.
164+
If you prefer to use the [aria-activedescendant][activedescendant] strategy instead, set
165+
`useActiveDescendant=true` on the listbox.
166+
167+
<!-- example({
168+
"example": "cdk-listbox-activedescendant",
169+
"file": "cdk-listbox-activedescendant-example.html",
170+
"region": "listbox"
171+
}) -->
172+
173+
#### Orientation
174+
175+
Listboxes assume a vertical orientation by default, but can be customized by setting the
176+
`cdkListboxOrientation` input. Note that this only affects the keyboard navigation. You
177+
will still need to adjust your CSS styles to change the visual appearance.
178+
179+
<!-- example({
180+
"example": "cdk-listbox-horizontal",
181+
"file": "cdk-listbox-horizontal-example.html",
182+
"region": "listbox"
183+
}) -->
184+
185+
#### Option typeahead
186+
187+
The CDK listbox supports typeahead based on the option text. If the typeahead text for your options
188+
needs to be different than the display text (e.g. to exclude emoji), this can be accomplished by
189+
setting the `cdkOptionTypeaheadLabel` on the option.
190+
191+
<!-- example({
192+
"example": "cdk-listbox-custom-typeahead",
193+
"file": "cdk-listbox-custom-typeahead-example.html",
194+
"region": "listbox"
195+
}) -->
196+
197+
#### Keyboard navigation options
198+
199+
When using keyboard navigation to navigate through the options, the navigation wraps when attempting
200+
to navigate past the start or end of the options. To change this, set
201+
`cdkListboxNavigationWrapDisabled` on the listbox.
202+
203+
Keyboard navigation skips disabled options by default. To change this set
204+
`cdkListboxNavigatesDisabledOptions` on the listbox.
205+
206+
<!-- example({
207+
"example": "cdk-listbox-custom-navigation",
208+
"file": "cdk-listbox-custom-navigation-example.html",
209+
"region": "listbox"
210+
}) -->
211+
212+
<!-- links -->
213+
214+
[aria]: https://www.w3.org/WAI/ARIA/apg/patterns/listbox/ 'WAI ARIA Listbox Pattern'
215+
[keyboard]: https://www.w3.org/WAI/ARIA/apg/patterns/listbox/#keyboard-interaction-11 'WAI ARIA Listbox Keyboard Interaction'
216+
[roving-tabindex]: https://developer.mozilla.org/en-US/docs/Web/Accessibility/Keyboard-navigable_JavaScript_widgets#technique_1_roving_tabindex 'MDN Roving Tabindex Technique'
217+
[activedescendant]: https://developer.mozilla.org/en-US/docs/Web/Accessibility/Keyboard-navigable_JavaScript_widgets#technique_2_aria-activedescendant 'MDN aria-activedescendant Technique'

src/cdk-experimental/listbox/listbox.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -908,8 +908,8 @@ describe('CdkOption and CdkListbox', () => {
908908
[cdkListboxDisabled]="isListboxDisabled"
909909
[cdkListboxUseActiveDescendant]="isActiveDescendant"
910910
[cdkListboxOrientation]="orientation"
911-
[cdkListboxKeyboardNavigationWraps]="navigationWraps"
912-
[cdkListboxKeyboardNavigationSkipsDisabled]="navigationSkipsDisabled"
911+
[cdkListboxNavigationWrapDisabled]="!navigationWraps"
912+
[cdkListboxNavigatesDisabledOptions]="!navigationSkipsDisabled"
913913
(cdkListboxValueChange)="onSelectionChange($event)">
914914
<div cdkOption="apple"
915915
[cdkOptionDisabled]="isAppleDisabled"

src/cdk-experimental/listbox/listbox.ts

Lines changed: 19 additions & 23 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()',
@@ -360,28 +358,28 @@ export class CdkListbox<T = unknown>
360358
* Whether the keyboard navigation should wrap when the user presses arrow down on the last item
361359
* or arrow up on the first item.
362360
*/
363-
@Input('cdkListboxKeyboardNavigationWraps')
364-
get keyboardNavigationWraps() {
365-
return this._keyboardNavigationWraps;
361+
@Input('cdkListboxNavigationWrapDisabled')
362+
get navigationWrapDisabled() {
363+
return this._navigationWrapDisabled;
366364
}
367-
set keyboardNavigationWraps(wrap: BooleanInput) {
368-
this._keyboardNavigationWraps = coerceBooleanProperty(wrap);
369-
this.listKeyManager?.withWrap(this._keyboardNavigationWraps);
365+
set navigationWrapDisabled(wrap: BooleanInput) {
366+
this._navigationWrapDisabled = coerceBooleanProperty(wrap);
367+
this.listKeyManager?.withWrap(!this._navigationWrapDisabled);
370368
}
371-
private _keyboardNavigationWraps = true;
369+
private _navigationWrapDisabled = false;
372370

373371
/** Whether keyboard navigation should skip over disabled items. */
374-
@Input('cdkListboxKeyboardNavigationSkipsDisabled')
375-
get keyboardNavigationSkipsDisabled() {
376-
return this._keyboardNavigationSkipsDisabled;
372+
@Input('cdkListboxNavigatesDisabledOptions')
373+
get navigateDisabledOptions() {
374+
return this._navigateDisabledOptions;
377375
}
378-
set keyboardNavigationSkipsDisabled(skip: BooleanInput) {
379-
this._keyboardNavigationSkipsDisabled = coerceBooleanProperty(skip);
376+
set navigateDisabledOptions(skip: BooleanInput) {
377+
this._navigateDisabledOptions = coerceBooleanProperty(skip);
380378
this.listKeyManager?.skipPredicate(
381-
this._keyboardNavigationSkipsDisabled ? this._skipDisabledPredicate : this._skipNonePredicate,
379+
this._navigateDisabledOptions ? this._skipNonePredicate : this._skipDisabledPredicate,
382380
);
383381
}
384-
private _keyboardNavigationSkipsDisabled = true;
382+
private _navigateDisabledOptions = false;
385383

386384
/** Emits when the selected value(s) in the listbox change. */
387385
@Output('cdkListboxValueChange') readonly valueChange = new Subject<ListboxValueChangeEvent<T>>();
@@ -862,14 +860,12 @@ export class CdkListbox<T = unknown>
862860
/** Initialize the key manager. */
863861
private _initKeyManager() {
864862
this.listKeyManager = new ActiveDescendantKeyManager(this.options)
865-
.withWrap(this._keyboardNavigationWraps)
863+
.withWrap(!this._navigationWrapDisabled)
866864
.withTypeAhead()
867865
.withHomeAndEnd()
868866
.withAllowedModifierKeys(['shiftKey'])
869867
.skipPredicate(
870-
this._keyboardNavigationSkipsDisabled
871-
? this._skipDisabledPredicate
872-
: this._skipNonePredicate,
868+
this._navigateDisabledOptions ? this._skipNonePredicate : this._skipDisabledPredicate,
873869
);
874870

875871
if (this.orientation === 'vertical') {
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,36 @@
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+
display: block;
26+
width: 20px;
27+
height: 20px;
28+
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="24" width="24"><path d="m9.55 18-5.7-5.7 1.425-1.425L9.55 15.15l9.175-9.175L20.15 7.4Z"/></svg>'); /* stylelint-disable-line */
29+
background-size: cover;
30+
position: absolute;
31+
left: 2px;
32+
}
33+
34+
.example-listbox:focus .cdk-option-active {
35+
background: rgba(0, 0, 0, 0.2);
36+
}
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>

0 commit comments

Comments
 (0)