-
Notifications
You must be signed in to change notification settings - Fork 6.8k
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
feat(cdk-experimental/listbox): selection logic and testing for listbox. #19690
Changes from 28 commits
8ff6997
364aa53
ee5746b
893ba4f
5d0617c
a3a6940
24acc81
a0d66ad
9369e30
b5ba387
2b4b436
362c169
4edff93
e13c411
02a0d59
41bda71
553df4c
a66964e
8aac577
4f9666f
c375efa
0060b40
5332e83
93a9d44
5df2643
76e4232
b900539
fa2e1da
618d9c6
48e792f
d39d9ae
75de55c
1e5f5ee
5be6915
a0eaea2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,102 @@ | ||||||
import { | ||||||
ComponentFixture, | ||||||
async, | ||||||
TestBed, | ||||||
} from '@angular/core/testing'; | ||||||
import {Component, DebugElement} from '@angular/core'; | ||||||
import {By} from '@angular/platform-browser'; | ||||||
import { | ||||||
CdkOption, | ||||||
CdkListboxModule, CdkListbox | ||||||
} from './index'; | ||||||
import {dispatchMouseEvent} from '@angular/cdk/testing/private'; | ||||||
|
||||||
describe('CdkOption', () => { | ||||||
|
||||||
describe('selection state change', () => { | ||||||
let fixture: ComponentFixture<ListboxWithCdkOptions>; | ||||||
let listbox: DebugElement; | ||||||
let listboxInstance: CdkListbox; | ||||||
let options: DebugElement[]; | ||||||
let optionInstances: CdkOption[]; | ||||||
let optionElements: Element[]; | ||||||
|
||||||
beforeEach(async(() => { | ||||||
TestBed.configureTestingModule({ | ||||||
imports: [CdkListboxModule], | ||||||
declarations: [ListboxWithCdkOptions], | ||||||
}).compileComponents(); | ||||||
})); | ||||||
|
||||||
beforeEach(async(() => { | ||||||
fixture = TestBed.createComponent(ListboxWithCdkOptions); | ||||||
fixture.detectChanges(); | ||||||
|
||||||
listbox = fixture.debugElement.query(By.directive(CdkListbox)); | ||||||
listboxInstance = listbox.injector.get<CdkListbox>(CdkListbox); | ||||||
|
||||||
options = fixture.debugElement.queryAll(By.directive(CdkOption)); | ||||||
jelbourn marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
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) { | ||||||
const id = instance._optionid; | ||||||
|
||||||
expect(optionIds.indexOf(id)).toBe(-1); | ||||||
optionIds.push(id); | ||||||
|
||||||
expect(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', () => { | ||||||
jelbourn marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
let selectedOptions = optionInstances.filter(option => option.selected); | ||||||
spyOn(listboxInstance, '_emitChangeEvent'); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rather than setting up a spy on the internal listbox method, you can have your There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok I see. Will change. |
||||||
|
||||||
expect(selectedOptions.length).toBe(0); | ||||||
expect(optionElements[0].getAttribute('aria-selected')).toBeNull(); | ||||||
expect(optionInstances[0].selected).toBeFalse(); | ||||||
expect(listboxInstance._emitChangeEvent).toHaveBeenCalledTimes(0); | ||||||
|
||||||
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(listboxInstance._emitChangeEvent).toHaveBeenCalledTimes(1); | ||||||
}); | ||||||
}); | ||||||
|
||||||
}); | ||||||
|
||||||
@Component({ | ||||||
template: ` | ||||||
<div cdkListbox> | ||||||
<div cdkOption>Void</div> | ||||||
<div cdkOption>Solar</div> | ||||||
<div cdkOption>Arc</div> | ||||||
<div cdkOption>Stasis</div> | ||||||
</div>` | ||||||
}) | ||||||
class ListboxWithCdkOptions { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
can also omit |
||||||
|
||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,19 +6,62 @@ | |
* 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'; | ||
|
||
/** | ||
* 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', | ||
'[attr.optionid]': '_optionid', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It does need to be the native There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see I think |
||
} | ||
}) | ||
export class CdkOption { | ||
private _selected: boolean = false; | ||
readonly _optionid: string; | ||
|
||
/** Whether the option is selected or not */ | ||
@Input() | ||
get selected(): boolean { | ||
return this._selected; | ||
} | ||
set selected(value: boolean) { | ||
this._selected = coerceBooleanProperty(value); | ||
} | ||
|
||
constructor(@Inject(forwardRef(() => CdkListbox)) public listbox: CdkListbox) { | ||
this._optionid = `cdk-option-${nextId++}`; | ||
} | ||
|
||
toggle() { | ||
this.selected = !this.selected; | ||
this.listbox._emitChangeEvent(this); | ||
} | ||
|
||
static ngAcceptInputType_selected: BooleanInput; | ||
} | ||
|
||
let nextId = 0; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would typically put this above the class where it's used There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah true I used to use it in Listbox and neglected moving it. |
||
|
||
/** | ||
* 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', | ||
|
@@ -28,4 +71,23 @@ 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<CdkOption> = new EventEmitter<CdkOption>(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I forgot if I mentioned this earlier, but in a follow-up we'll want to change this from emitting There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Alright sounds good. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I looked into it and it seemed pretty straightforward so I rolled it into a commit in this PR. If I didn't implement it right or you think it should go in separately I can remove it. |
||
|
||
/** Emits a selection change event, called when an option has its selected state changed */ | ||
_emitChangeEvent(option: CdkOption) { | ||
this.selectionChange.emit(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; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can typically be a little bit more specific with these and specify
HTMLElement