From 114ed42357aff01988fe13b3d475f8dd8a45ffb8 Mon Sep 17 00:00:00 2001 From: Victor Costa Date: Tue, 29 Aug 2017 11:59:53 -0300 Subject: [PATCH] feat(positioning): auto option for positioning (#1986) * feat(positioning): auto option for positioning fixes #1111 * Update tooltip.spec.ts Concistensy --- .../four-directions/four-directions.html | 7 ++ .../+popover/popover-section.component.ts | 1 + .../four-directions/four-directions.html | 6 ++ .../+tooltip/tooltip-section.component.ts | 1 + src/popover/popover.config.ts | 2 +- src/popover/popover.directive.ts | 2 +- src/positioning/ng-positioning.ts | 23 ++++- src/spec/ng-bootstrap/popover.spec.ts | 90 +++++++++++++++++++ src/spec/ng-bootstrap/tooltip.spec.ts | 14 +++ 9 files changed, 143 insertions(+), 3 deletions(-) diff --git a/demo/src/app/components/+popover/demos/four-directions/four-directions.html b/demo/src/app/components/+popover/demos/four-directions/four-directions.html index 20131853d9..94931e0961 100644 --- a/demo/src/app/components/+popover/demos/four-directions/four-directions.html +++ b/demo/src/app/components/+popover/demos/four-directions/four-directions.html @@ -25,3 +25,10 @@ placement="right"> Popover on right + + diff --git a/demo/src/app/components/+popover/popover-section.component.ts b/demo/src/app/components/+popover/popover-section.component.ts index fd5c68386c..4e4baed99c 100644 --- a/demo/src/app/components/+popover/popover-section.component.ts +++ b/demo/src/app/components/+popover/popover-section.component.ts @@ -51,6 +51,7 @@ let titleDoc = require('html-loader!markdown-loader!./docs/title.md');

Four directions

Four positioning options are available: top, right, bottom, and left aligned. + Besides that, auto option may be used to detect a position that fits the component on screen. diff --git a/demo/src/app/components/+tooltip/demos/four-directions/four-directions.html b/demo/src/app/components/+tooltip/demos/four-directions/four-directions.html index 6d31dc4aea..b3f67667e4 100644 --- a/demo/src/app/components/+tooltip/demos/four-directions/four-directions.html +++ b/demo/src/app/components/+tooltip/demos/four-directions/four-directions.html @@ -21,3 +21,9 @@ placement="right"> Tooltip on right + + diff --git a/demo/src/app/components/+tooltip/tooltip-section.component.ts b/demo/src/app/components/+tooltip/tooltip-section.component.ts index 5ad1ac88f2..0d9a2c3f34 100644 --- a/demo/src/app/components/+tooltip/tooltip-section.component.ts +++ b/demo/src/app/components/+tooltip/tooltip-section.component.ts @@ -48,6 +48,7 @@ let titleDoc = require('html-loader!markdown-loader!./docs/title.md');

Four directions

Four positioning options are available: top, right, bottom, and left aligned. + Besides that, auto option may be used to detect a position that fits the component on screen. diff --git a/src/popover/popover.config.ts b/src/popover/popover.config.ts index 884f2dcd4a..729f934609 100644 --- a/src/popover/popover.config.ts +++ b/src/popover/popover.config.ts @@ -9,7 +9,7 @@ import { Injectable } from '@angular/core'; @Injectable() export class PopoverConfig { /** - * Placement of a popover. Accepts: "top", "bottom", "left", "right" + * Placement of a popover. Accepts: "top", "bottom", "left", "right", "auto" */ public placement: string = 'top'; /** diff --git a/src/popover/popover.directive.ts b/src/popover/popover.directive.ts index d14c4efd2f..f57fbe08b6 100644 --- a/src/popover/popover.directive.ts +++ b/src/popover/popover.directive.ts @@ -26,7 +26,7 @@ export class PopoverDirective implements OnInit, OnDestroy { /** * Placement of a popover. Accepts: "top", "bottom", "left", "right" */ - @Input() public placement: 'top' | 'bottom' | 'left' | 'right'; + @Input() public placement: 'top' | 'bottom' | 'left' | 'right' | 'auto'; /** * Close popover on outside click */ diff --git a/src/positioning/ng-positioning.ts b/src/positioning/ng-positioning.ts index 56ba83762c..921598bff8 100644 --- a/src/positioning/ng-positioning.ts +++ b/src/positioning/ng-positioning.ts @@ -92,7 +92,7 @@ export class Positioning { bottom: hostElPosition.top + hostElPosition.height }; const targetElBCR = targetElement.getBoundingClientRect(); - const placementPrimary = placement.split(' ')[0] || 'top'; + let placementPrimary = placement.split(' ')[0] || 'top'; const placementSecondary = placement.split(' ')[1] || 'center'; let targetElPosition: ClientRect = { @@ -104,6 +104,13 @@ export class Positioning { right: targetElBCR.width || targetElement.offsetWidth }; + if (placementPrimary==="auto") { + let newPlacementPrimary = this.autoPosition(targetElPosition, hostElPosition, targetElement, placementSecondary); + if (!newPlacementPrimary) newPlacementPrimary = this.autoPosition(targetElPosition, hostElPosition, targetElement); + if (newPlacementPrimary) placementPrimary = newPlacementPrimary; + targetElement.classList.add(placementPrimary); + } + switch (placementPrimary) { case 'top': targetElPosition.top = hostElPosition.top - (targetElement.offsetHeight + parseFloat(targetElStyles.marginBottom)); @@ -139,10 +146,24 @@ export class Positioning { return targetElPosition; } + private autoPosition(targetElPosition: ClientRect, hostElPosition: ClientRect, targetElement: HTMLElement, preferredPosition?: string) { + if ((!preferredPosition || preferredPosition==="right") && targetElPosition.left + hostElPosition.left - targetElement.offsetWidth < 0) { + return "right"; + } else if ((!preferredPosition || preferredPosition==="top") && targetElPosition.bottom + hostElPosition.bottom + targetElement.offsetHeight > window.innerHeight) { + return "top"; + } else if ((!preferredPosition || preferredPosition==="bottom") && targetElPosition.top + hostElPosition.top - targetElement.offsetHeight < 0) { + return "bottom"; + } else if ((!preferredPosition || preferredPosition==="left") && targetElPosition.right + hostElPosition.right + targetElement.offsetWidth > window.innerWidth ) { + return "left"; + } + return null; + } + private getAllStyles(element: HTMLElement) { return window.getComputedStyle(element); } private getStyle(element: HTMLElement, prop: string): string { return (this.getAllStyles(element) as any)[prop]; } + private isStaticPositioned(element: HTMLElement): boolean { return (this.getStyle(element, 'position') || 'static') === 'static'; } diff --git a/src/spec/ng-bootstrap/popover.spec.ts b/src/spec/ng-bootstrap/popover.spec.ts index ae64e80cc5..88d386502b 100644 --- a/src/spec/ng-bootstrap/popover.spec.ts +++ b/src/spec/ng-bootstrap/popover.spec.ts @@ -232,6 +232,96 @@ describe('popover', () => { expect(windowEl.textContent.trim()) .toBe('Great tip!'); }); + + it('should set position to right when use auto position and fit on screen', () => { + const fixture = createTestComponent(`
`); + const directive = fixture.debugElement.query(By.directive(PopoverDirective)); + + directive.triggerEventHandler('click', {}); + fixture.detectChanges(); + const windowEl = getWindow(fixture.nativeElement); + + expect(windowEl) + .toHaveCssClass('popover'); + expect(windowEl) + .toHaveCssClass('popover-auto'); + expect(windowEl) + .toHaveCssClass('right'); + expect(windowEl.textContent.trim()) + .toBe('Great tip!'); + }); + + it('should set position to bottom when use auto position', () => { + const fixture = createTestComponent(`
`); + const directive = fixture.debugElement.query(By.directive(PopoverDirective)); + + directive.triggerEventHandler('click', {}); + fixture.detectChanges(); + const windowEl = getWindow(fixture.nativeElement); + + expect(windowEl) + .toHaveCssClass('popover'); + expect(windowEl) + .toHaveCssClass('popover-auto'); + expect(windowEl) + .toHaveCssClass('bottom'); + expect(windowEl.textContent.trim()) + .toBe('Great tip!'); + }); + + it('should set position to top when use auto position and fit on screen', () => { + const fixture = createTestComponent(`
`); + const directive = fixture.debugElement.query(By.directive(PopoverDirective)); + + directive.triggerEventHandler('click', {}); + fixture.detectChanges(); + const windowEl = getWindow(fixture.nativeElement); + + expect(windowEl) + .toHaveCssClass('popover'); + expect(windowEl) + .toHaveCssClass('popover-auto'); + expect(windowEl) + .toHaveCssClass('top'); + expect(windowEl.textContent.trim()) + .toBe('Great tip!'); + }); + + it('should set position to right when use auto position and fit on screen', () => { + const fixture = createTestComponent(`
`); + const directive = fixture.debugElement.query(By.directive(PopoverDirective)); + + directive.triggerEventHandler('click', {}); + fixture.detectChanges(); + const windowEl = getWindow(fixture.nativeElement); + + expect(windowEl) + .toHaveCssClass('popover'); + expect(windowEl) + .toHaveCssClass('popover-auto'); + expect(windowEl) + .toHaveCssClass('right'); + expect(windowEl.textContent.trim()) + .toBe('Great tip!'); + }); + + it('should set position to left when use auto position and fit on screen', () => { + const fixture = createTestComponent(`
`); + const directive = fixture.debugElement.query(By.directive(PopoverDirective)); + + directive.triggerEventHandler('click', {}); + fixture.detectChanges(); + const windowEl = getWindow(fixture.nativeElement); + + expect(windowEl) + .toHaveCssClass('popover'); + expect(windowEl) + .toHaveCssClass('popover-auto'); + expect(windowEl) + .toHaveCssClass('left'); + expect(windowEl.textContent.trim()) + .toBe('Great tip!'); + }); }); describe('container', () => { diff --git a/src/spec/ng-bootstrap/tooltip.spec.ts b/src/spec/ng-bootstrap/tooltip.spec.ts index 44588fd17d..0e8c5a6ade 100644 --- a/src/spec/ng-bootstrap/tooltip.spec.ts +++ b/src/spec/ng-bootstrap/tooltip.spec.ts @@ -187,6 +187,20 @@ describe('tooltip', () => { expect(windowEl).toHaveCssClass('tooltip-left'); expect(windowEl.textContent.trim()).toBe('Great tip!'); }); + + it('should use auto position', () => { + const fixture = createTestComponent(`
`); + const directive = fixture.debugElement.query(By.directive(TooltipDirective)); + + directive.triggerEventHandler('mouseover', {}); + fixture.detectChanges(); + const windowEl = getWindow(fixture.nativeElement); + + expect(windowEl).toHaveCssClass('tooltip'); + expect(windowEl).toHaveCssClass('tooltip-auto'); + expect(windowEl).toHaveCssClass('left'); + expect(windowEl.textContent.trim()).toBe('Great tip!'); + }); }); describe('triggers', () => {