Skip to content

Commit a20d257

Browse files
nielsr98wagnermaciel
authored andcommitted
Cdk listbox dev app (#20010)
* build: Added required files to listbox directory. * build: added listbox option directive and renamed listbox directive files. * build: Added required files to listbox directory. * build: added listbox option directive and renamed listbox directive files. * build: Added required files to listbox directory. * build: added listbox option directive and renamed listbox directive files. * build: Added required files to listbox directory. * build: added listbox option directive and renamed listbox directive files. * feat(listbox): added support for non-multiple listbox and aria activedescendant. * fix(listbox): formatted BUILD.bazel. * feat(dev-app/listbox): added cdk listbox example to the dev-app. * feat(listbox): added shift key selection. * test(listbox): added test for selection via shift key. * fix(listbox): formatted dev app build file. * fix(listbox): added codeowners for dev-app listbox example. * fix(listbox): removed width css property. * fix(listbox): removed private properties from host bindings to fix view engine bugs. * nit(listbox): changed double quote to single quote. * refactor(listbox): fixed some styling errors and added modifier key to createKeyboardEvent. * nit(listbox): changed spacing. * nit(listbox): spacing.: * nit(listbox): removed extra blank line. * refactor(listbox): changed test to have a stronger check on selected. * refactor(listbox): changed css classes to use demo- prefix. (cherry picked from commit add6ad1)
1 parent af54044 commit a20d257

File tree

11 files changed

+150
-17
lines changed

11 files changed

+150
-17
lines changed

.github/CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@
143143
/src/dev-app/button-toggle/** @jelbourn
144144
/src/dev-app/button/** @jelbourn
145145
/src/dev-app/card/** @jelbourn
146+
/src/dev-app/cdk-experimental-listbox/** @jelbourn @nielsr98
146147
/src/dev-app/cdk-experimental-menu/** @jelbourn @andy9775
147148
/src/dev-app/checkbox/** @jelbourn @devversion
148149
/src/dev-app/chips/** @jelbourn

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

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ import {
99
CdkOption,
1010
CdkListboxModule, ListboxSelectionChangeEvent, CdkListbox
1111
} from './index';
12-
import {dispatchKeyboardEvent, dispatchMouseEvent} from '@angular/cdk/testing/private';
12+
import {
13+
createKeyboardEvent,
14+
dispatchKeyboardEvent,
15+
dispatchMouseEvent
16+
} from '@angular/cdk/testing/private';
1317
import {A, DOWN_ARROW, END, HOME, SPACE} from '@angular/cdk/keycodes';
1418

1519
describe('CdkOption', () => {
@@ -60,8 +64,8 @@ describe('CdkOption', () => {
6064
});
6165

6266
it('should have set the selected input of the options to null by default', () => {
63-
for (const instance of optionInstances) {
64-
expect(instance.selected).toBeFalse();
67+
for (const option of optionElements) {
68+
expect(option.hasAttribute('aria-selected')).toBeFalse();
6569
}
6670
});
6771

@@ -327,13 +331,34 @@ describe('CdkOption', () => {
327331
expect(fixture.componentInstance.changedOption).toBeDefined();
328332
expect(fixture.componentInstance.changedOption.id).toBe(optionInstances[0].id);
329333
});
334+
335+
it('should focus and toggle the next item when pressing SHIFT + DOWN_ARROW', () => {
336+
let selectedOptions = optionInstances.filter(option => option.selected);
337+
const downKeyEvent =
338+
createKeyboardEvent('keydown', DOWN_ARROW, undefined, undefined, {shift: true});
339+
340+
expect(selectedOptions.length).toBe(0);
341+
expect(optionElements[0].hasAttribute('aria-selected')).toBeFalse();
342+
expect(optionInstances[0].selected).toBeFalse();
343+
expect(fixture.componentInstance.changedOption).toBeUndefined();
344+
345+
listboxInstance.setActiveOption(optionInstances[0]);
346+
listboxInstance._keydown(downKeyEvent);
347+
fixture.detectChanges();
348+
349+
selectedOptions = optionInstances.filter(option => option.selected);
350+
expect(selectedOptions.length).toBe(1);
351+
expect(optionElements[1].getAttribute('aria-selected')).toBe('true');
352+
expect(optionInstances[1].selected).toBeTrue();
353+
expect(fixture.componentInstance.changedOption).toBeDefined();
354+
expect(fixture.componentInstance.changedOption.id).toBe(optionInstances[1].id);
355+
});
330356
});
331357

332358
describe('with multiple selection', () => {
333359
let fixture: ComponentFixture<ListboxMultiselect>;
334360

335361
let testComponent: ListboxMultiselect;
336-
337362
let listbox: DebugElement;
338363
let listboxInstance: CdkListbox;
339364

@@ -353,7 +378,6 @@ describe('CdkOption', () => {
353378
fixture.detectChanges();
354379

355380
testComponent = fixture.debugElement.componentInstance;
356-
357381
listbox = fixture.debugElement.query(By.directive(CdkListbox));
358382
listboxInstance = listbox.injector.get<CdkListbox>(CdkListbox);
359383

src/cdk-experimental/listbox/listbox.ts

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
QueryList
1717
} from '@angular/core';
1818
import {ActiveDescendantKeyManager, Highlightable, ListKeyManagerOption} from '@angular/cdk/a11y';
19-
import {END, ENTER, HOME, SPACE} from '@angular/cdk/keycodes';
19+
import {DOWN_ARROW, END, ENTER, HOME, SPACE, UP_ARROW} from '@angular/cdk/keycodes';
2020
import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion';
2121
import {SelectionChange, SelectionModel} from '@angular/cdk/collections';
2222
import {defer, merge, Observable, Subject} from 'rxjs';
@@ -33,11 +33,12 @@ let nextId = 0;
3333
'(focus)': 'activate()',
3434
'(blur)': 'deactivate()',
3535
'[id]': 'id',
36-
'[attr.aria-selected]': '_selected || null',
36+
'[attr.aria-selected]': 'selected || null',
3737
'[attr.tabindex]': '_getTabIndex()',
3838
'[attr.aria-disabled]': '_isInteractionDisabled()',
3939
'[class.cdk-option-disabled]': '_isInteractionDisabled()',
40-
'[class.cdk-option-active]': '_active'
40+
'[class.cdk-option-active]': '_active',
41+
'[class.cdk-option-selected]': 'selected'
4142
}
4243
})
4344
export class CdkOption implements ListKeyManagerOption, Highlightable {
@@ -171,26 +172,27 @@ export class CdkOption implements ListKeyManagerOption, Highlightable {
171172
host: {
172173
'role': 'listbox',
173174
'(keydown)': '_keydown($event)',
174-
'[attr.aria-disabled]': '_disabled',
175-
'[attr.aria-multiselectable]': '_multiple',
175+
'[attr.tabindex]': '_tabIndex',
176+
'[attr.aria-disabled]': 'disabled',
177+
'[attr.aria-multiselectable]': 'multiple',
176178
'[attr.aria-activedescendant]': '_getAriaActiveDescendant()'
177179
}
178180
})
179181
export class CdkListbox implements AfterContentInit, OnDestroy, OnInit {
180182

181183
_listKeyManager: ActiveDescendantKeyManager<CdkOption>;
182184
_selectionModel: SelectionModel<CdkOption>;
185+
_tabIndex = 0;
183186

184187
readonly optionSelectionChanges: Observable<OptionSelectionChangeEvent> = defer(() => {
185-
const options = this._options;
188+
const options = this._options;
186189

187-
return options.changes.pipe(
188-
startWith(options),
189-
switchMap(() => merge(...options.map(option => option.selectionChange)))
190-
);
190+
return options.changes.pipe(
191+
startWith(options),
192+
switchMap(() => merge(...options.map(option => option.selectionChange)))
193+
);
191194
}) as Observable<OptionSelectionChangeEvent>;
192195

193-
194196
private _disabled: boolean = false;
195197
private _multiple: boolean = false;
196198
private _useActiveDescendant: boolean = true;
@@ -256,7 +258,10 @@ export class CdkListbox implements AfterContentInit, OnDestroy, OnInit {
256258

257259
private _initKeyManager() {
258260
this._listKeyManager = new ActiveDescendantKeyManager(this._options)
259-
.withWrap().withVerticalOrientation().withTypeAhead();
261+
.withWrap()
262+
.withVerticalOrientation()
263+
.withTypeAhead()
264+
.withAllowedModifierKeys(['shiftKey']);
260265

261266
this._listKeyManager.change.pipe(takeUntil(this._destroyed)).subscribe(() => {
262267
this._updateActiveOption();
@@ -284,6 +289,7 @@ export class CdkListbox implements AfterContentInit, OnDestroy, OnInit {
284289

285290
const manager = this._listKeyManager;
286291
const {keyCode} = event;
292+
const previousActiveIndex = manager.activeItemIndex;
287293

288294
if (keyCode === HOME || keyCode === END) {
289295
event.preventDefault();
@@ -297,6 +303,12 @@ export class CdkListbox implements AfterContentInit, OnDestroy, OnInit {
297303
} else {
298304
manager.onKeydown(event);
299305
}
306+
307+
/** Will select an option if shift was pressed while navigating to the option */
308+
const isArrow = (keyCode === UP_ARROW || keyCode === DOWN_ARROW);
309+
if (isArrow && event.shiftKey && previousActiveIndex !== this._listKeyManager.activeItemIndex) {
310+
this._toggleActiveOption();
311+
}
300312
}
301313

302314
/** Emits a selection change event, called when an option has its selected state changed. */

src/dev-app/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ ng_module(
2323
"//src/dev-app/button",
2424
"//src/dev-app/button-toggle",
2525
"//src/dev-app/card",
26+
"//src/dev-app/cdk-experimental-listbox",
2627
"//src/dev-app/cdk-experimental-menu",
2728
"//src/dev-app/checkbox",
2829
"//src/dev-app/chips",
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
load("//tools:defaults.bzl", "ng_module")
2+
3+
package(default_visibility = ["//visibility:public"])
4+
5+
ng_module(
6+
name = "cdk-experimental-listbox",
7+
srcs = glob(["**/*.ts"]),
8+
assets = [
9+
"cdk-listbox-demo.html",
10+
"cdk-listbox-demo.css",
11+
],
12+
deps = [
13+
"//src/cdk-experimental/listbox",
14+
"@npm//@angular/router",
15+
],
16+
)
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {NgModule} from '@angular/core';
10+
import {CommonModule} from '@angular/common';
11+
import {RouterModule} from '@angular/router';
12+
import {CdkListboxModule} from '@angular/cdk-experimental/listbox';
13+
14+
import {CdkListboxDemo} from './cdk-listbox-demo';
15+
16+
@NgModule({
17+
imports: [
18+
CdkListboxModule,
19+
CommonModule,
20+
RouterModule.forChild([{path: '', component: CdkListboxDemo}]),
21+
],
22+
declarations: [CdkListboxDemo],
23+
})
24+
export class CdkListboxDemoModule {}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
.demo-listbox {
2+
list-style-type: none;
3+
border: 1px solid black;
4+
cursor: default;
5+
padding: 0;
6+
}
7+
8+
.demo-listbox .cdk-option-active {
9+
background: lightgrey;
10+
}
11+
12+
.demo-listbox .cdk-option-selected {
13+
background: cornflowerblue;
14+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<ul cdkListbox class="demo-listbox" [multiple]="multiSelectable" [useActiveDescendant]="activeDescendant">
2+
<li cdkOption>Apple</li>
3+
<li cdkOption>Orange</li>
4+
<li cdkOption>Grapefruit</li>
5+
<li cdkOption>Peach</li>
6+
</ul>
7+
8+
<button (click)="toggleMultiple()">Toggle Multiple</button>
9+
<br>
10+
<button (click)="toggleActiveDescendant()">Toggle Active Descendant</button>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {Component} from '@angular/core';
10+
11+
@Component({
12+
templateUrl: 'cdk-listbox-demo.html',
13+
styleUrls: ['cdk-listbox-demo.css'],
14+
})
15+
export class CdkListboxDemo {
16+
multiSelectable = false;
17+
activeDescendant = true;
18+
19+
toggleMultiple() {
20+
this.multiSelectable = !this.multiSelectable;
21+
}
22+
23+
toggleActiveDescendant() {
24+
this.activeDescendant = !this.activeDescendant;
25+
}
26+
}

src/dev-app/dev-app/dev-app-layout.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export class DevAppLayout {
2929
{name: 'Button Toggle', route: '/button-toggle'},
3030
{name: 'Button', route: '/button'},
3131
{name: 'Card', route: '/card'},
32+
{name: 'Cdk Experimental Listbox', route: '/cdk-experimental-listbox'},
3233
{name: 'Cdk Experimental Menu', route: '/cdk-experimental-menu'},
3334
{name: 'Checkbox', route: '/checkbox'},
3435
{name: 'Chips', route: '/chips'},

src/dev-app/dev-app/routes.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ export const DEV_APP_ROUTES: Routes = [
2828
loadChildren: 'button-toggle/button-toggle-demo-module#ButtonToggleDemoModule'
2929
},
3030
{path: 'card', loadChildren: 'card/card-demo-module#CardDemoModule'},
31+
{
32+
path: 'cdk-experimental-listbox',
33+
loadChildren: 'cdk-experimental-listbox/cdk-listbox-demo-module#CdkListboxDemoModule'
34+
},
3135
{
3236
path: 'cdk-experimental-menu',
3337
loadChildren: 'cdk-experimental-menu/cdk-menu-demo-module#CdkMenuDemoModule'

0 commit comments

Comments
 (0)