Skip to content

Commit

Permalink
fix(positioning): use CSS translate for positioning
Browse files Browse the repository at this point in the history
Closes #2972
Fixes #2470
Fixes #2924 
Fixes #2069 
Fixes #2914 
Fixes #2775 
Fixes #2346
  • Loading branch information
fbasso authored and maxokorokov committed Feb 7, 2019
1 parent 932d7f6 commit 64d5716
Show file tree
Hide file tree
Showing 16 changed files with 324 additions and 276 deletions.
3 changes: 2 additions & 1 deletion e2e-app/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {DropdownAutoCloseComponent} from './dropdown/autoclose/dropdown-autoclos
import {ModalFocusComponent} from './modal/focus/modal-focus.component';
import {PopoverAutocloseComponent} from './popover/autoclose/popover-autoclose.component';
import {TooltipAutocloseComponent} from './tooltip/autoclose/tooltip-autoclose.component';
import {TooltipPositionComponent} from './tooltip/position/tooltip-position.component';
import {TypeaheadAutoCloseComponent} from './typeahead/autoclose/typeahead-autoclose.component';
import {TypeaheadFocusComponent} from './typeahead/focus/typeahead-focus.component';
import {TypeaheadValidationComponent} from './typeahead/validation/typeahead-validation.component';
Expand All @@ -22,7 +23,7 @@ import {TypeaheadValidationComponent} from './typeahead/validation/typeahead-val
declarations: [
AppComponent, NavigationComponent, DatepickerAutoCloseComponent, DatepickerFocusComponent,
DropdownAutoCloseComponent, ModalFocusComponent, PopoverAutocloseComponent, TooltipAutocloseComponent,
TypeaheadFocusComponent, TypeaheadValidationComponent, TypeaheadAutoCloseComponent
TooltipPositionComponent, TypeaheadFocusComponent, TypeaheadValidationComponent, TypeaheadAutoCloseComponent
],
imports: [BrowserModule, FormsModule, ReactiveFormsModule, routing, NgbModule],
bootstrap: [AppComponent]
Expand Down
11 changes: 9 additions & 2 deletions e2e-app/src/app/app.routing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {DropdownAutoCloseComponent} from './dropdown/autoclose/dropdown-autoclos
import {ModalFocusComponent} from './modal/focus/modal-focus.component';
import {PopoverAutocloseComponent} from './popover/autoclose/popover-autoclose.component';
import {TooltipAutocloseComponent} from './tooltip/autoclose/tooltip-autoclose.component';
import {TooltipPositionComponent} from './tooltip/position/tooltip-position.component';
import {TypeaheadAutoCloseComponent} from './typeahead/autoclose/typeahead-autoclose.component';
import {TypeaheadFocusComponent} from './typeahead/focus/typeahead-focus.component';
import {TypeaheadValidationComponent} from './typeahead/validation/typeahead-validation.component';
Expand All @@ -20,8 +21,14 @@ export const routes: Routes = [
},
{path: 'modal', children: [{path: 'focus', component: ModalFocusComponent}]},
{path: 'dropdown', children: [{path: 'autoclose', component: DropdownAutoCloseComponent}]},
{path: 'popover', children: [{path: 'autoclose', component: PopoverAutocloseComponent}]},
{path: 'tooltip', children: [{path: 'autoclose', component: TooltipAutocloseComponent}]}, {
{path: 'popover', children: [{path: 'autoclose', component: PopoverAutocloseComponent}]}, {
path: 'tooltip',
children: [
{path: 'autoclose', component: TooltipAutocloseComponent},
{path: 'position', component: TooltipPositionComponent}
]
},
{
path: 'typeahead',
children: [
{path: 'focus', component: TypeaheadFocusComponent}, {path: 'autoclose', component: TypeaheadAutoCloseComponent},
Expand Down
31 changes: 31 additions & 0 deletions e2e-app/src/app/tooltip/position/tooltip-position.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<h3>Tooltip positioning tests</h3>

<form id="default">
<div class="m-3">
<button class="btn btn-outline-secondary mx-1" id="flex-left" (click)="setPosition('start')">Left</button>
<button class="btn btn-outline-secondary mx-1" id="flex-center" (click)="setPosition('center')">Center</button>
<button class="btn btn-outline-secondary mx-1" id="flex-right" (click)="setPosition('end')">Right</button>
</div>
<div class="d-flex {{flexPosition}}">
<div class="d-flex flex-column mx-3">
<button id="btn-normal" type="button" container="body" triggers="click" class="btn btn-outline-secondary" placement="auto" [ngbTooltip]="content">
Normal tooltip
</button>

<button id="btn-innerHtml" type="button" container="body" triggers="click" class="btn btn-outline-secondary" [placement]="['top', 'top-left', 'top-right']" [ngbTooltip]="tooltipHtml">
InnerHtml
</button>

<button id="btn-body-off" type="button" container="body" triggers="click" class="btn btn-outline-secondary" placement="auto" [ngbTooltip]="content">
body off
</button>

</div>
<ng-template #tooltipHtml><div [innerHtml]="content"></div></ng-template>
</div>
<div class="m-3">
<button id="btn-fixed" type="button" triggers="click" class="btn btn-outline-secondary position-fixed" [placement]="['top', 'top-left', 'top-right']" [ngbTooltip]="tooltipHtml" [ngStyle]="{'left': fixedPositionLeft, 'right': fixedPositionRight}">
Fixed
</button>
</div>
</form>
27 changes: 27 additions & 0 deletions e2e-app/src/app/tooltip/position/tooltip-position.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import {Component} from '@angular/core';

@Component({templateUrl: './tooltip-position.component.html'})
export class TooltipPositionComponent {
flexPosition = 'justify-content-start';
content = 'Lorem ipsum dolor sit amet consectetur adipisicing elit.';
fixedPositionLeft = '20px';
fixedPositionRight = '';

setPosition(position: 'start' | 'center' | 'end') {
this.flexPosition = `justify-content-${position}`;
switch (position) {
case 'start':
this.fixedPositionLeft = '20px';
this.fixedPositionRight = '';
break;
case 'center':
this.fixedPositionLeft = '50%';
this.fixedPositionRight = '';
break;
case 'end':
this.fixedPositionLeft = '';
this.fixedPositionRight = '20px';
break;
}
}
}
93 changes: 93 additions & 0 deletions e2e-app/src/app/tooltip/position/tooltip-position.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import {openUrl} from '../../tools.po';
import {TooltipPositionPage as TooltipPositionPage} from './tooltip-position.po';

const roundLocation = function(location) {
location.x = Math.round(location.x);
location.y = Math.round(location.y);

return location;
};

describe('Tooltip Position', () => {
let page: TooltipPositionPage;

const expectTooltipsPosition = async(type: string, expectedPlacement: string) => {


const btn = page.getTooltipButton(type);
await btn.click();
const btnLocation = roundLocation(await btn.getLocation());
const btnSize = await btn.getSize();
const tooltip = await page.getTooltip();
const tooltipLocation = roundLocation(await tooltip.getLocation());
const tooltipSize = await tooltip.getSize();

let [primary, secondary] = expectedPlacement.split('-');
const classname = await tooltip.getAttribute('class');
expect(classname).toContain(`bs-tooltip-${primary}`, 'Missing primary class');
if (secondary) {
expect(classname).toContain(`bs-tooltip-${primary}-${secondary}`, 'Missing secondary class');
}

let yDiff: number, xDiff: number;

if (primary === 'top') {
yDiff = (tooltipLocation.y + tooltipSize.height) - btnLocation.y;
if (secondary === 'left') {
xDiff = tooltipLocation.x - btnLocation.x;
} else if (secondary === 'right') {
xDiff = (tooltipLocation.x + tooltipSize.width) - (btnLocation.x + btnSize.width);
} else {
xDiff = (tooltipLocation.x + tooltipSize.width / 2) - (btnLocation.x + (btnSize.width / 2));
}
}

if (primary === 'left') {
yDiff = (tooltipLocation.y + tooltipSize.height / 2) - (btnLocation.y + btnSize.height / 2);
xDiff = (tooltipLocation.x + tooltipSize.width) - btnLocation.x;
}

if (primary === 'right') {
yDiff = (tooltipLocation.y + tooltipSize.height / 2) - (btnLocation.y + btnSize.height / 2);
xDiff = tooltipLocation.x - (btnLocation.x + btnSize.width);
}


expect(Math.abs(yDiff))
.toBeLessThanOrEqual(1, `Tooltip top positionning for expected placement '${expectedPlacement}'`);
expect(Math.abs(xDiff))
.toBeLessThanOrEqual(1, `Tooltip left positionning for expected placement '${expectedPlacement}'`);

// Close the tooltip
await btn.click();
};

beforeAll(() => page = new TooltipPositionPage());

beforeEach(async() => await openUrl('tooltip/position'));

it(`should be well positionned on the left edge`, async() => {
page.selectPosition('left');
await expectTooltipsPosition('normal', 'right');
await expectTooltipsPosition('innerHtml', 'top-left');
await expectTooltipsPosition('body-off', 'right');
await expectTooltipsPosition('fixed', 'top-left');
});

it(`should be well positionned on the center`, async() => {
page.selectPosition('center');
await expectTooltipsPosition('normal', 'top');
await expectTooltipsPosition('innerHtml', 'top');
await expectTooltipsPosition('body-off', 'top');
await expectTooltipsPosition('fixed', 'top');
});

it(`should be well positionned on the right edge`, async() => {
page.selectPosition('right');
await expectTooltipsPosition('normal', 'left');
await expectTooltipsPosition('innerHtml', 'top-right');
await expectTooltipsPosition('body-off', 'left');
await expectTooltipsPosition('fixed', 'top-right');
});

});
9 changes: 9 additions & 0 deletions e2e-app/src/app/tooltip/position/tooltip-position.po.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import {$} from 'protractor';

export class TooltipPositionPage {
getTooltip() { return $('ngb-tooltip-window'); }

getTooltipButton(type: string) { return $(`#btn-${type}`); }

async selectPosition(position: string) { await $(`#flex-${position}`).click(); }
}
2 changes: 1 addition & 1 deletion src/datepicker/datepicker-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ export class NgbInputDatepicker implements OnChanges,
* "left", "left-top", "left-bottom", "right", "right-top", "right-bottom"
* and array of above values.
*/
@Input() placement: PlacementArray = 'bottom-left';
@Input() placement: PlacementArray = ['bottom-left', 'bottom-right', 'top-left', 'top-right'];

/**
* Whether to display days of the week
Expand Down
2 changes: 1 addition & 1 deletion src/dropdown/dropdown-config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ describe('ngb-dropdown-config', () => {
it('should have sensible default values', () => {
const config = new NgbDropdownConfig();

expect(config.placement).toBe('bottom-left');
expect(config.placement).toEqual(['bottom-left', 'bottom-right', 'top-left', 'top-right']);
expect(config.autoClose).toBe(true);
});

Expand Down
2 changes: 1 addition & 1 deletion src/dropdown/dropdown-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ import {PlacementArray} from '../util/positioning';
@Injectable({providedIn: 'root'})
export class NgbDropdownConfig {
autoClose: boolean | 'outside' | 'inside' = true;
placement: PlacementArray = 'bottom-left';
placement: PlacementArray = ['bottom-left', 'bottom-right', 'top-left', 'top-right'];
}
9 changes: 1 addition & 8 deletions src/popover/popover.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,18 +42,11 @@ describe('ngb-popover-window', () => {
fixture.detectChanges();

expect(fixture.nativeElement).toHaveCssClass('popover');
expect(fixture.nativeElement).toHaveCssClass('bs-popover-top');
expect(fixture.nativeElement).not.toHaveCssClass('bs-popover-top');
expect(fixture.nativeElement.getAttribute('role')).toBe('tooltip');
expect(fixture.nativeElement.querySelector('.popover-header').textContent).toBe('Test title');
});

it('should position popovers as requested', () => {
const fixture = TestBed.createComponent(NgbPopoverWindow);
fixture.componentInstance.placement = 'left';
fixture.detectChanges();
expect(fixture.nativeElement).toHaveCssClass('bs-popover-left');
});

it('should optionally have a custom class', () => {
const fixture = TestBed.createComponent(NgbPopoverWindow);
fixture.detectChanges();
Expand Down
41 changes: 6 additions & 35 deletions src/popover/popover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,7 @@ let nextId = 0;
selector: 'ngb-popover-window',
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
host: {
'[class]':
'"popover bs-popover-" + placement.split("-")[0]+" bs-popover-" + placement + (popoverClass ? " " + popoverClass : "")',
'role': 'tooltip',
'[id]': 'id'
},
host: {'[class]': '"popover" + (popoverClass ? " " + popoverClass : "")', 'role': 'tooltip', '[id]': 'id'},
template: `
<div class="arrow"></div>
<h3 class="popover-header" *ngIf="title != null">
Expand All @@ -52,28 +47,12 @@ let nextId = 0;
styleUrls: ['./popover.scss']
})
export class NgbPopoverWindow {
@Input() placement: Placement = 'top';
@Input() title: undefined | string | TemplateRef<any>;
@Input() id: string;
@Input() popoverClass: string;
@Input() context: any;

constructor(private _element: ElementRef<HTMLElement>, private _renderer: Renderer2) {}

isTitleTemplate() { return this.title instanceof TemplateRef; }

applyPlacement(_placement: Placement) {
// remove the current placement classes
this._renderer.removeClass(this._element.nativeElement, 'bs-popover-' + this.placement.toString().split('-')[0]);
this._renderer.removeClass(this._element.nativeElement, 'bs-popover-' + this.placement.toString());

// set the new placement classes
this.placement = _placement;

// apply the new placement
this._renderer.addClass(this._element.nativeElement, 'bs-popover-' + this.placement.toString().split('-')[0]);
this._renderer.addClass(this._element.nativeElement, 'bs-popover-' + this.placement.toString());
}
}

/**
Expand Down Expand Up @@ -103,10 +82,10 @@ export class NgbPopover implements OnInit, OnDestroy, OnChanges {
@Input() popoverTitle: string | TemplateRef<any>;
/**
* Placement of a popover accepts:
* "top", "top-left", "top-right", "bottom", "bottom-left", "bottom-right",
* "top", "top-left", "top-right", "bottom", "bottom-left", "bottom-right",
* "left", "left-top", "left-bottom", "right", "right-top", "right-bottom"
* and array of above values.
*/
*/
@Input() placement: PlacementArray;
/**
* Specifies events that should trigger. Supports a space separated list of event names.
Expand Down Expand Up @@ -169,10 +148,9 @@ export class NgbPopover implements OnInit, OnDestroy, OnChanges {

this._zoneSubscription = _ngZone.onStable.subscribe(() => {
if (this._windowRef) {
this._windowRef.instance.applyPlacement(
positionElements(
this._elementRef.nativeElement, this._windowRef.location.nativeElement, this.placement,
this.container === 'body'));
positionElements(
this._elementRef.nativeElement, this._windowRef.location.nativeElement, this.placement,
this.container === 'body', 'bs-popover');
}
});
}
Expand All @@ -196,15 +174,8 @@ export class NgbPopover implements OnInit, OnDestroy, OnChanges {
}

// apply styling to set basic css-classes on target element, before going for positioning
this._windowRef.changeDetectorRef.detectChanges();
this._windowRef.changeDetectorRef.markForCheck();

// position popover along the element
this._windowRef.instance.applyPlacement(
positionElements(
this._elementRef.nativeElement, this._windowRef.location.nativeElement, this.placement,
this.container === 'body'));

this._autoClose.install(
this.autoClose, () => this.close(), this.hidden, [this._windowRef.location.nativeElement]);
this.shown.emit();
Expand Down
9 changes: 1 addition & 8 deletions src/tooltip/tooltip.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,10 @@ describe('ngb-tooltip-window', () => {
fixture.detectChanges();

expect(fixture.nativeElement).toHaveCssClass('tooltip');
expect(fixture.nativeElement).toHaveCssClass('bs-tooltip-top');
expect(fixture.nativeElement).not.toHaveCssClass('bs-tooltip-top');
expect(fixture.nativeElement.getAttribute('role')).toBe('tooltip');
});

it('should position tooltips as requested', () => {
const fixture = TestBed.createComponent(NgbTooltipWindow);
fixture.componentInstance.placement = 'left';
fixture.detectChanges();
expect(fixture.nativeElement).toHaveCssClass('bs-tooltip-left');
});

it('should optionally have a custom class', () => {
const fixture = TestBed.createComponent(NgbTooltipWindow);
fixture.detectChanges();
Expand Down

0 comments on commit 64d5716

Please sign in to comment.