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

CBIIT / crdc-datahub-ui / 16006182009

01 Jul 2025 05:32PM UTC coverage: 62.703% (-8.6%) from 71.278%
16006182009

Pull #756

github

web-flow
Merge pull request #755 from CBIIT/revert-omb-date

revert: OMB expiration update
Pull Request #756: Sync 3.4.0 with 3.3.0

3560 of 6102 branches covered (58.34%)

Branch coverage included in aggregate %.

4920 of 7422 relevant lines covered (66.29%)

227.7 hits per line

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

3.88
/src/components/Questionnaire/CustomAutocomplete.tsx
1
import {
2
  AutocompleteChangeReason,
3
  AutocompleteProps,
4
  AutocompleteValue,
5
  FormControl,
6
  Grid,
7
  TextField,
8
  styled,
9
} from "@mui/material";
10
import { ReactNode, SyntheticEvent, useEffect, useId, useRef, useState } from "react";
11
import Tooltip from "../Tooltip";
12
import { updateInputValidity } from "../../utils";
13
import StyledLabel from "../StyledFormComponents/StyledLabel";
14
import StyledHelperText from "../StyledFormComponents/StyledHelperText";
15
import StyledAsterisk from "../StyledFormComponents/StyledAsterisk";
16
import StyledAutocomplete from "../StyledFormComponents/StyledAutocomplete";
17

18
const StyledFormControl = styled(FormControl)({
2✔
19
  height: "100%",
20
  justifyContent: "end",
21
  "& .MuiFormHelperText-root.Mui-error": {
22
    color: "#D54309 !important",
23
  },
24
});
25

26
const StyledTag = styled("div")({
2✔
27
  paddingLeft: "12px",
28
  position: "absolute",
29
});
30

31
const ProxySelect = styled("select")({
2✔
32
  display: "none",
33
});
34

35
export type CustomProps = {
36
  /**
37
   * The HTML form name attribute
38
   *
39
   * @note Used to parse the form data into a JSON object
40
   */
41
  name: string;
42
  /**
43
   * The label text for the input to display above the input field
44
   */
45
  label: string;
46
  /**
47
   * The value of the input field
48
   */
49
  value: string[];
50
  /**
51
   * The options to display in the input selection dropdown
52
   */
53
  options: string[];
54
  /**
55
   * The text to display in the placeholder when multiple values are selected
56
   */
57
  tagText: (value: string[]) => string;
58
  /**
59
   * The width of the input in the form grid
60
   */
61
  gridWidth?: 2 | 4 | 6 | 8 | 10 | 12;
62
  helpText?: string;
63
  tooltipText?: string | ReactNode;
64
  required?: boolean;
65
  validate?: (input: string | string[]) => boolean;
66
} & Omit<AutocompleteProps<string, boolean, true, true, "div">, "renderInput">;
67

68
/**
69
 * Provides a custom autocomplete input field with a label and helper text.  The primary focus is:
70
 *
71
 * - Disable the clear button
72
 * - Constrain the rendering of tags to a single line
73
 * - Sort the selected options above the unselected options after blur
74
 *
75
 * @note This component supports only string values currently
76
 * @param {CustomProps} props
77
 * @returns {JSX.Element}
78
 */
79
const CustomAutocomplete = ({
2✔
80
  tagText,
81
  name,
82
  label,
83
  gridWidth,
84
  helpText,
85
  tooltipText,
86
  required,
87
  value,
88
  onChange,
89
  options,
90
  validate,
91
  placeholder,
92
  readOnly,
93
  ...rest
94
}: CustomProps): JSX.Element => {
95
  const id = rest.id || useId();
×
96

97
  const [val, setVal] = useState<string[]>(value);
×
98
  const [error, setError] = useState<boolean>(false);
×
99
  const [hasFocus, setHasFocus] = useState<boolean>(false);
×
100
  const [separatedOptions, setSeparatedOptions] = useState<string[]>(options);
×
101

102
  const helperText = helpText || (required ? "This field is required" : " ");
×
103
  const inputRef = useRef<HTMLInputElement>(null);
×
104

105
  const processValue = (newValue: string[]) => {
×
106
    if (typeof validate === "function") {
×
107
      const customIsValid = validate(newValue);
×
108
      updateInputValidity(inputRef, !customIsValid ? helpText : "");
×
109
    } else if (required) {
×
110
      updateInputValidity(inputRef, !newValue ? helperText : "");
×
111
    }
112

113
    setVal(newValue);
×
114
  };
115

116
  const onChangeWrapper = (
×
117
    event: SyntheticEvent,
118
    newValue: AutocompleteValue<string[], false, false, false>,
119
    reason: AutocompleteChangeReason
120
  ): void => {
121
    if (typeof onChange === "function") {
×
122
      onChange(event, newValue, reason);
×
123
    }
124

125
    processValue(newValue);
×
126
    setError(false);
×
127
  };
128

129
  const handleInputBlur = () => {
×
130
    setHasFocus(false);
×
131
    sortOptions(true);
×
132
  };
133

134
  const sortOptions = (force = false) => {
×
135
    if (hasFocus && !force) {
×
136
      return;
×
137
    }
138

139
    const selectedOptions = val
×
140
      .filter((v) => options.includes(v))
×
141
      .sort((a, b) => a.localeCompare(b));
×
142
    const unselectedOptions = options.filter((o) => !selectedOptions.includes(o));
×
143

144
    setSeparatedOptions([...selectedOptions, ...unselectedOptions]);
×
145
  };
146

147
  useEffect(() => {
×
148
    const invalid = () => setError(true);
×
149

150
    inputRef.current?.addEventListener("invalid", invalid);
×
151
    return () => {
×
152
      inputRef.current?.removeEventListener("invalid", invalid);
×
153
    };
154
  }, [inputRef]);
155

156
  useEffect(() => {
×
157
    processValue(value);
×
158
  }, [value]);
159

160
  useEffect(() => {
×
161
    sortOptions();
×
162
  }, [options, val]);
163

164
  return (
×
165
    <Grid md={gridWidth || 6} xs={12} item>
×
166
      <StyledFormControl fullWidth error={error}>
167
        <StyledLabel htmlFor={id} id={`${id}-label`}>
168
          {label}
169
          {required ? <StyledAsterisk /> : ""}
×
170
          {tooltipText && <Tooltip placement="right" title={tooltipText} />}
×
171
        </StyledLabel>
172
        <StyledAutocomplete
173
          value={val}
174
          onChange={onChangeWrapper}
175
          options={separatedOptions}
176
          readOnly={readOnly}
177
          getOptionLabel={(option: string) => option}
×
178
          renderTags={(value: string[]) => {
179
            if (value?.length === 0 || hasFocus) {
×
180
              return null;
×
181
            }
182

183
            if (value.length === 1) {
×
184
              return <StyledTag>{value[0]}</StyledTag>;
×
185
            }
186

187
            return <StyledTag>{tagText(value)}</StyledTag>;
×
188
          }}
189
          renderInput={(params) => (
190
            <TextField
×
191
              {...params}
192
              inputRef={inputRef}
193
              placeholder={val?.length > 0 ? undefined : placeholder}
×
194
              id={id}
195
              onFocus={() => !readOnly && setHasFocus(true)}
×
196
              onBlur={handleInputBlur}
197
              onKeyDown={(event) => {
198
                // Prevent backspace from clearing input tags
199
                if (event.key === "Backspace") {
×
200
                  event.stopPropagation();
×
201
                }
202
              }}
203
            />
204
          )}
205
          {...rest}
206
        />
207
        <StyledHelperText>{!readOnly && error ? helperText : " "}</StyledHelperText>
×
208
      </StyledFormControl>
209
      <ProxySelect
210
        name={name}
211
        value={val}
212
        aria-labelledby={`${id}-label`}
213
        onChange={() => {}}
214
        multiple
215
        hidden
216
      >
217
        {val.map((v) => (
218
          <option key={v} value={v} aria-label={v} />
×
219
        ))}
220
      </ProxySelect>
221
    </Grid>
222
  );
223
};
224

225
export default CustomAutocomplete;
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