Skip to content

Commit 114ed42

Browse files
vfcostavalorkin
authored andcommitted
feat(positioning): auto option for positioning (#1986)
* feat(positioning): auto option for positioning fixes #1111 * Update tooltip.spec.ts Concistensy
1 parent 9f833eb commit 114ed42

File tree

9 files changed

+143
-3
lines changed

9 files changed

+143
-3
lines changed

demo/src/app/components/+popover/demos/four-directions/four-directions.html

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,10 @@
2525
placement="right">
2626
Popover on right
2727
</button>
28+
29+
<button type="button" class="btn btn-default btn-secondary"
30+
popover="Vivamus sagittis lacus vel augue laoreet rutrum faucibus."
31+
popoverTitle="Popover auto"
32+
placement="auto">
33+
Popover auto
34+
</button>

demo/src/app/components/+popover/popover-section.component.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ let titleDoc = require('html-loader!markdown-loader!./docs/title.md');
5151
<!-- four directions -->
5252
<h2 routerLink="." fragment="four-directions" id="four-directions">Four directions</h2>
5353
Four positioning options are available: top, right, bottom, and left aligned.
54+
Besides that, auto option may be used to detect a position that fits the component on screen.
5455
<ng-sample-box [ts]="demos.forDirections.component" [html]="demos.forDirections.html">
5556
<demo-popover-four-directions></demo-popover-four-directions>
5657
</ng-sample-box>

demo/src/app/components/+tooltip/demos/four-directions/four-directions.html

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,9 @@
2121
placement="right">
2222
Tooltip on right
2323
</button>
24+
25+
<button type="button" class="btn btn-default btn-secondary"
26+
tooltip="Vivamus sagittis lacus vel augue laoreet rutrum faucibus."
27+
placement="auto">
28+
Tooltip auto
29+
</button>

demo/src/app/components/+tooltip/tooltip-section.component.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ let titleDoc = require('html-loader!markdown-loader!./docs/title.md');
4848
<!-- four directions -->
4949
<h2 routerLink="." fragment="four-directions" id="four-directions">Four directions</h2>
5050
Four positioning options are available: top, right, bottom, and left aligned.
51+
Besides that, auto option may be used to detect a position that fits the component on screen.
5152
<ng-sample-box [ts]="demos.forDirections.component" [html]="demos.forDirections.html">
5253
<demo-tooltip-four-directions></demo-tooltip-four-directions>
5354
</ng-sample-box>

src/popover/popover.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { Injectable } from '@angular/core';
99
@Injectable()
1010
export class PopoverConfig {
1111
/**
12-
* Placement of a popover. Accepts: "top", "bottom", "left", "right"
12+
* Placement of a popover. Accepts: "top", "bottom", "left", "right", "auto"
1313
*/
1414
public placement: string = 'top';
1515
/**

src/popover/popover.directive.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export class PopoverDirective implements OnInit, OnDestroy {
2626
/**
2727
* Placement of a popover. Accepts: "top", "bottom", "left", "right"
2828
*/
29-
@Input() public placement: 'top' | 'bottom' | 'left' | 'right';
29+
@Input() public placement: 'top' | 'bottom' | 'left' | 'right' | 'auto';
3030
/**
3131
* Close popover on outside click
3232
*/

src/positioning/ng-positioning.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ export class Positioning {
9292
bottom: hostElPosition.top + hostElPosition.height
9393
};
9494
const targetElBCR = targetElement.getBoundingClientRect();
95-
const placementPrimary = placement.split(' ')[0] || 'top';
95+
let placementPrimary = placement.split(' ')[0] || 'top';
9696
const placementSecondary = placement.split(' ')[1] || 'center';
9797

9898
let targetElPosition: ClientRect = {
@@ -104,6 +104,13 @@ export class Positioning {
104104
right: targetElBCR.width || targetElement.offsetWidth
105105
};
106106

107+
if (placementPrimary==="auto") {
108+
let newPlacementPrimary = this.autoPosition(targetElPosition, hostElPosition, targetElement, placementSecondary);
109+
if (!newPlacementPrimary) newPlacementPrimary = this.autoPosition(targetElPosition, hostElPosition, targetElement);
110+
if (newPlacementPrimary) placementPrimary = newPlacementPrimary;
111+
targetElement.classList.add(placementPrimary);
112+
}
113+
107114
switch (placementPrimary) {
108115
case 'top':
109116
targetElPosition.top = hostElPosition.top - (targetElement.offsetHeight + parseFloat(targetElStyles.marginBottom));
@@ -139,10 +146,24 @@ export class Positioning {
139146
return targetElPosition;
140147
}
141148

149+
private autoPosition(targetElPosition: ClientRect, hostElPosition: ClientRect, targetElement: HTMLElement, preferredPosition?: string) {
150+
if ((!preferredPosition || preferredPosition==="right") && targetElPosition.left + hostElPosition.left - targetElement.offsetWidth < 0) {
151+
return "right";
152+
} else if ((!preferredPosition || preferredPosition==="top") && targetElPosition.bottom + hostElPosition.bottom + targetElement.offsetHeight > window.innerHeight) {
153+
return "top";
154+
} else if ((!preferredPosition || preferredPosition==="bottom") && targetElPosition.top + hostElPosition.top - targetElement.offsetHeight < 0) {
155+
return "bottom";
156+
} else if ((!preferredPosition || preferredPosition==="left") && targetElPosition.right + hostElPosition.right + targetElement.offsetWidth > window.innerWidth ) {
157+
return "left";
158+
}
159+
return null;
160+
}
161+
142162
private getAllStyles(element: HTMLElement) { return window.getComputedStyle(element); }
143163

144164
private getStyle(element: HTMLElement, prop: string): string { return (this.getAllStyles(element) as any)[prop]; }
145165

166+
146167
private isStaticPositioned(element: HTMLElement): boolean {
147168
return (this.getStyle(element, 'position') || 'static') === 'static';
148169
}

src/spec/ng-bootstrap/popover.spec.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,96 @@ describe('popover', () => {
232232
expect(windowEl.textContent.trim())
233233
.toBe('Great tip!');
234234
});
235+
236+
it('should set position to right when use auto position and fit on screen', () => {
237+
const fixture = createTestComponent(`<div popover="Great tip!" placement="auto"></div>`);
238+
const directive = fixture.debugElement.query(By.directive(PopoverDirective));
239+
240+
directive.triggerEventHandler('click', {});
241+
fixture.detectChanges();
242+
const windowEl = getWindow(fixture.nativeElement);
243+
244+
expect(windowEl)
245+
.toHaveCssClass('popover');
246+
expect(windowEl)
247+
.toHaveCssClass('popover-auto');
248+
expect(windowEl)
249+
.toHaveCssClass('right');
250+
expect(windowEl.textContent.trim())
251+
.toBe('Great tip!');
252+
});
253+
254+
it('should set position to bottom when use auto position', () => {
255+
const fixture = createTestComponent(`<div popover="Great tip!" placement="auto bottom"></div>`);
256+
const directive = fixture.debugElement.query(By.directive(PopoverDirective));
257+
258+
directive.triggerEventHandler('click', {});
259+
fixture.detectChanges();
260+
const windowEl = getWindow(fixture.nativeElement);
261+
262+
expect(windowEl)
263+
.toHaveCssClass('popover');
264+
expect(windowEl)
265+
.toHaveCssClass('popover-auto');
266+
expect(windowEl)
267+
.toHaveCssClass('bottom');
268+
expect(windowEl.textContent.trim())
269+
.toBe('Great tip!');
270+
});
271+
272+
it('should set position to top when use auto position and fit on screen', () => {
273+
const fixture = createTestComponent(`<div popover="Great tip!" placement="auto top"></div>`);
274+
const directive = fixture.debugElement.query(By.directive(PopoverDirective));
275+
276+
directive.triggerEventHandler('click', {});
277+
fixture.detectChanges();
278+
const windowEl = getWindow(fixture.nativeElement);
279+
280+
expect(windowEl)
281+
.toHaveCssClass('popover');
282+
expect(windowEl)
283+
.toHaveCssClass('popover-auto');
284+
expect(windowEl)
285+
.toHaveCssClass('top');
286+
expect(windowEl.textContent.trim())
287+
.toBe('Great tip!');
288+
});
289+
290+
it('should set position to right when use auto position and fit on screen', () => {
291+
const fixture = createTestComponent(`<div popover="Great tip!" placement="auto right"></div>`);
292+
const directive = fixture.debugElement.query(By.directive(PopoverDirective));
293+
294+
directive.triggerEventHandler('click', {});
295+
fixture.detectChanges();
296+
const windowEl = getWindow(fixture.nativeElement);
297+
298+
expect(windowEl)
299+
.toHaveCssClass('popover');
300+
expect(windowEl)
301+
.toHaveCssClass('popover-auto');
302+
expect(windowEl)
303+
.toHaveCssClass('right');
304+
expect(windowEl.textContent.trim())
305+
.toBe('Great tip!');
306+
});
307+
308+
it('should set position to left when use auto position and fit on screen', () => {
309+
const fixture = createTestComponent(`<div popover="Great tip!" placement="auto left"></div>`);
310+
const directive = fixture.debugElement.query(By.directive(PopoverDirective));
311+
312+
directive.triggerEventHandler('click', {});
313+
fixture.detectChanges();
314+
const windowEl = getWindow(fixture.nativeElement);
315+
316+
expect(windowEl)
317+
.toHaveCssClass('popover');
318+
expect(windowEl)
319+
.toHaveCssClass('popover-auto');
320+
expect(windowEl)
321+
.toHaveCssClass('left');
322+
expect(windowEl.textContent.trim())
323+
.toBe('Great tip!');
324+
});
235325
});
236326

237327
describe('container', () => {

src/spec/ng-bootstrap/tooltip.spec.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,20 @@ describe('tooltip', () => {
187187
expect(windowEl).toHaveCssClass('tooltip-left');
188188
expect(windowEl.textContent.trim()).toBe('Great tip!');
189189
});
190+
191+
it('should use auto position', () => {
192+
const fixture = createTestComponent(`<div tooltip="Great tip!" placement="auto"></div>`);
193+
const directive = fixture.debugElement.query(By.directive(TooltipDirective));
194+
195+
directive.triggerEventHandler('mouseover', {});
196+
fixture.detectChanges();
197+
const windowEl = getWindow(fixture.nativeElement);
198+
199+
expect(windowEl).toHaveCssClass('tooltip');
200+
expect(windowEl).toHaveCssClass('tooltip-auto');
201+
expect(windowEl).toHaveCssClass('left');
202+
expect(windowEl.textContent.trim()).toBe('Great tip!');
203+
});
190204
});
191205

192206
describe('triggers', () => {

0 commit comments

Comments
 (0)