src/dropdown/dropdown.component.ts
Drop-down lists enable users to select one or more items from a list.
OnInit
AfterContentInit
OnDestroy
ControlValueAccessor
providers |
{
provide: NG_VALUE_ACCESSOR, useExisting: Dropdown, multi: true
}
|
selector | ibm-dropdown |
template |
|
Properties |
|
Methods |
|
Inputs |
Outputs |
HostBindings |
HostListeners |
Accessors |
constructor(elementRef: ElementRef, i18n: I18n, dropdownService: DropdownService, appRef: ApplicationRef, elementService: ElementService)
|
||||||||||||||||||
Defined in src/dropdown/dropdown.component.ts:289
|
||||||||||||||||||
Creates an instance of Dropdown.
Parameters :
|
appendInline
|
set to
Default value : |
Defined in src/dropdown/dropdown.component.ts:207
|
appendToBody
|
Deprecated. Dropdown now defaults to appending inline
Set to |
Defined in src/dropdown/dropdown.component.ts:194
|
clearText
|
Sets the optional clear button tooltip text.
Type :
Default value : |
Defined in src/dropdown/dropdown.component.ts:152
|
disableArrowKeys
|
Set to
Default value : |
Defined in src/dropdown/dropdown.component.ts:181
|
disabled
|
Set to
Default value : |
Defined in src/dropdown/dropdown.component.ts:169
|
displayValue
|
The selected value from the
Type :
Default value : |
Defined in src/dropdown/dropdown.component.ts:148
|
helperText
|
Sets the optional helper text.
Type : |
Defined in src/dropdown/dropdown.component.ts:140
|
id
|
Default value : |
Defined in src/dropdown/dropdown.component.ts:132
|
inline
|
Set to
Default value : |
Defined in src/dropdown/dropdown.component.ts:177
|
invalid
|
Set to
Default value : |
Defined in src/dropdown/dropdown.component.ts:185
|
invalidText
|
Value displayed if dropdown is in invalid state.
Default value : |
Defined in src/dropdown/dropdown.component.ts:189
|
itemValueKey
|
Specifies the property to be used as the return value to
Type : |
Defined in src/dropdown/dropdown.component.ts:229
|
label
|
Label for the dropdown.
Type : |
Defined in src/dropdown/dropdown.component.ts:136
|
menuButtonLabel
|
Accessible label for the button that opens the dropdown list.
Defaults to the
Default value : |
Defined in src/dropdown/dropdown.component.ts:234
|
placeholder
|
Value displayed if no item is selected.
Default value : |
Defined in src/dropdown/dropdown.component.ts:144
|
scrollableContainer
|
Query string for the element that contains the
Type : |
Defined in src/dropdown/dropdown.component.ts:212
|
selectedLabel
|
Provides the label for the "# selected" text.
Defaults to the
Default value : |
Defined in src/dropdown/dropdown.component.ts:239
|
size
|
Size to render the dropdown field.
Type :
Default value : |
Defined in src/dropdown/dropdown.component.ts:156
|
skeleton
|
Set to
Default value : |
Defined in src/dropdown/dropdown.component.ts:173
|
theme
|
Type :
Default value : |
Defined in src/dropdown/dropdown.component.ts:165
|
type
|
Defines whether or not the
Type :
Default value : |
Defined in src/dropdown/dropdown.component.ts:161
|
value
|
Deprecated. Use
Type : |
Defined in src/dropdown/dropdown.component.ts:218
|
close
|
Emits event notifying to other classes that the $event Type: EventEmitter<any>
|
Defined in src/dropdown/dropdown.component.ts:251
|
onClose
|
Emits event notifying to other classes that the $event Type: EventEmitter<any>
|
Defined in src/dropdown/dropdown.component.ts:247
|
selected
|
Emits selection events. $event Type: EventEmitter<Object>
|
Defined in src/dropdown/dropdown.component.ts:243
|
class.bx--dropdown__wrapper |
class.bx--dropdown__wrapper:
|
Default value : true
|
Defined in src/dropdown/dropdown.component.ts:266
|
keydown |
Arguments : '$event'
|
keydown(event: KeyboardEvent)
|
Defined in src/dropdown/dropdown.component.ts:440
|
Adds keyboard functionality for navigation, selection and closing of the |
_appendToBody |
_appendToBody()
|
Defined in src/dropdown/dropdown.component.ts:599
|
Creates the
Returns :
void
|
_appendToDropdown |
_appendToDropdown()
|
Defined in src/dropdown/dropdown.component.ts:591
|
Creates the
Returns :
void
|
_keyboardNav | ||||||
_keyboardNav(event: KeyboardEvent)
|
||||||
Defined in src/dropdown/dropdown.component.ts:570
|
||||||
Handles keyboard events so users are controlling the
Parameters :
Returns :
void
|
_noop |
_noop()
|
Defined in src/dropdown/dropdown.component.ts:550
|
Returns :
void
|
_outsideClick | ||||
_outsideClick(event)
|
||||
Defined in src/dropdown/dropdown.component.ts:554
|
||||
Handles clicks outside of the
Parameters :
Returns :
void
|
_outsideKey | ||||
_outsideKey(event)
|
||||
Defined in src/dropdown/dropdown.component.ts:562
|
||||
Parameters :
Returns :
void
|
clearSelected |
clearSelected()
|
Defined in src/dropdown/dropdown.component.ts:533
|
Returns :
void
|
closedDropdownNavigation | ||||
closedDropdownNavigation(event)
|
||||
Defined in src/dropdown/dropdown.component.ts:472
|
||||
Parameters :
Returns :
void
|
closeMenu |
closeMenu()
|
Defined in src/dropdown/dropdown.component.ts:668
|
Collapsing the dropdown menu and removing unnecessary
Returns :
void
|
getDisplayStringValue |
getDisplayStringValue()
|
Defined in src/dropdown/dropdown.component.ts:492
|
Returns the display value if there is a selection and displayValue is set, if there is just a selection the ListItem content property will be returned, otherwise the placeholder will be returned.
Returns :
Observable<string>
|
getRenderTemplateContext |
getRenderTemplateContext()
|
Defined in src/dropdown/dropdown.component.ts:513
|
Returns :
{ items: any; item?: undefined; } | { item: any; items?: undefined; } | { items?: undefined; item...
|
getSelectedCount |
getSelectedCount()
|
Defined in src/dropdown/dropdown.component.ts:527
|
Returns :
number
|
isRenderString |
isRenderString()
|
Defined in src/dropdown/dropdown.component.ts:509
|
Returns :
boolean
|
Public isTemplate | ||||
isTemplate(value)
|
||||
Defined in src/dropdown/dropdown.component.ts:705
|
||||
Parameters :
Returns :
boolean
|
ngAfterContentInit |
ngAfterContentInit()
|
Defined in src/dropdown/dropdown.component.ts:314
|
Initializes classes and subscribes to events for single or multi selection.
Returns :
void
|
ngOnDestroy |
ngOnDestroy()
|
Defined in src/dropdown/dropdown.component.ts:357
|
Removing the
Returns :
void
|
ngOnInit |
ngOnInit()
|
Defined in src/dropdown/dropdown.component.ts:305
|
Updates the
Returns :
void
|
onBlur |
onBlur()
|
Defined in src/dropdown/dropdown.component.ts:404
|
Returns :
void
|
openMenu |
openMenu()
|
Defined in src/dropdown/dropdown.component.ts:610
|
Expands the dropdown menu in the view.
Returns :
void
|
registerOnChange | ||||||
registerOnChange(fn: any)
|
||||||
Defined in src/dropdown/dropdown.component.ts:408
|
||||||
Parameters :
Returns :
void
|
registerOnTouched | ||||||
registerOnTouched(fn: any)
|
||||||
Defined in src/dropdown/dropdown.component.ts:415
|
||||||
Registering the function injected to control the touch use of the
Parameters :
Returns :
void
|
setDisabledState | ||||||||
setDisabledState(isDisabled: boolean)
|
||||||||
Defined in src/dropdown/dropdown.component.ts:431
|
||||||||
ex:
Parameters :
Returns :
void
|
toggleMenu |
toggleMenu()
|
Defined in src/dropdown/dropdown.component.ts:697
|
Controls toggling menu states between open/expanded and closed/collapsed.
Returns :
void
|
valueSelected |
valueSelected()
|
Defined in src/dropdown/dropdown.component.ts:545
|
Returns
Returns :
boolean
|
writeValue | ||||||
writeValue(value: any)
|
||||||
Defined in src/dropdown/dropdown.component.ts:366
|
||||||
Propagates the injected
Parameters :
Returns :
void
|
dropdownButton |
dropdownButton:
|
Decorators :
@ViewChild('dropdownButton')
|
Defined in src/dropdown/dropdown.component.ts:260
|
Maintains a reference to the view DOM element of the |
Static dropdownCount |
dropdownCount:
|
Type : number
|
Default value : 0
|
Defined in src/dropdown/dropdown.component.ts:131
|
dropdownMenu |
dropdownMenu:
|
Decorators :
@ViewChild('dropdownMenu')
|
Defined in src/dropdown/dropdown.component.ts:264
|
ViewChid of the dropdown view. |
dropUp |
dropUp:
|
Default value : false
|
Defined in src/dropdown/dropdown.component.ts:275
|
controls wether the |
keyboardNav |
keyboardNav:
|
Default value : this._keyboardNav.bind(this)
|
Defined in src/dropdown/dropdown.component.ts:282
|
menuIsClosed |
menuIsClosed:
|
Default value : true
|
Defined in src/dropdown/dropdown.component.ts:270
|
Set to |
noop |
noop:
|
Default value : this._noop.bind(this)
|
Defined in src/dropdown/dropdown.component.ts:279
|
Protected onTouchedCallback |
onTouchedCallback:
|
Type : function
|
Default value : this._noop
|
Defined in src/dropdown/dropdown.component.ts:286
|
outsideClick |
outsideClick:
|
Default value : this._outsideClick.bind(this)
|
Defined in src/dropdown/dropdown.component.ts:280
|
outsideKey |
outsideKey:
|
Default value : this._outsideKey.bind(this)
|
Defined in src/dropdown/dropdown.component.ts:281
|
propagateChange |
propagateChange:
|
Default value : (_: any) => {}
|
Defined in src/dropdown/dropdown.component.ts:422
|
function passed in by |
view |
view:
|
Type : AbstractDropdownView
|
Decorators :
@ContentChild(AbstractDropdownView)
|
Defined in src/dropdown/dropdown.component.ts:256
|
Maintains a reference to the |
Protected visibilitySubscription |
visibilitySubscription:
|
Default value : new Subscription()
|
Defined in src/dropdown/dropdown.component.ts:284
|
Protected writtenValue |
writtenValue:
|
Type : []
|
Default value : []
|
Defined in src/dropdown/dropdown.component.ts:289
|
appendToBody | ||||
getappendToBody()
|
||||
Defined in src/dropdown/dropdown.component.ts:201
|
||||
setappendToBody(v)
|
||||
Defined in src/dropdown/dropdown.component.ts:194
|
||||
Deprecated. Dropdown now defaults to appending inline
Set to
Parameters :
Returns :
void
|
value | ||||||
getvalue()
|
||||||
Defined in src/dropdown/dropdown.component.ts:223
|
||||||
setvalue(newValue: string)
|
||||||
Defined in src/dropdown/dropdown.component.ts:218
|
||||||
Deprecated. Use
Parameters :
Returns :
void
|
import {
Component,
Input,
Output,
EventEmitter,
ElementRef,
ContentChild,
OnInit,
ViewChild,
AfterContentInit,
HostListener,
OnDestroy,
HostBinding,
TemplateRef,
ApplicationRef
} from "@angular/core";
import { NG_VALUE_ACCESSOR, ControlValueAccessor } from "@angular/forms";
// Observable import is required here so typescript can compile correctly
import {
Observable,
of,
Subscription
} from "rxjs";
import { AbstractDropdownView } from "./abstract-dropdown-view.class";
import { I18n } from "./../i18n/i18n.module";
// Import needed to avoid compiler issue.
import { ListItem } from "./list-item.interface";
import { DropdownService } from "./dropdown.service";
import { ElementService } from "./../utils/utils.module";
/**
* Drop-down lists enable users to select one or more items from a list.
*
* [See demo](../../?path=/story/dropdown--basic)
*
* <example-url>../../iframe.html?id=dropdown--basic</example-url>
*/
@Component({
selector: "ibm-dropdown",
template: `
<label *ngIf="label" [for]="id" class="bx--label">
<ng-container *ngIf="!isTemplate(label)">{{label}}</ng-container>
<ng-template *ngIf="isTemplate(label)" [ngTemplateOutlet]="label"></ng-template>
</label>
<div *ngIf="helperText" class="bx--form__helper-text">
<ng-container *ngIf="!isTemplate(helperText)">{{helperText}}</ng-container>
<ng-template *ngIf="isTemplate(helperText)" [ngTemplateOutlet]="helperText"></ng-template>
</div>
<div
[id]="id"
class="bx--dropdown bx--list-box"
[ngClass]="{
'bx--dropdown--light': theme === 'light',
'bx--list-box--inline': inline,
'bx--skeleton': skeleton,
'bx--dropdown--disabled bx--list-box--disabled': disabled,
'bx--dropdown--invalid': invalid
}">
<div
type="button"
#dropdownButton
class="bx--list-box__field"
[ngClass]="{'a': !menuIsClosed}"
[attr.aria-expanded]="!menuIsClosed"
[attr.aria-disabled]="disabled"
(click)="disabled ? $event.stopPropagation() : toggleMenu()"
(blur)="onBlur()"
[attr.disabled]="disabled ? true : null"
[tabindex]="disabled ? -1 : 0">
<div
(click)="clearSelected()"
(keydown.enter)="clearSelected()"
*ngIf="type === 'multi' && getSelectedCount() > 0"
class="bx--tag--filter bx--list-box__selection--multi"
tabindex="0"
[title]="clearText">
{{getSelectedCount()}}
<svg
focusable="false"
preserveAspectRatio="xMidYMid meet"
style="will-change: transform;"
role="img"
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 16 16"
aria-hidden="true">
<path d="M12 4.7l-.7-.7L8 7.3 4.7 4l-.7.7L7.3 8 4 11.3l.7.7L8 8.7l3.3 3.3.7-.7L8.7 8z"></path>
</svg>
</div>
<span *ngIf="isRenderString()" class="bx--list-box__label">{{getDisplayStringValue() | async}}</span>
<ng-template
*ngIf="!isRenderString()"
[ngTemplateOutletContext]="getRenderTemplateContext()"
[ngTemplateOutlet]="displayValue">
</ng-template>
<svg ibmIconWarningFilled16
*ngIf="invalid"
class="bx--dropdown__invalid-icon">
</svg>
<ibm-icon-chevron-down16
*ngIf="!skeleton"
class="bx--list-box__menu-icon"
[attr.aria-label]="menuButtonLabel"
[ngClass]="{'bx--list-box__menu-icon--open': !menuIsClosed }">
</ibm-icon-chevron-down16>
</div>
<div
#dropdownMenu
[ngClass]="{
'drop-up': dropUp
}">
<ng-content *ngIf="!menuIsClosed"></ng-content>
</div>
</div>
<div *ngIf="invalid" class="bx--form-requirement">
{{invalidText}}
</div>
`,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: Dropdown,
multi: true
}
]
})
export class Dropdown implements OnInit, AfterContentInit, OnDestroy, ControlValueAccessor {
static dropdownCount = 0;
@Input() id = `dropdown-${Dropdown.dropdownCount++}`;
/**
* Label for the dropdown.
*/
@Input() label: string | TemplateRef<any>;
/**
* Sets the optional helper text.
*/
@Input() helperText: string | TemplateRef<any>;
/**
* Value displayed if no item is selected.
*/
@Input() placeholder = "";
/**
* The selected value from the `Dropdown`. Can be a string or template.
*/
@Input() displayValue: string | TemplateRef<any> = "";
/**
* Sets the optional clear button tooltip text.
*/
@Input() clearText: string = this.i18n.get().DROPDOWN.CLEAR;
/**
* Size to render the dropdown field.
*/
@Input() size: "sm" | "md" | "lg" = "md";
/**
* Defines whether or not the `Dropdown` supports selecting multiple items as opposed to single
* item selection.
*/
@Input() type: "single" | "multi" = "single";
/**
* `light` or `dark` dropdown theme
*/
@Input() theme: "light" | "dark" = "dark";
/**
* Set to `true` to disable the dropdown.
*/
@Input() disabled = false;
/**
* Set to `true` for a loading dropdown.
*/
@Input() skeleton = false;
/**
* Set to `true` for an inline dropdown.
*/
@Input() inline = false;
/**
* Set to `true` for a dropdown without arrow key activation.
*/
@Input() disableArrowKeys = false;
/**
* Set to `true` for invalid state.
*/
@Input() invalid = false;
/**
* Value displayed if dropdown is in invalid state.
*/
@Input() invalidText = "";
/**
* Deprecated. Dropdown now defaults to appending inline
* Set to `true` if the `Dropdown` is to be appended to the DOM body.
*/
@Input() set appendToBody (v) {
console.warn("`appendToBody` has been deprecated. Dropdowns now append to the body by default.");
console.warn("Ensure you have an `ibm-placeholder` in your app.");
console.warn("Use `appendInline` if you need to position your dropdowns within the normal page flow.");
this.appendInline = !v;
}
get appendToBody() {
return !this.appendInline;
}
/**
* set to `true` to place the dropdown view inline with the component
*/
@Input() appendInline = false;
/**
* Query string for the element that contains the `Dropdown`.
* Used to trigger closing the dropdown if it scrolls outside of the viewport of the `scrollableContainer`.
*/
@Input() scrollableContainer: string;
/**
* Deprecated. Use `itemValueKey` instead.
* Specifies the property to be used as the return value to `ngModel`
* @deprecated use itemValueKey instead
*/
@Input() set value (newValue: string) {
console.warn("Dropdown `value` property has been deprecated. Use `itemValueKey` instead");
this.itemValueKey = newValue;
}
get value() {
return this.itemValueKey;
}
/**
* Specifies the property to be used as the return value to `ngModel`
*/
@Input() itemValueKey: string;
/**
* Accessible label for the button that opens the dropdown list.
* Defaults to the `DROPDOWN.OPEN` value from the i18n service.
*/
@Input() menuButtonLabel = this.i18n.get().DROPDOWN.OPEN;
/**
* Provides the label for the "# selected" text.
* Defaults to the `DROPDOWN.SELECTED` value from the i18n service.
*/
@Input() selectedLabel = this.i18n.get().DROPDOWN.SELECTED;
/**
* Emits selection events.
*/
@Output() selected: EventEmitter<Object> = new EventEmitter<Object>();
/**
* Emits event notifying to other classes that the `Dropdown` has been closed (collapsed).
*/
@Output() onClose: EventEmitter<any> = new EventEmitter<any>();
/**
* Emits event notifying to other classes that the `Dropdown` has been closed (collapsed).
*/
@Output() close: EventEmitter<any> = new EventEmitter<any>();
/**
* Maintains a reference to the `AbstractDropdownView` object within the content DOM.
*/
@ContentChild(AbstractDropdownView) view: AbstractDropdownView;
/**
* Maintains a reference to the view DOM element of the `Dropdown` button.
*/
@ViewChild("dropdownButton") dropdownButton;
/**
* ViewChid of the dropdown view.
*/
@ViewChild("dropdownMenu") dropdownMenu;
@HostBinding("class.bx--dropdown__wrapper") hostClass = true;
/**
* Set to `true` if the dropdown is closed (not expanded).
*/
menuIsClosed = true;
/**
* controls wether the `drop-up` class is applied
*/
dropUp = false;
// .bind creates a new function, so we declare the methods below
// but .bind them up here
noop = this._noop.bind(this);
outsideClick = this._outsideClick.bind(this);
outsideKey = this._outsideKey.bind(this);
keyboardNav = this._keyboardNav.bind(this);
protected visibilitySubscription = new Subscription();
protected onTouchedCallback: () => void = this._noop;
// primarily used to capture and propagate input to `writeValue` before the content is available
protected writtenValue = [];
/**
* Creates an instance of Dropdown.
*/
constructor(
protected elementRef: ElementRef,
protected i18n: I18n,
protected dropdownService: DropdownService,
protected appRef: ApplicationRef,
protected elementService: ElementService) {}
/**
* Updates the `type` property in the `@ContentChild`.
* The `type` property specifies whether the `Dropdown` allows single selection or multi selection.
*/
ngOnInit() {
if (this.view) {
this.view.type = this.type;
}
}
/**
* Initializes classes and subscribes to events for single or multi selection.
*/
ngAfterContentInit() {
if (!this.view) {
return;
}
if (this.writtenValue && this.writtenValue.length) {
this.writeValue(this.writtenValue);
}
this.view.type = this.type;
this.view.size = this.size;
this.view.select.subscribe(event => {
if (this.type === "multi") {
// if we have a `value` selector and selected items map them appropriately
if (this.itemValueKey && this.view.getSelected()) {
const values = this.view.getSelected().map(item => item[this.itemValueKey]);
this.propagateChange(values);
// otherwise just pass up the values from `getSelected`
} else {
this.propagateChange(this.view.getSelected());
}
} else {
this.closeMenu();
if (event.item && event.item.selected) {
if (this.itemValueKey) {
this.propagateChange(event.item[this.itemValueKey]);
} else {
this.propagateChange(event.item);
}
} else {
this.propagateChange(null);
}
}
// only emit selected for "organic" selections
if (event && !event.isUpdate) {
this.selected.emit(event);
}
// manually tick the app so the view picks up any changes
this.appRef.tick();
});
}
/**
* Removing the `Dropdown` from the body if it is appended to the body.
*/
ngOnDestroy() {
if (this.appendToBody) {
this._appendToDropdown();
}
}
/**
* Propagates the injected `value`.
*/
writeValue(value: any) {
// cache the written value so we can use it in `AfterContentInit`
this.writtenValue = value;
this.view.onItemsReady(() => {
// propagate null/falsey as an array (deselect everything)
if (!value) {
this.view.propagateSelected([value]);
} else if (this.type === "single") {
if (this.itemValueKey) {
// clone the specified item and update its state
const newValue = Object.assign({}, this.view.getListItems().find(item => item[this.itemValueKey] === value));
newValue.selected = true;
this.view.propagateSelected([newValue]);
} else {
// pass the singular value as an array of ListItem
this.view.propagateSelected([value]);
}
} else {
if (this.itemValueKey) {
// clone the items and update their state based on the received value array
// this way we don't lose any additional metadata that may be passed in via the `items` Input
let newValues = [];
for (const v of value) {
for (const item of this.view.getListItems()) {
if (item[this.itemValueKey] === v) {
newValues.push(Object.assign({}, item, { selected: true }));
}
}
}
this.view.propagateSelected(newValues);
} else {
// we can safely assume we're passing an array of `ListItem`s
this.view.propagateSelected(value);
}
}
});
}
onBlur() {
this.onTouchedCallback();
}
registerOnChange(fn: any) {
this.propagateChange = fn;
}
/**
* Registering the function injected to control the touch use of the `Dropdown`.
*/
registerOnTouched(fn: any) {
this.onTouchedCallback = fn;
}
/**
* function passed in by `registerOnChange`
*/
propagateChange = (_: any) => {};
/**
* `ControlValueAccessor` method to programmatically disable the dropdown.
*
* ex: `this.formGroup.get("myDropdown").disable();`
*
* @param isDisabled `true` to disable the input
*/
setDisabledState(isDisabled: boolean) {
this.disabled = isDisabled;
}
/**
* Adds keyboard functionality for navigation, selection and closing of the `Dropdown`.
*/
@HostListener("keydown", ["$event"])
// "Esc", "Spacebar", "Down", and "Up" are IE specific values
onKeyDown(event: KeyboardEvent) {
if ((event.key === "Escape" || event.key === "Esc") && !this.menuIsClosed) {
event.stopImmediatePropagation(); // don't unintentionally close other widgets that listen for Escape
}
if (event.key === "Escape" || event.key === "Esc") {
event.preventDefault();
this.closeMenu();
this.dropdownButton.nativeElement.focus();
} else if (this.menuIsClosed && (event.key === " " || event.key === "ArrowDown" || event.key === "ArrowUp" ||
event.key === "Spacebar" || event.key === "Down" || event.key === "Up")) {
if (this.disableArrowKeys && (event.key === "ArrowDown" || event.key === "ArrowUp" || event.key === "Down" || event.key === "Up")) {
return;
}
event.preventDefault();
this.openMenu();
}
if (!this.menuIsClosed && event.key === "Tab" && this.dropdownMenu.nativeElement.contains(event.target as Node)) {
this.closeMenu();
}
if (!this.menuIsClosed && event.key === "Tab" && event.shiftKey) {
this.closeMenu();
}
if (this.type === "multi") { return; }
if (this.menuIsClosed) {
this.closedDropdownNavigation(event);
}
}
closedDropdownNavigation(event) {
// "Down", and "Up" are IE specific values
if (event.key === "ArrowDown" || event.key === "Down") {
event.preventDefault();
this.view.getCurrentItem().selected = false;
let item = this.view.getNextItem();
if (item) { item.selected = true; }
} else if (event.key === "ArrowUp" || event.key === "Up") {
event.preventDefault();
this.view.getCurrentItem().selected = false;
let item = this.view.getPrevItem();
if (item) { item.selected = true; }
}
}
/**
* Returns the display value if there is a selection and displayValue is set,
* if there is just a selection the ListItem content property will be returned,
* otherwise the placeholder will be returned.
*/
getDisplayStringValue(): Observable<string> {
if (!this.view) {
return;
}
let selected = this.view.getSelected();
if (selected.length && (!this.displayValue || !this.isRenderString())) {
if (this.type === "multi") {
return of(this.placeholder);
} else {
return of(selected[0].content);
}
} else if (selected.length && this.isRenderString()) {
return of(this.displayValue as string);
}
return of(this.placeholder);
}
isRenderString(): boolean {
return typeof this.displayValue === "string";
}
getRenderTemplateContext() {
if (!this.view) {
return;
}
let selected = this.view.getSelected();
if (this.type === "multi") {
return {items: selected};
} else if (selected && selected.length > 0) {
return {item: selected[0]}; // this is to be compatible with the dropdown-list template
} else {
return {};
}
}
getSelectedCount(): number {
if (this.view.getSelected()) {
return this.view.getSelected().length;
}
}
clearSelected() {
if (this.disabled) { return; }
for (const item of this.view.getListItems()) {
item.selected = false;
}
this.selected.emit([]);
this.propagateChange([]);
}
/**
* Returns `true` if there is a value selected.
*/
valueSelected(): boolean {
if (this.view.getSelected()) { return true; }
return false;
}
_noop() {}
/**
* Handles clicks outside of the `Dropdown`.
*/
_outsideClick(event) {
if (!this.elementRef.nativeElement.contains(event.target) &&
// if we're appendToBody the list isn't within the _elementRef,
// so we've got to check if our target is possibly in there too.
!this.dropdownMenu.nativeElement.contains(event.target)) {
this.closeMenu();
}
}
_outsideKey(event) {
if (!this.menuIsClosed && event.key === "Tab" && this.dropdownMenu.nativeElement.contains(event.target as Node)) {
this.closeMenu();
}
}
/**
* Handles keyboard events so users are controlling the `Dropdown` instead of unintentionally controlling outside elements.
*/
_keyboardNav(event: KeyboardEvent) {
// "Esc" is an IE specific value
if ((event.key === "Escape" || event.key === "Esc") && !this.menuIsClosed) {
event.stopImmediatePropagation(); // don't unintentionally close modal if inside of it
}
if (event.key === "Escape" || event.key === "Esc") {
event.preventDefault();
this.closeMenu();
this.dropdownButton.nativeElement.focus();
} else if (!this.menuIsClosed && event.key === "Tab") {
// this way focus will start on the next focusable item from the dropdown
// not the top of the body!
this.dropdownButton.nativeElement.focus();
this.dropdownButton.nativeElement.dispatchEvent(new KeyboardEvent("keydown", {bubbles: true, cancelable: true, key: "Tab"}));
this.closeMenu();
}
}
/**
* Creates the `Dropdown` list appending it to the dropdown parent object instead of the body.
*/
_appendToDropdown() {
this.dropdownService.appendToDropdown(this.elementRef.nativeElement);
this.dropdownMenu.nativeElement.removeEventListener("keydown", this.keyboardNav, true);
}
/**
* Creates the `Dropdown` list as an element that is appended to the DOM body.
*/
_appendToBody() {
this.dropdownService.appendToBody(
this.dropdownButton.nativeElement,
this.dropdownMenu.nativeElement,
this.elementRef.nativeElement.className);
this.dropdownMenu.nativeElement.addEventListener("keydown", this.keyboardNav, true);
}
/**
* Expands the dropdown menu in the view.
*/
openMenu() {
// prevents the dropdown from opening when list of items is empty
if (this.view.getListItems().length === 0) {
return;
}
this.menuIsClosed = false;
// move the dropdown list to the body if we're not appending inline
// and position it relative to the dropdown wrapper
if (!this.appendInline) {
const target = this.dropdownButton.nativeElement;
const parent = this.elementRef.nativeElement;
this.visibilitySubscription = this.elementService
.visibility(target, parent)
.subscribe(value => {
this.dropdownService.updatePosition(this.dropdownButton.nativeElement);
if (!value.visible) {
this.closeMenu();
}
}
);
this._appendToBody();
}
// set the dropdown menu to drop up if it's near the bottom of the screen
// setTimeout lets us measure after it's visible in the DOM
setTimeout(() => {
const menu = this.dropdownMenu.nativeElement;
const boundingClientRect = menu.getBoundingClientRect();
if (boundingClientRect.bottom > window.innerHeight) {
// min height of 100px
if (window.innerHeight - boundingClientRect.top > 100) {
// remove the conditional once this api is settled and part of abstract-dropdown-view.class
if (this.view["enableScroll"]) {
this.view["enableScroll"]();
}
} else {
this.dropUp = true;
}
} else {
this.dropUp = false;
}
}, 0);
// we bind noop to document.body.firstElementChild to allow safari to fire events
// from document. Then we unbind everything later to keep things light.
document.body.firstElementChild.addEventListener("click", this.noop, true);
document.body.firstElementChild.addEventListener("keydown", this.noop, true);
document.addEventListener("click", this.outsideClick, true);
document.addEventListener("keydown", this.outsideKey, true);
setTimeout(() => this.view.initFocus(), 0);
}
/**
* Collapsing the dropdown menu and removing unnecessary `EventListeners`.
*/
closeMenu() {
// return early if the menu is already closed
if (this.menuIsClosed) { return; }
this.menuIsClosed = true;
this.onClose.emit();
this.close.emit();
// focus the trigger button when we close ...
this.dropdownButton.nativeElement.focus();
// remove the conditional once this api is settled and part of abstract-dropdown-view.class
if (this.view["disableScroll"]) {
this.view["disableScroll"]();
}
// move the list back in the component on close
if (!this.appendInline) {
this.visibilitySubscription.unsubscribe();
this._appendToDropdown();
}
document.body.firstElementChild.removeEventListener("click", this.noop, true);
document.body.firstElementChild.removeEventListener("keydown", this.noop, true);
document.removeEventListener("click", this.outsideClick, true);
document.removeEventListener("keydown", this.outsideKey, true);
}
/**
* Controls toggling menu states between open/expanded and closed/collapsed.
*/
toggleMenu() {
if (this.menuIsClosed) {
this.openMenu();
} else {
this.closeMenu();
}
}
public isTemplate(value) {
return value instanceof TemplateRef;
}
}