-
Notifications
You must be signed in to change notification settings - Fork 6.8k
Combobox listbox compatible #20291
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
Combobox listbox compatible #20291
Changes from 24 commits
9d96266
381a91d
4a2fc63
b3f282b
a5b7004
0ae5554
e13d328
c5abfd5
5d0868c
0610f75
6e8893e
5541e6e
1fe8976
ec79fe4
ba873a0
0d63586
cb3b93c
e3a461d
a2dd79a
8221c12
5009b9f
4c15d53
48d05c3
744d23a
e3dd27a
22495bc
24522be
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 | ||||
---|---|---|---|---|---|---|
|
@@ -11,8 +11,8 @@ import { | |||||
ContentChildren, | ||||||
Directive, | ||||||
ElementRef, EventEmitter, forwardRef, | ||||||
Inject, | ||||||
Input, OnDestroy, OnInit, Output, | ||||||
Inject, InjectionToken, | ||||||
Input, OnDestroy, OnInit, Optional, Output, | ||||||
QueryList | ||||||
} from '@angular/core'; | ||||||
import {ActiveDescendantKeyManager, Highlightable, ListKeyManagerOption} from '@angular/cdk/a11y'; | ||||||
|
@@ -22,15 +22,19 @@ import {SelectionChange, SelectionModel} from '@angular/cdk/collections'; | |||||
import {defer, merge, Observable, Subject} from 'rxjs'; | ||||||
import {startWith, switchMap, takeUntil} from 'rxjs/operators'; | ||||||
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms'; | ||||||
import {CdkComboboxPanel} from '@angular/cdk-experimental/combobox'; | ||||||
|
||||||
let nextId = 0; | ||||||
let listboxId = 0; | ||||||
|
||||||
export const CDK_LISTBOX_VALUE_ACCESSOR: any = { | ||||||
provide: NG_VALUE_ACCESSOR, | ||||||
useExisting: forwardRef(() => CdkListbox), | ||||||
multi: true | ||||||
}; | ||||||
|
||||||
export const PANEL = new InjectionToken<CdkComboboxPanel>('CdkComboboxPanel'); | ||||||
|
||||||
@Directive({ | ||||||
selector: '[cdkOption]', | ||||||
exportAs: 'cdkOption', | ||||||
|
@@ -172,6 +176,10 @@ export class CdkOption<T = unknown> implements ListKeyManagerOption, Highlightab | |||||
} | ||||||
} | ||||||
|
||||||
getElementRef() { | ||||||
return this._elementRef; | ||||||
} | ||||||
|
||||||
/** Sets the active property to true to enable the active css class. */ | ||||||
setActiveStyles() { | ||||||
this._active = true; | ||||||
|
@@ -191,6 +199,7 @@ export class CdkOption<T = unknown> implements ListKeyManagerOption, Highlightab | |||||
exportAs: 'cdkListbox', | ||||||
host: { | ||||||
'role': 'listbox', | ||||||
'[id]': 'id', | ||||||
'(keydown)': '_keydown($event)', | ||||||
'[attr.tabindex]': '_tabIndex', | ||||||
'[attr.aria-disabled]': 'disabled', | ||||||
|
@@ -231,6 +240,8 @@ export class CdkListbox<T> implements AfterContentInit, OnDestroy, OnInit, Contr | |||||
@Output() readonly selectionChange: EventEmitter<ListboxSelectionChangeEvent<T>> = | ||||||
new EventEmitter<ListboxSelectionChangeEvent<T>>(); | ||||||
|
||||||
@Input() id = `cdk-option-${listboxId++}`; | ||||||
|
||||||
/** | ||||||
* Whether the listbox allows multiple options to be selected. | ||||||
* If `multiple` switches from `true` to `false`, all options are deselected. | ||||||
|
@@ -263,18 +274,27 @@ export class CdkListbox<T> implements AfterContentInit, OnDestroy, OnInit, Contr | |||||
|
||||||
@Input() compareWith: (o1: T, o2: T) => boolean = (a1, a2) => a1 === a2; | ||||||
|
||||||
@Input('parentPanel') private readonly _explicitPanel: CdkComboboxPanel; | ||||||
|
||||||
constructor( | ||||||
private readonly _elementRef: ElementRef, | ||||||
@Optional() @Inject(PANEL) readonly _parentPanel?: CdkComboboxPanel<T>, | ||||||
) { } | ||||||
|
||||||
ngOnInit() { | ||||||
this._selectionModel = new SelectionModel<CdkOption<T>>(this.multiple); | ||||||
} | ||||||
|
||||||
ngAfterContentInit() { | ||||||
this._initKeyManager(); | ||||||
this._initSelectionModel(); | ||||||
this._registerWithPanel(); | ||||||
|
||||||
this.optionSelectionChanges.subscribe(event => { | ||||||
this._emitChangeEvent(event.source); | ||||||
this._updateSelectionModel(event.source); | ||||||
this.setActiveOption(event.source); | ||||||
this._updatePanel(event.source); | ||||||
}); | ||||||
} | ||||||
|
||||||
|
@@ -284,6 +304,16 @@ export class CdkListbox<T> implements AfterContentInit, OnDestroy, OnInit, Contr | |||||
this._destroyed.complete(); | ||||||
} | ||||||
|
||||||
private _registerWithPanel(): void { | ||||||
if (this._parentPanel === null || this._parentPanel === undefined) { | ||||||
if (this._explicitPanel !== null && this._explicitPanel !== undefined) { | ||||||
this._explicitPanel._registerContent(this.id, 'listbox'); | ||||||
} | ||||||
} else { | ||||||
this._parentPanel._registerContent(this.id, 'listbox'); | ||||||
} | ||||||
} | ||||||
|
||||||
private _initKeyManager() { | ||||||
this._listKeyManager = new ActiveDescendantKeyManager(this._options) | ||||||
.withWrap() | ||||||
|
@@ -358,6 +388,20 @@ export class CdkListbox<T> implements AfterContentInit, OnDestroy, OnInit, Contr | |||||
this._selectionModel.deselect(option); | ||||||
} | ||||||
|
||||||
_updatePanel(option: CdkOption<T>) { | ||||||
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
|
||||||
const data = option.selected ? option.value : null; | ||||||
if (!this.multiple) { | ||||||
if (this._parentPanel === null || this._parentPanel === undefined) { | ||||||
if (this._explicitPanel !== null && this._explicitPanel !== undefined) { | ||||||
this._explicitPanel.closePanel(data); | ||||||
} | ||||||
} else { | ||||||
option.selected ? | ||||||
this._parentPanel.closePanel(option.value) : this._parentPanel.closePanel(); | ||||||
} | ||||||
} | ||||||
} | ||||||
|
||||||
/** Toggles the selected state of the active option if not disabled. */ | ||||||
private _toggleActiveOption() { | ||||||
const activeOption = this._listKeyManager.activeItem; | ||||||
|
@@ -383,6 +427,10 @@ export class CdkListbox<T> implements AfterContentInit, OnDestroy, OnInit, Contr | |||||
|
||||||
if (!this.useActiveDescendant) { | ||||||
this._activeOption.focus(); | ||||||
} else { | ||||||
if (document.activeElement === this._activeOption.getElementRef().nativeElement) { | ||||||
this._elementRef.nativeElement.focus(); | ||||||
} | ||||||
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 don't quite follow this- why would the focus move back to the listbox? 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. This was something I added last night that might not make sense. But basically if the listbox is using aria-active descendant it was my understanding that we wouldn't want to focus the options on click. The way I had it implemented the options wouldn't focus when reached via keyboard but on click they would. This moved the focus back to the listbox. 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's a bit of a strange way to do it but that's what I could figure out last night. If there's a better way to do it then I can remove this for now and just note it as a current bug to fix this weekend. |
||||||
} | ||||||
} | ||||||
|
||||||
|
@@ -420,6 +468,7 @@ export class CdkListbox<T> implements AfterContentInit, OnDestroy, OnInit, Contr | |||||
/** Updates the key manager's active item to the given option. */ | ||||||
setActiveOption(option: CdkOption<T>) { | ||||||
this._listKeyManager.updateActiveItem(option); | ||||||
this._updateActiveOption(); | ||||||
} | ||||||
|
||||||
/** | ||||||
|
@@ -450,6 +499,11 @@ export class CdkListbox<T> implements AfterContentInit, OnDestroy, OnInit, Contr | |||||
this.disabled = isDisabled; | ||||||
} | ||||||
|
||||||
/** Returns the values of the currently selected options. */ | ||||||
getSelectedValues(): T[] { | ||||||
return this._options.filter(option => option.selected).map(option => option.value); | ||||||
} | ||||||
|
||||||
/** Selects an option that has the corresponding given value. */ | ||||||
private _setSelectionByValue(values: T | T[]) { | ||||||
for (const option of this._options.toArray()) { | ||||||
|
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.
Similar for
_updatePanel
belowThere 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.
Okay that's simpler.