Skip to content

Commit

Permalink
fix(modal): adjust modal background to avoid shifting
Browse files Browse the repository at this point in the history
Shifting potentially occurs when a scrollbar present on the container disappears once the modal opens.
The solution is taken from Bootstrap itself.

Fixes #641
Closes #2508
  • Loading branch information
ymeine authored and pkozlowski-opensource committed Jul 20, 2018
1 parent 8170295 commit 2871316
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 5 deletions.
10 changes: 6 additions & 4 deletions src/modal/modal-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,28 @@ import {

import {ContentRef} from '../util/popup';
import {isDefined, isString} from '../util/util';
import {ScrollBar} from '../util/scrollbar';

import {NgbModalBackdrop} from './modal-backdrop';
import {NgbModalWindow} from './modal-window';
import {NgbActiveModal, NgbModalRef} from './modal-ref';

@Injectable()
export class NgbModalStack {
private _document: any;
private _windowAttributes = ['ariaLabelledBy', 'backdrop', 'centered', 'keyboard', 'size', 'windowClass'];
private _backdropAttributes = ['backdropClass'];

constructor(
private _applicationRef: ApplicationRef, private _injector: Injector,
private _componentFactoryResolver: ComponentFactoryResolver, @Inject(DOCUMENT) document) {
this._document = document;
}
private _componentFactoryResolver: ComponentFactoryResolver, @Inject(DOCUMENT) private _document,
private _scrollBar: ScrollBar) {}

open(moduleCFR: ComponentFactoryResolver, contentInjector: Injector, content: any, options): NgbModalRef {
const containerEl =
isDefined(options.container) ? this._document.querySelector(options.container) : this._document.body;

const revertPaddingForScrollBar = this._scrollBar.compensate();

if (!containerEl) {
throw new Error(`The specified modal container "${options.container || 'body'}" was not found in the DOM.`);
}
Expand All @@ -45,6 +46,7 @@ export class NgbModalStack {
let windowCmptRef: ComponentRef<NgbModalWindow> = this._attachWindowComponent(containerEl, contentRef);
let ngbModalRef: NgbModalRef = new NgbModalRef(windowCmptRef, contentRef, backdropCmptRef, options.beforeDismiss);

ngbModalRef.result.then(revertPaddingForScrollBar, revertPaddingForScrollBar);
activeModal.close = (result: any) => { ngbModalRef.close(result); };
activeModal.dismiss = (reason: any) => { ngbModalRef.dismiss(reason); };

Expand Down
5 changes: 4 additions & 1 deletion src/modal/modal.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {NgbModalBackdrop} from './modal-backdrop';
import {NgbModalWindow} from './modal-window';
import {NgbModalStack} from './modal-stack';
import {NgbModal} from './modal';
import {ScrollBar} from '../util/scrollbar';

export {NgbModal, NgbModalOptions} from './modal';
export {NgbModalRef, NgbActiveModal} from './modal-ref';
Expand All @@ -15,5 +16,7 @@ export {ModalDismissReasons} from './modal-dismiss-reasons';
providers: [NgbModal]
})
export class NgbModalModule {
static forRoot(): ModuleWithProviders { return {ngModule: NgbModalModule, providers: [NgbModal, NgbModalStack]}; }
static forRoot(): ModuleWithProviders {
return {ngModule: NgbModalModule, providers: [NgbModal, NgbModalStack, ScrollBar]};
}
}
72 changes: 72 additions & 0 deletions src/util/scrollbar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import {Injectable, Inject} from '@angular/core';
import {DOCUMENT} from '@angular/common';


const noop = () => {};



/** Type for the callback used to revert the scrollbar compensation. */
export type CompensationReverter = () => void;



/**
* Utility to handle the scrollbar.
*
* It allows to compensate the lack of a vertical scrollbar by adding an
* equivalent padding on the right of the body, and to remove this compensation.
*/
@Injectable()
export class ScrollBar {
constructor(@Inject(DOCUMENT) private _document) {}

/**
* Detects if a scrollbar is present and if yes, already compensates for its
* removal by adding an equivalent padding on the right of the body.
*
* @return a callback used to revert the compensation (noop if there was none,
* otherwise a function removing the padding)
*/
compensate(): CompensationReverter { return !this._isPresent() ? noop : this._adjustBody(this._getWidth()); }

/**
* Adds a padding of the given width on the right of the body.
*
* @return a callback used to revert the padding to its previous value
*/
private _adjustBody(width: number): CompensationReverter {
const body = this._document.body;
const userSetPadding = body.style.paddingRight;
const paddingAmount = parseFloat(window.getComputedStyle(body)['padding-right']);
body.style['padding-right'] = `${paddingAmount + width}px`;
return () => body.style['padding-right'] = userSetPadding;
}

/**
* Tells whether a scrollbar is currently present on the body.
*
* @return true if scrollbar is present, false otherwise
*/
private _isPresent(): boolean {
const rect = this._document.body.getBoundingClientRect();
return rect.left + rect.right < window.innerWidth;
}

/**
* Calculates and returns the width of a scrollbar.
*
* @return the width of a scrollbar on this page
*/
private _getWidth(): number {
const measurer = this._document.createElement('div');
measurer.className = 'modal-scrollbar-measure';

const body = this._document.body;
body.appendChild(measurer);
const width = measurer.getBoundingClientRect().width - measurer.clientWidth;
body.removeChild(measurer);

return width;
}
}

0 comments on commit 2871316

Please sign in to comment.