Skip to content

Commit 2cfbfa9

Browse files
committed
refactor(backdrop, modal, offcanvas): move scrollbar adjustments to offcanvas, cleanups
1 parent a6c8bc6 commit 2cfbfa9

File tree

4 files changed

+158
-133
lines changed

4 files changed

+158
-133
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Inject, Injectable, Renderer2, RendererFactory2 } from '@angular/core';
1+
import { inject, Injectable, RendererFactory2 } from '@angular/core';
22
import { DOCUMENT } from '@angular/common';
33
import { Subject } from 'rxjs';
44

@@ -7,53 +7,76 @@ import { Subject } from 'rxjs';
77
})
88
export class BackdropService {
99

10-
private backdropClick = new Subject<boolean>();
11-
backdropClick$ = this.backdropClick.asObservable();
10+
readonly #backdropClick = new Subject<boolean>();
11+
readonly backdropClick$ = this.#backdropClick.asObservable();
1212

13-
private renderer: Renderer2;
14-
private unListen!: () => void;
13+
#document = inject(DOCUMENT);
14+
#rendererFactory = inject(RendererFactory2);
15+
#renderer = this.#rendererFactory.createRenderer(null, null);
16+
#unListen!: () => void;
1517

16-
constructor(
17-
@Inject(DOCUMENT) private document: Document,
18-
private rendererFactory: RendererFactory2
19-
) {
20-
this.renderer = rendererFactory.createRenderer(null, null);
21-
}
18+
activeBackdrop: any;
2219

23-
get scrollbarWidth() {
20+
get #scrollbarWidth() {
2421
// https://developer.mozilla.org/en-US/docs/Web/API/Window/innerWidth#usage_notes
25-
const documentWidth = this.document.documentElement.clientWidth;
22+
const documentWidth = this.#document.documentElement.clientWidth;
2623
const scrollbarWidth = Math.abs((window?.innerWidth ?? documentWidth) - documentWidth);
2724
return `${scrollbarWidth}px`;
2825
}
2926

27+
scrollbarWidth = this.#scrollbarWidth;
28+
3029
setBackdrop(type: string = 'modal'): any {
31-
const backdropElement = this.renderer.createElement('div');
32-
this.renderer.addClass(backdropElement, `${type}-backdrop`);
33-
this.renderer.addClass(backdropElement, 'fade');
34-
this.renderer.appendChild(this.document.body, backdropElement);
35-
this.unListen = this.renderer.listen(backdropElement, 'click', (e): void => {
30+
const backdropElement = this.#renderer.createElement('div');
31+
this.#renderer.addClass(backdropElement, `${type}-backdrop`);
32+
this.#renderer.addClass(backdropElement, 'fade');
33+
this.#renderer.appendChild(this.#document.body, backdropElement);
34+
this.#unListen = this.#renderer.listen(backdropElement, 'click', (e): void => {
3635
this.onClickHandler();
3736
});
37+
this.scrollbarWidth = this.#scrollbarWidth;
3838
setTimeout(() => {
39-
this.renderer.addClass(backdropElement, 'show');
39+
this.#renderer.addClass(backdropElement, 'show');
40+
// this.hideScrollbar();
4041
});
42+
this.activeBackdrop = backdropElement;
4143
return backdropElement;
4244
}
4345

44-
clearBackdrop(backdrop: any): any {
45-
if (backdrop) {
46-
this.unListen();
47-
this.renderer.removeClass(backdrop, 'show');
46+
clearBackdrop(backdropElement: any): any {
47+
if (backdropElement) {
48+
this.#unListen();
49+
this.#renderer.removeClass(backdropElement, 'show');
4850
setTimeout(() => {
49-
this.renderer.removeChild(this.document.body, backdrop);
50-
backdrop = undefined;
51+
this.#renderer.removeChild(this.#document.body, backdropElement);
52+
if (this.activeBackdrop === backdropElement) {
53+
this.resetScrollbar();
54+
}
55+
backdropElement = undefined;
5156
}, 300);
5257
}
53-
return backdrop;
58+
return undefined;
59+
}
60+
61+
get #isRTL() { return this.#document.documentElement.dir === 'rtl' || this.#document.body.dir === 'rtl'; }
62+
63+
#scrollBarVisible = true;
64+
65+
hideScrollbar(): void {
66+
if (this.#scrollBarVisible) {
67+
this.#renderer.setStyle(this.#document.body, 'overflow', 'hidden');
68+
this.#renderer.setStyle(this.#document.body, `padding-${this.#isRTL ? 'left' : 'right'}`, this.scrollbarWidth);
69+
this.#scrollBarVisible = false;
70+
}
71+
}
72+
73+
resetScrollbar(): void {
74+
this.#renderer.removeStyle(this.#document.body, 'overflow');
75+
this.#renderer.removeStyle(this.#document.body, `padding-${this.#isRTL ? 'left' : 'right'}`);
76+
this.#scrollBarVisible = true;
5477
}
5578

5679
onClickHandler(): void {
57-
this.backdropClick.next(true);
80+
this.#backdropClick.next(true);
5881
}
5982
}

projects/coreui-angular/src/lib/modal/modal/modal.component.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
[scrollable]="scrollable"
55
[size]="size">
66
<c-modal-content>
7-
<div [cdkTrapFocus]="visible" [cdkTrapFocusAutoCapture]="visible" style="display: contents;">
7+
<div [cdkTrapFocus]="visible" [cdkTrapFocusAutoCapture]="visible" style="display: contents;" #modalContentRef>
88
<ng-content></ng-content>
99
</div>
1010
</c-modal-content>

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

+57-52
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,28 @@
1+
import { animate, AnimationEvent, state, style, transition, trigger } from '@angular/animations';
2+
import { DOCUMENT } from '@angular/common';
13
import {
4+
AfterViewInit,
5+
booleanAttribute,
26
Component,
7+
DestroyRef,
8+
effect,
39
ElementRef,
410
EventEmitter,
511
HostBinding,
612
HostListener,
13+
inject,
714
Inject,
815
Input,
916
OnDestroy,
1017
OnInit,
1118
Output,
1219
Renderer2,
13-
ViewChild
20+
signal,
21+
ViewChild,
22+
WritableSignal
1423
} from '@angular/core';
15-
import { DOCUMENT } from '@angular/common';
16-
import { animate, AnimationEvent, state, style, transition, trigger } from '@angular/animations';
17-
import { BooleanInput, coerceBooleanProperty } from '@angular/cdk/coercion';
18-
import { A11yModule } from '@angular/cdk/a11y';
19-
import { Subscription } from 'rxjs';
24+
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
25+
import { A11yModule, FocusMonitor } from '@angular/cdk/a11y';
2026

2127
import { ModalService } from '../modal.service';
2228
import { BackdropService } from '../../backdrop/backdrop.service';
@@ -39,18 +45,18 @@ import { ModalDialogComponent } from '../modal-dialog/modal-dialog.component';
3945
// display: 'none'
4046
})
4147
),
42-
transition('visible <=> *', [animate('300ms')])
48+
transition('visible <=> *', [animate('150ms')])
4349
])
4450
],
4551
templateUrl: './modal.component.html',
4652
exportAs: 'cModal',
4753
standalone: true,
4854
imports: [ModalDialogComponent, ModalContentComponent, A11yModule]
4955
})
50-
export class ModalComponent implements OnInit, OnDestroy {
56+
export class ModalComponent implements OnInit, OnDestroy, AfterViewInit {
5157

52-
static ngAcceptInputType_scrollable: BooleanInput;
53-
static ngAcceptInputType_visible: BooleanInput;
58+
#destroyRef = inject(DestroyRef);
59+
#focusMonitor = inject(FocusMonitor);
5460

5561
constructor(
5662
@Inject(DOCUMENT) private document: Document,
@@ -83,7 +89,7 @@ export class ModalComponent implements OnInit, OnDestroy {
8389
* @type boolean
8490
* @default true
8591
*/
86-
@Input() keyboard = true;
92+
@Input({ transform: booleanAttribute }) keyboard = true;
8793
@Input() id?: string;
8894
/**
8995
* Size the component small, large, or extra large.
@@ -92,64 +98,65 @@ export class ModalComponent implements OnInit, OnDestroy {
9298
/**
9399
* Remove animation to create modal that simply appear rather than fade in to view.
94100
*/
95-
@Input() transition = true;
101+
@Input({ transform: booleanAttribute }) transition = true;
96102
/**
97103
* Default role for modal. [docs]
98104
* @type string
99105
* @default 'dialog'
100106
*/
101107
@Input() @HostBinding('attr.role') role = 'dialog';
108+
102109
/**
103110
* Set aria-modal html attr for modal. [docs]
104111
* @type boolean
105-
* @default true
112+
* @default null
106113
*/
107-
@Input() @HostBinding('attr.aria-modal') ariaModal = true;
114+
@Input() @HostBinding('attr.aria-modal')
115+
set ariaModal(value: boolean | null) {
116+
this.#ariaModal = value;
117+
}
118+
119+
get ariaModal(): boolean | null {
120+
return this.visible || this.#ariaModal ? true : null;
121+
};
122+
123+
#ariaModal: boolean | null = null;
108124

109125
/**
110126
* Create a scrollable modal that allows scrolling the modal body.
111127
* @type boolean
112128
*/
113-
@Input()
114-
set scrollable(value: boolean) {
115-
this._scrollable = coerceBooleanProperty(value);
116-
}
117-
118-
get scrollable(): boolean {
119-
return this._scrollable;
120-
}
121-
122-
private _scrollable = false;
129+
@Input({ transform: booleanAttribute }) scrollable: boolean = false;
123130

124131
/**
125132
* Toggle the visibility of modal component.
126133
* @type boolean
127134
*/
128-
@Input()
135+
@Input({ transform: booleanAttribute })
129136
set visible(value: boolean) {
130-
const newValue = coerceBooleanProperty(value);
131-
if (this._visible !== newValue) {
132-
this._visible = newValue;
133-
this.setBackdrop(this.backdrop !== false && newValue);
134-
this.setBodyStyles(newValue);
135-
this.visibleChange.emit(newValue);
137+
if (this.#visible() !== value) {
138+
this.#visible.set(value);
139+
this.setBackdrop(this.backdrop !== false && value);
140+
this.setBodyStyles(value);
141+
this.visibleChange.emit(value);
136142
}
137143
}
138144

139145
get visible(): boolean {
140-
return this._visible;
146+
return this.#visible();
141147
}
142148

143-
private _visible!: boolean;
149+
#visible: WritableSignal<boolean> = signal(false);
144150

145151
/**
146152
* Event triggered on modal dismiss.
147153
*/
148154
@Output() visibleChange = new EventEmitter<boolean>();
149155

150156
@ViewChild(ModalContentComponent, { read: ElementRef }) modalContent!: ElementRef;
151-
private activeBackdrop!: any;
152-
private stateToggleSubscription!: Subscription;
157+
@ViewChild('modalContentRef', { read: ElementRef }) modalContentRef!: ElementRef;
158+
159+
#activeBackdrop!: any;
153160

154161
// private inBoundingClientRect!: boolean;
155162

@@ -189,10 +196,8 @@ export class ModalComponent implements OnInit, OnDestroy {
189196

190197
@HostListener('@showHide.start', ['$event'])
191198
animateStart(event: AnimationEvent) {
192-
const scrollbarWidth = this.backdropService.scrollbarWidth;
193199
if (event.toState === 'visible') {
194-
this.renderer.setStyle(this.document.body, 'overflow', 'hidden');
195-
this.renderer.setStyle(this.document.body, 'padding-right', scrollbarWidth);
200+
this.backdropService.hideScrollbar();
196201
this.renderer.setStyle(this.hostElement.nativeElement, 'display', 'block');
197202
} else {
198203
if (!this.transition) {
@@ -206,8 +211,6 @@ export class ModalComponent implements OnInit, OnDestroy {
206211
setTimeout(() => {
207212
if (event.toState === 'hidden') {
208213
this.renderer.setStyle(this.hostElement.nativeElement, 'display', 'none');
209-
this.renderer.removeStyle(this.document.body, 'overflow');
210-
this.renderer.removeStyle(this.document.body, 'padding-right');
211214
}
212215
});
213216
this.show = this.visible;
@@ -255,14 +258,23 @@ export class ModalComponent implements OnInit, OnDestroy {
255258
this.stateToggleSubscribe();
256259
}
257260

261+
#afterViewInit = signal(false);
262+
263+
ngAfterViewInit(): void {
264+
this.#afterViewInit.set(true);
265+
}
266+
258267
ngOnDestroy(): void {
259268
this.modalService.toggle({ show: false, modal: this });
260-
this.stateToggleSubscribe(false);
269+
this.#afterViewInit.set(false);
261270
}
262271

263-
private stateToggleSubscribe(subscribe: boolean = true): void {
264-
if (subscribe) {
265-
this.stateToggleSubscription = this.modalService.modalState$.subscribe(
272+
private stateToggleSubscribe(): void {
273+
this.modalService.modalState$
274+
.pipe(
275+
takeUntilDestroyed(this.#destroyRef)
276+
)
277+
.subscribe(
266278
(action) => {
267279
if (this === action.modal || this.id === action.id) {
268280
if ('show' in action) {
@@ -275,17 +287,10 @@ export class ModalComponent implements OnInit, OnDestroy {
275287
}
276288
}
277289
);
278-
} else {
279-
this.stateToggleSubscription?.unsubscribe();
280-
}
281290
}
282291

283292
private setBackdrop(setBackdrop: boolean): void {
284-
if (setBackdrop) {
285-
this.activeBackdrop = this.backdropService.setBackdrop('modal');
286-
} else {
287-
this.activeBackdrop = this.backdropService.clearBackdrop(this.activeBackdrop);
288-
}
293+
this.#activeBackdrop = setBackdrop ? this.backdropService.setBackdrop('modal') : this.backdropService.clearBackdrop(this.#activeBackdrop);
289294
}
290295

291296
private setBodyStyles(open: boolean): void {

0 commit comments

Comments
 (0)