Skip to content

Commit bb165b1

Browse files
authored
Merge pull request #421 from cal-smith/dropdown-placeholder
fix(dropdown): add dropdown service to enable placeholder use
2 parents 94591f6 + 2a59768 commit bb165b1

File tree

7 files changed

+133
-48
lines changed

7 files changed

+133
-48
lines changed

src/dropdown/dropdown.component.spec.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { DropdownList } from "./list/dropdown-list.component";
88
import { ListItem } from "./list-item.interface";
99
import { ScrollableList } from "./scrollable-list.directive";
1010
import { I18nModule } from "../i18n/i18n.module";
11+
import { DropdownService } from "./dropdown.service";
12+
import { PlaceholderModule } from "./../placeholder/placeholder.module";
1113

1214
@Component({
1315
template: `
@@ -38,8 +40,10 @@ describe("Dropdown", () => {
3840
],
3941
imports: [
4042
StaticIconModule,
41-
I18nModule
42-
]
43+
I18nModule,
44+
PlaceholderModule
45+
],
46+
providers: [ DropdownService ]
4347
});
4448
});
4549

src/dropdown/dropdown.component.ts

Lines changed: 10 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@ import { NG_VALUE_ACCESSOR } from "@angular/forms";
1515

1616
// Observable import is required here so typescript can compile correctly
1717
import { Observable, fromEvent, of, Subscription } from "rxjs";
18-
import { throttleTime } from "rxjs/operators";
1918

2019
import { AbstractDropdownView } from "./abstract-dropdown-view.class";
2120
import { position } from "../utils/position";
2221
import { I18n } from "./../i18n/i18n.module";
2322
import { ListItem } from "./list-item.interface";
23+
import { DropdownService } from "./dropdown.service";
2424

2525
/**
2626
* Drop-down lists enable users to select one or more items from a list.
@@ -183,21 +183,12 @@ export class Dropdown implements OnInit, AfterContentInit, OnDestroy {
183183
*/
184184
dropUp = false;
185185

186-
/**
187-
* Used by the various appendToX methods to keep a reference to our wrapper div
188-
*/
189-
dropdownWrapper: HTMLElement;
190186
// .bind creates a new function, so we declare the methods below
191187
// but .bind them up here
192188
noop = this._noop.bind(this);
193189
outsideClick = this._outsideClick.bind(this);
194190
outsideKey = this._outsideKey.bind(this);
195191
keyboardNav = this._keyboardNav.bind(this);
196-
/**
197-
* Maintains an Event Observable Subscription for tracking window resizes.
198-
* Window resizing is tracked if the `Dropdown` is appended to the body, otherwise it does not need to be supported.
199-
*/
200-
resize: Subscription;
201192
/**
202193
* Maintians an Event Observable Subscription for tracking scrolling within the open `Dropdown` list.
203194
*/
@@ -208,7 +199,7 @@ export class Dropdown implements OnInit, AfterContentInit, OnDestroy {
208199
/**
209200
* Creates an instance of Dropdown.
210201
*/
211-
constructor(protected elementRef: ElementRef, protected i18n: I18n) {}
202+
constructor(protected elementRef: ElementRef, protected i18n: I18n, protected dropdownService: DropdownService) {}
212203

213204
/**
214205
* Updates the `type` property in the `@ContentChild`.
@@ -413,37 +404,19 @@ export class Dropdown implements OnInit, AfterContentInit, OnDestroy {
413404
* Creates the `Dropdown` list appending it to the dropdown parent object instead of the body.
414405
*/
415406
_appendToDropdown() {
416-
if (document.body.contains(this.dropdownWrapper)) {
417-
this.dropdownMenu.nativeElement.style.display = "none";
418-
this.elementRef.nativeElement.appendChild(this.dropdownMenu.nativeElement);
419-
document.body.removeChild(this.dropdownWrapper);
420-
this.resize.unsubscribe();
421-
this.dropdownWrapper.removeEventListener("keydown", this.keyboardNav, true);
422-
}
407+
this.dropdownService.appendToDropdown(this.elementRef.nativeElement);
408+
this.dropdownMenu.nativeElement.removeEventListener("keydown", this.keyboardNav, true);
423409
}
424410

425411
/**
426412
* Creates the `Dropdown` list as an element that is appended to the DOM body.
427413
*/
428414
_appendToBody() {
429-
const positionDropdown = () => {
430-
let pos = position.findAbsolute(this.dropdownButton.nativeElement, this.dropdownWrapper, "bottom");
431-
// add -40 to the top position to account for carbon styles
432-
pos = position.addOffset(pos, -40, 0);
433-
position.setElement(this.dropdownWrapper, pos);
434-
};
435-
this.dropdownMenu.nativeElement.style.display = "block";
436-
this.dropdownWrapper = document.createElement("div");
437-
this.dropdownWrapper.className = `dropdown ${this.elementRef.nativeElement.className}`;
438-
this.dropdownWrapper.style.width = this.dropdownButton.nativeElement.offsetWidth + "px";
439-
this.dropdownWrapper.style.position = "absolute";
440-
this.dropdownWrapper.appendChild(this.dropdownMenu.nativeElement);
441-
document.body.appendChild(this.dropdownWrapper);
442-
positionDropdown();
443-
this.dropdownWrapper.addEventListener("keydown", this.keyboardNav, true);
444-
this.resize = fromEvent(window, "resize")
445-
.pipe(throttleTime(100))
446-
.subscribe(() => positionDropdown());
415+
this.dropdownService.appendToBody(
416+
this.dropdownButton.nativeElement,
417+
this.dropdownMenu.nativeElement,
418+
this.elementRef.nativeElement.className);
419+
this.dropdownMenu.nativeElement.addEventListener("keydown", this.keyboardNav, true);
447420
}
448421

449422
/**
@@ -529,12 +502,7 @@ export class Dropdown implements OnInit, AfterContentInit, OnDestroy {
529502
this.scroll = fromEvent(container, "scroll")
530503
.subscribe(() => {
531504
if (this.isVisibleInContainer(this.elementRef.nativeElement, container)) {
532-
position.setElement(
533-
this.dropdownWrapper,
534-
position.addOffset(
535-
position.findAbsolute(this.elementRef.nativeElement, this.dropdownWrapper, "bottom")
536-
)
537-
);
505+
this.dropdownService.updatePosition(this.dropdownButton.nativeElement);
538506
} else {
539507
this.closeMenu();
540508
}

src/dropdown/dropdown.module.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,16 @@ import { DropdownList } from "./list/dropdown-list.component";
88

99
import { ScrollableList } from "./scrollable-list.directive";
1010
import { I18nModule } from "./../i18n/i18n.module";
11+
import { PlaceholderModule } from "./../placeholder/placeholder.module";
12+
import { DropdownService } from "./dropdown.service";
1113

1214
export { Dropdown } from "./dropdown.component";
1315
export { DropdownList } from "./list/dropdown-list.component";
1416

1517
export { ScrollableList } from "./scrollable-list.directive";
1618
export { AbstractDropdownView } from "./abstract-dropdown-view.class";
1719
export { ListItem } from "./list-item.interface";
20+
export { DropdownService } from "./dropdown.service";
1821

1922
@NgModule({
2023
declarations: [
@@ -31,7 +34,9 @@ export { ListItem } from "./list-item.interface";
3134
CommonModule,
3235
FormsModule,
3336
StaticIconModule,
34-
I18nModule
35-
]
37+
I18nModule,
38+
PlaceholderModule
39+
],
40+
providers: [ DropdownService ]
3641
})
3742
export class DropdownModule {}

src/dropdown/dropdown.service.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { Injectable, ElementRef } from "@angular/core";
2+
import { PlaceholderService } from "./../placeholder/placeholder.module";
3+
import { fromEvent, Subscription } from "rxjs";
4+
import { throttleTime } from "rxjs/operators";
5+
import position from "./../utils/position";
6+
7+
@Injectable()
8+
export class DropdownService {
9+
/**
10+
* reference to the body appended menu
11+
*/
12+
protected menuInstance: HTMLElement;
13+
14+
/**
15+
* Maintains an Event Observable Subscription for tracking window resizes.
16+
* Window resizing is tracked if the `Dropdown` is appended to the body, otherwise it does not need to be supported.
17+
*/
18+
protected resize: Subscription;
19+
20+
constructor(protected placeholderService: PlaceholderService) {}
21+
22+
/**
23+
* Appends the menu to the body, or a `ibm-placeholder` (if defined)
24+
*
25+
* @param parentRef container to position relative to
26+
* @param menuRef menu to be appended to body
27+
* @param classList any extra classes we should wrap the container with
28+
*/
29+
appendToBody(parentRef: HTMLElement, menuRef: HTMLElement, classList): HTMLElement {
30+
// build the dropdown list container
31+
menuRef.style.display = "block";
32+
const dropdownWrapper = document.createElement("div");
33+
dropdownWrapper.className = `dropdown ${classList}`;
34+
dropdownWrapper.style.width = parentRef.offsetWidth + "px";
35+
dropdownWrapper.style.position = "absolute";
36+
dropdownWrapper.appendChild(menuRef);
37+
38+
// append it to the placeholder
39+
if (this.placeholderService.hasPlaceholderRef()) {
40+
this.placeholderService.appendElement(dropdownWrapper);
41+
// or append it directly to the body
42+
} else {
43+
document.body.appendChild(dropdownWrapper);
44+
}
45+
46+
this.menuInstance = dropdownWrapper;
47+
48+
this.positionDropdown(parentRef, dropdownWrapper);
49+
this.resize = fromEvent(window, "resize")
50+
.pipe(throttleTime(100))
51+
.subscribe(() => this.positionDropdown(parentRef, dropdownWrapper));
52+
53+
return dropdownWrapper;
54+
}
55+
56+
/**
57+
* Reattach the dropdown menu to the parent container
58+
* @param hostRef container to append to
59+
*/
60+
appendToDropdown(hostRef: HTMLElement): HTMLElement {
61+
// if the instance is already removed don't try and remove it again
62+
if (!this.menuInstance) { return; }
63+
const instance = this.menuInstance;
64+
const menu = instance.firstElementChild as HTMLElement;
65+
// clean up the instance
66+
this.menuInstance = null;
67+
menu.style.display = "none";
68+
hostRef.appendChild(menu);
69+
this.resize.unsubscribe();
70+
if (this.placeholderService.hasPlaceholderRef() && this.placeholderService.hasElement(instance)) {
71+
this.placeholderService.removeElement(instance);
72+
} else if (document.body.contains(instance)) {
73+
document.body.removeChild(instance);
74+
}
75+
return instance;
76+
}
77+
78+
/**
79+
* position an open dropdown relative to the given parentRef
80+
*/
81+
updatePosition(parentRef) {
82+
this.positionDropdown(parentRef, this.menuInstance);
83+
}
84+
85+
protected positionDropdown(parentRef, menuRef) {
86+
let pos = position.findAbsolute(parentRef, menuRef, "bottom");
87+
// add -40 to the top position to account for carbon styles
88+
pos = position.addOffset(pos, -40, 0);
89+
position.setElement(menuRef, pos);
90+
}
91+
}

src/dropdown/dropdown.stories.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ import { withKnobs, select, boolean, object, text } from "@storybook/addon-knobs
55

66
import { DropdownModule } from "../";
77
import { of } from "rxjs";
8+
import { PlaceholderModule } from "../placeholder/placeholder.module";
89

910
storiesOf("Dropdown", module)
1011
.addDecorator(
1112
moduleMetadata({
1213
imports: [
13-
DropdownModule
14+
DropdownModule,
15+
PlaceholderModule
1416
]
1517
})
1618
)
@@ -27,6 +29,7 @@ storiesOf("Dropdown", module)
2729
<ibm-dropdown-list [items]="items"></ibm-dropdown-list>
2830
</ibm-dropdown>
2931
</div>
32+
<ibm-placeholder></ibm-placeholder>
3033
`,
3134
props: {
3235
disabled: boolean("disabled", false),

src/dropdown/list/dropdown-list.component.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,9 @@ export class DropdownList implements AbstractDropdownView, AfterViewInit, OnDest
9797
*/
9898
@Input() set items (value: Array<ListItem> | Observable<Array<ListItem>>) {
9999
if (isObservable(value)) {
100-
this._itemsSubscription.unsubscribe();
100+
if (this._itemsSubscription) {
101+
this._itemsSubscription.unsubscribe();
102+
}
101103
this._itemsSubscription = value.subscribe(v => this.updateList(v));
102104
} else {
103105
this.updateList(value);

src/placeholder/placeholder.service.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,16 @@ export class PlaceholderService {
5353
hasPlaceholderRef() {
5454
return !!this.viewContainerRef;
5555
}
56+
57+
appendElement(element: HTMLElement): HTMLElement {
58+
return this.viewContainerRef.element.nativeElement.appendChild(element);
59+
}
60+
61+
removeElement(element: HTMLElement): HTMLElement {
62+
return this.viewContainerRef.element.nativeElement.removeChild(element);
63+
}
64+
65+
hasElement(element: HTMLElement): boolean {
66+
return this.viewContainerRef.element.nativeElement.contains(element);
67+
}
5668
}

0 commit comments

Comments
 (0)