-
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
Merged
Merged
Combobox base #20211
Changes from all commits
Commits
Show all changes
26 commits
Select commit
Hold shift + click to select a range
06737c4
build: Added required files to listbox directory.
nielsr98 24c1ca6
build: added listbox option directive and renamed listbox directive f…
nielsr98 86d7b58
build: Added required files to listbox directory.
nielsr98 2e8ae02
build: added listbox option directive and renamed listbox directive f…
nielsr98 4c290e3
build: Added required files to listbox directory.
nielsr98 43a3d93
build: added listbox option directive and renamed listbox directive f…
nielsr98 906dea3
build: Added required files to listbox directory.
nielsr98 c289a7d
build: added listbox option directive and renamed listbox directive f…
nielsr98 4ac224b
feat(dev-app/listbox): added cdk listbox example to the dev-app.
nielsr98 df2e62f
fix(listbox): removed duplicate dep in dev-app build file.
nielsr98 f72129c
fix(listbox): deleted unused files.
nielsr98 a739263
feat(combobox): added all basic features to combobox and combobox pan…
nielsr98 0d0dea8
feat(combobox): finished basic tests and clean up.
nielsr98 a2a9c9b
fix(combobox): fixed the import path for coercion.
nielsr98 f5314db
fix(combobox): fixed import path for combobox panel.
nielsr98 cc2cb62
fix(combobox): fixed lint errors throughout.
nielsr98 5922cc9
refactor(combobox): removed unused panel test file.
nielsr98 a768b2c
refactor(combobox): changed import path for panel in combobox.
nielsr98 10d038c
refactor(combobox): cleaned up Inputs in combobox and moved aria-hasp…
nielsr98 a0e5d78
refactor(combobox): added jsdoc to the public functions.
nielsr98 a6f8750
fix(combobox): removed duplicate ContentType type and getPanelContent…
nielsr98 856b5c3
refactor(combobox): made contentId and contentType of combobox public.
nielsr98 f79781c
refactor(combobox): changed names and made coerceOpenActionProperty s…
nielsr98 681c9d7
fix(combobox): updated syntax for casting.
nielsr98 36b297a
refactor(combobox): changed casting syntax back.
nielsr98 9673d33
fix(combobox): fixed trailing whitespace.
nielsr98 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
import {Component, DebugElement} from '@angular/core'; | ||
import {ComponentFixture, TestBed, async} from '@angular/core/testing'; | ||
import {By} from '@angular/platform-browser'; | ||
import {CdkComboboxModule} from './combobox-module'; | ||
import {CdkCombobox} from './combobox'; | ||
import {dispatchMouseEvent} from '@angular/cdk/testing/private'; | ||
|
||
describe('Combobox', () => { | ||
describe('with a basic toggle trigger', () => { | ||
let fixture: ComponentFixture<ComboboxToggle>; | ||
|
||
let combobox: DebugElement; | ||
let comboboxInstance: CdkCombobox<unknown>; | ||
let comboboxElement: HTMLElement; | ||
|
||
beforeEach(async(() => { | ||
TestBed.configureTestingModule({ | ||
imports: [CdkComboboxModule], | ||
declarations: [ComboboxToggle], | ||
}).compileComponents(); | ||
})); | ||
|
||
beforeEach(() => { | ||
fixture = TestBed.createComponent(ComboboxToggle); | ||
fixture.detectChanges(); | ||
|
||
combobox = fixture.debugElement.query(By.directive(CdkCombobox)); | ||
comboboxInstance = combobox.injector.get<CdkCombobox<unknown>>(CdkCombobox); | ||
comboboxElement = combobox.nativeElement; | ||
}); | ||
|
||
it('should have the combobox role', () => { | ||
expect(comboboxElement.getAttribute('role')).toBe('combobox'); | ||
}); | ||
|
||
it('should update the aria disabled attribute', () => { | ||
comboboxInstance.disabled = true; | ||
fixture.detectChanges(); | ||
|
||
expect(comboboxElement.getAttribute('aria-disabled')).toBe('true'); | ||
|
||
comboboxInstance.disabled = false; | ||
fixture.detectChanges(); | ||
|
||
expect(comboboxElement.getAttribute('aria-disabled')).toBe('false'); | ||
}); | ||
|
||
it('should have a panel that is closed by default', () => { | ||
expect(comboboxInstance.hasPanel()).toBeTrue(); | ||
expect(comboboxInstance.isOpen()).toBeFalse(); | ||
}); | ||
|
||
it('should have an open action of click by default', () => { | ||
expect(comboboxInstance.isOpen()).toBeFalse(); | ||
|
||
dispatchMouseEvent(comboboxElement, 'click'); | ||
fixture.detectChanges(); | ||
|
||
expect(comboboxInstance.isOpen()).toBeTrue(); | ||
}); | ||
|
||
it('should not open panel when disabled', () => { | ||
expect(comboboxInstance.isOpen()).toBeFalse(); | ||
comboboxInstance.disabled = true; | ||
fixture.detectChanges(); | ||
|
||
dispatchMouseEvent(comboboxElement, 'click'); | ||
fixture.detectChanges(); | ||
|
||
expect(comboboxInstance.isOpen()).toBeFalse(); | ||
}); | ||
}); | ||
|
||
}); | ||
|
||
@Component({ | ||
template: ` | ||
<button cdkCombobox #toggleCombobox class="example-combobox" | ||
[cdkComboboxTriggerFor]="panel" | ||
[openAction]="'focus'"> | ||
No Value | ||
</button> | ||
|
||
<ng-template cdkComboboxPanel #panel="cdkComboboxPanel"> | ||
Panel Content | ||
</ng-template>`, | ||
}) | ||
class ComboboxToggle { | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,15 +6,189 @@ | |
* found in the LICENSE file at https://angular.io/license | ||
*/ | ||
|
||
import {Directive} from '@angular/core'; | ||
export type OpenAction = 'focus' | 'click' | 'downKey' | 'toggle'; | ||
export type OpenActionInput = OpenAction | OpenAction[] | string | null | undefined; | ||
|
||
import { | ||
AfterContentInit, | ||
Directive, | ||
ElementRef, | ||
EventEmitter, | ||
Input, | ||
OnDestroy, | ||
Optional, | ||
Output, ViewContainerRef | ||
} from '@angular/core'; | ||
import {CdkComboboxPanel, AriaHasPopupValue} from './combobox-panel'; | ||
import {TemplatePortal} from '@angular/cdk/portal'; | ||
import { | ||
ConnectedPosition, | ||
FlexibleConnectedPositionStrategy, | ||
Overlay, | ||
OverlayConfig, | ||
OverlayRef | ||
} from '@angular/cdk/overlay'; | ||
import {Directionality} from '@angular/cdk/bidi'; | ||
import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion'; | ||
|
||
|
||
@Directive({ | ||
selector: '[cdkCombobox]', | ||
exportAs: 'cdkCombobox', | ||
host: { | ||
'role': 'combobox' | ||
'role': 'combobox', | ||
'(click)': 'toggle()', | ||
'[attr.aria-disabled]': 'disabled', | ||
'[attr.aria-controls]': 'contentId', | ||
'[attr.aria-haspopup]': 'contentType' | ||
Comment on lines
+42
to
+43
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. Add unit tests for these attributes? |
||
} | ||
}) | ||
export class CdkCombobox<T = unknown> { | ||
export class CdkCombobox<T = unknown> implements OnDestroy, AfterContentInit { | ||
@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() | ||
value: T; | ||
|
||
@Input() | ||
get disabled(): boolean { return this._disabled; } | ||
set disabled(value: boolean) { this._disabled = coerceBooleanProperty(value); } | ||
private _disabled: boolean = false; | ||
|
||
@Input() | ||
get openAction(): OpenAction[] { | ||
return this._openActions; | ||
} | ||
set openAction(action: OpenAction[]) { | ||
this._openActions = this._coerceOpenActionProperty(action); | ||
} | ||
private _openActions: OpenAction[] = ['click']; | ||
|
||
@Output('comboboxPanelOpened') readonly opened: EventEmitter<void> = new EventEmitter<void>(); | ||
@Output('comboboxPanelClosed') readonly closed: EventEmitter<void> = new EventEmitter<void>(); | ||
@Output('panelValueChanged') readonly panelValueChanged: EventEmitter<T> = new EventEmitter<T>(); | ||
|
||
private _overlayRef: OverlayRef; | ||
private _panelContent: TemplatePortal; | ||
contentId: string = ''; | ||
contentType: AriaHasPopupValue; | ||
|
||
constructor( | ||
private readonly _elementRef: ElementRef<HTMLElement>, | ||
private readonly _overlay: Overlay, | ||
protected readonly _viewContainerRef: ViewContainerRef, | ||
@Optional() private readonly _directionality?: Directionality | ||
) {} | ||
|
||
ngAfterContentInit() { | ||
this._panel?.valueUpdated.subscribe(data => { | ||
this._setComboboxValue(data); | ||
this.close(); | ||
}); | ||
|
||
this._panel?.contentIdUpdated.subscribe(id => { | ||
this.contentId = id; | ||
}); | ||
|
||
this._panel?.contentTypeUpdated.subscribe(type => { | ||
this.contentType = type; | ||
}); | ||
} | ||
|
||
ngOnDestroy() { | ||
this.opened.complete(); | ||
this.closed.complete(); | ||
this.panelValueChanged.complete(); | ||
} | ||
|
||
/** Toggles the open state of the panel. */ | ||
toggle() { | ||
if (this.hasPanel()) { | ||
this.isOpen() ? this.close() : this.open(); | ||
} | ||
} | ||
|
||
/** If the combobox is closed and not disabled, opens the panel. */ | ||
open() { | ||
if (!this.isOpen() && !this.disabled) { | ||
this.opened.next(); | ||
this._overlayRef = this._overlayRef || this._overlay.create(this._getOverlayConfig()); | ||
this._overlayRef.attach(this._getPanelContent()); | ||
} | ||
} | ||
|
||
/** If the combobox is open and not disabled, closes the panel. */ | ||
close() { | ||
if (this.isOpen() && !this.disabled) { | ||
this.closed.next(); | ||
this._overlayRef.detach(); | ||
} | ||
} | ||
|
||
/** Returns true if panel is currently opened. */ | ||
isOpen(): boolean { | ||
return this._overlayRef ? this._overlayRef.hasAttached() : false; | ||
} | ||
|
||
/** Returns true if combobox has a child panel. */ | ||
hasPanel(): boolean { | ||
return !!this.panel; | ||
} | ||
|
||
private _setComboboxValue(value: T) { | ||
const valueChanged = (this.value !== value); | ||
this.value = value; | ||
|
||
if (valueChanged) { | ||
this.panelValueChanged.emit(value); | ||
} | ||
} | ||
|
||
private _getOverlayConfig() { | ||
return new OverlayConfig({ | ||
positionStrategy: this._getOverlayPositionStrategy(), | ||
scrollStrategy: this._overlay.scrollStrategies.block(), | ||
direction: this._directionality, | ||
}); | ||
} | ||
|
||
private _getOverlayPositionStrategy(): FlexibleConnectedPositionStrategy { | ||
return this._overlay | ||
.position() | ||
.flexibleConnectedTo(this._elementRef) | ||
.withPositions(this._getOverlayPositions()); | ||
} | ||
|
||
private _getOverlayPositions(): ConnectedPosition[] { | ||
return [ | ||
{originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top'}, | ||
{originX: 'start', originY: 'top', overlayX: 'start', overlayY: 'bottom'}, | ||
{originX: 'end', originY: 'bottom', overlayX: 'end', overlayY: 'top'}, | ||
{originX: 'end', originY: 'top', overlayX: 'end', overlayY: 'bottom'}, | ||
]; | ||
} | ||
|
||
private _getPanelContent() { | ||
const hasPanelChanged = this._panel?._templateRef !== this._panelContent?.templateRef; | ||
if (this._panel && (!this._panel || hasPanelChanged)) { | ||
this._panelContent = new TemplatePortal(this._panel._templateRef, this._viewContainerRef); | ||
} | ||
|
||
return this._panelContent; | ||
} | ||
|
||
private _coerceOpenActionProperty(input: string | OpenAction[]): OpenAction[] { | ||
let actions: OpenAction[] = []; | ||
if (typeof input === 'string') { | ||
actions.push(input as OpenAction); | ||
} else { | ||
actions = input; | ||
} | ||
return actions; | ||
} | ||
|
||
static ngAcceptInputType_disabled: BooleanInput; | ||
static ngAcceptInputType_openActions: OpenActionInput; | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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)