Skip to content

Commit 2b0b70d

Browse files
committed
feat: add CConditionalPortal component
1 parent 30e120f commit 2b0b70d

File tree

7 files changed

+202
-157
lines changed

7 files changed

+202
-157
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import React, { FC, ReactNode } from 'react'
2+
import { createPortal } from 'react-dom'
3+
import PropTypes from 'prop-types'
4+
5+
export interface CConditionalPortalProps {
6+
/**
7+
* @ignore
8+
*/
9+
children: ReactNode
10+
/**
11+
* Render some children into a different part of the DOM
12+
*/
13+
portal: boolean
14+
}
15+
16+
export const CConditionalPortal: FC<CConditionalPortalProps> = ({ children, portal }) => {
17+
return typeof window !== 'undefined' && portal ? (
18+
createPortal(children, document.body)
19+
) : (
20+
<>{children}</>
21+
)
22+
}
23+
24+
CConditionalPortal.propTypes = {
25+
children: PropTypes.node,
26+
portal: PropTypes.bool.isRequired,
27+
}
28+
29+
CConditionalPortal.displayName = 'CConditionalPortal'
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { CConditionalPortal } from './CConditionalPortal'
2+
3+
export { CConditionalPortal }

packages/coreui-react/src/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export * from './card'
1111
export * from './carousel'
1212
export * from './close-button'
1313
export * from './collapse'
14+
export * from './conditional-portal'
1415
export * from './dropdown'
1516
export * from './footer'
1617
export * from './form'

packages/coreui-react/src/components/modal/CModal.tsx

Lines changed: 85 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@ import React, {
88
useRef,
99
useState,
1010
} from 'react'
11-
import { createPortal } from 'react-dom'
1211
import PropTypes from 'prop-types'
1312
import classNames from 'classnames'
1413
import { Transition } from 'react-transition-group'
1514

1615
import { CBackdrop } from '../backdrop/CBackdrop'
16+
import { CConditionalPortal } from '../conditional-portal'
1717
import { CModalContent } from './CModalContent'
1818
import { CModalDialog } from './CModalDialog'
1919

@@ -119,22 +119,22 @@ export const CModal = forwardRef<HTMLDivElement, CModalProps>(
119119
const [_visible, setVisible] = useState(visible)
120120
const [staticBackdrop, setStaticBackdrop] = useState(false)
121121

122-
useEffect(() => {
123-
setVisible(visible)
124-
}, [visible])
125-
126122
const contextValues = {
127123
visible: _visible,
128124
setVisible,
129125
}
130126

131127
useEffect(() => {
132-
modalRef.current && modalRef.current.addEventListener('click', handleClickOutside)
133-
modalRef.current && modalRef.current.addEventListener('keyup', handleKeyDown)
128+
setVisible(visible)
129+
}, [visible])
130+
131+
useEffect(() => {
132+
document.addEventListener('click', handleClickOutside)
133+
document.addEventListener('keydown', handleKeyDown)
134134

135135
return () => {
136-
modalRef.current && modalRef.current.removeEventListener('click', handleClickOutside)
137-
modalRef.current && modalRef.current.removeEventListener('keyup', handleKeyDown)
136+
document.removeEventListener('click', handleClickOutside)
137+
document.removeEventListener('keydown', handleKeyDown)
138138
}
139139
}, [_visible])
140140

@@ -195,49 +195,45 @@ export const CModal = forwardRef<HTMLDivElement, CModalProps>(
195195
const handleKeyDown = useCallback(
196196
(event: KeyboardEvent) => {
197197
if (event.key === 'Escape' && keyboard) {
198-
return handleDismiss()
198+
handleDismiss()
199199
}
200200
},
201201
[modalRef, handleDismiss],
202202
)
203203

204-
const modal = (ref?: React.Ref<HTMLDivElement>, state?: string) => {
205-
return (
206-
<CModalContext.Provider value={contextValues}>
207-
<div
208-
className={classNames(
209-
'modal',
210-
{
211-
'modal-static': staticBackdrop,
212-
fade: transition,
213-
},
214-
state === 'entering'
215-
? 'd-block'
216-
: state === 'entered'
217-
? 'show d-block'
218-
: state === 'exiting'
219-
? 'd-block'
220-
: '',
221-
className,
222-
)}
223-
tabIndex={-1}
224-
role="dialog"
225-
ref={ref}
226-
>
227-
<CModalDialog
228-
alignment={alignment}
229-
fullscreen={fullscreen}
230-
scrollable={scrollable}
231-
size={size}
232-
>
233-
<CModalContent {...rest} ref={modalContentRef}>
234-
{children}
235-
</CModalContent>
236-
</CModalDialog>
237-
</div>
238-
</CModalContext.Provider>
239-
)
240-
}
204+
// const Modal = ({ ref, state }: { ref?: React.Ref<HTMLDivElement>; state?: string }) => (
205+
// <div
206+
// className={classNames(
207+
// 'modal',
208+
// {
209+
// 'modal-static': staticBackdrop,
210+
// fade: transition,
211+
// },
212+
// state === 'entering'
213+
// ? 'd-block'
214+
// : state === 'entered'
215+
// ? 'show d-block'
216+
// : state === 'exiting'
217+
// ? 'd-block'
218+
// : '',
219+
// className,
220+
// )}
221+
// tabIndex={-1}
222+
// role="dialog"
223+
// ref={ref}
224+
// >
225+
// <CModalDialog
226+
// alignment={alignment}
227+
// fullscreen={fullscreen}
228+
// scrollable={scrollable}
229+
// size={size}
230+
// >
231+
// <CModalContent {...rest} ref={modalContentRef}>
232+
// {children}
233+
// </CModalContent>
234+
// </CModalDialog>
235+
// </div>
236+
// )
241237

242238
return (
243239
<>
@@ -250,15 +246,49 @@ export const CModal = forwardRef<HTMLDivElement, CModalProps>(
250246
unmountOnExit={unmountOnClose}
251247
timeout={!transition ? 0 : duration}
252248
>
253-
{(state) => {
254-
return typeof window !== 'undefined' && portal
255-
? createPortal(modal(forkedRef, state), document.body)
256-
: modal(forkedRef, state)
257-
}}
249+
{(state) => (
250+
<CConditionalPortal portal={portal}>
251+
<CModalContext.Provider value={contextValues}>
252+
<div
253+
className={classNames(
254+
'modal',
255+
{
256+
'modal-static': staticBackdrop,
257+
fade: transition,
258+
},
259+
state === 'entering'
260+
? 'd-block'
261+
: state === 'entered'
262+
? 'show d-block'
263+
: state === 'exiting'
264+
? 'd-block'
265+
: '',
266+
className,
267+
)}
268+
tabIndex={-1}
269+
role="dialog"
270+
ref={forkedRef}
271+
>
272+
<CModalDialog
273+
alignment={alignment}
274+
fullscreen={fullscreen}
275+
scrollable={scrollable}
276+
size={size}
277+
>
278+
<CModalContent {...rest} ref={modalContentRef}>
279+
{children}
280+
</CModalContent>
281+
</CModalDialog>
282+
</div>
283+
</CModalContext.Provider>
284+
</CConditionalPortal>
285+
)}
258286
</Transition>
259-
{typeof window !== 'undefined' && portal
260-
? backdrop && createPortal(<CBackdrop visible={_visible} />, document.body)
261-
: backdrop && <CBackdrop visible={_visible} />}
287+
{backdrop && (
288+
<CConditionalPortal portal={portal}>
289+
<CBackdrop visible={_visible} />
290+
</CConditionalPortal>
291+
)}
262292
</>
263293
)
264294
},

packages/coreui-react/src/components/modal/__tests__/CModal.spec.tsx

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ test('CModal customize', async () => {
2626
})
2727

2828
test('CModal dialog close on press ESC', async () => {
29-
jest.useFakeTimers()
3029
const onClose = jest.fn()
3130
render(
3231
<CModal onClose={onClose} portal={false} visible>
@@ -36,32 +35,32 @@ test('CModal dialog close on press ESC', async () => {
3635
expect(onClose).toHaveBeenCalledTimes(0)
3736
const modal = document.querySelector('.modal')
3837
if (modal !== null) {
39-
fireEvent.keyUp(modal, {
38+
fireEvent.keyDown(modal, {
4039
key: 'Escape',
4140
code: 'Escape',
4241
keyCode: 27,
4342
charCode: 27,
4443
})
4544
}
45+
await new Promise((r) => setTimeout(r, 1000))
46+
console.log(modal)
47+
expect(onClose).toHaveBeenCalledTimes(1)
48+
})
49+
50+
test('CModal dialog close on backdrop', async () => {
51+
jest.useFakeTimers()
52+
const onClose = jest.fn()
53+
render(
54+
<CModal onClose={onClose} portal={false} visible={true}>
55+
Test
56+
</CModal>,
57+
)
58+
expect(onClose).toHaveBeenCalledTimes(0)
59+
const backdrop = document.querySelector('.modal-backdrop')
60+
if (backdrop !== null) {
61+
fireEvent.click(backdrop)
62+
}
4663
jest.runAllTimers()
4764
expect(onClose).toHaveBeenCalledTimes(1)
4865
jest.useRealTimers()
4966
})
50-
51-
// test('CModal dialog close on backdrop', async () => {
52-
// jest.useFakeTimers()
53-
// const onClose = jest.fn()
54-
// render(
55-
// <CModal onClose={onClose} portal={false} visible={true}>
56-
// Test
57-
// </CModal>,
58-
// )
59-
// expect(onClose).toHaveBeenCalledTimes(0)
60-
// const backdrop = document.querySelector('.modal-backdrop')
61-
// if (backdrop !== null) {
62-
// fireEvent.click(backdrop)
63-
// }
64-
// jest.runAllTimers()
65-
// expect(onClose).toHaveBeenCalledTimes(1)
66-
// jest.useRealTimers()
67-
// })

0 commit comments

Comments
 (0)