Skip to content

Commit 5c5003e

Browse files
authored
Merge pull request #208 from cal-smith/tooltip
fix(dialog): enable left/right flipping for OverflowMenu
2 parents e329c88 + 791cb08 commit 5c5003e

File tree

9 files changed

+116
-57
lines changed

9 files changed

+116
-57
lines changed

src/common/utils.ts

Lines changed: 1 addition & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,4 @@
1-
let _scrollbarWidth = -1;
2-
3-
export function getScrollbarWidth() {
4-
// lets not recreate this whole thing every time
5-
if (_scrollbarWidth >= 0) {
6-
return _scrollbarWidth;
7-
}
8-
9-
// do the calculations the first time
10-
const outer = document.createElement("div");
11-
outer.style.visibility = "hidden";
12-
outer.style.width = "100px";
13-
outer.style.msOverflowStyle = "scrollbar"; // needed for WinJS apps
14-
15-
document.body.appendChild(outer);
16-
17-
const widthNoScroll = outer.offsetWidth;
18-
// force scrollbars
19-
outer.style.overflow = "scroll";
20-
21-
// add innerdiv
22-
const inner = document.createElement("div");
23-
inner.style.width = "100%";
24-
outer.appendChild(inner);
25-
26-
const widthWithScroll = inner.offsetWidth;
27-
28-
// remove divs
29-
outer.parentNode.removeChild(outer);
30-
31-
_scrollbarWidth = widthNoScroll - widthWithScroll;
32-
return _scrollbarWidth;
33-
}
1+
export * from "./../utils/window-tools";
342

353
/**
364
* Does what python's `range` function does, with a slightly different

src/dialog/dialog.component.ts

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -219,13 +219,31 @@ export class Dialog implements OnInit, AfterViewInit, OnDestroy {
219219
// split always retuns an array, so we can just use the auto position logic
220220
// for single positions too
221221
const placements = this.dialogConfig.placement.split(",");
222-
for (const placement of placements) {
222+
const weightedPlacements = placements.map(placement => {
223223
const pos = findPosition(parentEl, el, placement);
224-
if (position.checkPlacement(el, pos)) {
225-
dialogPlacement = placement;
226-
break;
227-
}
228-
}
224+
let box = position.getPlacementBox(el, pos);
225+
let hiddenHeight = box.bottom - window.innerHeight - window.scrollY;
226+
let hiddenWidth = box.right - window.innerWidth - window.scrollX;
227+
// if the hiddenHeight or hiddenWidth is negative, reset to offsetHeight or offsetWidth
228+
hiddenHeight = hiddenHeight < 0 ? el.offsetHeight : hiddenHeight;
229+
hiddenWidth = hiddenWidth < 0 ? el.offsetWidth : hiddenWidth;
230+
const area = el.offsetHeight * el.offsetWidth;
231+
const hiddenArea = hiddenHeight * hiddenWidth;
232+
let visibleArea = area - hiddenArea;
233+
// if the visibleArea is 0 set it back to area (to calculate the percentage in a useful way)
234+
visibleArea = visibleArea === 0 ? area : visibleArea;
235+
const visiblePercent = visibleArea / area;
236+
return {
237+
placement,
238+
weight: visiblePercent
239+
};
240+
});
241+
242+
// sort the placments from best to worst
243+
weightedPlacements.sort((a, b) => b.weight - a.weight);
244+
// pick the best!
245+
dialogPlacement = weightedPlacements[0].placement;
246+
229247
// calculate the final position
230248
const pos = findPosition(parentEl, el, dialogPlacement);
231249

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@ import { I18n } from "./../../i18n/i18n.module";
1313
selector: "ibm-overflow-menu-pane",
1414
template: `
1515
<ul
16-
role="menu"
1716
[attr.aria-label]="dialogConfig.menuLabel"
17+
[ngClass]="{'bx--overflow-menu--flip': dialogConfig.flip}"
18+
role="menu"
1819
#dialog
1920
class="bx--overflow-menu-options bx--overflow-menu-options--open">
2021
<ng-template
@@ -40,7 +41,12 @@ export class OverflowMenuPane extends Dialog {
4041
* (position service trys it's best to center everything,
4142
* so we need to add some compensation)
4243
*/
43-
this.addGap["bottom"] = pos => position.addOffset(pos, -20, 60);
44+
this.addGap["bottom"] = pos => {
45+
if (this.dialogConfig.flip) {
46+
return position.addOffset(pos, -20, -60);
47+
}
48+
return position.addOffset(pos, -20, 60);
49+
}
4450

4551
if (!this.dialogConfig.menuLabel) {
4652
this.dialogConfig.menuLabel = this.i18n.get().OVERFLOW_MENU.OVERFLOW;

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { I18n } from "./../../i18n/i18n.module";
2020
[appendToBody]="true"
2121
[ngClass]="{'bx--overflow-menu--open': open === true}"
2222
[attr.aria-label]="buttonLabel"
23+
[flip]="flip"
2324
class="bx--overflow-menu"
2425
role="button"
2526
placement="bottom"
@@ -47,6 +48,8 @@ export class OverflowMenu {
4748

4849
@Input() buttonLabel = this.i18n.get().OVERFLOW_MENU.OVERFLOW;
4950

51+
@Input() flip = false;
52+
5053
constructor(protected elementRef: ElementRef, protected i18n: I18n) {}
5154

5255
get open() {

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,14 @@ import { OverflowMenuPane } from "./overflow-menu-pane.component";
3535
]
3636
})
3737
export class OverflowMenuDirective extends DialogDirective {
38+
/**
39+
* Takes a template ref of `OverflowMenuOptions`s
40+
*/
3841
@Input() ibmOverflowMenu: TemplateRef<any>;
42+
/**
43+
* Controls wether the overflow menu is flipped
44+
*/
45+
@Input() flip = false;
3946

4047
/**
4148
* Creates an instance of `OverflowMenuDirective`.
@@ -51,6 +58,7 @@ export class OverflowMenuDirective extends DialogDirective {
5158

5259
onDialogInit() {
5360
this.dialogConfig.content = this.ibmOverflowMenu;
61+
this.dialogConfig.flip = this.flip;
5462
}
5563

5664
@HostListener("keydown", ["$event"])

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,20 @@ storiesOf("Overflow Menu", module)
2424
<ibm-overflow-menu-option disabled="true">Disabled</ibm-overflow-menu-option>
2525
<ibm-overflow-menu-option type="danger">Danger option</ibm-overflow-menu-option>
2626
</ibm-overflow-menu>
27+
28+
<span>Flipped OverflowMenu</span>
29+
<ibm-overflow-menu flip="true" style="display: inline-block;">
30+
<ibm-overflow-menu-option>
31+
An example option that is really long to show what should be done to handle long text
32+
</ibm-overflow-menu-option>
33+
<ibm-overflow-menu-option>Option 2</ibm-overflow-menu-option>
34+
<li class="bx--overflow-menu-options__option">
35+
<button class="bx--overflow-menu-options__btn">A fully custom option</button>
36+
</li>
37+
<ibm-overflow-menu-option>Option 4</ibm-overflow-menu-option>
38+
<ibm-overflow-menu-option disabled="true">Disabled</ibm-overflow-menu-option>
39+
<ibm-overflow-menu-option type="danger">Danger option</ibm-overflow-menu-option>
40+
</ibm-overflow-menu>
2741
<ibm-dialog-placeholder></ibm-dialog-placeholder>
2842
`
2943
}));

src/utils/.editorconfig

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# http://editorconfig.org
2+
3+
root = true
4+
5+
[*]
6+
charset = utf-8
7+
end_of_file = lf
8+
indent_style = tab
9+
indent_size = 4
10+
trim_trailing_whitespace = true
11+
insert_final_newline = true

src/utils/position.ts

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
/**
22
* Utilites to manipulate the position of elements relative to other elements
33
*
4-
* @export
54
*/
65

6+
import { getScrollbarWidth } from "./window-tools";
7+
78
// possible positions ... this should probably be moved (along with some other types) to some central location
89
export type Placement =
910
"left" | "right" | "top" | "bottom" | "top-left" | "top-right" | "bottom-left" | "bottom-right" | "left-bottom" | "right-bottom";
@@ -117,22 +118,19 @@ export namespace position {
117118
return calculatePosition(referenceOffset, reference, target, placement);
118119
}
119120

120-
/** check if the placement is within the window. */
121-
export function checkPlacement(target: HTMLElement, position: AbsolutePosition): boolean {
122-
const elTop = position.top;
123-
const elLeft = position.left;
124-
const elBottom = target.offsetHeight + position.top;
125-
const elRight = target.offsetWidth + position.left;
126-
const windowTop = window.scrollY;
127-
const windowLeft = window.scrollX;
128-
// remove the target height so we get a reasonably accurate window height reading
129-
const windowBottom = (window.innerHeight + window.scrollY) - target.offsetHeight;
130-
const windowRight = window.innerWidth + window.scrollX;
121+
/**
122+
* Get the dimensions of the dialog from an AbsolutePosition and a reference element
123+
*/
124+
export function getPlacementBox(target: HTMLElement, position: AbsolutePosition) {
125+
const targetBottom = target.offsetHeight + position.top;
126+
const targetRight = target.offsetWidth + position.left;
131127

132-
if (elBottom < windowBottom && elRight < windowRight && elTop > windowTop && elLeft > windowLeft) {
133-
return true;
134-
}
135-
return false;
128+
return {
129+
top: position.top,
130+
bottom: targetBottom,
131+
left: position.left,
132+
right: targetRight
133+
};
136134
}
137135

138136
export function addOffset(position: AbsolutePosition, top = 0, left = 0): AbsolutePosition {

src/utils/window-tools.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
let _scrollbarWidth = -1;
2+
3+
export function getScrollbarWidth() {
4+
// lets not recreate this whole thing every time
5+
if (_scrollbarWidth >= 0) {
6+
return _scrollbarWidth;
7+
}
8+
9+
// do the calculations the first time
10+
const outer = document.createElement("div");
11+
outer.style.visibility = "hidden";
12+
outer.style.width = "100px";
13+
outer.style.msOverflowStyle = "scrollbar"; // needed for WinJS apps
14+
15+
document.body.appendChild(outer);
16+
17+
const widthNoScroll = outer.offsetWidth;
18+
// force scrollbars
19+
outer.style.overflow = "scroll";
20+
21+
// add innerdiv
22+
const inner = document.createElement("div");
23+
inner.style.width = "100%";
24+
outer.appendChild(inner);
25+
26+
const widthWithScroll = inner.offsetWidth;
27+
28+
// remove divs
29+
outer.parentNode.removeChild(outer);
30+
31+
_scrollbarWidth = widthNoScroll - widthWithScroll;
32+
return _scrollbarWidth;
33+
}

0 commit comments

Comments
 (0)