Skip to content

Commit 8958fc9

Browse files
authored
fix(layout): scroll block in with scroll mode (#1805)
1 parent a0efb6b commit 8958fc9

File tree

3 files changed

+249
-54
lines changed

3 files changed

+249
-54
lines changed

src/framework/theme/components/layout/_layout.component.theme.scss

Lines changed: 46 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@
77
@mixin window-mode($padding-top) {
88
padding-top: $padding-top;
99

10+
nb-layout-header.fixed {
11+
top: $padding-top;
12+
}
13+
14+
nb-layout-header.fixed ~ .layout-container nb-sidebar .main-container-fixed {
15+
height: calc(100vh - #{$padding-top} - #{nb-theme(header-height)});
16+
top: calc(#{$padding-top} + #{nb-theme(header-height)});
17+
}
18+
1019
nb-sidebar.fixed {
1120
left: calc((100vw - #{nb-theme(layout-window-mode-max-width)}) / 2);
1221
}
@@ -48,12 +57,7 @@
4857
}
4958
}
5059

51-
nb-layout.overlay-scroll-block .scrollable-container {
52-
overflow: hidden;
53-
}
54-
5560
.layout {
56-
// TODO: check this prop name
5761
min-width: nb-theme(layout-window-mode-min-width);
5862
}
5963

@@ -118,43 +122,6 @@
118122
line-height: nb-theme(layout-text-line-height);
119123
min-height: nb-theme(layout-min-height);
120124

121-
nb-layout-header {
122-
color: nb-theme(header-text-color);
123-
font-family: nb-theme(header-text-font-family);
124-
font-size: nb-theme(header-text-font-size);
125-
font-weight: nb-theme(header-text-font-weight);
126-
line-height: nb-theme(header-text-line-height);
127-
128-
nav {
129-
background: nb-theme(header-background-color);
130-
color: nb-theme(header-text-color);
131-
box-shadow: nb-theme(header-shadow);
132-
height: nb-theme(header-height);
133-
padding: nb-theme(header-padding);
134-
135-
a {
136-
color: nb-theme(header-text-color);
137-
138-
@include hover-focus-active {
139-
color: nb-theme(header-text-color);
140-
}
141-
}
142-
}
143-
144-
& ~ .layout-container {
145-
min-height: calc(#{nb-theme(layout-min-height)} - #{nb-theme(header-height)});
146-
}
147-
148-
&.fixed ~ .layout-container {
149-
padding-top: nb-theme(header-height);
150-
min-height: nb-theme(layout-min-height);
151-
}
152-
153-
&.fixed ~ .layout-container > nb-sidebar > .main-container {
154-
height: calc(#{nb-theme(sidebar-height)} - #{nb-theme(header-height)});
155-
}
156-
}
157-
158125
.layout-container {
159126

160127
nb-sidebar {
@@ -209,6 +176,43 @@
209176
}
210177
}
211178

179+
nb-layout-header {
180+
color: nb-theme(header-text-color);
181+
font-family: nb-theme(header-text-font-family);
182+
font-size: nb-theme(header-text-font-size);
183+
font-weight: nb-theme(header-text-font-weight);
184+
line-height: nb-theme(header-text-line-height);
185+
186+
nav {
187+
background: nb-theme(header-background-color);
188+
color: nb-theme(header-text-color);
189+
box-shadow: nb-theme(header-shadow);
190+
height: nb-theme(header-height);
191+
padding: nb-theme(header-padding);
192+
193+
a {
194+
color: nb-theme(header-text-color);
195+
196+
@include hover-focus-active {
197+
color: nb-theme(header-text-color);
198+
}
199+
}
200+
}
201+
202+
& ~ .layout-container {
203+
min-height: calc(#{nb-theme(layout-min-height)} - #{nb-theme(header-height)});
204+
}
205+
206+
&.fixed ~ .layout-container {
207+
padding-top: nb-theme(header-height);
208+
min-height: nb-theme(layout-min-height);
209+
}
210+
211+
&.fixed ~ .layout-container nb-sidebar .main-container {
212+
height: calc(#{nb-theme(sidebar-height)} - #{nb-theme(header-height)});
213+
}
214+
}
215+
212216
nb-layout.with-subheader {
213217
nb-sidebar .main-container {
214218
box-shadow: none; // so that we don't have a shadow over the header in this mode
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import { Component } from '@angular/core';
2+
import { TestBed, ComponentFixture, flush, fakeAsync } from '@angular/core/testing';
3+
import {
4+
NbLayoutComponent,
5+
NbLayoutScrollService,
6+
NbLayoutModule,
7+
NbThemeModule,
8+
NbLayoutDirectionService,
9+
NbLayoutDirection,
10+
} from '@nebular/theme';
11+
import { By } from '@angular/platform-browser';
12+
import { RouterTestingModule } from '@angular/router/testing';
13+
14+
@Component({
15+
template: `
16+
<nb-layout withScroll>
17+
<nb-layout-column>
18+
<div [style.height]="contentHeight" style="background: lightcoral;"></div>
19+
</nb-layout-column>
20+
</nb-layout>
21+
`,
22+
})
23+
export class LayoutWithScrollModeComponent {
24+
contentHeight: string = '200vh';
25+
}
26+
27+
describe('NbLayoutComponent', () => {
28+
29+
beforeEach(() => {
30+
TestBed.configureTestingModule({
31+
imports: [ RouterTestingModule.withRoutes([]), NbThemeModule.forRoot(), NbLayoutModule ],
32+
declarations: [ LayoutWithScrollModeComponent ],
33+
});
34+
});
35+
36+
describe('withScroll mode - scroll block', () => {
37+
let fixture: ComponentFixture<LayoutWithScrollModeComponent>;
38+
let layoutComponent: NbLayoutComponent;
39+
let scrollService: NbLayoutScrollService;
40+
41+
beforeEach(() => {
42+
fixture = TestBed.createComponent(LayoutWithScrollModeComponent);
43+
fixture.detectChanges();
44+
45+
layoutComponent = fixture.debugElement.query(By.directive(NbLayoutComponent)).componentInstance;
46+
scrollService = TestBed.get(NbLayoutScrollService);
47+
});
48+
49+
it('should hide overflow when scroll blocked', fakeAsync(() => {
50+
scrollService.scrollable(false);
51+
flush();
52+
fixture.detectChanges();
53+
54+
expect(layoutComponent.scrollableContainerRef.nativeElement.style.overflow).toEqual('hidden');
55+
}));
56+
57+
58+
// Comment this specs until global (theme) styles included into unit test build.
59+
// Currently scrollable container and layout has same width so no padding added and specs fail.
60+
// it('should add right padding to layout container in LTR mode when blocking scroll', fakeAsync(() => {
61+
// scrollService.scrollable(false);
62+
// flush();
63+
// fixture.detectChanges();
64+
//
65+
// expect(layoutComponent.layoutContainerRef.nativeElement.style.paddingRight).not.toEqual('');
66+
// }));
67+
//
68+
// it('should add left padding to layout container in RTL mode when blocking scroll', fakeAsync(() => {
69+
// const layoutDirectionService: NbLayoutDirectionService = TestBed.get(NbLayoutDirectionService);
70+
// layoutDirectionService.setDirection(NbLayoutDirection.RTL);
71+
// flush();
72+
// fixture.detectChanges();
73+
//
74+
// scrollService.scrollable(false);
75+
// flush();
76+
// fixture.detectChanges();
77+
//
78+
// expect(layoutComponent.layoutContainerRef.nativeElement.style.paddingLeft).not.toEqual('');
79+
// }));
80+
// it('should not change layout padding if content is not scrollable', fakeAsync(() => {
81+
// fixture.componentInstance.contentHeight = '1px';
82+
// fixture.detectChanges();
83+
//
84+
// layoutComponent.layoutContainerRef.nativeElement.style.paddingLeft = '1px';
85+
//
86+
// scrollService.scrollable(false);
87+
// flush();
88+
// fixture.detectChanges();
89+
//
90+
// expect(layoutComponent.layoutContainerRef.nativeElement.style.paddingLeft).toEqual('1px');
91+
// }));
92+
93+
it('should restore previous overflow value when enabling scroll', fakeAsync(() => {
94+
layoutComponent.scrollableContainerRef.nativeElement.style.overflow = 'auto';
95+
96+
scrollService.scrollable(false);
97+
flush();
98+
fixture.detectChanges();
99+
scrollService.scrollable(true);
100+
flush();
101+
fixture.detectChanges();
102+
103+
expect(layoutComponent.scrollableContainerRef.nativeElement.style.overflow).toEqual('auto');
104+
}));
105+
106+
it('should restore previous padding left value when enabling scroll in LTR mode', fakeAsync(() => {
107+
layoutComponent.layoutContainerRef.nativeElement.style.paddingLeft = '1px';
108+
109+
scrollService.scrollable(false);
110+
flush();
111+
fixture.detectChanges();
112+
scrollService.scrollable(true);
113+
flush();
114+
fixture.detectChanges();
115+
116+
expect(layoutComponent.layoutContainerRef.nativeElement.style.paddingLeft).toEqual('1px');
117+
}));
118+
119+
it('should restore previous padding right value when enabling scroll in RTL mode', fakeAsync(() => {
120+
const layoutDirectionService: NbLayoutDirectionService = TestBed.get(NbLayoutDirectionService);
121+
layoutDirectionService.setDirection(NbLayoutDirection.RTL);
122+
flush();
123+
fixture.detectChanges();
124+
layoutComponent.layoutContainerRef.nativeElement.style.paddingRight = '1px';
125+
126+
scrollService.scrollable(false);
127+
flush();
128+
fixture.detectChanges();
129+
scrollService.scrollable(true);
130+
flush();
131+
fixture.detectChanges();
132+
133+
expect(layoutComponent.layoutContainerRef.nativeElement.style.paddingRight).toEqual('1px');
134+
}));
135+
});
136+
});

src/framework/theme/components/layout/layout.component.ts

Lines changed: 67 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ import { NbOverlayContainerAdapter } from '../cdk/adapter/overlay-container-adap
129129
styleUrls: ['./layout.component.scss'],
130130
template: `
131131
<div class="scrollable-container" #scrollableContainer (scroll)="onScroll($event)">
132-
<div class="layout">
132+
<div class="layout" #layoutContainer>
133133
<ng-content select="nb-layout-header:not([subheader])"></ng-content>
134134
<div class="layout-container">
135135
<ng-content select="nb-sidebar"></ng-content>
@@ -147,13 +147,17 @@ import { NbOverlayContainerAdapter } from '../cdk/adapter/overlay-container-adap
147147
})
148148
export class NbLayoutComponent implements AfterViewInit, OnDestroy {
149149

150+
protected scrollBlockClass = 'nb-global-scrollblock';
151+
protected isScrollBlocked = false;
152+
protected scrollableContainerOverflowOldValue: string;
153+
protected layoutPaddingOldValue: { left: string; right: string };
154+
150155
centerValue: boolean = false;
151156
restoreScrollTopValue: boolean = true;
152157

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

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

208212
// TODO remove as of 5.0.0
209213
@ViewChild('layoutTopDynamicArea', { read: ViewContainerRef, static: false }) veryTopRef: ViewContainerRef;
210-
@ViewChild('scrollableContainer', { read: ElementRef, static: false }) scrollableContainerRef: ElementRef;
214+
215+
@ViewChild('scrollableContainer', { read: ElementRef, static: false })
216+
scrollableContainerRef: ElementRef<HTMLElement>;
217+
218+
@ViewChild('layoutContainer', { read: ElementRef, static: false })
219+
layoutContainerRef: ElementRef<HTMLElement>;
211220

212221
protected afterViewInit$ = new BehaviorSubject(null);
213222

@@ -300,20 +309,15 @@ export class NbLayoutComponent implements AfterViewInit, OnDestroy {
300309
filter(() => this.withScrollValue),
301310
)
302311
.subscribe((scrollable: boolean) => {
303-
const root = this.document.documentElement;
304-
const scrollBlockClass = 'nb-global-scrollblock';
305-
306-
this.overlayScrollBlock = !scrollable;
307-
308312
/**
309313
* In case when Nebular Layout custom scroll `withScroll` mode is enabled
310314
* we need to disable default CDK scroll blocker (@link NbBlockScrollStrategyAdapter) on HTML element
311315
* so that it won't add additional positioning.
312316
*/
313-
if (!scrollable) {
314-
this.renderer.addClass(root, scrollBlockClass);
317+
if (scrollable) {
318+
this.enableScroll();
315319
} else {
316-
this.renderer.removeClass(root, scrollBlockClass);
320+
this.blockScroll();
317321
}
318322
});
319323

@@ -443,6 +447,57 @@ export class NbLayoutComponent implements AfterViewInit, OnDestroy {
443447
this.window.scrollTo(x, y);
444448
}
445449
}
450+
451+
// TODO: Extract into block scroll strategy
452+
protected blockScroll() {
453+
if (this.isScrollBlocked) {
454+
return;
455+
}
456+
457+
this.isScrollBlocked = true;
458+
459+
this.renderer.addClass(this.document.documentElement, this.scrollBlockClass);
460+
461+
const scrollableContainerElement = this.scrollableContainerRef.nativeElement;
462+
const layoutElement = this.layoutContainerRef.nativeElement;
463+
464+
const layoutWithScrollWidth = layoutElement.clientWidth;
465+
this.scrollableContainerOverflowOldValue = scrollableContainerElement.style.overflow;
466+
scrollableContainerElement.style.overflow = 'hidden';
467+
const layoutWithoutScrollWidth = layoutElement.clientWidth;
468+
const scrollWidth = layoutWithoutScrollWidth - layoutWithScrollWidth;
469+
470+
if (!scrollWidth) {
471+
return;
472+
}
473+
474+
this.layoutPaddingOldValue = {
475+
left: layoutElement.style.paddingLeft,
476+
right: layoutElement.style.paddingRight,
477+
};
478+
479+
if (this.layoutDirectionService.isLtr()) {
480+
layoutElement.style.paddingRight = `${scrollWidth}px`;
481+
} else {
482+
layoutElement.style.paddingLeft = `${scrollWidth}px`;
483+
}
484+
}
485+
486+
private enableScroll() {
487+
if (this.isScrollBlocked) {
488+
this.isScrollBlocked = false;
489+
490+
this.renderer.removeClass(this.document.documentElement, this.scrollBlockClass);
491+
this.scrollableContainerRef.nativeElement.style.overflow = this.scrollableContainerOverflowOldValue;
492+
493+
if (this.layoutPaddingOldValue) {
494+
const layoutElement = this.layoutContainerRef.nativeElement;
495+
layoutElement.style.paddingLeft = this.layoutPaddingOldValue.left;
496+
layoutElement.style.paddingRight = this.layoutPaddingOldValue.right;
497+
this.layoutPaddingOldValue = null;
498+
}
499+
}
500+
}
446501
}
447502

448503
/**
@@ -477,7 +532,7 @@ export class NbLayoutColumnComponent {
477532
}
478533

479534
/**
480-
* Make columnt first in the layout.
535+
* Make column first in the layout.
481536
* @param {boolean} val
482537
*/
483538
@Input()

0 commit comments

Comments
 (0)