Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(layout): scroll block in with scroll mode #1805

Merged
merged 8 commits into from
Jul 12, 2019
88 changes: 46 additions & 42 deletions src/framework/theme/components/layout/_layout.component.theme.scss
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@
@mixin window-mode($padding-top) {
padding-top: $padding-top;

nb-layout-header.fixed {
top: $padding-top;
}

nb-layout-header.fixed ~ .layout-container nb-sidebar .main-container-fixed {
height: calc(100vh - #{$padding-top} - #{nb-theme(header-height)});
top: calc(#{$padding-top} + #{nb-theme(header-height)});
}

nb-sidebar.fixed {
left: calc((100vw - #{nb-theme(layout-window-mode-max-width)}) / 2);
}
Expand Down Expand Up @@ -48,12 +57,7 @@
}
}

nb-layout.overlay-scroll-block .scrollable-container {
overflow: hidden;
}

.layout {
// TODO: check this prop name
min-width: nb-theme(layout-window-mode-min-width);
}

Expand Down Expand Up @@ -118,43 +122,6 @@
line-height: nb-theme(layout-text-line-height);
min-height: nb-theme(layout-min-height);

nb-layout-header {
color: nb-theme(header-text-color);
font-family: nb-theme(header-text-font-family);
font-size: nb-theme(header-text-font-size);
font-weight: nb-theme(header-text-font-weight);
line-height: nb-theme(header-text-line-height);

nav {
background: nb-theme(header-background-color);
color: nb-theme(header-text-color);
box-shadow: nb-theme(header-shadow);
height: nb-theme(header-height);
padding: nb-theme(header-padding);

a {
color: nb-theme(header-text-color);

@include hover-focus-active {
color: nb-theme(header-text-color);
}
}
}

& ~ .layout-container {
min-height: calc(#{nb-theme(layout-min-height)} - #{nb-theme(header-height)});
}

&.fixed ~ .layout-container {
padding-top: nb-theme(header-height);
min-height: nb-theme(layout-min-height);
}

&.fixed ~ .layout-container > nb-sidebar > .main-container {
height: calc(#{nb-theme(sidebar-height)} - #{nb-theme(header-height)});
}
}

.layout-container {

nb-sidebar {
Expand Down Expand Up @@ -209,6 +176,43 @@
}
}

nb-layout-header {
color: nb-theme(header-text-color);
font-family: nb-theme(header-text-font-family);
font-size: nb-theme(header-text-font-size);
font-weight: nb-theme(header-text-font-weight);
line-height: nb-theme(header-text-line-height);

nav {
background: nb-theme(header-background-color);
color: nb-theme(header-text-color);
box-shadow: nb-theme(header-shadow);
height: nb-theme(header-height);
padding: nb-theme(header-padding);

a {
color: nb-theme(header-text-color);

@include hover-focus-active {
color: nb-theme(header-text-color);
}
}
}

& ~ .layout-container {
min-height: calc(#{nb-theme(layout-min-height)} - #{nb-theme(header-height)});
}

&.fixed ~ .layout-container {
padding-top: nb-theme(header-height);
min-height: nb-theme(layout-min-height);
}

&.fixed ~ .layout-container nb-sidebar .main-container {
height: calc(#{nb-theme(sidebar-height)} - #{nb-theme(header-height)});
}
}

nb-layout.with-subheader {
nb-sidebar .main-container {
box-shadow: none; // so that we don't have a shadow over the header in this mode
Expand Down
136 changes: 136 additions & 0 deletions src/framework/theme/components/layout/layout.component.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { Component } from '@angular/core';
import { TestBed, ComponentFixture, flush, fakeAsync } from '@angular/core/testing';
import {
NbLayoutComponent,
NbLayoutScrollService,
NbLayoutModule,
NbThemeModule,
NbLayoutDirectionService,
NbLayoutDirection,
} from '@nebular/theme';
import { By } from '@angular/platform-browser';
import { RouterTestingModule } from '@angular/router/testing';

@Component({
template: `
<nb-layout withScroll>
<nb-layout-column>
<div [style.height]="contentHeight" style="background: lightcoral;"></div>
</nb-layout-column>
</nb-layout>
`,
})
export class LayoutWithScrollModeComponent {
contentHeight: string = '200vh';
}

describe('NbLayoutComponent', () => {

beforeEach(() => {
TestBed.configureTestingModule({
imports: [ RouterTestingModule.withRoutes([]), NbThemeModule.forRoot(), NbLayoutModule ],
declarations: [ LayoutWithScrollModeComponent ],
});
});

describe('withScroll mode - scroll block', () => {
let fixture: ComponentFixture<LayoutWithScrollModeComponent>;
let layoutComponent: NbLayoutComponent;
let scrollService: NbLayoutScrollService;

beforeEach(() => {
fixture = TestBed.createComponent(LayoutWithScrollModeComponent);
fixture.detectChanges();

layoutComponent = fixture.debugElement.query(By.directive(NbLayoutComponent)).componentInstance;
scrollService = TestBed.get(NbLayoutScrollService);
});

it('should hide overflow when scroll blocked', fakeAsync(() => {
scrollService.scrollable(false);
flush();
fixture.detectChanges();

expect(layoutComponent.scrollableContainerRef.nativeElement.style.overflow).toEqual('hidden');
}));


// Comment this specs until global (theme) styles included into unit test build.
// Currently scrollable container and layout has same width so no padding added and specs fail.
// it('should add right padding to layout container in LTR mode when blocking scroll', fakeAsync(() => {
// scrollService.scrollable(false);
// flush();
// fixture.detectChanges();
//
// expect(layoutComponent.layoutContainerRef.nativeElement.style.paddingRight).not.toEqual('');
// }));
//
// it('should add left padding to layout container in RTL mode when blocking scroll', fakeAsync(() => {
// const layoutDirectionService: NbLayoutDirectionService = TestBed.get(NbLayoutDirectionService);
// layoutDirectionService.setDirection(NbLayoutDirection.RTL);
// flush();
// fixture.detectChanges();
//
// scrollService.scrollable(false);
// flush();
// fixture.detectChanges();
//
// expect(layoutComponent.layoutContainerRef.nativeElement.style.paddingLeft).not.toEqual('');
// }));
// it('should not change layout padding if content is not scrollable', fakeAsync(() => {
// fixture.componentInstance.contentHeight = '1px';
// fixture.detectChanges();
//
// layoutComponent.layoutContainerRef.nativeElement.style.paddingLeft = '1px';
//
// scrollService.scrollable(false);
// flush();
// fixture.detectChanges();
//
// expect(layoutComponent.layoutContainerRef.nativeElement.style.paddingLeft).toEqual('1px');
// }));

it('should restore previous overflow value when enabling scroll', fakeAsync(() => {
layoutComponent.scrollableContainerRef.nativeElement.style.overflow = 'auto';

scrollService.scrollable(false);
flush();
fixture.detectChanges();
scrollService.scrollable(true);
flush();
fixture.detectChanges();

expect(layoutComponent.scrollableContainerRef.nativeElement.style.overflow).toEqual('auto');
}));

it('should restore previous padding left value when enabling scroll in LTR mode', fakeAsync(() => {
layoutComponent.layoutContainerRef.nativeElement.style.paddingLeft = '1px';

scrollService.scrollable(false);
flush();
fixture.detectChanges();
scrollService.scrollable(true);
flush();
fixture.detectChanges();

expect(layoutComponent.layoutContainerRef.nativeElement.style.paddingLeft).toEqual('1px');
}));

it('should restore previous padding right value when enabling scroll in RTL mode', fakeAsync(() => {
const layoutDirectionService: NbLayoutDirectionService = TestBed.get(NbLayoutDirectionService);
layoutDirectionService.setDirection(NbLayoutDirection.RTL);
flush();
fixture.detectChanges();
layoutComponent.layoutContainerRef.nativeElement.style.paddingRight = '1px';

scrollService.scrollable(false);
flush();
fixture.detectChanges();
scrollService.scrollable(true);
flush();
fixture.detectChanges();

expect(layoutComponent.layoutContainerRef.nativeElement.style.paddingRight).toEqual('1px');
}));
});
});
79 changes: 67 additions & 12 deletions src/framework/theme/components/layout/layout.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ import { NbOverlayContainerAdapter } from '../cdk/adapter/overlay-container-adap
styleUrls: ['./layout.component.scss'],
template: `
<div class="scrollable-container" #scrollableContainer (scroll)="onScroll($event)">
<div class="layout">
<div class="layout" #layoutContainer>
<ng-content select="nb-layout-header:not([subheader])"></ng-content>
<div class="layout-container">
<ng-content select="nb-sidebar"></ng-content>
Expand All @@ -147,13 +147,17 @@ import { NbOverlayContainerAdapter } from '../cdk/adapter/overlay-container-adap
})
export class NbLayoutComponent implements AfterViewInit, OnDestroy {

protected scrollBlockClass = 'nb-global-scrollblock';
protected isScrollBlocked = false;
protected scrollableContainerOverflowOldValue: string;
protected layoutPaddingOldValue: { left: string; right: string };

centerValue: boolean = false;
restoreScrollTopValue: boolean = true;

@HostBinding('class.window-mode') windowModeValue: boolean = false;
@HostBinding('class.with-scroll') withScrollValue: boolean = false;
@HostBinding('class.with-subheader') withSubheader: boolean = false;
@HostBinding('class.overlay-scroll-block') overlayScrollBlock: boolean = false;

/**
* Defines whether the layout columns will be centered after some width
Expand Down Expand Up @@ -207,7 +211,12 @@ export class NbLayoutComponent implements AfterViewInit, OnDestroy {

// TODO remove as of 5.0.0
@ViewChild('layoutTopDynamicArea', { read: ViewContainerRef, static: false }) veryTopRef: ViewContainerRef;
@ViewChild('scrollableContainer', { read: ElementRef, static: false }) scrollableContainerRef: ElementRef;

@ViewChild('scrollableContainer', { read: ElementRef, static: false })
scrollableContainerRef: ElementRef<HTMLElement>;

@ViewChild('layoutContainer', { read: ElementRef, static: false })
layoutContainerRef: ElementRef<HTMLElement>;

protected afterViewInit$ = new BehaviorSubject(null);

Expand Down Expand Up @@ -300,20 +309,15 @@ export class NbLayoutComponent implements AfterViewInit, OnDestroy {
filter(() => this.withScrollValue),
)
.subscribe((scrollable: boolean) => {
const root = this.document.documentElement;
const scrollBlockClass = 'nb-global-scrollblock';

this.overlayScrollBlock = !scrollable;

/**
* In case when Nebular Layout custom scroll `withScroll` mode is enabled
* we need to disable default CDK scroll blocker (@link NbBlockScrollStrategyAdapter) on HTML element
* so that it won't add additional positioning.
*/
if (!scrollable) {
this.renderer.addClass(root, scrollBlockClass);
if (scrollable) {
this.enableScroll();
} else {
this.renderer.removeClass(root, scrollBlockClass);
this.blockScroll();
}
});

Expand Down Expand Up @@ -443,6 +447,57 @@ export class NbLayoutComponent implements AfterViewInit, OnDestroy {
this.window.scrollTo(x, y);
}
}

// TODO: Extract into block scroll strategy
protected blockScroll() {
if (this.isScrollBlocked) {
return;
}

this.isScrollBlocked = true;

this.renderer.addClass(this.document.documentElement, this.scrollBlockClass);

const scrollableContainerElement = this.scrollableContainerRef.nativeElement;
const layoutElement = this.layoutContainerRef.nativeElement;

const layoutWithScrollWidth = layoutElement.clientWidth;
this.scrollableContainerOverflowOldValue = scrollableContainerElement.style.overflow;
scrollableContainerElement.style.overflow = 'hidden';
const layoutWithoutScrollWidth = layoutElement.clientWidth;
const scrollWidth = layoutWithoutScrollWidth - layoutWithScrollWidth;

if (!scrollWidth) {
return;
}

this.layoutPaddingOldValue = {
left: layoutElement.style.paddingLeft,
right: layoutElement.style.paddingRight,
};

if (this.layoutDirectionService.isLtr()) {
layoutElement.style.paddingRight = `${scrollWidth}px`;
} else {
layoutElement.style.paddingLeft = `${scrollWidth}px`;
}
}

private enableScroll() {
if (this.isScrollBlocked) {
this.isScrollBlocked = false;

this.renderer.removeClass(this.document.documentElement, this.scrollBlockClass);
this.scrollableContainerRef.nativeElement.style.overflow = this.scrollableContainerOverflowOldValue;

if (this.layoutPaddingOldValue) {
const layoutElement = this.layoutContainerRef.nativeElement;
layoutElement.style.paddingLeft = this.layoutPaddingOldValue.left;
layoutElement.style.paddingRight = this.layoutPaddingOldValue.right;
this.layoutPaddingOldValue = null;
}
}
}
}

/**
Expand Down Expand Up @@ -477,7 +532,7 @@ export class NbLayoutColumnComponent {
}

/**
* Make columnt first in the layout.
* Make column first in the layout.
* @param {boolean} val
*/
@Input()
Expand Down