• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

react-ui-org / react-ui / 13409901360

19 Feb 2025 09:59AM UTC coverage: 91.956%. First build
13409901360

Pull #544

github

web-flow
Merge b0b2c90f9 into 87ee7df12
Pull Request #544: Re-implement `Modal` component using HTMLDialogElement (#461)

785 of 859 branches covered (91.39%)

Branch coverage included in aggregate %.

31 of 33 new or added lines in 4 files covered. (93.94%)

724 of 782 relevant lines covered (92.58%)

72.79 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.59
/src/components/Modal/Modal.jsx
1
import PropTypes from 'prop-types';
2
import React, {
3
  useCallback,
4
  useEffect,
5
  useImperativeHandle,
6
  useRef,
7
} from 'react';
8
import { createPortal } from 'react-dom';
9
import { withGlobalProps } from '../../providers/globalProps';
10
import { classNames } from '../../utils/classNames';
11
import { transferProps } from '../../utils/transferProps';
12
import { dialogOnCancelHandler } from './_helpers/dialogOnCancelHandler';
13
import { dialogOnClickHandler } from './_helpers/dialogOnClickHandler';
14
import { dialogOnCloseHandler } from './_helpers/dialogOnCloseHandler';
15
import { dialogOnKeyDownHandler } from './_helpers/dialogOnKeyDownHandler';
16
import { getPositionClassName } from './_helpers/getPositionClassName';
17
import { getSizeClassName } from './_helpers/getSizeClassName';
18
import { useModalFocus } from './_hooks/useModalFocus';
19
import { useModalScrollPrevention } from './_hooks/useModalScrollPrevention';
20
import styles from './Modal.module.scss';
21

22
const preRender = (
4✔
23
  children,
24
  dialogRef,
25
  position,
26
  size,
27
  events,
28
  restProps,
29
) => (
30
  <dialog
50✔
31
    {...transferProps(restProps)}
32
    {...transferProps(events)}
33
    className={classNames(
34
      styles.root,
35
      getSizeClassName(size, styles),
36
      getPositionClassName(position, styles),
37
    )}
38
    ref={dialogRef}
39
  >
40
    {children}
41
  </dialog>
42
);
43

44
export const Modal = ({
4✔
45
  allowCloseOnBackdropClick,
46
  allowCloseOnEscapeKey,
47
  allowPrimaryActionOnEnterKey,
48
  autoFocus,
49
  children,
50
  closeButtonRef,
51
  dialogRef,
52
  portalId,
53
  position,
54
  preventScrollUnderneath,
55
  primaryButtonRef,
56
  size,
57
  ...restProps
58
}) => {
59
  const internalDialogRef = useRef();
50✔
60

61
  useEffect(() => {
50✔
62
    internalDialogRef.current.showModal();
50✔
63
  }, []);
64

65
  // We need to have a reference to the dialog element to be able to call its methods,
66
  // but at the same time we want to expose this reference to the parent component for
67
  // case someone wants to call dialog methods from outside the component.
68
  useImperativeHandle(dialogRef, () => internalDialogRef.current);
50✔
69

70
  useModalFocus(autoFocus, internalDialogRef, primaryButtonRef);
50✔
71
  useModalScrollPrevention(preventScrollUnderneath);
50✔
72

73
  const onCancel = useCallback(
50✔
NEW
74
    (e) => dialogOnCancelHandler(e, closeButtonRef, restProps.onCancel),
×
75
    [closeButtonRef, restProps.onCancel],
76
  );
77
  const onClick = useCallback(
50✔
78
    (e) => dialogOnClickHandler(e, closeButtonRef, internalDialogRef, allowCloseOnBackdropClick),
6✔
79
    [allowCloseOnBackdropClick, closeButtonRef, internalDialogRef],
80
  );
81
  const onClose = useCallback(
50✔
NEW
82
    (e) => dialogOnCloseHandler(e, closeButtonRef, restProps.onClose),
×
83
    [closeButtonRef, restProps.onClose],
84
  );
85
  const onKeyDown = useCallback(
50✔
86
    (e) => dialogOnKeyDownHandler(
6✔
87
      e,
88
      closeButtonRef,
89
      primaryButtonRef,
90
      allowCloseOnEscapeKey,
91
      allowPrimaryActionOnEnterKey,
92
    ),
93
    [
94
      allowCloseOnEscapeKey,
95
      allowPrimaryActionOnEnterKey,
96
      closeButtonRef,
97
      primaryButtonRef,
98
    ],
99
  );
100
  const events = {
50✔
101
    onCancel,
102
    onClick,
103
    onClose,
104
    onKeyDown,
105
  };
106

107
  if (portalId === null) {
50✔
108
    return preRender(
48✔
109
      children,
110
      internalDialogRef,
111
      position,
112
      size,
113
      events,
114
      restProps,
115
    );
116
  }
117

118
  return createPortal(
2✔
119
    preRender(
120
      children,
121
      internalDialogRef,
122
      position,
123
      size,
124
      events,
125
      restProps,
126
    ),
127
    document.getElementById(portalId),
128
  );
129
};
130

131
Modal.defaultProps = {
4✔
132
  allowCloseOnBackdropClick: true,
133
  allowCloseOnEscapeKey: true,
134
  allowPrimaryActionOnEnterKey: true,
135
  autoFocus: true,
136
  children: null,
137
  closeButtonRef: null,
138
  dialogRef: null,
139
  portalId: null,
140
  position: 'center',
141
  preventScrollUnderneath: window.document.body,
142
  primaryButtonRef: null,
143
  size: 'medium',
144
};
145

146
Modal.propTypes = {
4✔
147
  /**
148
   * If `true`, the `Modal` can be closed by clicking on the backdrop.
149
   */
150
  allowCloseOnBackdropClick: PropTypes.bool,
151
  /**
152
   * If `true`, the `Modal` can be closed by pressing the Escape key.
153
   */
154
  allowCloseOnEscapeKey: PropTypes.bool,
155
  /**
156
   * If `true`, the `Modal` can be submitted by pressing the Enter key.
157
   */
158
  allowPrimaryActionOnEnterKey: PropTypes.bool,
159
  /**
160
   * If `true`, focus the first input element in the `Modal`, or primary button (referenced by the `primaryButtonRef`
161
   * prop), or other focusable element when the `Modal` is opened. If there are none or `autoFocus` is set to `false`,
162
   * focus the Modal itself.
163
   */
164
  autoFocus: PropTypes.bool,
165
  /**
166
   * Nested elements. Supported types are:
167
   *
168
   * * `ModalHeader`
169
   * * `ModalBody`
170
   * * `ModalFooter`
171
   *
172
   * At least `ModalBody` is required.
173
   */
174
  children: PropTypes.node,
175
  /**
176
   * Reference to close button element. It is used to close modal when Escape key is pressed
177
   * or the backdrop is clicked.
178
   */
179
  closeButtonRef: PropTypes.shape({
180
    // eslint-disable-next-line react/forbid-prop-types
181
    current: PropTypes.any,
182
  }),
183
  /**
184
   * Reference to dialog element
185
   */
186
  dialogRef: PropTypes.shape({
187
    // eslint-disable-next-line react/forbid-prop-types
188
    current: PropTypes.any,
189
  }),
190
  /**
191
   * If set, modal is rendered in the React Portal with that ID.
192
   */
193
  portalId: PropTypes.string,
194
  /**
195
   * Vertical position of the modal inside browser window.
196
   */
197
  position: PropTypes.oneOf(['top', 'center']),
198
  /**
199
   * Mode in which Modal prevents scroll of elements bellow:
200
   * * `off` - Modal does not prevent any scroll
201
   * * [HTMLElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement) - Modal prevents scroll on this HTML element
202
   * * object
203
   *   * `reset` - method called on Modal's unmount to reset scroll prevention
204
   *   * `start` - method called on Modal's mount to custom scroll prevention
205
   */
206
  preventScrollUnderneath: PropTypes.oneOfType([
207
    PropTypes.oneOf([
208
      HTMLElement,
209
      'off',
210
    ]),
211
    PropTypes.shape({
212
      reset: PropTypes.func,
213
      start: PropTypes.func,
214
    }),
215
  ]),
216
  /**
217
   * Reference to primary button element. It is used to submit modal when Enter key is pressed and as fallback
218
   * when `autoFocus` functionality does not find any input element to be focused.
219
   */
220
  primaryButtonRef: PropTypes.shape({
221
    // eslint-disable-next-line react/forbid-prop-types
222
    current: PropTypes.any,
223
  }),
224
  /**
225
   * Size of the modal.
226
   */
227
  size: PropTypes.oneOf(['small', 'medium', 'large', 'fullscreen', 'auto']),
228
};
229

230
export const ModalWithGlobalProps = withGlobalProps(Modal, 'Modal');
4✔
231

232
export default ModalWithGlobalProps;
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc