https://angular.carbondesignsystem.com/?path=/story/components-dropdown—basic

组件及属性

dropdown

  • Input
  • Output

    dropdown-list

  • Input

  • Output

    Tips

Code

  1. import {
  2. Component,
  3. Input,
  4. Output,
  5. OnDestroy,
  6. EventEmitter,
  7. TemplateRef,
  8. AfterViewInit,
  9. ViewChild,
  10. ElementRef,
  11. ViewChildren,
  12. QueryList
  13. } from "@angular/core";
  14. import { Observable, isObservable, Subscription, of } from "rxjs";
  15. import { first } from "rxjs/operators";
  16. import { I18n } from "carbon-components-angular/i18n";
  17. import { AbstractDropdownView } from "../abstract-dropdown-view.class";
  18. import { ListItem } from "../list-item.interface";
  19. import { watchFocusJump } from "../dropdowntools";
  20. import { ScrollCustomEvent } from "./scroll-custom-event.interface";
  21. /**
  22. * ```html
  23. * <ibm-dropdown-list [items]="listItems"></ibm-dropdown-list>
  24. *
  • ```typescript
  • listItems = [
  • {
  • content: “item one”,
  • selected: false
  • },
  • {
  • content: “item two”,
  • selected: false,
  • },
  • {
  • content: “item three”,
  • selected: false
  • },
  • {
  • content: “item four”,
  • selected: false
  • }
  • ];
    1. */
    2. @Component({
    3. selector: "ibm-dropdown-list",
    4. template: `
    5. <ul
    6. #list
    7. role="listbox"
    8. class="bx--list-box__menu bx--multi-select"
    9. (scroll)="emitScroll($event)"
    10. (keydown)="navigateList($event)"
    11. tabindex="-1"
    12. [attr.aria-label]="ariaLabel"
    13. [attr.aria-activedescendant]="highlightedItem">
    14. <li
    15. role="option"
    16. *ngFor="let item of displayItems; let i = index"
    17. (click)="doClick($event, item)"
    18. class="bx--list-box__menu-item"
    19. [attr.aria-selected]="item.selected"
    20. [id]="getItemId(i)"
    21. [attr.title]=" showTitles ? item.content : null"
    22. [ngClass]="{
    23. 'bx--list-box__menu-item--active': item.selected,
    24. 'bx--list-box__menu-item--highlighted': highlightedItem === getItemId(i),
    25. disabled: item.disabled
    26. }">
    27. <div
    28. #listItem
    29. tabindex="-1"
    30. class="bx--list-box__menu-item__option">
    31. <div
    32. *ngIf="!listTpl && type === 'multi'"
    33. class="bx--form-item bx--checkbox-wrapper">
    34. <label
    35. [attr.data-contained-checkbox-state]="item.selected"
    36. class="bx--checkbox-label">
    37. <input
    38. class="bx--checkbox"
    39. type="checkbox"
    40. [checked]="item.selected"
    41. [disabled]="item.disabled"
    42. tabindex="-1">
    43. <span class="bx--checkbox-appearance"></span>
    44. <span class="bx--checkbox-label-text">{{item.content}}</span>
    45. </label>
    46. </div>
    47. <ng-container *ngIf="!listTpl && type === 'single'">{{item.content}}</ng-container>
    48. <svg
    49. *ngIf="!listTpl && type === 'single'"
    50. ibmIcon="checkmark"
    51. size="16"
    52. class="bx--list-box__menu-item__selected-icon">
    53. </svg>
    54. <ng-template
    55. *ngIf="listTpl"
    56. [ngTemplateOutletContext]="{item: item}"
    57. [ngTemplateOutlet]="listTpl">
    58. </ng-template>
    59. </div>
    60. </li>
    61. </ul>`,
    62. providers: [
    63. {
    64. provide: AbstractDropdownView,
    65. useExisting: DropdownList
    66. }
    67. ]
    68. })
    69. // 注意,这里实现了 AbstractDropdownView 类
    70. export class DropdownList implements AbstractDropdownView, AfterViewInit, OnDestroy {
    71. static listCount = 0;
    72. @Input() ariaLabel = this.i18n.get().DROPDOWN_LIST.LABEL;
    73. /**
    74. * The list items belonging to the `DropdownList`.
    75. */
    76. @Input() set items (value: Array<ListItem> | Observable<Array<ListItem>>) {
    77. if (isObservable(value)) {
    78. if (this._itemsSubscription) {
    79. this._itemsSubscription.unsubscribe();
    80. }
    81. this._itemsReady = new Observable<boolean>((observer) => {
    82. this._itemsSubscription = value.subscribe(v => {
    83. this.updateList(v);
    84. observer.next(true);
    85. observer.complete();
    86. });
    87. });
    88. this.onItemsReady(null);
    89. } else {
    90. this.updateList(value);
    91. }
    92. this._originalItems = value;
    93. }
    94. get items(): Array<ListItem> | Observable<Array<ListItem>> {
    95. return this._originalItems;
    96. }
    97. /**
    98. * Template to bind to items in the `DropdownList` (optional).
    99. */
    100. @Input() listTpl: string | TemplateRef<any> = null;
    101. /**
    102. * Event to emit selection of a list item within the `DropdownList`.
    103. */
    104. @Output() select: EventEmitter<{ item: ListItem, isUpdate?: boolean } | ListItem[]> = new EventEmitter();
    105. /**
    106. * Event to emit scroll event of a list within the `DropdownList`.
    107. */
    108. @Output() scroll: EventEmitter<ScrollCustomEvent> = new EventEmitter();
    109. /**
    110. * Event to suggest a blur on the view.
    111. * Emits _after_ the first/last item has been focused.
    112. * ex.
    113. * ArrowUp -> focus first item
    114. * ArrowUp -> emit event
    115. *
    116. * When this event fires focus should be placed on some element outside of the list - blurring the list as a result
    117. */
    118. @Output() blurIntent = new EventEmitter<"top" | "bottom">();
    119. /**
    120. * Maintains a reference to the view DOM element for the unordered list of items within the `DropdownList`.
    121. */
    122. // @ts-ignore
    123. @ViewChild("list", { static: true }) list: ElementRef;
    124. /**
    125. * Defines whether or not the `DropdownList` supports selecting multiple items as opposed to single
    126. * item selection.
    127. */
    128. @Input() type: "single" | "multi" = "single";
    129. /**
    130. * Defines whether to show title attribute or not
    131. */
    132. @Input() showTitles = true;
    133. /**
    134. * Defines the rendering size of the `DropdownList` input component.
    135. *
    136. * @deprecated since v4
    137. */
    138. public size: "sm" | "md" | "xl" = "md";
    139. public listId = `listbox-${DropdownList.listCount++}`;
    140. public highlightedItem = null;
    141. /**
    142. * Holds the list of items that will be displayed in the `DropdownList`.
    143. * It differs from the the complete set of items when filtering is used (but
    144. * it is always a subset of the total items in `DropdownList`).
    145. */
    146. public displayItems: Array<ListItem> = [];
    147. /**
    148. * Maintains the index for the selected item within the `DropdownList`.
    149. */
    150. protected index = -1;
    151. /**
    152. * An array holding the HTML list elements in the view.
    153. */
    154. @ViewChildren("listItem") protected listElementList: QueryList<ElementRef>;
    155. /**
    156. * Observable bound to keydown events to control filtering.
    157. */
    158. protected focusJump;
    159. /**
    160. * Tracks the current (if any) subscription to the items observable so we can clean up when the input is updated.
    161. */
    162. protected _itemsSubscription: Subscription;
    163. /**
    164. * Used to retain the original items passed to the setter.
    165. */
    166. protected _originalItems: Array<ListItem> | Observable<Array<ListItem>>;
    167. /**
    168. * Useful representation of the items, should be accessed via `getListItems`.
    169. */
    170. protected _items: Array<ListItem> = [];
    171. /**
    172. * Used to wait for items in case they are passed through an observable.
    173. */
    174. protected _itemsReady: Observable<boolean>;
    175. /**
    176. * Creates an instance of `DropdownList`.
    177. */
    178. constructor(public elementRef: ElementRef, protected i18n: I18n) {}
    179. /**
    180. * Retrieves array of list items and index of the selected item after view has rendered.
    181. * Additionally, any Observables for the `DropdownList` are initialized.
    182. */
    183. ngAfterViewInit() {
    184. this.index = this.getListItems().findIndex(item => item.selected);
    185. this.setupFocusObservable();
    186. setTimeout(() => {
    187. this.doEmitSelect(true);
    188. });
    189. }
    190. /**
    191. * Removes any Observables on destruction of the component.
    192. */
    193. ngOnDestroy() {
    194. if (this.focusJump) {
    195. this.focusJump.unsubscribe();
    196. }
    197. if (this._itemsSubscription) {
    198. this._itemsSubscription.unsubscribe();
    199. }
    200. }
    201. doEmitSelect(isUpdate = true) {
    202. if (this.type === "single") {
    203. this.select.emit({ item: this._items.find(item => item.selected), isUpdate: isUpdate });
    204. } else {
    205. // abuse javascripts object mutability until we can break the API and switch to
    206. // { items: [], isUpdate: true }
    207. const selected = this.getSelected() || [];
    208. selected["isUpdate"] = isUpdate;
    209. this.select.emit(selected);
    210. }
    211. }
    212. getItemId(index: number) {
    213. return `${this.listId}-${index}`;
    214. }
    215. /**
    216. * Updates the displayed list of items and then retrieves the most current properties for the `DropdownList` from the DOM.
    217. */
    218. updateList(items) {
    219. this._items = items.map(item => Object.assign({}, item));
    220. this.displayItems = this._items;
    221. this.updateIndex();
    222. this.setupFocusObservable();
    223. setTimeout(() => {
    224. if (this.getSelected() !== []) { return; }
    225. this.doEmitSelect();
    226. });
    227. }
    228. /**
    229. * Filters the items being displayed in the DOM list.
    230. */
    231. filterBy(query = "") {
    232. if (query) {
    233. this.displayItems = this.getListItems().filter(item => item.content.toLowerCase().includes(query.toLowerCase()));
    234. } else {
    235. this.displayItems = this.getListItems();
    236. }
    237. this.updateIndex();
    238. }
    239. /**
    240. * Initializes (or re-initializes) the Observable that handles switching focus to an element based on
    241. * key input matching the first letter of the item in the list.
    242. */
    243. setupFocusObservable() {
    244. if (!this.list) { return; }
    245. if (this.focusJump) {
    246. this.focusJump.unsubscribe();
    247. }
    248. let elList = Array.from(this.list.nativeElement.querySelectorAll("li"));
    249. this.focusJump = watchFocusJump(this.list.nativeElement, elList)
    250. .subscribe(el => {
    251. el.focus();
    252. });
    253. }
    254. /**
    255. * Returns the `ListItem` that is subsequent to the selected item in the `DropdownList`.
    256. */
    257. getNextItem(): ListItem {
    258. if (this.index < this.displayItems.length - 1) {
    259. this.index++;
    260. }
    261. return this.displayItems[this.index];
    262. }
    263. /**
    264. * Returns `true` if the selected item is not the last item in the `DropdownList`.
    265. */
    266. hasNextElement(): boolean {
    267. return this.index < this.displayItems.length - 1 &&
    268. (!(this.index === this.displayItems.length - 2) || !this.displayItems[this.index + 1].disabled);
    269. }
    270. /**
    271. * Returns the `HTMLElement` for the item that is subsequent to the selected item.
    272. */
    273. getNextElement(): HTMLElement {
    274. if (this.index < this.displayItems.length - 1) {
    275. this.index++;
    276. }
    277. let item = this.displayItems[this.index];
    278. if (item.disabled) {
    279. return this.getNextElement();
    280. }
    281. let elemList = this.listElementList ? this.listElementList.toArray() : [];
    282. // TODO: update to optional chaining after upgrading typescript
    283. // to v3.7+
    284. if (elemList[this.index] && elemList[this.index].nativeElement) {
    285. return elemList[this.index].nativeElement;
    286. } else {
    287. return null;
    288. }
    289. }
    290. /**
    291. * Returns the `ListItem` that precedes the selected item within `DropdownList`.
    292. */
    293. getPrevItem(): ListItem {
    294. if (this.index > 0) {
    295. this.index--;
    296. }
    297. return this.displayItems[this.index];
    298. }
    299. /**
    300. * Returns `true` if the selected item is not the first in the list.
    301. */
    302. hasPrevElement(): boolean {
    303. return this.index > 0 && (!(this.index === 1) || !this.displayItems[0].disabled);
    304. }
    305. /**
    306. * Returns the `HTMLElement` for the item that precedes the selected item.
    307. */
    308. getPrevElement(): HTMLElement {
    309. if (this.index > 0) {
    310. this.index--;
    311. }
    312. let item = this.displayItems[this.index];
    313. if (item.disabled) {
    314. return this.getPrevElement();
    315. }
    316. let elemList = this.listElementList ? this.listElementList.toArray() : [];
    317. // TODO: update to optional chaining after upgrading typescript
    318. // to v3.7+
    319. if (elemList[this.index] && elemList[this.index].nativeElement) {
    320. return elemList[this.index].nativeElement;
    321. } else {
    322. return null;
    323. }
    324. }
    325. /**
    326. * Returns the `ListItem` that is selected within `DropdownList`.
    327. */
    328. getCurrentItem(): ListItem {
    329. if (this.index < 0) {
    330. return this.displayItems[0];
    331. }
    332. return this.displayItems[this.index];
    333. }
    334. /**
    335. * Returns the `HTMLElement` for the item that is selected within the `DropdownList`.
    336. */
    337. getCurrentElement(): HTMLElement {
    338. if (this.index < 0) {
    339. return this.listElementList.first.nativeElement;
    340. }
    341. return this.listElementList.toArray()[this.index].nativeElement;
    342. }
    343. /**
    344. * Returns the items as an Array
    345. */
    346. getListItems(): Array<ListItem> {
    347. return this._items;
    348. }
    349. /**
    350. * Returns a list containing the selected item(s) in the `DropdownList`.
    351. */
    352. getSelected(): ListItem[] {
    353. let selected = this.getListItems().filter(item => item.selected);
    354. if (selected.length === 0) {
    355. return [];
    356. }
    357. return selected;
    358. }
    359. /**
    360. * Transforms array input list of items to the correct state by updating the selected item(s).
    361. */
    362. propagateSelected(value: Array<ListItem>): void {
    363. // if we get a non-array, log out an error (since it is one)
    364. if (!Array.isArray(value)) {
    365. console.error(`${this.constructor.name}.propagateSelected expects an Array<ListItem>, got ${JSON.stringify(value)}`);
    366. }
    367. this.onItemsReady(() => {
    368. // loop through the list items and update the `selected` state for matching items in `value`
    369. for (let oldItem of this.getListItems()) {
    370. // copy the item
    371. let tempOldItem: string | ListItem = Object.assign({}, oldItem);
    372. // deleted selected because it's what we _want_ to change
    373. delete tempOldItem.selected;
    374. // stringify for compare
    375. tempOldItem = JSON.stringify(tempOldItem);
    376. for (let newItem of value) {
    377. // copy the item
    378. let tempNewItem: string | ListItem = Object.assign({}, newItem);
    379. // deleted selected because it's what we _want_ to change
    380. delete tempNewItem.selected;
    381. // stringify for compare
    382. tempNewItem = JSON.stringify(tempNewItem);
    383. // do the compare
    384. if (tempOldItem.includes(tempNewItem)) {
    385. oldItem.selected = newItem.selected;
    386. // if we've found a matching item, we can stop looping
    387. break;
    388. } else {
    389. oldItem.selected = false;
    390. }
    391. }
    392. }
    393. });
    394. }
    395. /**
    396. * Initializes focus in the list, effectively a wrapper for `getCurrentElement().focus()`
    397. */
    398. initFocus() {
    399. if (this.index < 0) {
    400. this.updateIndex();
    401. }
    402. this.list.nativeElement.focus();
    403. setTimeout(() => {
    404. this.highlightedItem = this.getItemId(this.index);
    405. });
    406. }
    407. updateIndex() {
    408. // initialize index on the first selected item or
    409. // on the next non disabled item if no items are selected
    410. const selected = this.getSelected();
    411. if (selected.length) {
    412. this.index = this.displayItems.indexOf(selected[0]);
    413. } else if (this.hasNextElement()) {
    414. this.getNextElement();
    415. }
    416. }
    417. /**
    418. * Manages the keyboard accessibility for navigation and selection within a `DropdownList`.
    419. * @deprecated since v4
    420. */
    421. doKeyDown(event: KeyboardEvent, item: ListItem) {
    422. // "Spacebar", "Down", and "Up" are IE specific values
    423. if (event.key === "Enter" || event.key === " " || event.key === "Spacebar") {
    424. if (this.listElementList.some(option => option.nativeElement === event.target)) {
    425. event.preventDefault();
    426. }
    427. if (event.key === "Enter") {
    428. this.doClick(event, item);
    429. }
    430. } else if (event.key === "ArrowDown" || event.key === "ArrowUp" || event.key === "Down" || event.key === "Up") {
    431. event.preventDefault();
    432. if (event.key === "ArrowDown" || event.key === "Down") {
    433. if (this.hasNextElement()) {
    434. // this.getNextElement().focus();
    435. this.getNextElement();
    436. } else {
    437. this.blurIntent.emit("bottom");
    438. }
    439. } else if (event.key === "ArrowUp" || event.key === "Up") {
    440. if (this.hasPrevElement()) {
    441. // this.getPrevElement().focus();
    442. this.getPrevElement();
    443. } else {
    444. this.blurIntent.emit("top");
    445. }
    446. }
    447. }
    448. }
    449. /**
    450. * Manages the keyboard accessibility for navigation and selection within a `DropdownList`.
    451. */
    452. navigateList(event: KeyboardEvent) {
    453. // "Spacebar", "Down", and "Up" are IE specific values
    454. if (event.key === "Enter" || event.key === " " || event.key === "Spacebar") {
    455. if (this.listElementList.some(option => option.nativeElement === event.target)) {
    456. event.preventDefault();
    457. }
    458. if (event.key === "Enter") {
    459. this.doClick(event, this.getCurrentItem());
    460. }
    461. } else if (event.key === "ArrowDown" || event.key === "ArrowUp" || event.key === "Down" || event.key === "Up") {
    462. event.preventDefault();
    463. if (event.key === "ArrowDown" || event.key === "Down") {
    464. if (this.hasNextElement()) {
    465. this.getNextElement();
    466. } else {
    467. this.blurIntent.emit("bottom");
    468. }
    469. } else if (event.key === "ArrowUp" || event.key === "Up") {
    470. if (this.hasPrevElement()) {
    471. this.getPrevElement();
    472. } else {
    473. this.blurIntent.emit("top");
    474. }
    475. }
    476. setTimeout(() => {
    477. this.highlightedItem = this.getItemId(this.index);
    478. });
    479. }
    480. }
    481. /**
    482. * Emits the selected item or items after a mouse click event has occurred.
    483. */
    484. doClick(event, item) {
    485. event.preventDefault();
    486. if (!item.disabled) {
    487. this.list.nativeElement.focus();
    488. if (this.type === "single") {
    489. item.selected = true;
    490. // reset the selection
    491. for (let otherItem of this.getListItems()) {
    492. if (item !== otherItem) { otherItem.selected = false; }
    493. }
    494. } else {
    495. item.selected = !item.selected;
    496. }
    497. this.index = this.displayItems.indexOf(item);
    498. this.highlightedItem = this.getItemId(this.index);
    499. this.doEmitSelect(false);
    500. }
    501. }
    502. onItemFocus(index) {
    503. const element = this.listElementList.toArray()[index].nativeElement;
    504. element.classList.add("bx--list-box__menu-item--highlighted");
    505. element.tabIndex = 0;
    506. }
    507. onItemBlur(index) {
    508. const element = this.listElementList.toArray()[index].nativeElement;
    509. element.classList.remove("bx--list-box__menu-item--highlighted");
    510. element.tabIndex = -1;
    511. }
    512. /**
    513. * Emits the scroll event of the options list
    514. */
    515. emitScroll(event) {
    516. const atTop: boolean = event.srcElement.scrollTop === 0;
    517. const atBottom: boolean = event.srcElement.scrollHeight - event.srcElement.scrollTop === event.srcElement.clientHeight;
    518. const customScrollEvent = { atTop, atBottom, event };
    519. this.scroll.emit(customScrollEvent);
    520. }
    521. /**
    522. * Subscribe the function passed to an internal observable that will resolve once the items are ready
    523. */
    524. onItemsReady(subcription: () => void): void {
    525. // this subscription will auto unsubscribe because of the `first()` pipe
    526. (this._itemsReady || of(true)).pipe(first()).subscribe(subcription);
    527. }
    528. reorderSelected(moveFocus = true): void {
    529. this.displayItems = [...this.getSelected(), ...this.getListItems().filter(item => !item.selected)];
    530. if (moveFocus) {
    531. setTimeout(() => {
    532. this.updateIndex();
    533. this.highlightedItem = this.getItemId(this.index);
    534. });
    535. }
    536. }
    537. }

    ```typescript import { Input, Output, EventEmitter, Directive } from “@angular/core”; import { ListItem } from “./list-item.interface”; import { Observable } from “rxjs”;

/**

  • A component that intends to be used within Dropdown must provide an implementation that extends this base class.
  • It also must provide the base class in the @Component meta-data.
  • ex: providers: [{provide: AbstractDropdownView, useExisting: forwardRef(() => MyDropdownView)}] / @Directive({ selector: “[ibmAbstractDropdownView]” }) export class AbstractDropdownView { /*

    • The items to be displayed in the list within the AbstractDropDownView. */ @Input() set items(value: Array | Observable>) { }

      get items(): Array | Observable> { return; } /**

    • Emits selection events to controlling classes / @Output() select: EventEmitter<{item: ListItem } | ListItem[]>; /*
    • Event to suggest a blur on the view.
    • Emits after the first/last item has been focused.
    • ex.
    • ArrowUp -> focus first item
    • ArrowUp -> emit event *
    • It’s recommended that the implementing view include a specific type union of possible blurs
    • ex. @Output() blurIntent = new EventEmitter<"top" | "bottom">(); / @Output() blurIntent: EventEmitter; /*
    • Specifies whether or not the DropdownList supports selecting multiple items as opposed to single
    • item selection. / public type: “single” | “multi” = “single”; /*
    • Specifies the render size of the items within the AbstractDropdownView. *
    • @deprecated since v4 / public size: “sm” | “md” | “xl” = “md”; /*
    • Returns the ListItem that is subsequent to the selected item in the DropdownList. / getNextItem(): ListItem { return; } /*
    • Returns a boolean if the currently selected item is preceded by another / hasNextElement(): boolean { return; } /*
    • Returns the HTMLElement for the item that is subsequent to the selected item. / getNextElement(): HTMLElement { return; } /*
    • Returns the ListItem that precedes the selected item within DropdownList. / getPrevItem(): ListItem { return; } /*
    • Returns a boolean if the currently selected item is followed by another / hasPrevElement(): boolean { return; } /*
    • Returns the HTMLElement for the item that precedes the selected item. / getPrevElement(): HTMLElement { return; } /*
    • Returns the selected leaf level item(s) within the DropdownList. / getSelected(): ListItem[] { return; } /*
    • Returns the ListItem that is selected within DropdownList. / getCurrentItem(): ListItem { return; } /*
    • Returns the HTMLElement for the item that is selected within the DropdownList. / getCurrentElement(): HTMLElement { return; } /*
    • Guaranteed to return the current items as an Array. / getListItems(): Array { return; } /*
    • Transforms array input list of items to the correct state by updating the selected item(s). / propagateSelected(value: Array): void {} /**
    • @param value value to filter the list by / filterBy(value: string): void {} /*
    • Initializes focus in the list
    • In most cases this just calls getCurrentElement().focus() / initFocus(): void {} /*
    • Subscribe the function passed to an internal observable that will resolve once the items are ready / onItemsReady(subcription: () => void): void {} /*
    • Reorder selected items bringing them to the top of the list */ reorderSelected(moveFocus?: boolean): void {} } typescript import { Injectable, ElementRef, OnDestroy } from “@angular/core”; import { PlaceholderService } from “carbon-components-angular/placeholder”; import { Subscription } from “rxjs”; import { position } from “@carbon/utils-position”; import { AnimationFrameService } from “carbon-components-angular/utils”; import { closestAttr } from “carbon-components-angular/utils”;

const defaultOffset = { top: 0, left: 0 };

@Injectable() export class DropdownService implements OnDestroy { public set offset(value: { top?: number, left?: number }) { this._offset = Object.assign({}, defaultOffset, value); }

  1. public get offset() {
  2. return this._offset;
  3. }
  4. /**
  5. * reference to the body appended menu
  6. */
  7. protected menuInstance: HTMLElement;
  8. /**
  9. * Maintains an Event Observable Subscription for the global requestAnimationFrame.
  10. * requestAnimationFrame is tracked only if the `Dropdown` is appended to the body otherwise we don't need it
  11. */
  12. protected animationFrameSubscription = new Subscription();
  13. protected _offset = defaultOffset;
  14. constructor(
  15. protected placeholderService: PlaceholderService,
  16. protected animationFrameService: AnimationFrameService
  17. ) {}
  18. /**
  19. * Appends the menu to the body, or a `ibm-placeholder` (if defined)
  20. *
  21. * @param parentRef container to position relative to
  22. * @param menuRef menu to be appended to body
  23. * @param classList any extra classes we should wrap the container with
  24. */
  25. appendToBody(parentRef: HTMLElement, menuRef: HTMLElement, classList): HTMLElement {
  26. // build the dropdown list container
  27. menuRef.style.display = "block";
  28. const dropdownWrapper = document.createElement("div");
  29. dropdownWrapper.className = `dropdown ${classList}`;
  30. dropdownWrapper.style.width = parentRef.offsetWidth + "px";
  31. dropdownWrapper.style.position = "absolute";
  32. dropdownWrapper.appendChild(menuRef);
  33. // append it to the placeholder
  34. if (this.placeholderService.hasPlaceholderRef()) {
  35. this.placeholderService.appendElement(dropdownWrapper);
  36. // or append it directly to the body
  37. } else {
  38. document.body.appendChild(dropdownWrapper);
  39. }
  40. this.menuInstance = dropdownWrapper;
  41. this.animationFrameSubscription = this.animationFrameService.tick.subscribe(() => {
  42. this.positionDropdown(parentRef, dropdownWrapper);
  43. });
  44. // run one position in sync, so we're less likely to have the view "jump" as we focus
  45. this.positionDropdown(parentRef, dropdownWrapper);
  46. return dropdownWrapper;
  47. }
  48. /**
  49. * Reattach the dropdown menu to the parent container
  50. * @param hostRef container to append to
  51. */
  52. appendToDropdown(hostRef: HTMLElement): HTMLElement {
  53. // if the instance is already removed don't try and remove it again
  54. if (!this.menuInstance) { return; }
  55. const instance = this.menuInstance;
  56. const menu = instance.firstElementChild as HTMLElement;
  57. // clean up the instance
  58. this.menuInstance = null;
  59. menu.style.display = "none";
  60. hostRef.appendChild(menu);
  61. this.animationFrameSubscription.unsubscribe();
  62. if (this.placeholderService.hasPlaceholderRef() && this.placeholderService.hasElement(instance)) {
  63. this.placeholderService.removeElement(instance);
  64. } else if (document.body.contains(instance)) {
  65. document.body.removeChild(instance);
  66. }
  67. return instance;
  68. }
  69. /**
  70. * position an open dropdown relative to the given parentRef
  71. */
  72. updatePosition(parentRef) {
  73. this.positionDropdown(parentRef, this.menuInstance);
  74. }
  75. ngOnDestroy() {
  76. this.animationFrameSubscription.unsubscribe();
  77. }
  78. protected positionDropdown(parentRef, menuRef) {
  79. if (!menuRef) {
  80. return;
  81. }
  82. let leftOffset = 0;
  83. const boxMenu = menuRef.querySelector(".bx--list-box__menu");
  84. if (boxMenu) {
  85. // If the parentRef and boxMenu are in a different left position relative to the
  86. // window, the the boxMenu position has already been flipped and a check needs to be done
  87. // to see if it needs to stay flipped.
  88. if (parentRef.getBoundingClientRect().left !== boxMenu.getBoundingClientRect().left) {
  89. // The getBoundingClientRect().right of the boxMenu if it were hypothetically flipped
  90. // back into the original position before the flip.
  91. const testBoxMenuRightEdgePos =
  92. parentRef.getBoundingClientRect().left - boxMenu.getBoundingClientRect().left + boxMenu.getBoundingClientRect().right;
  93. if (testBoxMenuRightEdgePos > (window.innerWidth || document.documentElement.clientWidth)) {
  94. leftOffset = parentRef.offsetWidth - boxMenu.offsetWidth;
  95. }
  96. // If it has not already been flipped, check if it is necessary to flip, ie. if the
  97. // boxMenu is outside of the right viewPort.
  98. } else if (boxMenu.getBoundingClientRect().right > (window.innerWidth || document.documentElement.clientWidth)) {
  99. leftOffset = parentRef.offsetWidth - boxMenu.offsetWidth;
  100. }
  101. }
  102. // If ibm-placeholder has a parent with a position(relative|fixed|absolute) account for the parent offset
  103. const closestMenuWithPos = closestAttr("position", ["relative", "fixed", "absolute"], menuRef.parentElement);
  104. const topPos = closestMenuWithPos ? closestMenuWithPos.getBoundingClientRect().top * -1 : this.offset.top;
  105. const leftPos = closestMenuWithPos ? closestMenuWithPos.getBoundingClientRect().left * -1 : this.offset.left + leftOffset;
  106. let pos = position.findAbsolute(parentRef, menuRef, "bottom");
  107. pos = position.addOffset(pos, topPos, leftPos);
  108. position.setElement(menuRef, pos);
  109. }

} ```