|
1 |
| -import React, { FC, HTMLAttributes, useCallback, useLayoutEffect, useRef, useState } from 'react' |
| 1 | +import React, { |
| 2 | + forwardRef, |
| 3 | + HTMLAttributes, |
| 4 | + useCallback, |
| 5 | + useLayoutEffect, |
| 6 | + useRef, |
| 7 | + useState, |
| 8 | +} from 'react' |
2 | 9 | import { createPortal } from 'react-dom'
|
3 | 10 | import PropTypes from 'prop-types'
|
4 | 11 | import classNames from 'classnames'
|
5 | 12 | import { CSSTransition } from 'react-transition-group'
|
6 | 13 |
|
| 14 | +import { useForkedRef } from '../../utils/hooks' |
| 15 | + |
7 | 16 | import { CBackdrop } from '../backdrop/CBackdrop'
|
8 | 17 | import { CModalContent } from './CModalContent'
|
9 | 18 | import { CModalDialog } from './CModalDialog'
|
@@ -64,121 +73,128 @@ export interface CModalProps extends HTMLAttributes<HTMLDivElement> {
|
64 | 73 | visible?: boolean
|
65 | 74 | }
|
66 | 75 |
|
67 |
| -export const CModal: FC<CModalProps> = ({ |
68 |
| - children, |
69 |
| - alignment, |
70 |
| - backdrop = true, |
71 |
| - className, |
72 |
| - duration = 150, |
73 |
| - fullscreen, |
74 |
| - keyboard = true, |
75 |
| - onDismiss, |
76 |
| - portal = true, |
77 |
| - scrollable, |
78 |
| - size, |
79 |
| - transition = true, |
80 |
| - visible, |
81 |
| -}) => { |
82 |
| - const ref = useRef<HTMLDivElement>(null) |
83 |
| - const [staticBackdrop, setStaticBackdrop] = useState(false) |
| 76 | +export const CModal = forwardRef<HTMLDivElement, CModalProps>( |
| 77 | + ( |
| 78 | + { |
| 79 | + children, |
| 80 | + alignment, |
| 81 | + backdrop = true, |
| 82 | + className, |
| 83 | + duration = 150, |
| 84 | + fullscreen, |
| 85 | + keyboard = true, |
| 86 | + onDismiss, |
| 87 | + portal = true, |
| 88 | + scrollable, |
| 89 | + size, |
| 90 | + transition = true, |
| 91 | + visible, |
| 92 | + }, |
| 93 | + ref, |
| 94 | + ) => { |
| 95 | + const [staticBackdrop, setStaticBackdrop] = useState(false) |
84 | 96 |
|
85 |
| - const handleDismiss = () => { |
86 |
| - if (typeof onDismiss === 'undefined') { |
87 |
| - return setStaticBackdrop(true) |
88 |
| - } |
89 |
| - return onDismiss && onDismiss() |
90 |
| - } |
| 97 | + const modalRef = useRef<HTMLDivElement>(null) |
| 98 | + const forkedRef = useForkedRef(ref, modalRef) |
91 | 99 |
|
92 |
| - useLayoutEffect(() => { |
93 |
| - setTimeout(() => setStaticBackdrop(false), duration) |
94 |
| - }, [staticBackdrop]) |
| 100 | + const handleDismiss = () => { |
| 101 | + if (typeof onDismiss === 'undefined') { |
| 102 | + return setStaticBackdrop(true) |
| 103 | + } |
| 104 | + return onDismiss && onDismiss() |
| 105 | + } |
95 | 106 |
|
96 |
| - const getTransitionClass = (state: string) => { |
97 |
| - return state === 'entering' |
98 |
| - ? 'd-block' |
99 |
| - : state === 'entered' |
100 |
| - ? 'show d-block' |
101 |
| - : state === 'exiting' |
102 |
| - ? 'd-block' |
103 |
| - : '' |
104 |
| - } |
105 |
| - const _className = classNames( |
106 |
| - 'modal', |
107 |
| - { |
108 |
| - 'modal-static': staticBackdrop, |
109 |
| - fade: transition, |
110 |
| - }, |
111 |
| - className, |
112 |
| - ) |
| 107 | + useLayoutEffect(() => { |
| 108 | + setTimeout(() => setStaticBackdrop(false), duration) |
| 109 | + }, [staticBackdrop]) |
113 | 110 |
|
114 |
| - // Set focus to modal after open |
115 |
| - useLayoutEffect(() => { |
116 |
| - if (visible) { |
117 |
| - document.body.classList.add('modal-open') |
118 |
| - setTimeout( |
119 |
| - () => { |
120 |
| - ref.current && ref.current.focus() |
121 |
| - }, |
122 |
| - !transition ? 0 : duration, |
123 |
| - ) |
124 |
| - } else { |
125 |
| - document.body.classList.remove('modal-open') |
| 111 | + const getTransitionClass = (state: string) => { |
| 112 | + return state === 'entering' |
| 113 | + ? 'd-block' |
| 114 | + : state === 'entered' |
| 115 | + ? 'show d-block' |
| 116 | + : state === 'exiting' |
| 117 | + ? 'd-block' |
| 118 | + : '' |
126 | 119 | }
|
127 |
| - return () => document.body.classList.remove('modal-open') |
128 |
| - }, [visible]) |
| 120 | + const _className = classNames( |
| 121 | + 'modal', |
| 122 | + { |
| 123 | + 'modal-static': staticBackdrop, |
| 124 | + fade: transition, |
| 125 | + }, |
| 126 | + className, |
| 127 | + ) |
129 | 128 |
|
130 |
| - const handleKeyDown = useCallback( |
131 |
| - (event) => { |
132 |
| - if (event.key === 'Escape' && keyboard) { |
133 |
| - return handleDismiss() |
| 129 | + // Set focus to modal after open |
| 130 | + useLayoutEffect(() => { |
| 131 | + if (visible) { |
| 132 | + document.body.classList.add('modal-open') |
| 133 | + setTimeout( |
| 134 | + () => { |
| 135 | + modalRef.current && modalRef.current.focus() |
| 136 | + }, |
| 137 | + !transition ? 0 : duration, |
| 138 | + ) |
| 139 | + } else { |
| 140 | + document.body.classList.remove('modal-open') |
134 | 141 | }
|
135 |
| - }, |
136 |
| - [ref, handleDismiss], |
137 |
| - ) |
| 142 | + return () => document.body.classList.remove('modal-open') |
| 143 | + }, [visible]) |
| 144 | + |
| 145 | + const handleKeyDown = useCallback( |
| 146 | + (event) => { |
| 147 | + if (event.key === 'Escape' && keyboard) { |
| 148 | + return handleDismiss() |
| 149 | + } |
| 150 | + }, |
| 151 | + [modalRef, handleDismiss], |
| 152 | + ) |
| 153 | + |
| 154 | + const modal = (ref?: React.Ref<HTMLDivElement>, transitionClass?: string) => { |
| 155 | + return ( |
| 156 | + <> |
| 157 | + <div |
| 158 | + className={classNames(_className, transitionClass)} |
| 159 | + tabIndex={-1} |
| 160 | + role="dialog" |
| 161 | + ref={ref} |
| 162 | + > |
| 163 | + <CModalDialog |
| 164 | + alignment={alignment} |
| 165 | + fullscreen={fullscreen} |
| 166 | + scrollable={scrollable} |
| 167 | + size={size} |
| 168 | + onClick={(event) => event.stopPropagation()} |
| 169 | + > |
| 170 | + <CModalContent>{children}</CModalContent> |
| 171 | + </CModalDialog> |
| 172 | + </div> |
| 173 | + {backdrop && <CBackdrop visible={visible} />} |
| 174 | + </> |
| 175 | + ) |
| 176 | + } |
138 | 177 |
|
139 |
| - const modal = (ref?: React.Ref<HTMLDivElement>, transitionClass?: string) => { |
140 | 178 | return (
|
141 |
| - <> |
142 |
| - <div |
143 |
| - className={classNames(_className, transitionClass)} |
144 |
| - tabIndex={-1} |
145 |
| - role="dialog" |
146 |
| - ref={ref} |
| 179 | + <div onClick={handleDismiss} onKeyDown={handleKeyDown}> |
| 180 | + <CSSTransition |
| 181 | + in={visible} |
| 182 | + timeout={!transition ? 0 : duration} |
| 183 | + onExit={onDismiss} |
| 184 | + mountOnEnter |
| 185 | + unmountOnExit |
147 | 186 | >
|
148 |
| - <CModalDialog |
149 |
| - alignment={alignment} |
150 |
| - fullscreen={fullscreen} |
151 |
| - scrollable={scrollable} |
152 |
| - size={size} |
153 |
| - onClick={(event) => event.stopPropagation()} |
154 |
| - > |
155 |
| - <CModalContent>{children}</CModalContent> |
156 |
| - </CModalDialog> |
157 |
| - </div> |
158 |
| - {backdrop && <CBackdrop visible={visible} />} |
159 |
| - </> |
| 187 | + {(state) => { |
| 188 | + const transitionClass = getTransitionClass(state) |
| 189 | + return typeof window !== 'undefined' && portal |
| 190 | + ? createPortal(modal(forkedRef, transitionClass), document.body) |
| 191 | + : modal(forkedRef, transitionClass) |
| 192 | + }} |
| 193 | + </CSSTransition> |
| 194 | + </div> |
160 | 195 | )
|
161 |
| - } |
162 |
| - |
163 |
| - return ( |
164 |
| - <div onClick={handleDismiss} onKeyDown={handleKeyDown}> |
165 |
| - <CSSTransition |
166 |
| - in={visible} |
167 |
| - timeout={!transition ? 0 : duration} |
168 |
| - onExit={onDismiss} |
169 |
| - mountOnEnter |
170 |
| - unmountOnExit |
171 |
| - > |
172 |
| - {(state) => { |
173 |
| - const transitionClass = getTransitionClass(state) |
174 |
| - return typeof window !== 'undefined' && portal |
175 |
| - ? createPortal(modal(ref, transitionClass), document.body) |
176 |
| - : modal(ref, transitionClass) |
177 |
| - }} |
178 |
| - </CSSTransition> |
179 |
| - </div> |
180 |
| - ) |
181 |
| -} |
| 196 | + }, |
| 197 | +) |
182 | 198 |
|
183 | 199 | CModal.propTypes = {
|
184 | 200 | alignment: PropTypes.oneOf(['top', 'center']),
|
|
0 commit comments