Skip to content

Commit 926ef62

Browse files
committed
feat(offcanvas): add responsive variations
1 parent 8c15b75 commit 926ef62

File tree

2 files changed

+78
-27
lines changed

2 files changed

+78
-27
lines changed

projects/coreui-angular/src/lib/offcanvas/offcanvas/offcanvas.component.ts

Lines changed: 77 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ import {
1515
import { DOCUMENT, isPlatformBrowser } from '@angular/common';
1616
import { animate, AnimationEvent, state, style, transition, trigger } from '@angular/animations';
1717
import { BooleanInput, coerceBooleanProperty } from '@angular/cdk/coercion';
18+
import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout';
1819
import { Subscription } from 'rxjs';
20+
import { filter } from 'rxjs/operators';
1921

2022
import { BackdropService } from '../../backdrop/backdrop.service';
2123
import { OffcanvasService } from '../offcanvas.service';
@@ -55,7 +57,8 @@ export class OffcanvasComponent implements OnInit, OnDestroy {
5557
private renderer: Renderer2,
5658
private hostElement: ElementRef,
5759
private offcanvasService: OffcanvasService,
58-
private backdropService: BackdropService
60+
private backdropService: BackdropService,
61+
private breakpointObserver: BreakpointObserver
5962
) {}
6063

6164
/**
@@ -79,20 +82,29 @@ export class OffcanvasComponent implements OnInit, OnDestroy {
7982
*/
8083
@Input() placement: string | 'start' | 'end' | 'top' | 'bottom' = 'start';
8184

85+
/**
86+
* Responsive offcanvas property hides content outside the viewport from a specified breakpoint and down.
87+
* @type boolean | 'sm' | 'md' | 'lg' | 'xl' | 'xxl';
88+
* @default true
89+
* @since 4.3.10
90+
*/
91+
@Input() responsive?: boolean | 'sm' | 'md' | 'lg' | 'xl' | 'xxl' = true;
92+
8293
/**
8394
* Allow body scrolling while offcanvas is visible.
8495
* @type boolean
96+
* @default false
8597
*/
8698
@Input()
8799
set scroll(value: boolean) {
88-
this._scroll = coerceBooleanProperty(value);
100+
this.#scroll = coerceBooleanProperty(value);
89101
}
90102

91103
get scroll() {
92-
return this._scroll;
104+
return this.#scroll;
93105
}
94106

95-
private _scroll = false;
107+
#scroll = false;
96108

97109
@Input() id = `offcanvas-${this.placement}-${nextId++}`;
98110

@@ -117,37 +129,40 @@ export class OffcanvasComponent implements OnInit, OnDestroy {
117129
*/
118130
@Input()
119131
set visible(value: boolean) {
120-
this._visible = coerceBooleanProperty(value);
121-
if (this._visible) {
132+
this.#visible = coerceBooleanProperty(value);
133+
if (this.#visible) {
122134
this.setBackdrop(this.backdrop);
123135
this.setFocus();
124136
} else {
125137
this.setBackdrop(false);
126138
}
139+
this.layoutChangeSubscribe(this.#visible);
127140
this.visibleChange.emit(value);
128141
}
129142

130143
get visible(): boolean {
131-
return this._visible;
144+
return this.#visible;
132145
}
133146

134-
private _visible: boolean = false;
147+
#visible: boolean = false;
135148

136149
/**
137150
* Event triggered on visible change.
138151
*/
139-
@Output() visibleChange = new EventEmitter<boolean>();
152+
@Output() readonly visibleChange = new EventEmitter<boolean>();
140153

141-
private activeBackdrop!: HTMLDivElement;
142-
private scrollbarWidth!: string;
154+
#activeBackdrop!: HTMLDivElement;
155+
#scrollbarWidth!: string;
143156

144-
private stateToggleSubscription!: Subscription;
145-
private backdropClickSubscription!: Subscription;
157+
#stateToggleSubscription!: Subscription;
158+
#backdropClickSubscription!: Subscription;
159+
#layoutChangeSubscription!: Subscription;
146160

147161
@HostBinding('class')
148162
get hostClasses(): any {
149163
return {
150-
offcanvas: true,
164+
offcanvas: typeof this.responsive === 'boolean',
165+
[`offcanvas-${this.responsive}`]: typeof this.responsive !== 'boolean',
151166
[`offcanvas-${this.placement}`]: !!this.placement,
152167
show: this.show
153168
};
@@ -169,18 +184,18 @@ export class OffcanvasComponent implements OnInit, OnDestroy {
169184
}
170185

171186
get show(): boolean {
172-
return this.visible && this._show;
187+
return this.visible && this.#show;
173188
}
174189

175190
set show(value: boolean) {
176-
this._show = value;
191+
this.#show = value;
177192
}
178193

179-
private _show = false;
194+
#show = false;
180195

181196
@HostListener('@showHide.start', ['$event'])
182197
animateStart(event: AnimationEvent) {
183-
const scrollbarWidth = this.scrollbarWidth;
198+
const scrollbarWidth = this.#scrollbarWidth;
184199
if (event.toState === 'visible') {
185200
if (!this.scroll) {
186201
this.renderer.setStyle(this.document.body, 'overflow', 'hidden');
@@ -220,7 +235,7 @@ export class OffcanvasComponent implements OnInit, OnDestroy {
220235
}
221236

222237
ngOnInit(): void {
223-
this.scrollbarWidth = this.backdropService.scrollbarWidth;
238+
this.#scrollbarWidth = this.backdropService.scrollbarWidth;
224239
this.stateToggleSubscribe();
225240
// hotfix to avoid end offcanvas flicker on first render
226241
this.renderer.setStyle(this.hostElement.nativeElement, 'display', 'flex');
@@ -233,7 +248,7 @@ export class OffcanvasComponent implements OnInit, OnDestroy {
233248

234249
private stateToggleSubscribe(subscribe: boolean = true): void {
235250
if (subscribe) {
236-
this.stateToggleSubscription =
251+
this.#stateToggleSubscription =
237252
this.offcanvasService.offcanvasState$.subscribe((action) => {
238253
if (this === action.offcanvas || this.id === action.id) {
239254
if ('show' in action) {
@@ -243,25 +258,25 @@ export class OffcanvasComponent implements OnInit, OnDestroy {
243258
}
244259
});
245260
} else {
246-
this.stateToggleSubscription.unsubscribe();
261+
this.#stateToggleSubscription.unsubscribe();
247262
}
248263
}
249264

250265
private backdropClickSubscribe(subscribe: boolean = true): void {
251266
if (subscribe) {
252-
this.backdropClickSubscription =
267+
this.#backdropClickSubscription =
253268
this.backdropService.backdropClick$.subscribe((clicked) => {
254269
this.offcanvasService.toggle({ show: !clicked, id: this.id });
255270
});
256271
} else {
257-
this.backdropClickSubscription?.unsubscribe();
272+
this.#backdropClickSubscription?.unsubscribe();
258273
}
259274
}
260275

261276
private setBackdrop(setBackdrop: boolean | 'static'): void {
262-
this.scrollbarWidth = this.backdropService.scrollbarWidth;
263-
this.activeBackdrop = !!setBackdrop ? this.backdropService.setBackdrop('offcanvas')
264-
: this.backdropService.clearBackdrop(this.activeBackdrop);
277+
this.#scrollbarWidth = this.backdropService.scrollbarWidth;
278+
this.#activeBackdrop = !!setBackdrop ? this.backdropService.setBackdrop('offcanvas')
279+
: this.backdropService.clearBackdrop(this.#activeBackdrop);
265280
setBackdrop === true ? this.backdropClickSubscribe()
266281
: this.backdropClickSubscribe(false);
267282
}
@@ -271,4 +286,40 @@ export class OffcanvasComponent implements OnInit, OnDestroy {
271286
setTimeout(() => this.hostElement.nativeElement.focus());
272287
}
273288
}
289+
290+
get responsiveBreakpoint(): string | false {
291+
if (typeof this.responsive !== 'string') {
292+
return false;
293+
}
294+
const element: Element = this.document.documentElement;
295+
const responsiveBreakpoint = this.responsive;
296+
const breakpointValue = getComputedStyle(element).getPropertyValue(`--cui-breakpoint-${responsiveBreakpoint.trim()}`) || false;
297+
return breakpointValue ? `${parseFloat(breakpointValue.trim()) - 0.02}px` : false;
298+
}
299+
300+
private layoutChangeSubscribe(subscribe: boolean = true): void {
301+
302+
if (subscribe) {
303+
304+
if (!this.responsiveBreakpoint) {
305+
return;
306+
}
307+
308+
const responsiveBreakpoint = `(max-width: ${this.responsiveBreakpoint})`;
309+
310+
const layoutChanges = this.breakpointObserver.observe([responsiveBreakpoint]);
311+
312+
this.#layoutChangeSubscription = layoutChanges
313+
.pipe(
314+
filter(breakpointState => !breakpointState.matches)
315+
)
316+
.subscribe(
317+
(breakpointState: BreakpointState) => {
318+
this.visible = breakpointState.matches;
319+
}
320+
);
321+
} else {
322+
this.#layoutChangeSubscription?.unsubscribe();
323+
}
324+
}
274325
}

projects/coreui-angular/src/lib/sidebar/sidebar/sidebar.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ export class SidebarComponent implements OnChanges, OnDestroy, OnInit {
258258
}
259259
});
260260
} else {
261-
this.#stateToggleSubscription.unsubscribe();
261+
this.#stateToggleSubscription?.unsubscribe();
262262
}
263263
}
264264

0 commit comments

Comments
 (0)