Skip to content

Commit bad0696

Browse files
committed
feat(frontend:modals): improve modal animations, lifecycle handling, and state management
1 parent 5b411f0 commit bad0696

File tree

2 files changed

+92
-29
lines changed

2 files changed

+92
-29
lines changed

frontend/src/app/layout/layout.service.ts

Lines changed: 77 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,26 @@ export class LayoutService {
7373
map((e: any) => (e.matches ? themeDark : themeLight))
7474
)
7575
// Modal section
76-
private modalIDS: (number | string)[] = []
77-
private readonly dialogConfig: ModalOptions = { animated: true, keyboard: true, backdrop: true, ignoreBackdropClick: true }
76+
private modalIDS = new Set<number | string>()
77+
private readonly dialogConfig: ModalOptions = {
78+
animated: true,
79+
keyboard: false,
80+
backdrop: true,
81+
focus: true,
82+
ignoreBackdropClick: true,
83+
closeInterceptor: () => {
84+
if (this.bsModal['lastDismissReason'] !== 'browser-back-navigation-clicked') {
85+
// allow to close for other cases
86+
return Promise.resolve()
87+
}
88+
// avoid browser navigation when a modal is open
89+
if (this.atLeastOneModalOpen()) {
90+
// back to initial route
91+
history.forward()
92+
}
93+
return Promise.reject('blocked-by-interceptor')
94+
}
95+
}
7896

7997
constructor() {
8098
setTheme('bs5')
@@ -129,19 +147,14 @@ export class LayoutService {
129147
return this.restoreDialog(componentStates.id)
130148
}
131149
const modalRef = this.bsModal.show(dialog, Object.assign(componentStates, { ...this.dialogConfig, ...override }, { class: dialogClass }))
132-
if (modalRef.id && this.modalIDS.indexOf(modalRef.id) === -1) {
133-
this.modalIDS.push(modalRef.id)
134-
}
150+
this.modalIDS.add(modalRef.id)
135151
return modalRef
136152
}
137153

138154
minimizeDialog(modalID: any, element: { name: string; mimeUrl: string }): BsModalRef<unknown> {
139155
const modal = this.getModal(modalID)
140156
if (modal) {
141-
this.bsModal['_renderer'].setAttribute(modal['instance']._element.nativeElement, 'aria-hidden', 'true')
142-
this.bsModal['_renderer'].removeClass(modal['instance']._element.nativeElement, 'show')
143-
setTimeout(() => this.bsModal['_renderer'].setStyle(modal['instance']._element.nativeElement, 'display', 'none'), 100)
144-
157+
this.closeModalWithEffect(modal)
145158
if (!this.minimizedWindows.getValue().find((m: AppWindow) => m.id === modalID)) {
146159
this.minimizedWindows.next([...this.minimizedWindows.getValue(), { id: modalID, element }])
147160
}
@@ -152,36 +165,40 @@ export class LayoutService {
152165
restoreDialog(modalID: any): BsModalRef<unknown> {
153166
const modal: BsModalRef<unknown> = this.getModal(modalID)
154167
if (modal) {
155-
this.bsModal['_renderer'].setAttribute(modal['instance']._element.nativeElement, 'aria-hidden', 'false')
156-
this.bsModal['_renderer'].setStyle(modal['instance']._element.nativeElement, 'display', 'block')
157-
setTimeout(() => {
158-
this.bsModal['_renderer'].addClass(modal['instance']._element.nativeElement, 'show')
159-
}, 100)
168+
this.openModalWithEffect(modal)
169+
this.minimizedWindows.next(this.minimizedWindows.getValue().filter((m: AppWindow) => m.id !== modalID))
160170
}
161171
return modal
162172
}
163173

164-
closeDialog(delay: number | null = null, id: any = null, all = false) {
174+
closeDialog(delay: number | null = null, id: number | string = null, all = false) {
165175
if (all) {
166176
this.bsModal.hide()
167-
this.modalIDS = []
168-
} else {
169-
if (id) {
170-
this.modalIDS = this.modalIDS.filter((mid) => mid !== id)
171-
} else {
172-
id = this.modalIDS.pop()
177+
this.modalIDS.clear()
178+
this.minimizedWindows.next([])
179+
return
180+
}
181+
if (!id) {
182+
let last: string | number
183+
for (const value of this.modalIDS) {
184+
last = value
173185
}
174-
if (delay) {
175-
setTimeout(() => this.bsModal.hide(id), delay)
186+
if (last !== undefined) {
187+
id = last
176188
} else {
177-
this.bsModal.hide(id)
189+
console.warn('Last modal id not found')
190+
return
178191
}
179-
this.minimizedWindows.next(this.minimizedWindows.getValue().filter((m) => m.id !== id))
180192
}
181-
}
182-
183-
getModal(modalID: any): BsModalRef {
184-
return this.bsModal['loaders'].find((loader: any) => loader.instance?.config.id === modalID)
193+
this.modalIDS.delete(id)
194+
const modal: BsModalRef<unknown> = this.getModal(id)
195+
if (!modal) return
196+
if (delay) {
197+
setTimeout(() => this.closeModalWithEffect(modal, true), delay)
198+
} else {
199+
this.closeModalWithEffect(modal, true)
200+
}
201+
this.minimizedWindows.next(this.minimizedWindows.getValue().filter((m) => m.id !== id))
185202
}
186203

187204
openContextMenu(event: any, component: ContextMenuComponent<any>) {
@@ -269,6 +286,37 @@ export class LayoutService {
269286
this.closeDialog(null, null, true)
270287
}
271288

289+
private openModalWithEffect(modal: BsModalRef<unknown>) {
290+
this.bsModal['_renderer'].setAttribute(modal['instance']._element.nativeElement, 'aria-hidden', 'false')
291+
this.bsModal['_renderer'].setStyle(modal['instance']._element.nativeElement, 'display', 'block')
292+
setTimeout(() => {
293+
this.bsModal['_renderer'].addClass(modal['instance']._element.nativeElement, 'show')
294+
}, 100)
295+
}
296+
297+
private closeModalWithEffect(modal: BsModalRef<unknown>, hide = false) {
298+
this.bsModal['_renderer'].setAttribute(modal['instance']._element.nativeElement, 'aria-hidden', 'true')
299+
this.bsModal['_renderer'].removeClass(modal['instance']._element.nativeElement, 'show')
300+
setTimeout(() => this.bsModal['_renderer'].setStyle(modal['instance']._element.nativeElement, 'display', 'none'), 500)
301+
if (hide) {
302+
this.bsModal.hide(modal.id)
303+
}
304+
}
305+
306+
private getModal(modalID: any): BsModalRef {
307+
const modal = this.bsModal['loaders'].find((loader: any) => loader.instance?.config.id === modalID)
308+
if (modal) {
309+
modal.id = modalID
310+
return modal
311+
}
312+
console.warn(`Modal ${modalID} not found`)
313+
return null
314+
}
315+
316+
private atLeastOneModalOpen(): boolean {
317+
return this.modalIDS.size - this.minimizedWindows.getValue().length > 0
318+
}
319+
272320
private setTheme(theme: string) {
273321
this.electron.send(EVENT.MISC.SWITCH_THEME, theme)
274322
this.ngZone.run(() => this.switchTheme.next(theme))

frontend/src/styles/components/_modal.scss

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,18 @@
8585
border-bottom-left-radius: var(--bs-modal-inner-border-radius);
8686
border-bottom-right-radius: var(--bs-modal-inner-border-radius);
8787
}
88+
89+
/* Initial state (before opening) */
90+
.modal.fade .modal-dialog {
91+
opacity: 0;
92+
transform: translateY(-20px) scale(0.95);
93+
transition:
94+
transform 0.45s cubic-bezier(0.16, 1, 0.3, 1),
95+
opacity 0.3s ease-out;
96+
}
97+
98+
/* Final state (modal visible) */
99+
.modal.show .modal-dialog {
100+
opacity: 1;
101+
transform: translateY(0) scale(1);
102+
}

0 commit comments

Comments
 (0)