Skip to content

feat(cdk-experimental/listbox): selection logic and testing for listbox. #19690

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 35 commits into from
Jun 26, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
8ff6997
build: Added required files to listbox directory.
nielsr98 Jun 11, 2020
364aa53
build: added listbox option directive and renamed listbox directive f…
nielsr98 Jun 11, 2020
ee5746b
build: Added required files to listbox directory.
nielsr98 Jun 11, 2020
893ba4f
build: added listbox option directive and renamed listbox directive f…
nielsr98 Jun 11, 2020
5d0617c
fix(cdk-experimental/listbox): deleted unused files.
nielsr98 Jun 18, 2020
a3a6940
feat(cdk-experimental/listbox): added tests for listbox and options, …
nielsr98 Jun 18, 2020
24acc81
chore(cdk-experimental/listbox): formated BUILD.bazel.
nielsr98 Jun 18, 2020
a0d66ad
fix(cdk-experimental/listbox): fixed style bugs caught by lint test.
nielsr98 Jun 18, 2020
9369e30
chore(cdk-experimental/listbox): fixed style bugs and made CdkListbox…
nielsr98 Jun 18, 2020
b5ba387
fix(cdk-experimental/listbox): made click handler function public.
nielsr98 Jun 18, 2020
2b4b436
refactor(cdk-experimental/listbox): refactored functions to condense …
nielsr98 Jun 18, 2020
362c169
fix(cdk-experimental/listbox): removed unused import.
nielsr98 Jun 18, 2020
4edff93
refactor(cdk-experimental/listbox): cleaned up variable and method na…
nielsr98 Jun 22, 2020
e13c411
:
nielsr98 Jun 22, 2020
02a0d59
fix(cdk-experimental/listbox): moved compileComponents and createComp…
nielsr98 Jun 22, 2020
41bda71
refactor(cdk-experimental/listbox): used DebugElement for testing ins…
nielsr98 Jun 22, 2020
553df4c
refactor(cdk-experimental/listbox): removed unused variable.
nielsr98 Jun 22, 2020
a66964e
fix(cdk-experimental/listbox): removed unused import.
nielsr98 Jun 22, 2020
8aac577
feat(cdk-experimental/listbox): created selectionChange event emitter…
nielsr98 Jun 25, 2020
4f9666f
refactor(cdk-experimental/listbox): removed click handler on listbox …
nielsr98 Jun 25, 2020
c375efa
refactor(cdk-experimental/listbox): created selectionChange emitter f…
nielsr98 Jun 25, 2020
0060b40
fix(cdk-experimental:listbox): added toggle function to change select…
nielsr98 Jun 25, 2020
5332e83
refactor(cdk-experimental/listbox): added tests for unique ids and th…
nielsr98 Jun 25, 2020
93a9d44
refactor(cdk-experimental/listbox): removed unused function to get el…
nielsr98 Jun 25, 2020
5df2643
fix(cdk-experimental/listbox): fixed double quote lint error.
nielsr98 Jun 26, 2020
76e4232
refactor(cdk-experimental/listbox): coerce boolean property when sett…
nielsr98 Jun 26, 2020
b900539
fix(cdk-experimental/listbox): add static ngAcceptInputType for _sele…
nielsr98 Jun 26, 2020
fa2e1da
fix(cdk-experimental/listbox): fixed BUILD file format issue.
nielsr98 Jun 26, 2020
618d9c6
refactor(cdk-experimental/listbox): use the id attribute instead of c…
nielsr98 Jun 26, 2020
48e792f
fix(cdk-experimental/listbox): removed unused variable.
nielsr98 Jun 26, 2020
d39d9ae
fix(cdk-experimental/listbox): removed unused listbox variable.
nielsr98 Jun 26, 2020
75de55c
fix(cdk-experimental/listbox): removed unused CdkListbox import.
nielsr98 Jun 26, 2020
1e5f5ee
feat(cdk-experimental/listbox): added interface for selection change …
nielsr98 Jun 26, 2020
5be6915
fix(cdk-experimental/listbox): fix line length.
nielsr98 Jun 26, 2020
a0eaea2
refactor(cdk-experimental/listbox): removed spy from test.
nielsr98 Jun 26, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion src/cdk-experimental/listbox/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
load("//tools:defaults.bzl", "ng_module")
load("//tools:defaults.bzl", "ng_module", "ng_test_library", "ng_web_test_suite")

package(default_visibility = ["//visibility:public"])

Expand All @@ -9,4 +9,25 @@ ng_module(
exclude = ["**/*.spec.ts"],
),
module_name = "@angular/cdk-experimental/listbox",
deps = [
"//src/cdk/coercion",
],
)

ng_test_library(
name = "unit_test_sources",
srcs = glob(
["**/*.spec.ts"],
exclude = ["**/*.e2e.spec.ts"],
),
deps = [
":listbox",
"//src/cdk/testing/private",
"@npm//@angular/platform-browser",
],
)

ng_web_test_suite(
name = "unit_tests",
deps = [":unit_test_sources"],
)
99 changes: 99 additions & 0 deletions src/cdk-experimental/listbox/listbox.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import {
ComponentFixture,
async,
TestBed,
} from '@angular/core/testing';
import {Component, DebugElement} from '@angular/core';
import {By} from '@angular/platform-browser';
import {
CdkOption,
CdkListboxModule, ListboxSelectionChangeEvent
} from './index';
import {dispatchMouseEvent} from '@angular/cdk/testing/private';

describe('CdkOption', () => {

describe('selection state change', () => {
let fixture: ComponentFixture<ListboxWithOptions>;
let options: DebugElement[];
let optionInstances: CdkOption[];
let optionElements: HTMLElement[];

beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [CdkListboxModule],
declarations: [ListboxWithOptions],
}).compileComponents();
}));

beforeEach(async(() => {
fixture = TestBed.createComponent(ListboxWithOptions);
fixture.detectChanges();

options = fixture.debugElement.queryAll(By.directive(CdkOption));
optionInstances = options.map(o => o.injector.get<CdkOption>(CdkOption));
optionElements = options.map(o => o.nativeElement);
}));

it('should generate a unique optionId for each option', () => {
let optionIds: string[] = [];
for (const instance of optionInstances) {
expect(optionIds.indexOf(instance.id)).toBe(-1);
optionIds.push(instance.id);

expect(instance.id).toMatch(/cdk-option-\d+/);
}
});

it('should have set the selected input of the options to null by default', () => {
for (const instance of optionInstances) {
expect(instance.selected).toBeFalse();
}
});

it('should update aria-selected when selected is changed programmatically', () => {
expect(optionElements[0].getAttribute('aria-selected')).toBeNull();
optionInstances[1].selected = true;
fixture.detectChanges();

expect(optionElements[1].getAttribute('aria-selected')).toBe('true');
});

it('should update selected option on click event', () => {
let selectedOptions = optionInstances.filter(option => option.selected);

expect(selectedOptions.length).toBe(0);
expect(optionElements[0].getAttribute('aria-selected')).toBeNull();
expect(optionInstances[0].selected).toBeFalse();
expect(fixture.componentInstance.changedOption).toBeUndefined();

dispatchMouseEvent(optionElements[0], 'click');
fixture.detectChanges();

selectedOptions = optionInstances.filter(option => option.selected);
expect(selectedOptions.length).toBe(1);
expect(optionElements[0].getAttribute('aria-selected')).toBe('true');
expect(optionInstances[0].selected).toBeTrue();
expect(fixture.componentInstance.changedOption).toBeDefined();
expect(fixture.componentInstance.changedOption.id).toBe(optionInstances[0].id);
});
});

});

@Component({
template: `
<div cdkListbox (selectionChange)="onSelectionChange($event)">
<div cdkOption>Void</div>
<div cdkOption>Solar</div>
<div cdkOption>Arc</div>
<div cdkOption>Stasis</div>
</div>`
})
class ListboxWithOptions {
changedOption: CdkOption;

onSelectionChange(event: ListboxSelectionChangeEvent) {
this.changedOption = event.option;
}
}
75 changes: 74 additions & 1 deletion src/cdk-experimental/listbox/listbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,63 @@
* found in the LICENSE file at https://angular.io/license
*/

import {Directive} from '@angular/core';
import {
ContentChildren,
Directive,
EventEmitter, forwardRef, Inject,
Input, Output,
QueryList
} from '@angular/core';
import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion';

let nextId = 0;

/**
* Directive that applies interaction patterns to an element following the aria role of option.
* Typically meant to be placed inside a listbox. Logic handling selection, disabled state, and
* value is built in.
*/
@Directive({
selector: '[cdkOption]',
exportAs: 'cdkOption',
host: {
role: 'option',
'(click)': 'toggle()',
'[attr.aria-selected]': 'selected || null',
'[id]': 'id',
}
})
export class CdkOption {
private _selected: boolean = false;

/** Whether the option is selected or not */
@Input()
get selected(): boolean {
return this._selected;
}
set selected(value: boolean) {
this._selected = coerceBooleanProperty(value);
}

/** The id of the option, set to a uniqueid if the user does not provide one */
@Input() id = `cdk-option-${nextId++}`;

constructor(@Inject(forwardRef(() => CdkListbox)) public listbox: CdkListbox) {}

/** Toggles the selected state, emits a change event through the injected listbox */
toggle() {
this.selected = !this.selected;
this.listbox._emitChangeEvent(this);
}

static ngAcceptInputType_selected: BooleanInput;
}

/**
* Directive that applies interaction patterns to an element following the aria role of listbox.
* Typically CdkOption elements are placed inside the listbox. Logic to handle keyboard navigation,
* selection of options, active options, and disabled states is built in.
*/
@Directive({
selector: '[cdkListbox]',
exportAs: 'cdkListbox',
Expand All @@ -28,4 +72,33 @@ export class CdkOption {
})
export class CdkListbox {

/** A query list containing all CdkOption elements within this listbox */
@ContentChildren(CdkOption, {descendants: true}) _options: QueryList<CdkOption>;

@Output() readonly selectionChange: EventEmitter<ListboxSelectionChangeEvent> =
new EventEmitter<ListboxSelectionChangeEvent>();

/** Emits a selection change event, called when an option has its selected state changed */
_emitChangeEvent(option: CdkOption) {
this.selectionChange.emit(new ListboxSelectionChangeEvent(this, option));
}

/** Sets the given option's selected state to true */
select(option: CdkOption) {
option.selected = true;
}

/** Sets the given option's selected state to null. Null is preferable for screen readers */
deselect(option: CdkOption) {
option.selected = false;
}
}

/** Change event that is being fired whenever the selected state of an option changes. */
export class ListboxSelectionChangeEvent {
constructor(
/** Reference to the listbox that emitted the event. */
public source: CdkListbox,
/** Reference to the option that has been changed. */
public option: CdkOption) {}
}