Skip to content

Commit 99f2dcd

Browse files
committed
✨ Add multimodal support to Modal component
1 parent d499bc3 commit 99f2dcd

File tree

4 files changed

+117
-60
lines changed

4 files changed

+117
-60
lines changed

scripts/utilityTypes.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,13 @@ export type getLayoutClassesConfig = {
7373
wrap?: Responsive<Wrap>
7474
}
7575
76+
export type ModalInstance = {
77+
open: (freeze?: boolean) => void
78+
close: () => void
79+
replaceWith: (modal: ModalInstance) => void
80+
remove: () => void
81+
}
82+
7683
export type ModalCallback = {
7784
trigger: Element | null
7885
modal: HTMLElement
@@ -179,10 +186,7 @@ declare module 'webcoreui' {
179186
180187
export const isOneOf: <T extends string>(values: readonly T[]) => (value: string) => value is T
181188
182-
export const modal: (config: Modal | string) => {
183-
open: () => void
184-
remove: () => void
185-
} | void
189+
export const modal: (config: Modal | string) => ModalInstance | undefined
186190
export const closeModal: (modal: string) => void
187191
188192
export const popover: (config: Popover) => {

src/pages/components/modal.astro

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,26 @@ const sections = getSections({
232232
/>
233233
</section.component>
234234
</ComponentWrapper>
235+
236+
<ComponentWrapper title="Multimodal">
237+
<Button theme="secondary" id={`multimodal-btn-${section.type}`}>
238+
Trigger
239+
</Button>
240+
<section.component title="Modal #1" id={`multimodal-${section.type}`}>
241+
<p>Click the button to navigate to the next modal.</p>
242+
243+
<Button id={`next-modal-${section.type}`}>
244+
Next Modal {'->'}
245+
</Button>
246+
</section.component>
247+
<section.component title="Modal #2" id={`multimodal-1-${section.type}`}>
248+
<p>Click the button to navigate to the previous modal.</p>
249+
250+
<Button id={`prev-modal-${section.type}`} theme="secondary">
251+
{'<-'} Previous Modal
252+
</Button>
253+
</section.component>
254+
</ComponentWrapper>
235255
</div>
236256
))}
237257
</Layout>
@@ -247,7 +267,7 @@ const sections = getSections({
247267
'react'
248268
]
249269

250-
const ids = Array(10).fill(10).map((x, index) => x * (index + 1))
270+
const ids = Array(11).fill(10).map((x, index) => x * (index + 1))
251271
const variants = [
252272
'00',
253273
'01',
@@ -266,6 +286,27 @@ const sections = getSections({
266286
on(`#modal-close-btn-${selector}`, 'click', () => closeModal(`#modal-${selector}`))
267287
})
268288

289+
selectors.forEach(selector => {
290+
const modal1 = modal({
291+
trigger: `#multimodal-btn-${selector}`,
292+
modal: `#multimodal-${selector}`
293+
})
294+
295+
const modal2 = modal(`#multimodal-1-${selector}`)
296+
297+
on(`#next-modal-${selector}`, 'click', () => {
298+
if (modal1 && modal2) {
299+
modal1.replaceWith(modal2)
300+
}
301+
})
302+
303+
on(`#prev-modal-${selector}`, 'click', () => {
304+
if (modal1 && modal2) {
305+
modal2.replaceWith(modal1)
306+
}
307+
})
308+
})
309+
269310
variants.forEach(variant => {
270311
modal({
271312
trigger: `#modal-btn-${variant}`,

src/tests/unit/modal.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,22 @@ describe('modal', () => {
3939
})
4040

4141
triggerElement.click()
42+
4243
expect(modalElement.dataset.show).toBe('true')
4344
})
4445

46+
it('should open modal when .open is called and close it on .close', () => {
47+
const modalInstance = modal('#modal')
48+
49+
modalInstance?.open()
50+
51+
expect(modalElement.dataset.show).toBe('true')
52+
53+
modalInstance?.close()
54+
55+
expect(modalElement.dataset.show).toBe('')
56+
})
57+
4558
it('should close modal when close icon is clicked', () => {
4659
const onClose = vi.fn()
4760

src/utils/modal.ts

Lines changed: 54 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
import { bodyFreeze } from './bodyFreeze'
22

3+
export type ModalInstance = {
4+
open: (freeze?: boolean) => void
5+
close: () => void
6+
replaceWith: (modal: ModalInstance) => void
7+
remove: () => void
8+
}
9+
310
export type ModalCallback = {
411
trigger: Element | null
512
modal: HTMLElement
@@ -12,7 +19,7 @@ export type Modal = {
1219
onClose?: (args: ModalCallback) => unknown
1320
}
1421

15-
export const modal = (config: Modal | string) => {
22+
export const modal = (config: Modal | string): ModalInstance | undefined => {
1623
const {
1724
trigger,
1825
modal,
@@ -23,44 +30,51 @@ export const modal = (config: Modal | string) => {
2330
const modalSelector = typeof config === 'string' ? config : modal
2431

2532
const triggerDOM = document.querySelector(trigger)
26-
const modalDOM = document.querySelector(modalSelector) as HTMLElement
33+
const modalDOM = document.querySelector(modalSelector)
2734

28-
if (modalDOM) {
35+
if (modalDOM instanceof HTMLElement) {
2936
const closeOptions = modalDOM.dataset.close?.split(',')
3037

31-
const handleClose = {
32-
icon() {
33-
const close = modalDOM.querySelector('[data-id="close"]')
38+
const handleOpen = (_e?: Event, freeze = true) => {
39+
modalDOM.dataset.show = 'true'
40+
41+
if (freeze) {
42+
bodyFreeze()
43+
}
44+
45+
onOpen?.({
46+
trigger: triggerDOM,
47+
modal: modalDOM
48+
})
49+
}
3450

35-
const listener = () => {
36-
modalDOM.dataset.show = ''
51+
const handleClose = (_e?: Event, unfreeze = true) => {
52+
modalDOM.dataset.show = ''
3753

38-
bodyFreeze(false)
54+
if (unfreeze) {
55+
bodyFreeze(false)
56+
}
3957

40-
onClose?.({
41-
trigger: triggerDOM,
42-
modal: modalDOM
43-
})
44-
}
58+
onClose?.({
59+
trigger: triggerDOM,
60+
modal: modalDOM
61+
})
62+
}
63+
64+
const closeHandlers = {
65+
icon() {
66+
const close = modalDOM.querySelector('[data-id="close"]')
4567

4668
return {
47-
add: () => close?.addEventListener('click', listener),
48-
remove: () => close?.removeEventListener('click', listener)
69+
add: () => close?.addEventListener('click', handleClose),
70+
remove: () => close?.removeEventListener('click', handleClose)
4971
}
5072
},
5173

5274
esc() {
5375
const listener = (event: KeyboardEvent) => {
5476
if (modalDOM.dataset.show && event.key === 'Escape') {
55-
modalDOM.dataset.show = ''
56-
57-
bodyFreeze(false)
58-
59-
onClose?.({
60-
trigger: triggerDOM,
61-
modal: modalDOM
62-
})
63-
77+
handleClose()
6478
}
6579
}
6680

@@ -73,60 +87,45 @@ export const modal = (config: Modal | string) => {
7387
overlay() {
7488
const close = modalDOM.nextElementSibling
7589

76-
const listener = () => {
77-
modalDOM.dataset.show = ''
78-
79-
bodyFreeze(false)
80-
81-
onClose?.({
82-
trigger: triggerDOM,
83-
modal: modalDOM
84-
})
85-
}
86-
8790
return {
88-
add: () => close?.addEventListener('click', listener),
89-
remove: () => close?.removeEventListener('click', listener)
91+
add: () => close?.addEventListener('click', handleClose),
92+
remove: () => close?.removeEventListener('click', handleClose)
9093
}
9194
}
9295
}
9396

94-
const handleOpen = () => {
95-
modalDOM.dataset.show = 'true'
96-
97-
bodyFreeze()
98-
99-
onOpen?.({
100-
trigger: triggerDOM,
101-
modal: modalDOM
102-
})
103-
}
104-
10597
triggerDOM?.addEventListener('click', handleOpen)
10698

10799
closeOptions?.forEach(option => {
108-
handleClose[option as keyof typeof handleClose]().add()
100+
closeHandlers[option as keyof typeof closeHandlers]().add()
109101
})
110102

111103
return {
112-
open() {
113-
handleOpen()
104+
open(freeze?: boolean) {
105+
handleOpen(undefined, freeze)
106+
},
107+
close() {
108+
handleClose()
109+
},
110+
replaceWith(modal: ModalInstance) {
111+
modal.open(false)
112+
handleClose(undefined, false)
114113
},
115114
remove() {
116115
triggerDOM?.removeEventListener('click', handleOpen)
117116

118117
closeOptions?.forEach(option => {
119-
handleClose[option as keyof typeof handleClose]().remove()
118+
closeHandlers[option as keyof typeof closeHandlers]().remove()
120119
})
121120
}
122121
}
123122
}
124123
}
125124

126125
export const closeModal = (modal: string) => {
127-
const modalDOM = document.querySelector(modal) as HTMLElement
126+
const modalDOM = document.querySelector(modal)
128127

129-
if (modalDOM) {
128+
if (modalDOM instanceof HTMLElement) {
130129
modalDOM.dataset.show = ''
131130

132131
bodyFreeze(false)

0 commit comments

Comments
 (0)