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

react-ui-org / react-ui / 12768023900

14 Jan 2025 12:51PM UTC coverage: 82.267% (-9.7%) from 91.979%
12768023900

Pull #544

github

web-flow
Merge 2514fd74a into 684d5abff
Pull Request #544: Re-implement `Modal` component using HTMLDialogElement (#461)

743 of 905 branches covered (82.1%)

Branch coverage included in aggregate %.

4 of 58 new or added lines in 7 files covered. (6.9%)

35 existing lines in 5 files now uncovered.

658 of 798 relevant lines covered (82.46%)

63.38 hits per line

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

18.52
/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 '../../provider';
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
) => (
NEW
30
  <dialog
×
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
}) => {
NEW
59
  const internalDialogRef = useRef();
×
60

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

NEW
65
  useImperativeHandle(dialogRef, () => internalDialogRef.current);
×
66

NEW
67
  useModalFocus(autoFocus, internalDialogRef, primaryButtonRef);
×
UNCOV
68
  useModalScrollPrevention(preventScrollUnderneath);
×
69

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

UNCOV
104
  if (portalId === null) {
×
UNCOV
105
    return preRender(
×
106
      children,
107
      internalDialogRef,
108
      position,
109
      size,
110
      events,
111
      restProps,
112
    );
113
  }
114

UNCOV
115
  return createPortal(
×
116
    preRender(
117
      children,
118
      internalDialogRef,
119
      position,
120
      size,
121
      events,
122
      restProps,
123
    ),
124
    document.getElementById(portalId),
125
  );
126
};
127

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

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

227
export const ModalWithGlobalProps = withGlobalProps(Modal, 'Modal');
4✔
228

229
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