Passed
Push — feature/add-2fa-support ( 23e818...85b1f3 )
by Chris
24:19 queued 11:16
created

resources/assets/js/components/Modal.tsx   A

Complexity

Total Complexity 12
Complexity/F 6

Size

Lines of Code 216
Function Count 2

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 166
dl 0
loc 216
rs 10
c 0
b 0
f 0
wmc 12
mnd 10
bc 10
fnc 2
bpm 5
cpm 6
noi 0

1 Function

Rating   Name   Duplication   Size   Complexity  
D Modal.tsx ➔ Modal 0 109 11
1
import React, {
2
  createContext,
3
  useContext,
4
  useEffect,
5
  useRef,
6
  useState,
7
} from "react";
8
import { createPortal } from "react-dom";
9
10
interface ModalProps {
11
  id: string;
12
  parentElement: Element | null;
13
  visible: boolean;
14
  children: React.ReactNode;
15
  onModalConfirm: (e: React.MouseEvent<HTMLButtonElement>) => void;
16
  onModalMiddle?: (e: React.MouseEvent<HTMLButtonElement>) => void;
17
  onModalCancel: (
18
    e: React.MouseEvent<HTMLButtonElement> | KeyboardEvent,
19
  ) => void;
20
}
21
22
// Partial helper allows empty defaults in the createContext call:
23
// https://fettblog.eu/typescript-react/context/#context-without-default-values
24
const modalContext = createContext<Partial<ModalProps>>({});
25
26
export default function Modal({
27
  id,
28
  parentElement,
29
  visible,
30
  children,
31
  onModalConfirm,
32
  onModalMiddle,
33
  onModalCancel,
34
}: ModalProps): React.ReactPortal | null {
35
  // Set up div ref to measure modal height
36
  const modalRef = useRef<HTMLDivElement>(null);
37
38
  const handleTabKey = (e: KeyboardEvent): void => {
39
    if (modalRef && modalRef.current) {
40
      const focusableModalElements = modalRef.current.querySelectorAll(
41
        'a[href], button, textarea, input[type="text"], input[type="email"], input[type="radio"], select',
42
      );
43
      const firstElement = focusableModalElements[0] as HTMLElement;
44
      const lastElement = focusableModalElements[
45
        focusableModalElements.length - 1
46
      ] as HTMLElement;
47
48
      const focusableModalElementsArray = Array.from(focusableModalElements);
49
50
      if (
51
        document.activeElement &&
52
        !focusableModalElementsArray.includes(document.activeElement)
53
      ) {
54
        firstElement.focus();
55
        e.preventDefault();
56
      }
57
58
      if (!e.shiftKey && document.activeElement === lastElement) {
59
        firstElement.focus();
60
        e.preventDefault();
61
      }
62
63
      if (e.shiftKey && document.activeElement === firstElement) {
64
        lastElement.focus();
65
        e.preventDefault();
66
      }
67
    }
68
  };
69
70
  // Collection of key codes and event listeners
71
  const keyListenersMap = new Map([[27, onModalCancel], [9, handleTabKey]]);
72
73
  // Runs every time visible changes to set the overflow on the modal and update the body overflow
74
  useEffect((): (() => void) => {
75
    function setBodyStyle(): void {
76
      document.body.style.overflow = visible ? "hidden" : "visible";
77
    }
78
    setBodyStyle();
79
    // Runs on component unmount
80
    return (): void => {
81
      setBodyStyle();
82
    };
83
  }, [visible]);
84
85
  // Adds various key commands to the modal
86
  useEffect((): (() => void) => {
87
    let keyListener;
88
    if (visible) {
89
      keyListener = (e: KeyboardEvent): void => {
90
        const listener = keyListenersMap.get(e.keyCode);
91
        return listener && listener(e);
92
      };
93
      document.addEventListener("keydown", keyListener);
94
    }
95
96
    return (): void => {
97
      if (keyListener !== undefined) {
98
        document.removeEventListener("keydown", keyListener);
99
      }
100
    };
101
  }, [keyListenersMap, visible]);
102
103
  if (parentElement !== null) {
104
    return createPortal(
105
      <div
106
        aria-describedby={`${id}-description`}
107
        aria-hidden={!visible}
108
        aria-labelledby={`${id}-title`}
109
        data-c-dialog={visible ? "active--overflowing" : ""}
110
        data-c-padding="top(double) bottom(double)"
111
        role="dialog"
112
        ref={modalRef}
113
      >
114
        <div data-c-background="white(100)" data-c-radius="rounded">
115
          <modalContext.Provider
116
            value={{
117
              id,
118
              parentElement,
119
              visible,
120
              onModalConfirm,
121
              onModalMiddle,
122
              onModalCancel,
123
            }}
124
          >
125
            {children}
126
          </modalContext.Provider>
127
        </div>
128
      </div>,
129
      parentElement,
130
    );
131
  }
132
133
  return null;
134
}
135
136
Modal.Header = function ModalHeader(props): React.ReactElement {
137
  return props.children;
138
};
139
140
Modal.Body = function ModalBody(props): React.ReactElement {
141
  const { children } = props;
142
  return <div data-c-border="bottom(thin, solid, black)">{children}</div>;
143
};
144
145
Modal.Footer = function ModalFooter(props): React.ReactElement {
146
  const { children } = props;
147
  return (
148
    <div data-c-padding="normal">
149
      <div data-c-grid="gutter middle">
150
        {Array.isArray(children) && children.length > 0
151
          ? children.map(
152
              (btn, index): React.ReactElement => (
153
                <div
154
                  // eslint-disable-next-line react/no-array-index-key
155
                  key={index}
156
                  data-c-grid-item={`base(1of${children.length})`}
157
                >
158
                  {btn}
159
                </div>
160
              ),
161
            )
162
          : children}
163
      </div>
164
    </div>
165
  );
166
};
167
168
Modal.FooterConfirmBtn = function ConfirmBtn(props): React.ReactElement {
169
  const { onModalConfirm } = useContext(modalContext);
170
  return (
171
    <div data-c-alignment="base(right)">
172
      <button
173
        {...props}
174
        data-c-button="solid(c1)"
175
        data-c-dialog-action="close"
176
        data-c-radius="rounded"
177
        type="button"
178
        onClick={onModalConfirm}
179
      />
180
    </div>
181
  );
182
};
183
184
Modal.FooterCancelBtn = function CancelBtn(props): React.ReactElement {
185
  const { onModalCancel } = useContext(modalContext);
186
  return (
187
    <div>
188
      <button
189
        {...props}
190
        data-c-button="outline(c1)"
191
        data-c-dialog-action="close"
192
        data-c-radius="rounded"
193
        type="button"
194
        onClick={onModalCancel}
195
      />
196
    </div>
197
  );
198
};
199
200
Modal.FooterMiddleBtn = function MiddleBtn(props): React.ReactElement {
201
  const { onModalMiddle } = useContext(modalContext);
202
  return (
203
    <div data-c-alignment="base(center)">
204
      <button
205
        {...props}
206
        data-c-button="solid(c1)"
207
        data-c-dialog-action="close"
208
        data-c-radius="rounded"
209
        type="button"
210
        disabled={onModalMiddle === undefined}
211
        onClick={onModalMiddle}
212
      />
213
    </div>
214
  );
215
};
216