Skip to content

Commit 12f9749

Browse files
authored
Merge pull request #508 from cal-smith/master
chore(position): switch to @carbon/utils-position
2 parents 2abb484 + 5f5ef80 commit 12f9749

File tree

8 files changed

+71
-284
lines changed

8 files changed

+71
-284
lines changed

package-lock.json

Lines changed: 54 additions & 49 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@
146146
"zone.js": "0.9.0"
147147
},
148148
"dependencies": {
149+
"@carbon/utils-position": "1.0.0",
149150
"flatpickr": "4.5.7",
150151
"ng2-flatpickr": "7.0.3"
151152
}

src/dialog/dialog.component.ts

Lines changed: 9 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
} from "rxjs";
1919
import { throttleTime } from "rxjs/operators";
2020
// the AbsolutePosition is required to import the declaration correctly
21-
import position, { AbsolutePosition } from "./../utils/position";
21+
import { position, AbsolutePosition } from "@carbon/utils-position";
2222
import { cycleTabs, getFocusElementList } from "./../common/tab.service";
2323
import { DialogConfig } from "./dialog-config.interface";
2424

@@ -40,63 +40,46 @@ import { DialogConfig } from "./dialog-config.interface";
4040
export class Dialog implements OnInit, AfterViewInit, OnDestroy {
4141
/**
4242
* One static event observable to handle window resizing.
43-
* @protected
44-
* @static
45-
* @type {Observable<any>}
46-
* @memberof Dialog
4743
*/
4844
protected static resizeObservable: Observable<any> = fromEvent(window, "resize").pipe(throttleTime(100));
4945
/**
5046
* Emits event that handles the closing of a `Dialog` object.
51-
* @type {EventEmitter<any>}
52-
* @memberof Dialog
5347
*/
5448
@Output() close: EventEmitter<any> = new EventEmitter();
5549
/**
5650
* Receives `DialogConfig` interface object with properties of `Dialog`
57-
* explictly defined.
58-
* @type {DialogConfig}
59-
* @memberof Dialog
51+
* explicitly defined.
6052
*/
6153
@Input() dialogConfig: DialogConfig;
6254
/**
6355
* Maintains a reference to the view DOM element of the `Dialog`.
64-
* @type {ElementRef}
65-
* @memberof Dialog
6656
*/
6757
@ViewChild("dialog") dialog: ElementRef;
6858

6959
/**
7060
* Stores the data received from `dialogConfig`.
71-
* @memberof Dialog
7261
*/
7362
public data = {};
7463

7564
/**
76-
* The placement of the `Dialog` is recieved from the `Position` service.
77-
* @type {Placement}
78-
* @memberof Dialog
65+
* The placement of the `Dialog` is received from the `Position` service.
7966
*/
8067
public placement: string;
8168

8269
/**
8370
* `Subscription` used to update placement in the event of a window resize.
84-
* @protected
85-
* @type {Subscription}
86-
* @memberof Dialog
8771
*/
8872
protected resizeSubscription: Subscription;
8973
/**
9074
* Subscription to all the scrollable parents `scroll` event
9175
*/
92-
// add a new subscription temprarily so that contexts (such as tests)
76+
// add a new subscription temporarily so that contexts (such as tests)
9377
// that don't run ngAfterViewInit have something to unsubscribe in ngOnDestroy
9478
protected scrollSubscription: Subscription = new Subscription();
9579
/**
9680
* Handles offsetting the `Dialog` item based on the defined position
9781
* to not obscure the content beneath.
9882
* @protected
99-
* @memberof Dialog
10083
*/
10184
protected addGap = {
10285
"left": pos => position.addOffset(pos, 0, -this.dialogConfig.gap),
@@ -110,13 +93,11 @@ export class Dialog implements OnInit, AfterViewInit, OnDestroy {
11093
/**
11194
* Creates an instance of `Dialog`.
11295
* @param {ElementRef} elementRef
113-
* @memberof Dialog
11496
*/
115-
constructor(protected elementRef: ElementRef) {}
97+
constructor(protected elementRef: ElementRef) { }
11698

11799
/**
118-
* Initilize the `Dialog`, set the placement and gap, and add a `Subscription` to resize events.
119-
* @memberof Dialog
100+
* Initialize the `Dialog`, set the placement and gap, and add a `Subscription` to resize events.
120101
*/
121102
ngOnInit() {
122103
this.placement = this.dialogConfig.placement.split(",")[0];
@@ -126,14 +107,13 @@ export class Dialog implements OnInit, AfterViewInit, OnDestroy {
126107
this.placeDialog();
127108
});
128109

129-
// run any additional initlization code that consuming classes may have
110+
// run any additional initialization code that consuming classes may have
130111
this.onDialogInit();
131112
}
132113

133114
/**
134115
* After the DOM is ready, focus is set and dialog is placed
135116
* in respect to the parent element.
136-
* @memberof Dialog
137117
*/
138118
ngAfterViewInit() {
139119
const dialogElement = this.dialog.nativeElement;
@@ -236,30 +216,9 @@ export class Dialog implements OnInit, AfterViewInit, OnDestroy {
236216
// split always returns an array, so we can just use the auto position logic
237217
// for single positions too
238218
const placements = this.dialogConfig.placement.split(",");
239-
const weightedPlacements = placements.map(placement => {
240-
const pos = findPosition(parentEl, el, placement);
241-
let box = position.getPlacementBox(el, pos);
242-
let hiddenHeight = box.bottom - window.innerHeight - window.scrollY;
243-
let hiddenWidth = box.right - window.innerWidth - window.scrollX;
244-
// if the hiddenHeight or hiddenWidth is negative, reset to offsetHeight or offsetWidth
245-
hiddenHeight = hiddenHeight < 0 ? el.offsetHeight : hiddenHeight;
246-
hiddenWidth = hiddenWidth < 0 ? el.offsetWidth : hiddenWidth;
247-
const area = el.offsetHeight * el.offsetWidth;
248-
const hiddenArea = hiddenHeight * hiddenWidth;
249-
let visibleArea = area - hiddenArea;
250-
// if the visibleArea is 0 set it back to area (to calculate the percentage in a useful way)
251-
visibleArea = visibleArea === 0 ? area : visibleArea;
252-
const visiblePercent = visibleArea / area;
253-
return {
254-
placement,
255-
weight: visiblePercent
256-
};
257-
});
258219

259-
// sort the placements from best to worst
260-
weightedPlacements.sort((a, b) => b.weight - a.weight);
261-
// pick the best!
262-
dialogPlacement = weightedPlacements[0].placement;
220+
// find the best placement
221+
dialogPlacement = position.findBestPlacement(parentEl, el, placements);
263222

264223
// calculate the final position
265224
const pos = findPosition(parentEl, el, dialogPlacement);
@@ -293,7 +252,6 @@ export class Dialog implements OnInit, AfterViewInit, OnDestroy {
293252
* Sets up a event Listener to close `Dialog` if click event occurs outside
294253
* `Dialog` object.
295254
* @param {any} event
296-
* @memberof Dialog
297255
*/
298256
@HostListener("document:click", ["$event"])
299257
clickClose(event) {
@@ -305,15 +263,13 @@ export class Dialog implements OnInit, AfterViewInit, OnDestroy {
305263

306264
/**
307265
* Closes `Dialog` object by emitting the close event upwards to parents.
308-
* @memberof Dialog
309266
*/
310267
public doClose() {
311268
this.close.emit();
312269
}
313270

314271
/**
315272
* At destruction of component, `Dialog` unsubscribes from handling window resizing changes.
316-
* @memberof Dialog
317273
*/
318274
ngOnDestroy() {
319275
this.resizeSubscription.unsubscribe();

src/dialog/overflow-menu/overflow-menu-pane.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Component, HostListener, ElementRef, AfterViewInit } from "@angular/core";
22
import { Dialog } from "../dialog.component";
3-
import { position } from "../../utils/position";
3+
import { position } from "@carbon/utils-position";
44
import { isFocusInLastItem, isFocusInFirstItem } from "./../../common/tab.service";
55
import { I18n } from "./../../i18n/i18n.module";
66
import { ExperimentalService } from "./../../experimental.module";

src/dialog/overflow-menu/overflow-menu.stories.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ storiesOf("Overflow Menu", module)
4848
}))
4949
.add("With links", () => ({
5050
template: `
51-
<app-experimental-component></app-experimental-component>
5251
<ibm-overflow-menu [flip]="flip" >
5352
<ibm-overflow-menu-option href="https://www.ibm.com" (selected)="selected($event)" (click)="click($event)">
5453
An example option that is really long to show what should be done to handle long text
@@ -57,7 +56,9 @@ storiesOf("Overflow Menu", module)
5756
<ibm-overflow-menu-option href="https://www.ibm.com" (selected)="selected($event)">Option 3</ibm-overflow-menu-option>
5857
<ibm-overflow-menu-option href="https://www.ibm.com" (selected)="selected($event)">Option 4</ibm-overflow-menu-option>
5958
<ibm-overflow-menu-option href="https://www.ibm.com" disabled="true" (selected)="selected($event)">Disabled</ibm-overflow-menu-option>
60-
<ibm-overflow-menu-option href="https://www.ibm.com" type="danger" (selected)="selected($event)">Danger option</ibm-overflow-menu-option>
59+
<ibm-overflow-menu-option href="https://www.ibm.com" type="danger" (selected)="selected($event)">
60+
Danger option
61+
</ibm-overflow-menu-option>
6162
</ibm-overflow-menu>
6263
<ibm-placeholder></ibm-placeholder>
6364
`,

src/dropdown/dropdown.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { NG_VALUE_ACCESSOR } from "@angular/forms";
1717
import { Observable, fromEvent, of, Subscription } from "rxjs";
1818

1919
import { AbstractDropdownView } from "./abstract-dropdown-view.class";
20-
import { position } from "../utils/position";
20+
import { position } from "@carbon/utils-position";
2121
import { I18n } from "./../i18n/i18n.module";
2222
import { ListItem } from "./list-item.interface";
2323
import { DropdownService } from "./dropdown.service";

src/dropdown/dropdown.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Injectable, ElementRef } from "@angular/core";
22
import { PlaceholderService } from "./../placeholder/placeholder.module";
33
import { fromEvent, Subscription } from "rxjs";
44
import { throttleTime } from "rxjs/operators";
5-
import position from "./../utils/position";
5+
import { position } from "@carbon/utils-position";
66

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

src/utils/position.ts

Lines changed: 1 addition & 177 deletions
Original file line numberDiff line numberDiff line change
@@ -1,177 +1 @@
1-
/**
2-
* Utilities to manipulate the position of elements relative to other elements
3-
*
4-
*/
5-
6-
// possible positions ... this should probably be moved (along with some other types) to some central location
7-
export type Placement =
8-
"left" | "right" | "top" | "bottom" | "top-left" | "top-right" | "bottom-left" | "bottom-right" | "left-bottom" | "right-bottom";
9-
10-
export interface AbsolutePosition {
11-
top: number;
12-
left: number;
13-
position?: AbsolutePosition;
14-
}
15-
16-
export type Offset = { left: number, top: number };
17-
18-
function calculatePosition(referenceOffset: Offset, reference: HTMLElement, target: HTMLElement, placement: Placement): AbsolutePosition {
19-
// calculate offsets for a given position
20-
const referenceRect = reference.getBoundingClientRect();
21-
switch (placement) {
22-
case "left":
23-
return {
24-
top: referenceOffset.top - Math.round(target.offsetHeight / 2) + Math.round(referenceRect.height / 2),
25-
left: Math.round(referenceOffset.left - target.offsetWidth)
26-
};
27-
case "right":
28-
return {
29-
top: referenceOffset.top - Math.round(target.offsetHeight / 2) + Math.round(referenceRect.height / 2),
30-
left: Math.round(referenceOffset.left + referenceRect.width)
31-
};
32-
case "top":
33-
return {
34-
top: Math.round(referenceOffset.top - target.offsetHeight),
35-
left: referenceOffset.left - Math.round(target.offsetWidth / 2) + Math.round(referenceRect.width / 2)
36-
};
37-
case "bottom":
38-
return {
39-
top: Math.round(referenceOffset.top + referenceRect.height),
40-
left: referenceOffset.left - Math.round(target.offsetWidth / 2) + Math.round(referenceRect.width / 2)
41-
};
42-
case "left-bottom":
43-
return {
44-
// 22 == half of popover header height
45-
top: referenceOffset.top - 22 + Math.round(referenceRect.height / 2),
46-
left: Math.round(referenceOffset.left - target.offsetWidth)
47-
};
48-
case "right-bottom":
49-
return {
50-
top: referenceOffset.top - 22 + Math.round(referenceRect.height / 2),
51-
left: Math.round(referenceOffset.left + referenceRect.width)
52-
};
53-
// matter currently doesn't support these, so the popover is broken anyway
54-
case "top-left":
55-
return {
56-
top: 0,
57-
left: 0
58-
};
59-
case "top-right":
60-
return {
61-
top: 0,
62-
left: 0
63-
};
64-
case "bottom-left":
65-
return {
66-
top: referenceOffset.top + referenceRect.height,
67-
left: referenceOffset.left + referenceRect.width - target.offsetWidth
68-
};
69-
case "bottom-right":
70-
return {
71-
top: referenceOffset.top + referenceRect.height,
72-
left: referenceOffset.left
73-
};
74-
}
75-
}
76-
77-
export namespace position {
78-
export function getRelativeOffset(target: HTMLElement): Offset {
79-
// start with the initial element offsets
80-
let offsets = {
81-
left: target.offsetLeft,
82-
top: target.offsetTop
83-
};
84-
// get each static (i.e. not absolute or relative) offsetParent and sum the left/right offsets
85-
while (target.offsetParent && getComputedStyle(target.offsetParent).position === "static") {
86-
offsets.left += target.offsetLeft;
87-
offsets.top += target.offsetTop;
88-
target = target.offsetParent as HTMLElement;
89-
}
90-
return offsets;
91-
}
92-
93-
export function getAbsoluteOffset(target: HTMLElement): Offset {
94-
let currentNode = target;
95-
let margins = {
96-
top: 0,
97-
left: 0
98-
};
99-
100-
// searches for containing elements with additional margins
101-
while (currentNode.offsetParent) {
102-
const computed = getComputedStyle(currentNode.offsetParent);
103-
// find static elements with additional margins
104-
// since they tend to throw off our positioning
105-
// (usually this is just the body)
106-
if (
107-
computed.position === "static" &&
108-
computed.marginLeft &&
109-
computed.marginTop
110-
) {
111-
if (parseInt(computed.marginTop, 10)) {
112-
margins.top += parseInt(computed.marginTop, 10);
113-
}
114-
if (parseInt(computed.marginLeft, 10)) {
115-
margins.left += parseInt(computed.marginLeft, 10);
116-
}
117-
}
118-
119-
currentNode = currentNode.offsetParent as HTMLElement;
120-
}
121-
122-
const targetRect = target.getBoundingClientRect();
123-
const relativeRect = document.body.getBoundingClientRect();
124-
return {
125-
top: targetRect.top - relativeRect.top + margins.top,
126-
left: targetRect.left - relativeRect.left + margins.left
127-
};
128-
}
129-
130-
// finds the position relative to the `reference` element
131-
export function findRelative(reference: HTMLElement, target: HTMLElement, placement: Placement): AbsolutePosition {
132-
const referenceOffset = getRelativeOffset(reference);
133-
return calculatePosition(referenceOffset, reference, target, placement);
134-
}
135-
136-
export function findAbsolute(reference: HTMLElement, target: HTMLElement, placement: Placement): AbsolutePosition {
137-
const referenceOffset = getAbsoluteOffset(reference);
138-
return calculatePosition(referenceOffset, reference, target, placement);
139-
}
140-
141-
export function findPosition(reference: HTMLElement,
142-
target: HTMLElement,
143-
placement: Placement,
144-
offsetFunction = getRelativeOffset): AbsolutePosition {
145-
const referenceOffset = offsetFunction(reference);
146-
return calculatePosition(referenceOffset, reference, target, placement);
147-
}
148-
149-
/**
150-
* Get the dimensions of the dialog from an AbsolutePosition and a reference element
151-
*/
152-
export function getPlacementBox(target: HTMLElement, position: AbsolutePosition) {
153-
const targetBottom = target.offsetHeight + position.top;
154-
const targetRight = target.offsetWidth + position.left;
155-
156-
return {
157-
top: position.top,
158-
bottom: targetBottom,
159-
left: position.left,
160-
right: targetRight
161-
};
162-
}
163-
164-
export function addOffset(position: AbsolutePosition, top = 0, left = 0): AbsolutePosition {
165-
return Object.assign({}, position, {
166-
top: position.top + top,
167-
left: position.left + left
168-
});
169-
}
170-
171-
export function setElement(element: HTMLElement, position: AbsolutePosition): void {
172-
element.style.top = `${position.top}px`;
173-
element.style.left = `${position.left}px`;
174-
}
175-
}
176-
177-
export default position;
1+
export * from "@carbon/utils-position";

0 commit comments

Comments
 (0)