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

react-ui-org / react-ui / 12747202941

13 Jan 2025 12:27PM UTC coverage: 80.471% (-11.5%) from 91.979%
12747202941

Pull #544

github

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

743 of 923 branches covered (80.5%)

Branch coverage included in aggregate %.

4 of 43 new or added lines in 7 files covered. (9.3%)

65 existing lines in 5 files now uncovered.

658 of 818 relevant lines covered (80.44%)

61.83 hits per line

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

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

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

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

NEW
59
  useEffect(() => {
×
NEW
60
    dialogRef.current.showModal();
×
61
  }, []);
62

NEW
63
  useModalFocus(allowPrimaryActionOnEnterKey, autoFocus, dialogRef, primaryButtonRef);
×
UNCOV
64
  useModalScrollPrevention(preventScrollUnderneath);
×
65

NEW
66
  const onCancel = useCallback(
×
NEW
67
    (e) => dialogOnCancelHandler(e, closeButtonRef),
×
68
    [closeButtonRef],
69
  );
NEW
70
  const onClick = useCallback(
×
NEW
71
    (e) => dialogOnClickHandler(e, closeButtonRef, dialogRef, allowCloseOnBackdropClick),
×
72
    [allowCloseOnBackdropClick, closeButtonRef, dialogRef],
73
  );
NEW
74
  const onClose = useCallback(
×
NEW
75
    (e) => dialogOnCloseHandler(e, closeButtonRef),
×
76
    [closeButtonRef],
77
  );
NEW
78
  const onKeyDown = useCallback(
×
NEW
79
    (e) => dialogOnKeyDownHandler(e, closeButtonRef, allowCloseOnEscapeKey),
×
80
    [allowCloseOnEscapeKey, closeButtonRef],
81
  );
NEW
82
  const events = {
×
83
    onCancel,
84
    onClick,
85
    onClose,
86
    onKeyDown,
87
  };
88

UNCOV
89
  if (portalId === null) {
×
UNCOV
90
    return preRender(
×
91
      children,
92
      dialogRef,
93
      position,
94
      size,
95
      events,
96
      restProps,
97
    );
98
  }
99

UNCOV
100
  return createPortal(
×
101
    preRender(
102
      children,
103
      dialogRef,
104
      position,
105
      size,
106
      events,
107
      restProps,
108
    ),
109
    document.getElementById(portalId),
110
  );
111
};
112

113
Modal.defaultProps = {
4✔
114
  allowCloseOnBackdropClick: true,
115
  allowCloseOnEscapeKey: true,
116
  allowPrimaryActionOnEnterKey: true,
117
  autoFocus: true,
118
  children: null,
119
  closeButtonRef: null,
120
  portalId: null,
121
  position: 'center',
122
  preventScrollUnderneath: window.document.body,
123
  primaryButtonRef: null,
124
  size: 'medium',
125
};
126

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

204
export const ModalWithGlobalProps = withGlobalProps(Modal, 'Modal');
4✔
205

206
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