src/dropdown/dropdown.component.ts
Drop-down lists enable users to select one or more items from a list.
By default the dropdown will try to figure out the best placement for the dropdown list.
If it's not contained within any scrolling elements, it will open inline, if it is
contained within a scrolling container it will try to open in the body, or an cds-placeholder
.
To control this behavior you can use the appendInline
input:
[appendInline]="null"
is the default (auto detection)[appendInline]="false"
will always append to the body/cds-placeholder
[appendInline]="true"
will always append inline (next to the dropdown button)Get started with importing the module:
Example :import { DropdownModule } from 'carbon-components-angular';
OnInit
AfterContentInit
AfterViewInit
OnDestroy
ControlValueAccessor
providers |
{
provide: NG_VALUE_ACCESSOR, useExisting: Dropdown, multi: true
}
|
selector | cds-dropdown, ibm-dropdown |
template |
|
Properties |
|
Methods |
|
Inputs |
Outputs |
HostBindings |
HostListeners |
Accessors |
constructor(elementRef: ElementRef, i18n: I18n, dropdownService: DropdownService, elementService: ElementService)
|
|||||||||||||||
Defined in src/dropdown/dropdown.component.ts:376
|
|||||||||||||||
Creates an instance of Dropdown.
Parameters :
|
appendInline | |
Type : boolean
|
|
Default value : null
|
|
Defined in src/dropdown/dropdown.component.ts:287
|
|
set to |
clearText | |
Type : string
|
|
Default value : this.i18n.get().DROPDOWN.CLEAR
|
|
Defined in src/dropdown/dropdown.component.ts:233
|
|
Sets the optional clear button tooltip text. |
disableArrowKeys | |
Type : boolean
|
|
Default value : false
|
|
Defined in src/dropdown/dropdown.component.ts:267
|
|
Set to |
disabled | |
Type : boolean
|
|
Default value : false
|
|
Defined in src/dropdown/dropdown.component.ts:251
|
|
Set to |
displayValue | |
Type : string | TemplateRef<any>
|
|
Default value : ""
|
|
Defined in src/dropdown/dropdown.component.ts:229
|
|
The selected value from the |
dropUp | |
Type : boolean
|
|
Defined in src/dropdown/dropdown.component.ts:317
|
|
Overrides the automatic dropUp. |
fluid | |
Type : boolean
|
|
Default value : false
|
|
Defined in src/dropdown/dropdown.component.ts:350
|
|
Experimental: enable fluid state |
helperText | |
Type : string | TemplateRef<any>
|
|
Defined in src/dropdown/dropdown.component.ts:221
|
|
Sets the optional helper text. |
hideLabel | |
Type : boolean
|
|
Default value : false
|
|
Defined in src/dropdown/dropdown.component.ts:217
|
|
Hide label while keeping it accessible for screen readers |
id | |
Type : string
|
|
Default value : `dropdown-${Dropdown.dropdownCount++}`
|
|
Defined in src/dropdown/dropdown.component.ts:209
|
inline | |
Type : boolean
|
|
Default value : false
|
|
Defined in src/dropdown/dropdown.component.ts:263
|
|
Set to |
invalid | |
Type : boolean
|
|
Default value : false
|
|
Defined in src/dropdown/dropdown.component.ts:271
|
|
Set to |
invalidText | |
Type : string | TemplateRef<any>
|
|
Defined in src/dropdown/dropdown.component.ts:275
|
|
Value displayed if dropdown is in invalid state. |
itemValueKey | |
Type : string
|
|
Defined in src/dropdown/dropdown.component.ts:296
|
|
Specifies the property to be used as the return value to |
label | |
Type : string | TemplateRef<any>
|
|
Defined in src/dropdown/dropdown.component.ts:213
|
|
Label for the dropdown. |
menuButtonLabel | |
Type : any
|
|
Default value : this.i18n.get().DROPDOWN.OPEN
|
|
Defined in src/dropdown/dropdown.component.ts:308
|
|
Accessible label for the button that opens the dropdown list.
Defaults to the |
placeholder | |
Type : string
|
|
Default value : ""
|
|
Defined in src/dropdown/dropdown.component.ts:225
|
|
Value displayed if no item is selected. |
readonly | |
Type : boolean
|
|
Default value : false
|
|
Defined in src/dropdown/dropdown.component.ts:255
|
|
Set to |
scrollableContainer | |
Type : string
|
|
Defined in src/dropdown/dropdown.component.ts:292
|
|
Query string for the element that contains the |
selectedLabel | |
Type : any
|
|
Default value : this.i18n.get().DROPDOWN.SELECTED
|
|
Defined in src/dropdown/dropdown.component.ts:313
|
|
Provides the label for the "# selected" text.
Defaults to the |
selectionFeedback | |
Type : "top" | "fixed" | "top-after-reopen"
|
|
Default value : "top-after-reopen"
|
|
Defined in src/dropdown/dropdown.component.ts:303
|
|
Specify feedback (mode) of the selection.
|
size | |
Type : "sm" | "md" | "lg"
|
|
Default value : "md"
|
|
Defined in src/dropdown/dropdown.component.ts:237
|
|
Size to render the dropdown field. |
skeleton | |
Type : boolean
|
|
Default value : false
|
|
Defined in src/dropdown/dropdown.component.ts:259
|
|
Set to |
theme | |
Type : "light" | "dark"
|
|
Default value : "dark"
|
|
Defined in src/dropdown/dropdown.component.ts:247
|
type | |
Type : "single" | "multi"
|
|
Default value : "single"
|
|
Defined in src/dropdown/dropdown.component.ts:242
|
|
Defines whether or not the |
warn | |
Type : boolean
|
|
Default value : false
|
|
Defined in src/dropdown/dropdown.component.ts:279
|
|
Set to |
warnText | |
Type : string | TemplateRef<any>
|
|
Defined in src/dropdown/dropdown.component.ts:283
|
|
Sets the warning text |
close | |
Type : EventEmitter<any>
|
|
Defined in src/dropdown/dropdown.component.ts:329
|
|
Emits event notifying to other classes that the |
onClose | |
Type : EventEmitter<any>
|
|
Defined in src/dropdown/dropdown.component.ts:325
|
|
Emits event notifying to other classes that the |
selected | |
Type : EventEmitter<Object>
|
|
Defined in src/dropdown/dropdown.component.ts:321
|
|
Emits selection events. |
class.cds--dropdown__wrapper |
Type : boolean
|
Default value : true
|
Defined in src/dropdown/dropdown.component.ts:344
|
class.cds--list-box__wrapper |
Type : boolean
|
Default value : true
|
Defined in src/dropdown/dropdown.component.ts:346
|
class.cds--list-box__wrapper--fluid--focus |
Type : boolean
|
Defined in src/dropdown/dropdown.component.ts:194
|
class.cds--list-box__wrapper--fluid--invalid |
Type : boolean
|
Defined in src/dropdown/dropdown.component.ts:190
|
keydown |
Arguments : '$event'
|
keydown(event: KeyboardEvent)
|
Defined in src/dropdown/dropdown.component.ts:545
|
Adds keyboard functionality for navigation, selection and closing of the |
_appendToBody |
_appendToBody()
|
Defined in src/dropdown/dropdown.component.ts:707
|
Creates the
Returns :
void
|
_appendToDropdown |
_appendToDropdown()
|
Defined in src/dropdown/dropdown.component.ts:699
|
Creates the
Returns :
void
|
_keyboardNav | ||||||
_keyboardNav(event: KeyboardEvent)
|
||||||
Defined in src/dropdown/dropdown.component.ts:679
|
||||||
Handles keyboard events so users are controlling the
Parameters :
Returns :
void
|
_noop |
_noop()
|
Defined in src/dropdown/dropdown.component.ts:659
|
Returns :
void
|
_outsideClick | ||||
_outsideClick(event)
|
||||
Defined in src/dropdown/dropdown.component.ts:663
|
||||
Handles clicks outside of the
Parameters :
Returns :
void
|
_outsideKey | ||||
_outsideKey(event)
|
||||
Defined in src/dropdown/dropdown.component.ts:671
|
||||
Parameters :
Returns :
void
|
_shouldDropUp |
_shouldDropUp()
|
Defined in src/dropdown/dropdown.component.ts:721
|
Detects whether or not the
Returns :
any
|
Protected checkForReorder |
checkForReorder()
|
Defined in src/dropdown/dropdown.component.ts:837
|
Controls when it's needed to apply the selection feedback
Returns :
void
|
clearSelected |
clearSelected()
|
Defined in src/dropdown/dropdown.component.ts:640
|
Returns :
void
|
closedDropdownNavigation | ||||
closedDropdownNavigation(event)
|
||||
Defined in src/dropdown/dropdown.component.ts:580
|
||||
Parameters :
Returns :
void
|
closeMenu |
closeMenu()
|
Defined in src/dropdown/dropdown.component.ts:785
|
Collapsing the dropdown menu and removing unnecessary
Returns :
void
|
getDisplayStringValue |
getDisplayStringValue()
|
Defined in src/dropdown/dropdown.component.ts:599
|
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:620
|
Returns :
{ items: any; item?: undefined; } | { item: any; items?: undefined; } | { items?: undefined; item?: undefined; }
|
getSelectedCount |
getSelectedCount()
|
Defined in src/dropdown/dropdown.component.ts:634
|
Returns :
number
|
handleFocus | ||||||
handleFocus(event: FocusEvent)
|
||||||
Defined in src/dropdown/dropdown.component.ts:827
|
||||||
Parameters :
Returns :
void
|
isRenderString |
isRenderString()
|
Defined in src/dropdown/dropdown.component.ts:616
|
Returns :
boolean
|
Public isTemplate | ||||
isTemplate(value)
|
||||
Defined in src/dropdown/dropdown.component.ts:823
|
||||
Parameters :
Returns :
boolean
|
ngAfterContentInit |
ngAfterContentInit()
|
Defined in src/dropdown/dropdown.component.ts:400
|
Initializes classes and subscribes to events for single or multi selection.
Returns :
void
|
ngAfterViewInit |
ngAfterViewInit()
|
Defined in src/dropdown/dropdown.component.ts:445
|
Returns :
void
|
ngOnDestroy |
ngOnDestroy()
|
Defined in src/dropdown/dropdown.component.ts:462
|
Removing the
Returns :
void
|
ngOnInit |
ngOnInit()
|
Defined in src/dropdown/dropdown.component.ts:391
|
Updates the
Returns :
void
|
onBlur |
onBlur()
|
Defined in src/dropdown/dropdown.component.ts:510
|
Returns :
void
|
onKeyDown | ||||||
onKeyDown(event: KeyboardEvent)
|
||||||
Decorators :
@HostListener('keydown', ['$event'])
|
||||||
Defined in src/dropdown/dropdown.component.ts:545
|
||||||
Adds keyboard functionality for navigation, selection and closing of the
Parameters :
Returns :
void
|
openMenu |
openMenu()
|
Defined in src/dropdown/dropdown.component.ts:741
|
Expands the dropdown menu in the view.
Returns :
void
|
registerOnChange | ||||||
registerOnChange(fn: any)
|
||||||
Defined in src/dropdown/dropdown.component.ts:514
|
||||||
Parameters :
Returns :
void
|
registerOnTouched | ||||||
registerOnTouched(fn: any)
|
||||||
Defined in src/dropdown/dropdown.component.ts:521
|
||||||
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:537
|
||||||||
ex:
Parameters :
Returns :
void
|
toggleMenu |
toggleMenu()
|
Defined in src/dropdown/dropdown.component.ts:815
|
Controls toggling menu states between open/expanded and closed/collapsed.
Returns :
void
|
valueSelected |
valueSelected()
|
Defined in src/dropdown/dropdown.component.ts:654
|
Returns
Returns :
boolean
|
writeValue | ||||||
writeValue(value: any)
|
||||||
Defined in src/dropdown/dropdown.component.ts:471
|
||||||
Propagates the injected
Parameters :
Returns :
void
|
_dropUp |
Default value : false
|
Defined in src/dropdown/dropdown.component.ts:360
|
controls whether the |
Protected _isFocused |
Default value : false
|
Defined in src/dropdown/dropdown.component.ts:373
|
Private _writtenValue |
Type : any
|
Default value : []
|
Defined in src/dropdown/dropdown.component.ts:376
|
dropdownButton |
Decorators :
@ViewChild('dropdownButton', {static: true})
|
Defined in src/dropdown/dropdown.component.ts:338
|
Maintains a reference to the view DOM element of the |
Static dropdownCount |
Type : number
|
Default value : 0
|
Defined in src/dropdown/dropdown.component.ts:189
|
dropdownMenu |
Decorators :
@ViewChild('dropdownMenu', {static: true})
|
Defined in src/dropdown/dropdown.component.ts:342
|
ViewChid of the dropdown view. |
hostClass |
Default value : true
|
Decorators :
@HostBinding('class.cds--dropdown__wrapper')
|
Defined in src/dropdown/dropdown.component.ts:344
|
hostWrapperClass |
Default value : true
|
Decorators :
@HostBinding('class.cds--list-box__wrapper')
|
Defined in src/dropdown/dropdown.component.ts:346
|
keyboardNav |
Default value : this._keyboardNav.bind(this)
|
Defined in src/dropdown/dropdown.component.ts:367
|
menuIsClosed |
Default value : true
|
Defined in src/dropdown/dropdown.component.ts:355
|
Set to |
noop |
Default value : this._noop.bind(this)
|
Defined in src/dropdown/dropdown.component.ts:364
|
Protected onTouchedCallback |
Type : function
|
Default value : this._noop
|
Defined in src/dropdown/dropdown.component.ts:371
|
outsideClick |
Default value : this._outsideClick.bind(this)
|
Defined in src/dropdown/dropdown.component.ts:365
|
outsideKey |
Default value : this._outsideKey.bind(this)
|
Defined in src/dropdown/dropdown.component.ts:366
|
propagateChange |
Default value : () => {...}
|
Defined in src/dropdown/dropdown.component.ts:528
|
function passed in by |
view |
Type : AbstractDropdownView
|
Decorators :
@ContentChild(AbstractDropdownView, {static: true})
|
Defined in src/dropdown/dropdown.component.ts:334
|
Maintains a reference to the |
Protected visibilitySubscription |
Default value : new Subscription()
|
Defined in src/dropdown/dropdown.component.ts:369
|
fluidInvalidClass |
getfluidInvalidClass()
|
Defined in src/dropdown/dropdown.component.ts:190
|
fluidFocusClass |
getfluidFocusClass()
|
Defined in src/dropdown/dropdown.component.ts:194
|
writtenValue | ||||||
getwrittenValue()
|
||||||
Defined in src/dropdown/dropdown.component.ts:198
|
||||||
setwrittenValue(val: any[])
|
||||||
Defined in src/dropdown/dropdown.component.ts:202
|
||||||
Parameters :
Returns :
void
|
import {
Component,
Input,
Output,
EventEmitter,
ElementRef,
ContentChild,
OnInit,
ViewChild,
AfterContentInit,
HostListener,
OnDestroy,
HostBinding,
TemplateRef,
AfterViewInit
} 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 "carbon-components-angular/i18n";
import { ListItem } from "./list-item.interface";
import { DropdownService } from "./dropdown.service";
import { ElementService, getScrollableParents } from "carbon-components-angular/utils";
import { hasScrollableParents } from "carbon-components-angular/utils";
/**
* Drop-down lists enable users to select one or more items from a list.
*
* #### Opening behavior/List DOM placement
* By default the dropdown will try to figure out the best placement for the dropdown list.
*
* If it's not contained within any scrolling elements, it will open inline, if it _is_
* contained within a scrolling container it will try to open in the body, or an `cds-placeholder`.
*
* To control this behavior you can use the `appendInline` input:
* - `[appendInline]="null"` is the default (auto detection)
* - `[appendInline]="false"` will always append to the body/`cds-placeholder`
* - `[appendInline]="true"` will always append inline (next to the dropdown button)
*
* Get started with importing the module:
*
* ```typescript
* import { DropdownModule } from 'carbon-components-angular';
* ```
*
* [See demo](../../?path=/story/components-dropdown--basic)
*/
@Component({
selector: "cds-dropdown, ibm-dropdown",
template: `
<label
*ngIf="label && !skeleton"
[for]="id"
class="cds--label"
[ngClass]="{
'cds--label--disabled': disabled,
'cds--visually-hidden': hideLabel
}">
<ng-container *ngIf="!isTemplate(label)">{{label}}</ng-container>
<ng-template *ngIf="isTemplate(label)" [ngTemplateOutlet]="label"></ng-template>
</label>
<div
class="cds--list-box"
[ngClass]="{
'cds--dropdown': type !== 'multi' && !(skeleton && fluid),
'cds--multiselect': type === 'multi',
'cds--multi-select--selected': type === 'multi' && getSelectedCount() > 0,
'cds--dropdown--light': theme === 'light',
'cds--list-box--light': theme === 'light',
'cds--list-box--inline': inline,
'cds--skeleton': skeleton,
'cds--dropdown--disabled cds--list-box--disabled': disabled,
'cds--dropdown--readonly': readonly,
'cds--dropdown--invalid': invalid,
'cds--dropdown--warning cds--list-box--warning': warn,
'cds--dropdown--sm cds--list-box--sm': size === 'sm',
'cds--dropdown--md cds--list-box--md': size === 'md',
'cds--dropdown--lg cds--list-box--lg': size === 'lg',
'cds--list-box--expanded': !menuIsClosed,
'cds--list-box--invalid': invalid
}"
[attr.data-invalid]="invalid ? true : null">
<div *ngIf="skeleton && fluid" class="cds--list-box__label"></div>
<button
#dropdownButton
[id]="id"
type="button"
class="cds--list-box__field"
[ngClass]="{'a': !menuIsClosed}"
[attr.aria-expanded]="!menuIsClosed"
[attr.aria-disabled]="disabled"
aria-haspopup="listbox"
(click)="disabled || readonly ? $event.stopPropagation() : toggleMenu()"
(focus)="fluid ? handleFocus($event) : null"
(blur)="fluid ? handleFocus($event) : onBlur()"
[attr.disabled]="disabled ? true : null">
<div
(click)="clearSelected()"
(keydown.enter)="clearSelected()"
*ngIf="type === 'multi' && getSelectedCount() > 0"
class="cds--list-box__selection cds--tag--filter cds--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="cds--list-box__label">{{getDisplayStringValue() | async}}</span>
<ng-template
*ngIf="!isRenderString()"
[ngTemplateOutletContext]="getRenderTemplateContext()"
[ngTemplateOutlet]="displayValue">
</ng-template>
<span class="cds--list-box__menu-icon">
<svg
*ngIf="!skeleton"
cdsIcon="chevron--down"
size="16"
[attr.aria-label]="menuButtonLabel"
[ngClass]="{'cds--list-box__menu-icon--open': !menuIsClosed }">
</svg>
</span>
</button>
<svg
*ngIf="invalid"
class="cds--list-box__invalid-icon"
cdsIcon="warning--filled"
size="16">
</svg>
<svg
*ngIf="!invalid && warn"
cdsIcon="warning--alt--filled"
size="16"
class="cds--list-box__invalid-icon cds--list-box__invalid-icon--warning">
</svg>
<div
#dropdownMenu
[ngClass]="{
'cds--list-box--up': this.dropUp !== null && this.dropUp !== undefined ? dropUp : _dropUp
}">
<ng-content *ngIf="!menuIsClosed"></ng-content>
</div>
</div>
<hr *ngIf="fluid" class="cds--list-box__divider" />
<div
*ngIf="helperText && !invalid && !warn && !skeleton && !fluid"
class="cds--form__helper-text"
[ngClass]="{
'cds--form__helper-text--disabled': disabled
}">
<ng-container *ngIf="!isTemplate(helperText)">{{helperText}}</ng-container>
<ng-template *ngIf="isTemplate(helperText)" [ngTemplateOutlet]="helperText"></ng-template>
</div>
<div *ngIf="invalid" class="cds--form-requirement">
<ng-container *ngIf="!isTemplate(invalidText)">{{ invalidText }}</ng-container>
<ng-template *ngIf="isTemplate(invalidText)" [ngTemplateOutlet]="invalidText"></ng-template>
</div>
<div *ngIf="!invalid && warn" class="cds--form-requirement">
<ng-container *ngIf="!isTemplate(warnText)">{{warnText}}</ng-container>
<ng-template *ngIf="isTemplate(warnText)" [ngTemplateOutlet]="warnText"></ng-template>
</div>
`,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: Dropdown,
multi: true
}
]
})
export class Dropdown implements OnInit, AfterContentInit, AfterViewInit, OnDestroy, ControlValueAccessor {
static dropdownCount = 0;
@HostBinding("class.cds--list-box__wrapper--fluid--invalid") get fluidInvalidClass() {
return this.invalid && this.fluid;
}
@HostBinding("class.cds--list-box__wrapper--fluid--focus") get fluidFocusClass() {
return this.fluid && this._isFocused && this.menuIsClosed;
}
protected get writtenValue() {
return this._writtenValue;
}
protected set writtenValue(val: any[]) {
if (val && val.length === 0) {
this.clearSelected();
}
this._writtenValue = val;
}
@Input() id = `dropdown-${Dropdown.dropdownCount++}`;
/**
* Label for the dropdown.
*/
@Input() label: string | TemplateRef<any>;
/**
* Hide label while keeping it accessible for screen readers
*/
@Input() hideLabel = false;
/**
* 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";
/**
* @deprecated since v5 - Use `cdsLayer` directive instead
* `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 readonly state.
*/
@Input() readonly = 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: string | TemplateRef<any>;
/**
* Set to `true` to show a warning (contents set by warningText)
*/
@Input() warn = false;
/**
* Sets the warning text
*/
@Input() warnText: string | TemplateRef<any>;
/**
* set to `true` to place the dropdown view inline with the component
*/
@Input() appendInline: boolean = null;
/**
* 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;
/**
* Specifies the property to be used as the return value to `ngModel`
*/
@Input() itemValueKey: string;
/**
* Specify feedback (mode) of the selection.
* `top`: selected item jumps to top
* `fixed`: selected item stays at it's position
* `top-after-reopen`: selected item jump to top after reopen dropdown
*/
@Input() selectionFeedback: "top" | "fixed" | "top-after-reopen" = "top-after-reopen";
/**
* 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;
/**
* Overrides the automatic dropUp.
*/
@Input() dropUp: boolean;
/**
* 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, { static: true }) view: AbstractDropdownView;
/**
* Maintains a reference to the view DOM element of the `Dropdown` button.
*/
@ViewChild("dropdownButton", { static: true }) dropdownButton;
/**
* ViewChid of the dropdown view.
*/
@ViewChild("dropdownMenu", { static: true }) dropdownMenu;
@HostBinding("class.cds--dropdown__wrapper") hostClass = true;
@HostBinding("class.cds--list-box__wrapper") hostWrapperClass = true;
/**
* Experimental: enable fluid state
*/
@HostBinding("class.cds--list-box__wrapper--fluid") @Input() fluid = false;
/**
* Set to `true` if the dropdown is closed (not expanded).
*/
menuIsClosed = true;
/**
* controls whether 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;
protected _isFocused = false;
// primarily used to capture and propagate input to `writeValue` before the content is available
private _writtenValue: any = [];
/**
* Creates an instance of Dropdown.
*/
constructor(
protected elementRef: ElementRef,
protected i18n: I18n,
protected dropdownService: DropdownService,
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) || typeof this.writtenValue === "number") {
this.writeValue(this.writtenValue);
}
this.view.type = this.type;
this.view.size = this.size;
// function to check if the event is organic (isUpdate === false) or programmatic
const isUpdate = event => event && event.isUpdate;
this.view.select.subscribe(event => {
if (this.type === "single" && !isUpdate(event) && !Array.isArray(event)) {
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);
}
}
if (this.type === "multi" && !isUpdate(event)) {
// 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());
}
}
// only emit selected for "organic" selections
if (!isUpdate(event)) {
this.checkForReorder();
this.selected.emit(event);
}
});
}
ngAfterViewInit() {
// if appendInline is default valued (null) we should:
// 1. if there are scrollable parents (not including body) don't append inline
// this should also cover the case where the dropdown is in a modal
// (where we _do_ want to append to the placeholder)
if (this.appendInline === null && hasScrollableParents(this.elementRef.nativeElement)) {
this.appendInline = false;
// 2. otherwise we should append inline
} else if (this.appendInline === null) {
this.appendInline = true;
}
this.checkForReorder();
}
/**
* Removing the `Dropdown` from the body if it is appended to the body.
*/
ngOnDestroy() {
if (!this.appendInline) {
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);
}
}
this.checkForReorder();
});
}
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"])
onKeyDown(event: KeyboardEvent) {
if (this.readonly) {
return;
}
if ((event.key === "Escape") && !this.menuIsClosed) {
event.stopImmediatePropagation(); // don't unintentionally close other widgets that listen for Escape
}
if (event.key === "Escape") {
event.preventDefault();
this.closeMenu();
this.dropdownButton.nativeElement.focus();
} else if (this.menuIsClosed && (event.key === " " || event.key === "ArrowDown" || event.key === "ArrowUp")) {
if (this.disableArrowKeys && (event.key === "ArrowDown" || event.key === "ArrowUp")) {
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) {
if (event.key === "ArrowDown") {
event.preventDefault();
this.view.getCurrentItem().selected = false;
let item = this.view.getNextItem();
if (item) { item.selected = true; }
} else if (event.key === "ArrowUp") {
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 || this.skeleton) {
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 || this.getSelectedCount() === 0) {
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) {
if (event.key === "Escape" && !this.menuIsClosed) {
event.stopImmediatePropagation(); // don't unintentionally close modal if inside of it
}
if (event.key === "Escape") {
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() {
const lightClass = this.theme === "light" ? " cds--list-box--light" : "";
const expandedClass = !this.menuIsClosed ? " cds--list-box--expanded" : "";
this.dropdownService.appendToBody(
this.dropdownButton.nativeElement,
this.dropdownMenu.nativeElement,
`${this.elementRef.nativeElement.className}${lightClass}${expandedClass}`);
this.dropdownMenu.nativeElement.addEventListener("keydown", this.keyboardNav, true);
}
/**
* Detects whether or not the `Dropdown` list is visible within all scrollable parents.
* This can be overridden by passing in a value to the `dropUp` input.
*/
_shouldDropUp() {
// check if dropdownMenu exists first.
const menu = this.dropdownMenu && this.dropdownMenu.nativeElement.querySelector(".cds--list-box__menu");
// check if menu exists first.
const menuRect = menu && menu.getBoundingClientRect();
if (menu && menuRect) {
const scrollableParents = getScrollableParents(menu);
return scrollableParents.reduce((shouldDropUp: boolean, parent: HTMLElement) => {
const parentRect = parent.getBoundingClientRect();
const isBelowParent = !(menuRect.bottom <= parentRect.bottom);
return shouldDropUp || isBelowParent;
}, false);
}
return false;
}
/**
* 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._dropUp = false;
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 => {
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(() => {
if (this.dropUp === null || this.dropUp === undefined) {
this._dropUp = this._shouldDropUp();
}
}, 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.checkForReorder();
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;
}
handleFocus(event: FocusEvent) {
this._isFocused = event.type === "focus";
if (event.type === "blur") {
this.onBlur();
}
}
/**
* Controls when it's needed to apply the selection feedback
*/
protected checkForReorder() {
const topAfterReopen = this.menuIsClosed && this.selectionFeedback === "top-after-reopen";
if ((this.type === "multi") && (topAfterReopen || this.selectionFeedback === "top")) {
this.view.reorderSelected();
}
}
}