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

CBIIT / crdc-datahub-ui / 24157199810

08 Apr 2026 08:31PM UTC coverage: 82.981% (-0.1%) from 83.116%
24157199810

push

github

web-flow
Merge pull request #976 from CBIIT/CRDCDH-3615

CRDCDH-3615 Expand SRF Review Comment Dialog

5896 of 6467 branches covered (91.17%)

Branch coverage included in aggregate %.

160 of 226 new or added lines in 2 files covered. (70.8%)

5 existing lines in 2 files now uncovered.

34823 of 42603 relevant lines covered (81.74%)

236.85 hits per line

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

94.96
/src/components/Questionnaire/TextInput.tsx
1
import {
1✔
2
  FormControl,
3
  FormHelperText,
4
  Grid,
5
  OutlinedInput,
6
  OutlinedInputProps,
7
  styled,
8
} from "@mui/material";
9
import React, { FC, ReactNode, useEffect, useId, useRef, useState } from "react";
1✔
10

11
import { updateInputValidity } from "../../utils";
1✔
12
import Tooltip from "../Tooltip";
1✔
13

14
const StyledGridWrapper = styled(Grid)(({ theme }) => ({
1✔
15
  "& .MuiFormHelperText-root": {
1,091✔
16
    color: "#083A50",
1,091✔
17
    marginLeft: "0",
1,091✔
18
    [theme.breakpoints.up("lg")]: {
1,091✔
19
      whiteSpace: "nowrap",
1,091✔
20
    },
1,091✔
21
  },
1,091✔
22
  "& .MuiFormHelperText-root.Mui-error": {
1,091✔
23
    color: "#D54309 !important",
1,091✔
24
  },
1,091✔
25
}));
1✔
26

27
const StyledFormControl = styled(FormControl)(() => ({
1✔
28
  height: "100%",
1,091✔
29
  justifyContent: "end",
1,091✔
30
}));
1✔
31

32
export const StyledLabelWrapper = styled("div")(() => ({
1✔
33
  display: "flex",
1,091✔
34
  justifyContent: "flex-start",
1,091✔
35
  alignItems: "center",
1,091✔
36
  minHeight: "20px",
1,091✔
37
  color: "#083A50",
1,091✔
38
  marginBottom: "4px",
1,091✔
39
}));
1✔
40

41
export const StyledLabel = styled("label")(() => ({
1✔
42
  fontWeight: 700,
1,238✔
43
  fontSize: "16px",
1,238✔
44
  lineHeight: "19.6px",
1,238✔
45
  minHeight: "20px",
1,238✔
46
  color: "#083A50",
1,238✔
47
}));
1✔
48

49
const StyledAsterisk = styled("span")(() => ({
1✔
50
  color: "#C93F08",
800✔
51
  marginLeft: "2px",
800✔
52
}));
1✔
53

54
const StyledHelperText = styled(FormHelperText)(() => ({
1✔
55
  marginTop: "4px",
1,091✔
56
  minHeight: "20px",
1,091✔
57
}));
1✔
58

59
const StyledOutlinedInput = styled(OutlinedInput, {
1✔
60
  shouldForwardProp: (prop) => prop !== "resize" && prop !== "rowHeight",
1✔
61
})<OutlinedInputProps & { resize: boolean; rowHeight: number }>(
1✔
62
  ({ resize, rowHeight, rows, minRows, maxRows }) => ({
1✔
63
    borderRadius: "8px",
1,091✔
64
    backgroundColor: "#fff",
1,091✔
65
    color: "#083A50",
1,091✔
66
    "& .MuiInputBase-input": {
1,091✔
67
      fontWeight: 400,
1,091✔
68
      fontSize: "16px",
1,091✔
69
      fontFamily: "'Nunito', 'Rubik', sans-serif",
1,091✔
70
      lineHeight: "19.6px",
1,091✔
71
      padding: "12px",
1,091✔
72
      height: "20px",
1,091✔
73
    },
1,091✔
74
    "&.MuiInputBase-multiline": {
1,091✔
75
      padding: "12px",
1,091✔
76
    },
1,091✔
77
    "& .MuiInputBase-inputMultiline": {
1,091✔
78
      resize: resize ? "vertical" : "none",
1,091✔
79
      minHeight: resize && rowHeight ? `${(+rows || +minRows || 1) * rowHeight}px` : 0,
1,091!
80
      maxHeight: resize && maxRows && rowHeight ? `${+maxRows * rowHeight}px` : "none",
1,091!
81
    },
1,091✔
82
    "&.MuiInputBase-multiline .MuiInputBase-input": {
1,091✔
83
      lineHeight: `${rowHeight}px`,
1,091✔
84
      padding: 0,
1,091✔
85
    },
1,091✔
86
    "& .MuiOutlinedInput-notchedOutline": {
1,091✔
87
      borderColor: "#6B7294",
1,091✔
88
    },
1,091✔
89
    "&.Mui-focused .MuiOutlinedInput-notchedOutline": {
1,091✔
90
      border: "1px solid #209D7D",
1,091✔
91
      boxShadow:
1,091✔
92
        "2px 2px 4px 0px rgba(38, 184, 147, 0.10), -1px -1px 6px 0px rgba(38, 184, 147, 0.20)",
1,091✔
93
    },
1,091✔
94
    "& .MuiInputBase-input::placeholder": {
1,091✔
95
      color: "#87878C",
1,091✔
96
      fontWeight: 400,
1,091✔
97
      opacity: 1,
1,091✔
98
    },
1,091✔
99
    // Override the input error border color
100
    "&.Mui-error fieldset": {
1,091✔
101
      borderColor: "#D54309 !important",
1,091✔
102
    },
1,091✔
103
    // Target readOnly <textarea> inputs
104
    "&.MuiInputBase-multiline.Mui-readOnly": {
1,091✔
105
      backgroundColor: "#E5EEF4",
1,091✔
106
      color: "#083A50",
1,091✔
107
      cursor: "not-allowed",
1,091✔
108
      borderRadius: "8px",
1,091✔
109
    },
1,091✔
110
    // Target readOnly <input> inputs
111
    "& .MuiOutlinedInput-input:read-only": {
1,091✔
112
      backgroundColor: "#E5EEF4",
1,091✔
113
      color: "#083A50",
1,091✔
114
      cursor: "not-allowed",
1,091✔
115
      borderRadius: "8px",
1,091✔
116
    },
1,091✔
117
  })
1,091✔
118
);
1✔
119

120
type Props = {
121
  label?: string | ReactNode;
122
  labelStartAdornment?: ReactNode;
123
  labelEndAdornment?: ReactNode;
124
  infoText?: string;
125
  errorText?: string;
126
  tooltipText?: string | ReactNode;
127
  gridWidth?: 2 | 4 | 6 | 8 | 10 | 12;
128
  maxLength?: number;
129
  hideValidation?: boolean;
130
  resize?: boolean;
131
  validate?: (input: string) => boolean;
132
  filter?: (input: string) => string;
133
  parentStateSetter?: (string) => void;
134
} & OutlinedInputProps;
135

136
/**
137
 * Generates a generic text input with a label and help text
138
 *
139
 * NOTE:
140
 * - We're using a custom wrapper for Material UI's OutlinedInput component
141
 *   instead of using the TextField component because of the forced
142
 *   floating label behavior of TextField.
143
 *
144
 * @param {Props} props & number props
145
 * @returns {JSX.Element}
146
 */
147
const TextInput: FC<Props> = ({
1✔
148
  classes,
1,356✔
149
  value,
1,356✔
150
  label,
1,356✔
151
  labelStartAdornment,
1,356✔
152
  labelEndAdornment,
1,356✔
153
  required = false,
1,356✔
154
  gridWidth,
1,356✔
155
  maxLength,
1,356✔
156
  infoText,
1,356✔
157
  errorText,
1,356✔
158
  tooltipText,
1,356✔
159
  hideValidation,
1,356✔
160
  validate,
1,356✔
161
  filter,
1,356✔
162
  type,
1,356✔
163
  readOnly,
1,356✔
164
  rows,
1,356✔
165
  multiline,
1,356✔
166
  resize,
1,356✔
167
  inputProps,
1,356✔
168
  onChange,
1,356✔
169
  parentStateSetter,
1,356✔
170
  ...rest
1,356✔
171
}) => {
1,356✔
172
  const id = rest.id || useId();
1,356!
173

174
  const [val, setVal] = useState(value);
1,356✔
175
  const [error, setError] = useState(false);
1,356✔
176
  const errorMsg = errorText || (required ? "This field is required" : null);
1,356✔
177
  const inputRef = useRef<HTMLInputElement>(null);
1,356✔
178
  const ROW_HEIGHT = 25; // line height of each row in a multiline
1,356✔
179

180
  const processValue = (inputVal: string) => {
1,356✔
181
    let newVal = inputVal;
608✔
182

183
    if (typeof filter === "function") {
608✔
184
      newVal = filter(newVal);
143✔
185
    }
143✔
186
    if (typeof maxLength === "number" && newVal?.length > maxLength) {
608!
UNCOV
187
      newVal = newVal.slice(0, maxLength);
×
UNCOV
188
    }
×
189
    if (typeof validate === "function") {
608✔
190
      const customIsValid = validate(newVal);
135✔
191
      updateInputValidity(inputRef, !customIsValid ? errorMsg : "");
135✔
192
    }
135✔
193
    if (typeof parentStateSetter === "function") {
608!
194
      parentStateSetter(newVal);
×
195
    }
×
196
    setVal(newVal);
608✔
197
  };
608✔
198

199
  const onChangeWrapper = (event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
1,356✔
200
    if (typeof onChange === "function") {
8✔
201
      onChange(event);
8✔
202
    }
8✔
203
    const newVal = event.target.value;
8✔
204

205
    processValue(newVal);
8✔
206
    setError(false);
8✔
207
  };
8✔
208

209
  useEffect(() => {
1,356✔
210
    const invalid = () => setError(true);
598✔
211

212
    inputRef.current?.addEventListener("invalid", invalid);
598✔
213
    return () => {
598✔
214
      inputRef.current?.removeEventListener("invalid", invalid);
598!
215
    };
598✔
216
  }, [inputRef]);
1,356✔
217

218
  useEffect(() => {
1,356✔
219
    processValue(value?.toString());
600✔
220
  }, [value]);
1,356✔
221

222
  /* MUI sets the height for multiline input using inline styling. Needs to be overwritten to have a working minHeight */
223
  const customInputProps =
1,356✔
224
    resize && multiline
1,356✔
225
      ? { style: { height: `${(+rows || 1) * ROW_HEIGHT}px`, overflow: "auto" } }
357!
226
      : {};
999✔
227

228
  return (
1,356✔
229
    <StyledGridWrapper md={gridWidth || 6} xs={12} item>
1,356✔
230
      <StyledFormControl fullWidth error={error}>
1,356✔
231
        <StyledLabelWrapper>
1,356✔
232
          {labelStartAdornment}
1,356✔
233
          {label && (
1,356✔
234
            <StyledLabel htmlFor={id}>
1,356✔
235
              {label}
1,356✔
236
              {required && label ? <StyledAsterisk>*</StyledAsterisk> : ""}
1,356✔
237
              {tooltipText && <Tooltip placement="right" title={tooltipText} />}
1,356✔
238
            </StyledLabel>
1,356✔
239
          )}
240
          {labelEndAdornment}
1,356✔
241
        </StyledLabelWrapper>
1,356✔
242
        <StyledOutlinedInput
1,356✔
243
          inputRef={inputRef}
1,356✔
244
          type={type || "text"}
1,356✔
245
          size="small"
1,356✔
246
          value={val ?? ""}
1,356✔
247
          onChange={onChangeWrapper}
1,356✔
248
          required={required}
1,356✔
249
          readOnly={readOnly}
1,356✔
250
          rows={rows}
1,356✔
251
          multiline={multiline}
1,356✔
252
          resize={resize}
1,356✔
253
          rowHeight={ROW_HEIGHT}
1,356✔
254
          {...rest}
1,356✔
255
          inputProps={{ ...customInputProps, ...inputProps }}
1,356✔
256
          id={id}
1,356✔
257
        />
258
        <StyledHelperText>
1,356✔
259
          {(!hideValidation && !readOnly && error ? errorMsg : infoText) || " "}
1,356✔
260
        </StyledHelperText>
1,356✔
261
      </StyledFormControl>
1,356✔
262
    </StyledGridWrapper>
1,356✔
263
  );
264
};
1,356✔
265

266
export default TextInput;
1✔
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