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

CBIIT / crdc-datahub-ui / 14916590230

08 May 2025 09:34PM UTC coverage: 62.426% (-0.06%) from 62.485%
14916590230

push

github

web-flow
Merge pull request #700 from CBIIT/CRDCDH-2585

CRDCDH-2585 Support system-managed programs

3461 of 5947 branches covered (58.2%)

Branch coverage included in aggregate %.

1 of 11 new or added lines in 4 files covered. (9.09%)

2 existing lines in 2 files now uncovered.

4783 of 7259 relevant lines covered (65.89%)

198.65 hits per line

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

7.96
/src/components/Questionnaire/SelectInput.tsx
1
import React, { FC, ReactNode, useEffect, useId, useRef, useState } from "react";
2
import {
3
  FormControl,
4
  FormHelperText,
5
  Grid,
6
  MenuItem,
7
  Select,
8
  SelectProps,
9
  styled,
10
} from "@mui/material";
11
import dropdownArrowsIcon from "../../assets/icons/dropdown_arrows.svg";
12
import Tooltip from "../Tooltip";
13
import { updateInputValidity } from "../../utils";
14

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

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

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

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

66
const ProxySelect = styled("select")(() => ({
2✔
67
  display: "none",
68
}));
69

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

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

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

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

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

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

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

205
    setVal(newValue || "");
×
206
  };
207

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

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

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

NEW
226
    setMinWidth(selectRef.current.offsetWidth);
×
227
  };
228

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

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

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

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

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

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