Skip to content

Commit

Permalink
feat: allow configuring Popper via popperOptions API (#4323)
Browse files Browse the repository at this point in the history
Introduces the new `@Input() popperOptions: (options: Partial<Options>) => Partial<Options>` to all components positioned with popper.
  • Loading branch information
fbasso committed Aug 2, 2022
1 parent 5122159 commit a6f6803
Show file tree
Hide file tree
Showing 23 changed files with 211 additions and 29 deletions.
10 changes: 10 additions & 0 deletions demo/src/app/components/popover/demos/config/popover-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,15 @@ export class NgbdPopoverConfig {
// customize default values of popovers used by this component tree
config.placement = 'end';
config.triggers = 'hover';

// example of usage for popperOptions
config.popperOptions = (options) => {
for (const modifier of(options.modifiers || [])) {
if (modifier.name === 'offset' && modifier.options) {
modifier.options.offset = () => [30, 8];
}
}
return options;
};
}
}
1 change: 1 addition & 0 deletions src/datepicker/datepicker-input-config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ describe('NgbInputDatepickerConfig', () => {
expect(config.container).toBeUndefined();
expect(config.positionTarget).toBeUndefined();
expect(config.placement).toEqual(['bottom-start', 'bottom-end', 'top-start', 'top-end']);
expect(config.popperOptions({})).toEqual({});
expect(config.restoreFocus).toBe(true);
});
});
2 changes: 2 additions & 0 deletions src/datepicker/datepicker-input-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {Injectable} from '@angular/core';

import {NgbDatepickerConfig} from './datepicker-config';
import {PlacementArray} from '../util/positioning';
import {Options} from '@popperjs/core';

/**
* A configuration service for the [`NgbDatepickerInput`](#/components/datepicker/api#NgbDatepicker) component.
Expand All @@ -17,5 +18,6 @@ export class NgbInputDatepickerConfig extends NgbDatepickerConfig {
container: null | 'body';
positionTarget: string | HTMLElement;
placement: PlacementArray = ['bottom-start', 'bottom-end', 'top-start', 'top-end'];
popperOptions = (options: Partial<Options>) => options;
restoreFocus: true | HTMLElement | string = true;
}
24 changes: 24 additions & 0 deletions src/datepicker/datepicker-input.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {NgbDatepicker} from './datepicker';
import {NgbDateStruct} from './ngb-date-struct';
import {NgbDate} from './ngb-date';
import {NgbInputDatepickerConfig} from './datepicker-input-config';
import {Options} from '@popperjs/core';

const createTestCmpt = (html: string) =>
createGenericTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
Expand Down Expand Up @@ -981,6 +982,29 @@ describe('NgbInputDatepicker', () => {
// browser starts scrolling if focus was set before popper positioning
setTimeout(() => expect(document.documentElement.scrollTop).toBe(0), 10);
}));

it('should modify the popper options', (done) => {
const fixture = createTestCmpt(`
<input ngbDatepicker container="body"/>
<div style="height: 10000px"></div>
`);
const datepickerInput =
fixture.debugElement.query(By.directive(NgbInputDatepicker)).injector.get(NgbInputDatepicker);

const spy = createSpy();
datepickerInput.popperOptions = (options: Partial<Options>) => {
options.modifiers !.push({name: 'test', enabled: true, phase: 'main', fn: spy});
return options;
};
datepickerInput.open();

queueMicrotask(() => {
expect(spy).toHaveBeenCalledTimes(1);
done();
});
});


});

describe('focus restore', () => {
Expand Down
22 changes: 16 additions & 6 deletions src/datepicker/datepicker-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
import {ngbAutoClose} from '../util/autoclose';
import {ngbFocusTrap} from '../util/focus-trap';
import {PlacementArray, ngbPositioning} from '../util/positioning';
import {Options} from '@popperjs/core';

import {NgbDateAdapter} from './adapters/ngb-date-adapter';
import {NgbDatepicker, NgbDatepickerNavigateEvent} from './datepicker';
Expand Down Expand Up @@ -191,13 +192,21 @@ export class NgbInputDatepicker implements OnChanges,
@Input() placement: PlacementArray;

/**
* If `true`, when closing datepicker will focus element that was focused before datepicker was opened.
* Allow to change the default options for popper.
*
* Alternatively you could provide a selector or an `HTMLElement` to focus. If the element doesn't exist or invalid,
* we'll fallback to focus document body.
*
* @since 5.2.0
* The provided function receives the current options in the first parameter
* and will return the new options
*/
@Input() popperOptions: (options: Partial<Options>) => Partial<Options>;

/**
* If `true`, when closing datepicker will focus element that was focused before datepicker was opened.
*
* Alternatively you could provide a selector or an `HTMLElement` to focus. If the element doesn't exist or invalid,
* we'll fallback to focus document body.
*
* @since 5.2.0
*/
@Input() restoreFocus: true | string | HTMLElement;

/**
Expand Down Expand Up @@ -289,6 +298,7 @@ export class NgbInputDatepicker implements OnChanges,
@Inject(DOCUMENT) private _document: any, private _changeDetector: ChangeDetectorRef,
config: NgbInputDatepickerConfig) {
['autoClose', 'container', 'positionTarget', 'placement'].forEach(input => this[input] = config[input]);
this.popperOptions = config.popperOptions;
}

registerOnChange(fn: (value: any) => any): void { this._onChange = fn; }
Expand Down Expand Up @@ -394,7 +404,7 @@ export class NgbInputDatepicker implements OnChanges,
targetElement: this._cRef.location.nativeElement,
placement: this.placement,
appendToBody: this.container === 'body',
updatePopperOptions: addPopperOffset([0, 2])
updatePopperOptions: (options) => this.popperOptions(addPopperOffset([0, 2])(options)),
});

this._zoneSubscription = this._ngZone.onStable.subscribe(() => this._positioning.update());
Expand Down
1 change: 1 addition & 0 deletions src/dropdown/dropdown-config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ describe('ngb-dropdown-config', () => {
const config = new NgbDropdownConfig();

expect(config.placement).toEqual(['bottom-start', 'bottom-end', 'top-start', 'top-end']);
expect(config.popperOptions({})).toEqual({});
expect(config.autoClose).toBe(true);
});

Expand Down
2 changes: 2 additions & 0 deletions src/dropdown/dropdown-config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {Injectable} from '@angular/core';
import {Options} from '@popperjs/core';
import {PlacementArray} from '../util/positioning';

/**
Expand All @@ -11,5 +12,6 @@ import {PlacementArray} from '../util/positioning';
export class NgbDropdownConfig {
autoClose: boolean | 'outside' | 'inside' = true;
placement: PlacementArray = ['bottom-start', 'bottom-end', 'top-start', 'top-end'];
popperOptions = (options: Partial<Options>) => options;
container: null | 'body';
}
29 changes: 29 additions & 0 deletions src/dropdown/dropdown.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {Component} from '@angular/core';
import {NgbDropdown, NgbDropdownModule} from './dropdown.module';
import {NgbDropdownConfig} from './dropdown-config';
import {By} from '@angular/platform-browser';
import {Options} from '@popperjs/core';

const createTestComponent = (html: string) =>
createGenericTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
Expand Down Expand Up @@ -462,6 +463,34 @@ describe('ngb-dropdown-toggle', () => {

}));

it('should modify the popper options', (done) => {
const fixture = createTestComponent(`
<div ngbDropdown placement="start-top end-top">
<button ngbDropdownToggle>
<span class="toggle">Toggle dropdown</span>
</button>
<div ngbDropdownMenu>
<a ngbDropdownItem>dropDown item</a>
<a ngbDropdownItem>dropDown item</a>
</div>
</div>`);
const dropdown = fixture.debugElement.query(By.directive(NgbDropdown)).injector.get(NgbDropdown);

const spy = createSpy();
dropdown.popperOptions = (options: Partial<Options>) => {
options.modifiers !.push({name: 'test', enabled: true, phase: 'main', fn: spy});
return options;
};
dropdown.open();

queueMicrotask(() => {
expect(spy).toHaveBeenCalledTimes(1);
done();
});
});



describe('ngb-dropdown-navbar', () => {
it(`shouldn't position the menu`, () => {
const html = `
Expand Down
22 changes: 16 additions & 6 deletions src/dropdown/dropdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {fromEvent, Subject, Subscription} from 'rxjs';
import {take} from 'rxjs/operators';

import {Placement, PlacementArray, ngbPositioning} from '../util/positioning';
import {Options} from '@popperjs/core';
import {addPopperOffset} from '../util/positioning-util';
import {ngbAutoClose, SOURCE} from '../util/autoclose';
import {Key} from '../util/key';
Expand Down Expand Up @@ -191,11 +192,19 @@ export class NgbDropdown implements AfterContentInit, OnChanges, OnDestroy {
@Input() placement: PlacementArray;

/**
* A selector specifying the element the dropdown should be appended to.
* Currently only supports "body".
*
* @since 4.1.0
*/
* Allow to change the default options for popper.
*
* The provided function receives the current options in the first parameter
* and will return the new options
*/
@Input() popperOptions: (options: Partial<Options>) => Partial<Options>;

/**
* A selector specifying the element the dropdown should be appended to.
* Currently only supports "body".
*
* @since 4.1.0
*/
@Input() container: null | 'body';

/**
Expand All @@ -222,6 +231,7 @@ export class NgbDropdown implements AfterContentInit, OnChanges, OnDestroy {
private _ngZone: NgZone, private _elementRef: ElementRef<HTMLElement>, private _renderer: Renderer2,
@Optional() ngbNavbar: NgbNavbar) {
this.placement = config.placement;
this.popperOptions = config.popperOptions;
this.container = config.container;
this.autoClose = config.autoClose;

Expand Down Expand Up @@ -286,7 +296,7 @@ export class NgbDropdown implements AfterContentInit, OnChanges, OnDestroy {
targetElement: this._bodyContainer || this._menu.nativeElement,
placement: this.placement,
appendToBody: this.container === 'body',
updatePopperOptions: addPopperOffset([0, 2]),
updatePopperOptions: (options) => this.popperOptions(addPopperOffset([0, 2])(options)),
});
this._applyPlacementClasses();
this._zoneSubscription = this._ngZone.onStable.subscribe(() => this._positionMenu());
Expand Down
1 change: 1 addition & 0 deletions src/popover/popover-config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ describe('ngb-popover-config', () => {
expect(config.animation).toBe(ngbConfig.animation);
expect(config.autoClose).toBe(true);
expect(config.placement).toBe('auto');
expect(config.popperOptions({})).toEqual({});
expect(config.triggers).toBe('click');
expect(config.container).toBeUndefined();
expect(config.disablePopover).toBe(false);
Expand Down
2 changes: 2 additions & 0 deletions src/popover/popover-config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {Injectable} from '@angular/core';
import {PlacementArray} from '../util/positioning';
import {NgbConfig} from '../ngb-config';
import {Options} from '@popperjs/core';

/**
* A configuration service for the [`NgbPopover`](#/components/popover/api#NgbPopover) component.
Expand All @@ -12,6 +13,7 @@ import {NgbConfig} from '../ngb-config';
export class NgbPopoverConfig {
autoClose: boolean | 'inside' | 'outside' = true;
placement: PlacementArray = 'auto';
popperOptions = (options: Partial<Options>) => options;
triggers = 'click';
container: string;
disablePopover = false;
Expand Down
17 changes: 17 additions & 0 deletions src/popover/popover.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {NgbPopoverConfig} from './popover-config';
import {NgbTooltip, NgbTooltipModule} from '..';
import {NgbConfig} from '../ngb-config';
import {NgbConfigAnimation} from '../test/ngb-config-animation';
import {Options} from '@popperjs/core';

@Injectable()
class SpyService {
Expand Down Expand Up @@ -508,6 +509,22 @@ describe('ngb-popover', () => {
expect(windowEl.textContent.trim()).toBe('Great tip!');
}));

it('should modify the popper options', (done) => {
const fixture = createTestComponent(`<div ngbPopover="Great tip!" placement="right">Trigger</div>`);
const popover = fixture.debugElement.query(By.directive(NgbPopover)).injector.get(NgbPopover);

const spy = createSpy();
popover.popperOptions = (options: Partial<Options>) => {
options.modifiers !.push({name: 'test', enabled: true, phase: 'main', fn: spy});
return options;
};
popover.open();

queueMicrotask(() => {
expect(spy).toHaveBeenCalledTimes(1);
done();
});
});
});

describe('container', () => {
Expand Down
12 changes: 11 additions & 1 deletion src/popover/popover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {ngbPositioning, PlacementArray} from '../util/positioning';
import {PopupService} from '../util/popup';

import {NgbPopoverConfig} from './popover-config';
import {Options} from '@popperjs/core';

import {addPopperOffset} from '../util/positioning-util';
import {Subscription} from 'rxjs';
Expand Down Expand Up @@ -113,6 +114,14 @@ export class NgbPopover implements OnInit, OnDestroy, OnChanges {
*/
@Input() placement: PlacementArray;

/**
* Allow to change the default options for popper.
*
* The provided function receives the current options in the first parameter
* and will return the new options
*/
@Input() popperOptions: (options: Partial<Options>) => Partial<Options>;

/**
* Specifies events that should trigger the tooltip.
*
Expand Down Expand Up @@ -192,6 +201,7 @@ export class NgbPopover implements OnInit, OnDestroy, OnChanges {
this.animation = config.animation;
this.autoClose = config.autoClose;
this.placement = config.placement;
this.popperOptions = config.popperOptions;
this.triggers = config.triggers;
this.container = config.container;
this.disablePopover = config.disablePopover;
Expand Down Expand Up @@ -246,7 +256,7 @@ export class NgbPopover implements OnInit, OnDestroy, OnChanges {
placement: this.placement,
appendToBody: this.container === 'body',
baseClass: 'bs-popover',
updatePopperOptions: addPopperOffset([0, 8]),
updatePopperOptions: (options) => this.popperOptions(addPopperOffset([0, 8])(options)),
});

Promise.resolve().then(() => {
Expand Down
1 change: 1 addition & 0 deletions src/tooltip/tooltip-config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ describe('ngb-tooltip-config', () => {
expect(config.animation).toBe(ngbConfig.animation);
expect(config.autoClose).toBe(true);
expect(config.placement).toBe('auto');
expect(config.popperOptions({})).toEqual({});
expect(config.triggers).toBe('hover focus');
expect(config.container).toBeUndefined();
expect(config.disableTooltip).toBe(false);
Expand Down
2 changes: 2 additions & 0 deletions src/tooltip/tooltip-config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {Injectable} from '@angular/core';
import {PlacementArray} from '../util/positioning';
import {NgbConfig} from '../ngb-config';
import {Options} from '@popperjs/core';

/**
* A configuration service for the [`NgbTooltip`](#/components/tooltip/api#NgbTooltip) component.
Expand All @@ -12,6 +13,7 @@ import {NgbConfig} from '../ngb-config';
export class NgbTooltipConfig {
autoClose: boolean | 'inside' | 'outside' = true;
placement: PlacementArray = 'auto';
popperOptions = (options: Partial<Options>) => options;
triggers = 'hover focus';
container: string;
disableTooltip = false;
Expand Down
18 changes: 18 additions & 0 deletions src/tooltip/tooltip.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {NgbTooltipConfig} from './tooltip-config';
import {NgbConfig} from '../ngb-config';
import {NgbConfigAnimation} from '../test/ngb-config-animation';
import createSpy = jasmine.createSpy;
import {Options} from '@popperjs/core';

const createTestComponent =
(html: string) => <ComponentFixture<TestComponent>>createGenericTestComponent(html, TestComponent);
Expand Down Expand Up @@ -402,6 +403,23 @@ describe('ngb-tooltip', () => {
expect(windowEl.textContent.trim()).toBe('Great tip!');
}));

it('should modify the popper options', (done) => {
const fixture = createTestComponent(`<div ngbTooltip="Great tip!" placement="auto"></div>`);
const tooltip = fixture.debugElement.query(By.directive(NgbTooltip)).injector.get(NgbTooltip);

const spy = createSpy();
tooltip.popperOptions = (options: Partial<Options>) => {
options.modifiers !.push({name: 'test', enabled: true, phase: 'main', fn: spy});
return options;
};
tooltip.open();

queueMicrotask(() => {
expect(spy).toHaveBeenCalledTimes(1);
done();
});
});

});

describe('triggers', () => {
Expand Down
Loading

0 comments on commit a6f6803

Please sign in to comment.