|
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