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

CBIIT / crdc-datahub-ui / 11074479132

27 Sep 2024 04:44PM UTC coverage: 44.982% (+26.5%) from 18.435%
11074479132

Pull #479

github

web-flow
Merge a0867d25a into 3d8b55818
Pull Request #479: 3.0.0 Release

1727 of 4418 branches covered (39.09%)

Branch coverage included in aggregate %.

2612 of 5228 relevant lines covered (49.96%)

128.96 hits per line

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

8.82
/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);
×
177
  const helperText = helpText || (required ? "This field is required" : " ");
×
178
  const inputRef = useRef(null);
×
179

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

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

203
    setVal(newValue || "");
×
204
  };
205

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

215
    processValue(filteredVal);
×
216
    setError(false);
×
217
  };
218

219
  useEffect(() => {
×
220
    const invalid = () => setError(true);
×
221

222
    inputRef.current?.node?.addEventListener("invalid", invalid);
×
223
    return () => {
×
224
      inputRef.current?.node?.removeEventListener("invalid", invalid);
×
225
    };
226
  }, [inputRef]);
227

228
  useEffect(() => {
×
229
    processValue(value);
×
230
  }, [value]);
231

232
  return (
×
233
    <GridItem md={gridWidth || 6} xs={12} item>
×
234
      <FormControl fullWidth error={error}>
235
        <StyledFormLabel htmlFor={id} id={`${id}-label`}>
236
          {label}
237
          {required ? <StyledAsterisk>*</StyledAsterisk> : ""}
×
238
          {tooltipText && <Tooltip placement="right" title={tooltipText} />}
×
239
        </StyledFormLabel>
240
        <StyledSelect
241
          size="small"
242
          value={val}
243
          onChange={(e) => onChangeWrapper(e.target.value)}
×
244
          required={required}
245
          IconComponent={DropdownArrowsIcon}
246
          MenuProps={{ disablePortal: true }}
247
          slotProps={{ input: { id } }}
248
          multiple={multiple}
249
          placeholderText={placeholder}
250
          readOnly={readOnly}
251
          inputRef={inputRef}
252
          {...rest}
253
          id={id}
254
        >
255
          {options.map((option) => (
256
            <MenuItem key={option.value} value={option.value}>
×
257
              {option.label}
258
            </MenuItem>
259
          ))}
260
        </StyledSelect>
261

262
        {/* Proxy select for the form parser to correctly parse data if multiple attribute is on */}
263
        <ProxySelect
264
          name={name}
265
          value={val}
266
          onChange={() => {}}
267
          multiple={multiple}
268
          aria-labelledby={`${id}-label`}
269
          hidden
270
        >
271
          {options.map((option) => (
272
            <option key={option.value} value={option.value} aria-label={`${option.value}`} />
×
273
          ))}
274
        </ProxySelect>
275
        <StyledHelperText>{!readOnly && error ? helperText : " "}</StyledHelperText>
×
276
      </FormControl>
277
    </GridItem>
278
  );
279
};
280

281
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