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

CBIIT / crdc-datahub-ui / 18789341118

24 Oct 2025 06:57PM UTC coverage: 78.178% (+15.5%) from 62.703%
18789341118

push

github

web-flow
Merge pull request #888 from CBIIT/3.4.0

3.4.0 Release

4977 of 5488 branches covered (90.69%)

Branch coverage included in aggregate %.

8210 of 9264 new or added lines in 257 files covered. (88.62%)

6307 existing lines in 120 files now uncovered.

30203 of 39512 relevant lines covered (76.44%)

213.36 hits per line

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

9.88
/src/components/Questionnaire/SelectInput.tsx
1
import {
1✔
2
  FormControl,
3
  FormHelperText,
4
  Grid,
5
  MenuItem,
6
  Select,
7
  SelectProps,
8
  styled,
9
} from "@mui/material";
10
import React, { FC, ReactNode, useEffect, useId, useRef, useState } from "react";
1✔
11

12
import dropdownArrowsIcon from "../../assets/icons/dropdown_arrows.svg?url";
1✔
13
import { updateInputValidity } from "../../utils";
1✔
14
import Tooltip from "../Tooltip";
1✔
15

16
const DropdownArrowsIcon = styled("div")(() => ({
1✔
NEW
17
  backgroundImage: `url("${dropdownArrowsIcon}")`,
×
UNCOV
18
  backgroundSize: "contain",
×
UNCOV
19
  backgroundRepeat: "no-repeat",
×
UNCOV
20
  width: "10px",
×
UNCOV
21
  height: "18px",
×
22
}));
1✔
23

24
const GridItem = styled(Grid)(() => ({
1✔
UNCOV
25
  "& .MuiFormHelperText-root.Mui-error": {
×
UNCOV
26
    color: "#D54309 !important",
×
UNCOV
27
    marginLeft: "0px",
×
UNCOV
28
  },
×
UNCOV
29
  "& .MuiOutlinedInput-notchedOutline": {
×
UNCOV
30
    borderRadius: "8px",
×
UNCOV
31
    borderColor: "#6B7294",
×
UNCOV
32
    padding: "0 12px",
×
UNCOV
33
  },
×
UNCOV
34
  "&.Mui-error fieldset": {
×
UNCOV
35
    borderColor: "#D54309 !important",
×
UNCOV
36
  },
×
UNCOV
37
  "& .MuiSelect-icon": {
×
UNCOV
38
    right: "12px",
×
UNCOV
39
  },
×
UNCOV
40
  "& .MuiSelect-iconOpen": {
×
UNCOV
41
    transform: "none",
×
UNCOV
42
  },
×
UNCOV
43
  "& .Mui-focused .MuiOutlinedInput-notchedOutline": {
×
UNCOV
44
    border: "1px solid #209D7D !important",
×
UNCOV
45
    boxShadow:
×
UNCOV
46
      "2px 2px 4px 0px rgba(38, 184, 147, 0.10), -1px -1px 6px 0px rgba(38, 184, 147, 0.20)",
×
UNCOV
47
  },
×
48
}));
1✔
49

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

55
const StyledFormLabel = styled("label")(({ theme }) => ({
1✔
UNCOV
56
  fontWeight: 700,
×
UNCOV
57
  fontSize: "16px",
×
UNCOV
58
  lineHeight: "19.6px",
×
UNCOV
59
  minHeight: "20px",
×
UNCOV
60
  color: "#083A50",
×
UNCOV
61
  marginBottom: "4px",
×
UNCOV
62
  [theme.breakpoints.up("lg")]: {
×
UNCOV
63
    whiteSpace: "nowrap",
×
UNCOV
64
  },
×
65
}));
1✔
66

67
const ProxySelect = styled("select")(() => ({
1✔
UNCOV
68
  display: "none",
×
69
}));
1✔
70

71
const StyledSelect = styled(Select, {
1✔
72
  shouldForwardProp: (prop) => prop !== "placeholderText",
1✔
73
})<SelectProps & { placeholderText: string }>((props) => ({
1✔
UNCOV
74
  "& .MuiSelect-select .notranslate::after": {
×
75
    // content: `'${(props) => props.placeholderText || "none"}'`,
UNCOV
76
    content: `'${props.placeholderText ?? "Select"}'`,
×
UNCOV
77
    color: "#87878C",
×
UNCOV
78
    fontWeight: 400,
×
UNCOV
79
    opacity: 1,
×
UNCOV
80
  },
×
UNCOV
81
  "& .MuiPaper-root": {
×
UNCOV
82
    borderRadius: "8px",
×
UNCOV
83
    border: "1px solid #6B7294",
×
UNCOV
84
    marginTop: "2px",
×
UNCOV
85
    "& .MuiList-root": {
×
UNCOV
86
      padding: 0,
×
UNCOV
87
      overflow: "auto",
×
UNCOV
88
      maxHeight: "40vh",
×
UNCOV
89
    },
×
UNCOV
90
    "& .MuiMenuItem-root": {
×
UNCOV
91
      padding: "0 10px",
×
UNCOV
92
      height: "35px",
×
UNCOV
93
      color: "#083A50",
×
UNCOV
94
      background: "#FFFFFF",
×
UNCOV
95
    },
×
UNCOV
96
    "& .MuiMenuItem-root.Mui-selected": {
×
UNCOV
97
      backgroundColor: "#3E7E6D",
×
UNCOV
98
      color: "#FFFFFF",
×
UNCOV
99
    },
×
UNCOV
100
    "& .MuiMenuItem-root:hover": {
×
UNCOV
101
      background: "#3E7E6D",
×
UNCOV
102
      color: "#FFFFFF",
×
UNCOV
103
    },
×
UNCOV
104
    "& .MuiMenuItem-root.Mui-focused": {
×
UNCOV
105
      backgroundColor: "#3E7E6D !important",
×
UNCOV
106
      color: "#FFFFFF",
×
UNCOV
107
    },
×
UNCOV
108
  },
×
UNCOV
109
  "& .MuiInputBase-input": {
×
UNCOV
110
    backgroundColor: "#fff",
×
UNCOV
111
    color: "#083A50 !important",
×
UNCOV
112
    fontWeight: 400,
×
UNCOV
113
    fontSize: "16px",
×
UNCOV
114
    fontFamily: "'Nunito', 'Rubik', sans-serif",
×
UNCOV
115
    lineHeight: "19.6px",
×
UNCOV
116
    padding: "12px",
×
UNCOV
117
    height: "20px !important",
×
UNCOV
118
    minHeight: "20px !important",
×
UNCOV
119
    "&::placeholder": {
×
UNCOV
120
      color: "#87878C",
×
UNCOV
121
      fontWeight: 400,
×
UNCOV
122
      opacity: 1,
×
UNCOV
123
    },
×
UNCOV
124
  },
×
125
  // Target readOnly <input> inputs
UNCOV
126
  "& .Mui-readOnly.MuiOutlinedInput-input:read-only": {
×
UNCOV
127
    backgroundColor: "#E5EEF4",
×
UNCOV
128
    color: "#083A50",
×
UNCOV
129
    cursor: "not-allowed",
×
UNCOV
130
    borderRadius: "8px",
×
UNCOV
131
  },
×
132
}));
1✔
133

134
const StyledHelperText = styled(FormHelperText)(() => ({
1✔
UNCOV
135
  marginTop: "4px",
×
UNCOV
136
  minHeight: "20px",
×
137
}));
1✔
138

139
type Props = {
140
  value: string | string[];
141
  options: SelectOption[];
142
  name?: string;
143
  label: string;
144
  required?: boolean;
145
  helpText?: string;
146
  tooltipText?: string | ReactNode;
147
  gridWidth?: 2 | 4 | 6 | 8 | 10 | 12;
148
  onChange?: (value: string | string[]) => void;
149
  filter?: (input: string | string[]) => string | string[];
150
} & Omit<SelectProps, "onChange">;
151

152
/**
153
 * Generates a generic select box with a label and help text
154
 *
155
 * @param {Props} props
156
 * @returns {JSX.Element}
157
 */
158
const SelectInput: FC<Props> = ({
1✔
UNCOV
159
  value,
×
UNCOV
160
  name,
×
UNCOV
161
  label,
×
UNCOV
162
  options,
×
UNCOV
163
  required = false,
×
UNCOV
164
  helpText,
×
UNCOV
165
  tooltipText,
×
UNCOV
166
  gridWidth,
×
UNCOV
167
  onChange,
×
UNCOV
168
  filter,
×
UNCOV
169
  multiple,
×
UNCOV
170
  placeholder,
×
UNCOV
171
  readOnly,
×
UNCOV
172
  ...rest
×
UNCOV
173
}) => {
×
174
  const id = rest.id || useId();
×
175

176
  const [val, setVal] = useState(multiple ? [] : "");
×
177
  const [error, setError] = useState(false);
×
178
  const [minWidth, setMinWidth] = useState<number | null>(null);
×
179
  const helperText = helpText || (required ? "This field is required" : " ");
×
180
  const selectRef = useRef(null);
×
181
  const inputRef = useRef(null);
×
182

183
  const processValue = (newValue: string | string[]) => {
×
184
    const inputIsArray = Array.isArray(newValue);
×
185
    if (multiple && !inputIsArray) {
×
186
      updateInputValidity(inputRef, "Please select at least one option");
×
187
    } else if (inputIsArray) {
×
188
      const containsOnlyValidOptions = newValue.every(
×
189
        (value: string) => !!options.find((option) => option.value === value)
×
UNCOV
190
      );
×
191
      updateInputValidity(
×
UNCOV
192
        inputRef,
×
UNCOV
193
        containsOnlyValidOptions ? "" : "Please select only valid options"
×
UNCOV
194
      );
×
195
    } else if (required && !options.findIndex((option) => option.value === newValue)) {
×
196
      updateInputValidity(inputRef, "Please select an entry from the list");
×
UNCOV
197
    } else {
×
198
      updateInputValidity(inputRef, "");
×
UNCOV
199
    }
×
200

201
    if (!newValue && multiple) {
×
202
      setVal([]);
×
203
      return;
×
UNCOV
204
    }
×
205

206
    setVal(newValue || "");
×
UNCOV
207
  };
×
208

209
  const onChangeWrapper = (newVal) => {
×
210
    let filteredVal = newVal;
×
211
    if (typeof filter === "function") {
×
212
      filteredVal = filter(newVal);
×
UNCOV
213
    }
×
214
    if (typeof onChange === "function") {
×
215
      onChange(filteredVal);
×
UNCOV
216
    }
×
217

218
    processValue(filteredVal);
×
219
    setError(false);
×
UNCOV
220
  };
×
221

222
  const handleOpen = () => {
×
223
    if (!selectRef.current) {
×
224
      return;
×
UNCOV
225
    }
×
226

227
    setMinWidth(selectRef.current.offsetWidth);
×
UNCOV
228
  };
×
229

230
  useEffect(() => {
×
231
    const invalid = () => setError(true);
×
232

233
    inputRef.current?.node?.addEventListener("invalid", invalid);
×
234
    return () => {
×
235
      inputRef.current?.node?.removeEventListener("invalid", invalid);
×
UNCOV
236
    };
×
UNCOV
237
  }, [inputRef]);
×
238

239
  useEffect(() => {
×
240
    processValue(value);
×
UNCOV
241
  }, [value]);
×
242

243
  return (
×
UNCOV
244
    <GridItem md={gridWidth || 6} xs={12} item>
×
UNCOV
245
      <FormControl fullWidth error={error}>
×
UNCOV
246
        <StyledFormLabel htmlFor={id} id={`${id}-label`}>
×
UNCOV
247
          {label}
×
UNCOV
248
          {required ? <StyledAsterisk>*</StyledAsterisk> : ""}
×
UNCOV
249
          {tooltipText && <Tooltip placement="right" title={tooltipText} />}
×
UNCOV
250
        </StyledFormLabel>
×
UNCOV
251
        <StyledSelect
×
UNCOV
252
          ref={selectRef}
×
UNCOV
253
          size="small"
×
UNCOV
254
          value={val}
×
255
          onChange={(e) => onChangeWrapper(e.target.value)}
×
UNCOV
256
          required={required}
×
UNCOV
257
          IconComponent={DropdownArrowsIcon}
×
UNCOV
258
          onOpen={handleOpen}
×
UNCOV
259
          MenuProps={{
×
UNCOV
260
            disablePortal: true,
×
UNCOV
261
            sx: { width: minWidth ? `${minWidth}px` : "auto" },
×
UNCOV
262
          }}
×
UNCOV
263
          slotProps={{ input: { id } }}
×
UNCOV
264
          multiple={multiple}
×
UNCOV
265
          placeholderText={placeholder}
×
UNCOV
266
          readOnly={readOnly}
×
UNCOV
267
          inputRef={inputRef}
×
UNCOV
268
          {...rest}
×
UNCOV
269
          id={id}
×
270
        >
UNCOV
271
          {options.map((option) => (
×
272
            <MenuItem key={option.value} value={option.value}>
×
UNCOV
273
              {option.label}
×
UNCOV
274
            </MenuItem>
×
UNCOV
275
          ))}
×
UNCOV
276
        </StyledSelect>
×
277

278
        {/* Proxy select for the form parser to correctly parse data if multiple attribute is on */}
UNCOV
279
        <ProxySelect
×
UNCOV
280
          name={name}
×
UNCOV
281
          value={val}
×
UNCOV
282
          onChange={() => {}}
×
UNCOV
283
          multiple={multiple}
×
UNCOV
284
          aria-labelledby={`${id}-label`}
×
UNCOV
285
          hidden
×
286
        >
UNCOV
287
          <option value="" aria-label="Empty" />
×
UNCOV
288
          {options.map((option) => (
×
289
            <option key={option.value} value={option.value} aria-label={`${option.value}`} />
×
UNCOV
290
          ))}
×
UNCOV
291
        </ProxySelect>
×
UNCOV
292
        <StyledHelperText>{!readOnly && error ? helperText : " "}</StyledHelperText>
×
UNCOV
293
      </FormControl>
×
UNCOV
294
    </GridItem>
×
295
  );
UNCOV
296
};
×
297

298
export default SelectInput;
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