-
Notifications
You must be signed in to change notification settings - Fork 6.8k
Combobox base #20211
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 base #20211
Conversation
…el, and some tests.
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.
Got most of the way through before my next meeting- will finish looking later today!
|
||
@Directive({ | ||
selector: 'ng-template[cdkComboboxPanel]', | ||
exportAs: 'cdkComboboxPanel', | ||
host: { | ||
'aria-controls': 'contentId', |
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.
I don't think these aria attributes would go on the panel, they would go on the combobox trigger. The values are only necessary in the panel so that it can pass them back to the trigger
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.
So I guess using a Subject for these two as well would be appropriate? Then on register of the content the panel updated the Subject and the trigger can grab the new value.
}) | ||
export class CdkComboboxPanel<T = unknown> { | ||
|
||
valueUpdated: Subject<T> = new Subject<T>(); | ||
contentId: string = ''; | ||
contentType: string = ''; |
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.
Rather than being string
, this could be the allowed set of values for aria-haspopup
contentType: 'false' | 'true' | 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog';
|
||
constructor(readonly _templateRef: TemplateRef<unknown>) {} | ||
|
||
closePanel(data?: T) { |
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.
Add JsDoc for public methods
(here and below)
_registerContent(contentId: string, contentType: string) { | ||
this.contentId = contentId; | ||
if (contentType !== 'listbox' && contentType !== 'dialog') { | ||
throw Error('CdkComboboxPanel content must be either a listbox or dialog'); |
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.
I might tweak the wording here that this only currently supports "listbox" or "dialog"
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.
Gotcha will do.
'role': 'combobox' | ||
'role': 'combobox', | ||
'(click)': 'toggle()', | ||
'[attr.aria-disabled]': 'disabled' |
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.
The aria-haspopup
and aria-controls
should go here.
I also realized just now that there's a mode of interaction where this element controls a listbox while focus remains on the combobox element and it uses aria-activedescendant
to indicate which listbox option is active. Seems like something to consider later on
@Input('triggerFor') | ||
get comboboxPanel(): CdkComboboxPanel<T> | undefined { | ||
return this._comboboxPanel; | ||
} | ||
set comboboxPanel(panel: CdkComboboxPanel<T> | undefined) { | ||
this._comboboxPanel = panel; | ||
} | ||
private _comboboxPanel: CdkComboboxPanel<T> | undefined; |
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.
@Input('triggerFor') | |
get comboboxPanel(): CdkComboboxPanel<T> | undefined { | |
return this._comboboxPanel; | |
} | |
set comboboxPanel(panel: CdkComboboxPanel<T> | undefined) { | |
this._comboboxPanel = panel; | |
} | |
private _comboboxPanel: CdkComboboxPanel<T> | undefined; | |
@Input('cdkComboboxTriggerFor') | |
get panel(): CdkComboboxPanel<T> | undefined { | |
return this._panel; | |
} | |
set panel(panel: CdkComboboxPanel<T> | undefined) { | |
this._panel= panel; | |
} | |
private _panel: CdkComboboxPanel<T> | undefined; |
@Input() | ||
get value(): T { | ||
return this._value; | ||
} | ||
set value(val: T) { | ||
this._value = val; | ||
} | ||
private _value: T; |
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.
@Input() | |
get value(): T { | |
return this._value; | |
} | |
set value(val: T) { | |
this._value = val; | |
} | |
private _value: T; | |
@Input() | |
value: T; |
I'd avoid adding the getter/setter until you need it
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.
Ah okay.
get disabled(): boolean { | ||
return this._disabled; | ||
} | ||
set disabled(value: boolean) { | ||
this._disabled = coerceBooleanProperty(value); | ||
} | ||
private _disabled: boolean = 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.
optional: you can condense simple getter/setters like this
get disabled(): boolean { | |
return this._disabled; | |
} | |
set disabled(value: boolean) { | |
this._disabled = coerceBooleanProperty(value); | |
} | |
private _disabled: boolean = false; | |
get disabled(): boolean { return this._disabled; } | |
set disabled(value: boolean) { this._disabled = coerceBooleanProperty(value); } | |
private _disabled: boolean = false; |
]; | ||
} | ||
|
||
private _getPortal() { |
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.
private _getPortal() { | |
private _getPanelContent() { |
…opup and aria-controls to the combobox not the panel.
@@ -6,12 +6,40 @@ | |||
* found in the LICENSE file at https://angular.io/license | |||
*/ | |||
|
|||
import {Directive} from '@angular/core'; | |||
export type ContentType = 'false' | 'true' | 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog'; |
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.
export type ContentType = 'false' | 'true' | 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog'; | |
export type AriaHasPopupValue = 'false' | 'true' | 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog'; |
get comboboxPanel(): CdkComboboxPanel<T> | undefined { return this._panel; } | ||
set comboboxPanel(panel: CdkComboboxPanel<T> | undefined) { this._panel = panel; } |
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.
get comboboxPanel(): CdkComboboxPanel<T> | undefined { return this._panel; } | |
set comboboxPanel(panel: CdkComboboxPanel<T> | undefined) { this._panel = panel; } | |
get panel(): CdkComboboxPanel<T> | undefined { return this._panel; } | |
set panel(panel: CdkComboboxPanel<T> | undefined) { this._panel = panel; } |
return this._panelContent; | ||
} | ||
|
||
private _coerceOpenActionProperty(input: any): OpenAction[] { |
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.
Rather than any
, this can be the union of all the types this can handle
const actions: OpenAction[] = []; | ||
|
||
const inputArray = coerceArray(input); | ||
for (const action of inputArray) { | ||
if (action !== 'focus' && action !== 'click' && action !== 'downKey' && action !== 'toggle') { | ||
throw Error('Not a valid open action.'); | ||
} else { | ||
actions.push(action); | ||
} | ||
} | ||
|
||
return actions; |
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.
I would just have this whole function be:
const actions: OpenAction[] = []; | |
const inputArray = coerceArray(input); | |
for (const action of inputArray) { | |
if (action !== 'focus' && action !== 'click' && action !== 'downKey' && action !== 'toggle') { | |
throw Error('Not a valid open action.'); | |
} else { | |
actions.push(action); | |
} | |
} | |
return actions; | |
return input; |
And add the rest of the behavior when you add support for splitting a string
'[attr.aria-controls]': 'contentId', | ||
'[attr.aria-haspopup]': 'contentType' |
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.
Add unit tests for these attributes?
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.
LGTM
This issue has been automatically locked due to inactivity. Read more about our automatic conversation locking policy. This action has been performed automatically by a bot. |
Added most base features of combobox and panel. By default this combobox will always trigger on click. More unit tests are required. Only basic unit tests are implemented thus far.