Skip to content

Commit bf7e2a3

Browse files
authored
fix(sidebar): update state when responsive setting change (#2654)
1 parent 137ad4d commit bf7e2a3

File tree

2 files changed

+128
-25
lines changed

2 files changed

+128
-25
lines changed

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

Lines changed: 43 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ import {
1616
OnInit,
1717
Output,
1818
} from '@angular/core';
19-
import { Subject } from 'rxjs';
20-
import { takeUntil, filter } from 'rxjs/operators';
19+
import { combineLatest, Subject } from 'rxjs';
20+
import { takeUntil, filter, map, startWith } from 'rxjs/operators';
2121

2222
import { convertToBoolProperty, NbBooleanInput } from '../helpers';
2323
import { NbThemeService } from '../../services/theme.service';
@@ -146,6 +146,7 @@ export class NbSidebarFooterComponent {
146146
})
147147
export class NbSidebarComponent implements OnInit, OnDestroy {
148148

149+
protected readonly responsiveValueChange$: Subject<boolean> = new Subject<boolean>();
149150
protected responsiveState: NbSidebarResponsiveState = 'pc';
150151

151152
protected destroy$ = new Subject<void>();
@@ -254,7 +255,7 @@ export class NbSidebarComponent implements OnInit, OnDestroy {
254255
set state(value: NbSidebarState) {
255256
this._state = value;
256257
}
257-
protected _state: NbSidebarState;
258+
protected _state: NbSidebarState = 'expanded';
258259

259260
/**
260261
* Makes sidebar listen to media query events and change its behaviour
@@ -265,7 +266,10 @@ export class NbSidebarComponent implements OnInit, OnDestroy {
265266
return this._responsive;
266267
}
267268
set responsive(value: boolean) {
268-
this._responsive = convertToBoolProperty(value);
269+
if (this.responsive !== convertToBoolProperty(value)) {
270+
this._responsive = !this.responsive;
271+
this.responsiveValueChange$.next(this.responsive);
272+
}
269273
}
270274
protected _responsive: boolean = false;
271275
static ngAcceptInputType_responsive: NbBooleanInput;
@@ -344,13 +348,26 @@ export class NbSidebarComponent implements OnInit, OnDestroy {
344348
.subscribe(() => this.compact());
345349

346350
getSidebarState$
347-
.pipe(filter(({ tag }) => !this.tag || this.tag === tag))
351+
.pipe(
352+
filter(({ tag }) => !this.tag || this.tag === tag),
353+
takeUntil(this.destroy$),
354+
)
348355
.subscribe(({ observer }) => observer.next(this.state));
349356

350357
getSidebarResponsiveState$
351-
.pipe(filter(({ tag }) => !this.tag || this.tag === tag))
358+
.pipe(
359+
filter(({ tag }) => !this.tag || this.tag === tag),
360+
takeUntil(this.destroy$),
361+
)
352362
.subscribe(({ observer }) => observer.next(this.responsiveState));
353363

364+
this.responsiveValueChange$
365+
.pipe(
366+
filter((responsive: boolean) => !responsive),
367+
takeUntil(this.destroy$),
368+
)
369+
.subscribe(() => this.expand());
370+
354371
this.subscribeToMediaQueryChange();
355372
}
356373

@@ -376,27 +393,21 @@ export class NbSidebarComponent implements OnInit, OnDestroy {
376393
* Collapses the sidebar
377394
*/
378395
collapse() {
379-
this.state = 'collapsed';
380-
this.stateChange.emit(this.state);
381-
this.cd.markForCheck();
396+
this.updateState('collapsed');
382397
}
383398

384399
/**
385400
* Expands the sidebar
386401
*/
387402
expand() {
388-
this.state = 'expanded';
389-
this.stateChange.emit(this.state);
390-
this.cd.markForCheck();
403+
this.updateState('expanded');
391404
}
392405

393406
/**
394407
* Compacts the sidebar (minimizes)
395408
*/
396409
compact() {
397-
this.state = 'compacted';
398-
this.stateChange.emit(this.state);
399-
this.cd.markForCheck();
410+
this.updateState('compacted');
400411
}
401412

402413
/**
@@ -418,18 +429,20 @@ export class NbSidebarComponent implements OnInit, OnDestroy {
418429
}
419430

420431
if (this.state === 'compacted' || this.state === 'collapsed') {
421-
this.state = 'expanded';
432+
this.updateState('expanded');
422433
} else {
423-
this.state = compact ? 'compacted' : 'collapsed';
434+
this.updateState(compact ? 'compacted' : 'collapsed');
424435
}
425-
this.stateChange.emit(this.state);
426-
this.cd.markForCheck();
427436
}
428437

429438
protected subscribeToMediaQueryChange() {
430-
this.themeService.onMediaQueryChange()
439+
combineLatest([
440+
this.responsiveValueChange$.pipe(startWith(this.responsive)),
441+
this.themeService.onMediaQueryChange(),
442+
])
431443
.pipe(
432-
filter(() => this.responsive),
444+
filter(([responsive]) => responsive),
445+
map(([, breakpoints]) => breakpoints),
433446
takeUntil(this.destroy$),
434447
)
435448
.subscribe(([prev, current]: [NbMediaBreakpoint, NbMediaBreakpoint]) => {
@@ -449,7 +462,7 @@ export class NbSidebarComponent implements OnInit, OnDestroy {
449462
this.collapse();
450463
newResponsiveState = 'mobile';
451464
}
452-
if (!isCollapsed && !isCompacted && prev.width < current.width) {
465+
if (!isCollapsed && !isCompacted && (!prev.width || prev.width < current.width)) {
453466
this.expand();
454467
this.fixed = false;
455468
newResponsiveState = 'pc';
@@ -475,6 +488,14 @@ export class NbSidebarComponent implements OnInit, OnDestroy {
475488
return this.getMenuLink(element.parentElement);
476489
}
477490

491+
protected updateState(state: NbSidebarState): void {
492+
if (this.state !== state) {
493+
this.state = state;
494+
this.stateChange.emit(this.state);
495+
this.cd.markForCheck();
496+
}
497+
}
498+
478499
/**
479500
* @deprecated Use `responsive` property instead
480501
* @breaking-change Remove @8.0.0

src/framework/theme/components/sidebar/sidebar.spec.ts

Lines changed: 85 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
import { Component, DebugElement } from '@angular/core';
1+
import { Component, DebugElement, Injectable } from '@angular/core';
22
import { ComponentFixture, TestBed } from '@angular/core/testing';
33
import { By } from '@angular/platform-browser';
44
import { RouterTestingModule } from '@angular/router/testing';
55
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
6+
import { Observable, Subject } from 'rxjs';
7+
import { pairwise, startWith } from 'rxjs/operators';
68
import {
79
NbSidebarComponent,
810
NbMenuModule,
@@ -11,12 +13,17 @@ import {
1113
NbMenuComponent,
1214
NbThemeModule,
1315
NbMenuItemComponent,
14-
NbIconComponent, NbSidebarService,
16+
NbIconComponent,
17+
NbSidebarService,
18+
NbSidebarState,
19+
NbMediaBreakpoint,
20+
NbThemeService,
21+
NbMediaBreakpointsService,
1522
} from '@nebular/theme';
1623

1724
@Component({
1825
template: `
19-
<nb-sidebar>
26+
<nb-sidebar [responsive]="responsive" [state]="state">
2027
<button id="button-outside-menu"></button>
2128
<nb-menu [items]="menuItems"></nb-menu>
2229
</nb-sidebar>
@@ -36,6 +43,32 @@ export class SidebarExpandTestComponent {
3643
group: true,
3744
},
3845
];
46+
47+
responsive = false;
48+
state: NbSidebarState = 'expanded';
49+
}
50+
51+
@Injectable()
52+
export class MockThemeService {
53+
private breakpoint$ = new Subject<NbMediaBreakpoint>();
54+
55+
constructor(private breakpointsService: NbMediaBreakpointsService) {
56+
}
57+
58+
setBreakpointTo(breakpointName: string): void {
59+
this.breakpoint$.next(this.breakpointsService.getByName(breakpointName));
60+
}
61+
62+
onMediaQueryChange(): Observable<NbMediaBreakpoint[]> {
63+
const breakpoints = this.breakpointsService.getBreakpoints();
64+
const largestBreakpoint = breakpoints[breakpoints.length - 1];
65+
66+
return this.breakpoint$
67+
.pipe(
68+
startWith({ name: 'unknown', width: undefined }, largestBreakpoint),
69+
pairwise(),
70+
);
71+
}
3972
}
4073

4174
describe('NbSidebarComponent', () => {
@@ -48,19 +81,25 @@ describe('NbSidebarComponent', () => {
4881
NbSidebarModule.forRoot(),
4982
NbMenuModule.forRoot(),
5083
],
84+
providers: [
85+
MockThemeService,
86+
{ provide: NbThemeService, useExisting: MockThemeService },
87+
],
5188
declarations: [ SidebarExpandTestComponent ],
5289
});
5390
});
5491

5592
describe('States (expanded, collapsed, compacted)', () => {
5693
let fixture: ComponentFixture<SidebarExpandTestComponent>;
5794
let sidebarComponent: NbSidebarComponent;
95+
let themeService: MockThemeService;
5896

5997
beforeEach(() => {
6098
fixture = TestBed.createComponent(SidebarExpandTestComponent);
6199
fixture.detectChanges();
62100

63101
sidebarComponent = fixture.debugElement.query(By.directive(NbSidebarComponent)).componentInstance;
102+
themeService = TestBed.inject(MockThemeService);
64103
});
65104

66105
it(`should collapse when collapse method called`, () => {
@@ -142,5 +181,48 @@ describe('NbSidebarComponent', () => {
142181

143182
expect(sidebarComponent.compacted).toEqual(true);
144183
});
184+
185+
it('should expand when responsive is set to false', () => {
186+
sidebarComponent.responsive = true;
187+
sidebarComponent.collapse();
188+
189+
expect(sidebarComponent.state).toEqual('collapsed');
190+
191+
sidebarComponent.responsive = false;
192+
193+
expect(sidebarComponent.state).toEqual('expanded');
194+
});
195+
196+
it('should update state according to the current breakpoint when responsive turns on (update to compacted)', () => {
197+
const compactedBreakpoints = sidebarComponent.compactedBreakpoints;
198+
const largestCompactedBreakpointName = compactedBreakpoints[compactedBreakpoints.length - 1];
199+
themeService.setBreakpointTo(largestCompactedBreakpointName);
200+
201+
expect(sidebarComponent.state).toEqual('expanded');
202+
203+
sidebarComponent.responsive = true;
204+
205+
expect(sidebarComponent.state).toEqual('compacted');
206+
});
207+
208+
it('should update state according to the current breakpoint when responsive turns on (update to collapsed)', () => {
209+
themeService.setBreakpointTo(sidebarComponent.collapsedBreakpoints[0]);
210+
211+
expect(sidebarComponent.state).toEqual('expanded');
212+
213+
sidebarComponent.responsive = true;
214+
215+
expect(sidebarComponent.state).toEqual('collapsed');
216+
});
217+
218+
it('should expand when responsive and initial state is in different breakpoint', () => {
219+
fixture = TestBed.createComponent(SidebarExpandTestComponent);
220+
fixture.componentInstance.responsive = true;
221+
fixture.componentInstance.state = 'compacted';
222+
fixture.detectChanges();
223+
sidebarComponent = fixture.debugElement.query(By.directive(NbSidebarComponent)).componentInstance;
224+
225+
expect(sidebarComponent.state).toEqual('expanded');
226+
});
145227
});
146228
});

0 commit comments

Comments
 (0)