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

zendeskgarden / react-components / 16379365509

18 Jul 2025 08:06PM CUT coverage: 95.543%. First build
16379365509

Pull #2006

github

web-flow
Merge bfa591438 into 968211eae
Pull Request #2006: chore(deps): update dependency eslint-config-prettier to v10

3564 of 3984 branches covered (89.46%)

Branch coverage included in aggregate %.

9747 of 9948 relevant lines covered (97.98%)

234.99 hits per line

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

52.24
/packages/dropdowns.legacy/src/elements/Select/Select.tsx
1
/**
2
 * Copyright Zendesk, Inc.
3
 *
4
 * Use of this source code is governed under the Apache License, Version 2.0
5
 * found at http://www.apache.org/licenses/LICENSE-2.0.
6
 */
7

8
import React, { useRef, useState, useEffect, useCallback } from 'react';
21✔
9
import { composeEventHandlers, KEY_CODES } from '@zendeskgarden/container-utilities';
21✔
10
import Chevron from '@zendeskgarden/svg-icons/src/16/chevron-down-stroke.svg';
21✔
11
import { VALIDATION } from '@zendeskgarden/react-forms';
21✔
12
import PropTypes from 'prop-types';
21✔
13
import { Reference } from 'react-popper';
21✔
14
import { mergeRefs } from 'react-merge-refs';
21✔
15
import { ISelectProps } from '../../types';
16
import { StyledFauxInput, StyledInput, StyledSelect } from '../../styled';
21✔
17
import useDropdownContext from '../../utils/useDropdownContext';
21✔
18
import useFieldContext from '../../utils/useFieldContext';
21✔
19

20
/**
21
 * @deprecated use `@zendeskgarden/react-dropdowns` Combobox instead
22
 *
23
 * @extends HTMLAttributes<HTMLDivElement>
24
 */
25
export const Select = React.forwardRef<HTMLDivElement, ISelectProps>(
21✔
26
  ({ children, start, ...props }, ref) => {
27
    const {
28
      popperReferenceElementRef,
29
      itemSearchRegistry,
30
      downshift: {
31
        getToggleButtonProps,
32
        getInputProps,
33
        isOpen,
34
        highlightedIndex,
35
        setHighlightedIndex,
36
        selectItemAtIndex,
37
        closeMenu
38
      }
39
    } = useDropdownContext();
63✔
40
    const { isLabelHovered } = useFieldContext();
63✔
41
    const [isHovered, setIsHovered] = useState(false);
63✔
42
    const [isFocused, setIsFocused] = useState(false);
63✔
43
    const hiddenInputRef = useRef<HTMLInputElement>();
63✔
44
    const triggerRef = useRef<HTMLDivElement>();
63✔
45
    const previousIsOpenRef = useRef<boolean | undefined>(undefined);
63✔
46
    const [searchString, setSearchString] = useState('');
63✔
47
    const searchTimeoutRef = useRef<number>();
63✔
48
    const currentSearchIndexRef = useRef<number>(0);
63✔
49

50
    useEffect(() => {
63✔
51
      // Focus internal input when Menu is opened
52
      if (hiddenInputRef.current && isOpen && !previousIsOpenRef.current) {
29✔
53
        hiddenInputRef.current.focus();
10✔
54
      }
55

56
      // Focus trigger when Menu is closed
57
      if (triggerRef.current && !isOpen && previousIsOpenRef.current) {
29✔
58
        triggerRef.current.focus();
2✔
59
      }
60

61
      previousIsOpenRef.current = isOpen;
29✔
62
    }, [isOpen, triggerRef]);
63

64
    /**
65
     * Handle timeouts for clearing search text
66
     */
67
    useEffect(() => {
63✔
68
      // Cancel existing timeout if keys are pressed rapidly
69
      if (searchTimeoutRef.current) {
17!
70
        clearTimeout(searchTimeoutRef.current);
×
71
      }
72

73
      // Reset search string after delay
74
      searchTimeoutRef.current = window.setTimeout(() => {
17✔
75
        setSearchString('');
×
76
      }, 500);
77

78
      return () => {
17✔
79
        clearTimeout(searchTimeoutRef.current);
17✔
80
      };
81
    }, [searchString]);
82

83
    /**
84
     * Search item value registry based around current highlight bounds
85
     */
86
    const searchItems = useCallback(
63✔
87
      (searchValue: string, startIndex: number, endIndex: number) => {
88
        for (let index = startIndex; index < endIndex; index++) {
×
89
          const itemTextValue = itemSearchRegistry.current[index];
×
90

91
          if (
×
92
            itemTextValue &&
×
93
            itemTextValue.toUpperCase().indexOf(searchValue.toUpperCase()) === 0
94
          ) {
95
            return index;
×
96
          }
97
        }
98

99
        return undefined;
×
100
      },
101
      [itemSearchRegistry]
102
    );
103

104
    const onInputKeyDown = useCallback(
63✔
105
      (e: React.KeyboardEvent) => {
106
        if (e.keyCode === KEY_CODES.SPACE) {
×
107
          // Prevent space from closing Menu only if used with existing search value
108
          if (searchString) {
×
109
            e.preventDefault();
×
110
            e.stopPropagation();
×
111
          } else if (highlightedIndex !== null && highlightedIndex !== undefined) {
×
112
            e.preventDefault();
×
113
            e.stopPropagation();
×
114

115
            selectItemAtIndex(highlightedIndex);
×
116
            closeMenu();
×
117
          }
118
        }
119

120
        // Only search with alphanumeric keys
121
        if (
×
122
          (e.keyCode < 48 || e.keyCode > 57) &&
×
123
          (e.keyCode < 65 || e.keyCode > 90) &&
124
          e.keyCode !== KEY_CODES.SPACE
125
        ) {
126
          return;
×
127
        }
128

129
        const character = String.fromCharCode(e.which || e.keyCode);
×
130

131
        if (!character || character.length === 0) {
×
132
          return;
×
133
        }
134

135
        // Reset starting search index after delay has removed previous values
136
        if (!searchString) {
×
137
          if (highlightedIndex === null || highlightedIndex === undefined) {
×
138
            currentSearchIndexRef.current = -1;
×
139
          } else {
140
            currentSearchIndexRef.current = highlightedIndex;
×
141
          }
142
        }
143

144
        const newSearchString = searchString + character;
×
145

146
        setSearchString(newSearchString);
×
147

148
        let matchingIndex = searchItems(
×
149
          newSearchString,
150
          currentSearchIndexRef.current + 1,
151
          itemSearchRegistry.current.length
152
        );
153

154
        if (matchingIndex === undefined) {
×
155
          matchingIndex = searchItems(newSearchString, 0, currentSearchIndexRef.current);
×
156
        }
157

158
        if (matchingIndex !== undefined) {
×
159
          setHighlightedIndex(matchingIndex);
×
160
        }
161
      },
162
      [
163
        searchString,
164
        searchItems,
165
        itemSearchRegistry,
166
        highlightedIndex,
167
        selectItemAtIndex,
168
        closeMenu,
169
        setHighlightedIndex
170
      ]
171
    );
172

173
    /**
174
     * Destructure type out of props so that `type="button"`
175
     * is not spread onto the Select Dropdown `div`.
176
     */
177
    /* eslint-disable @typescript-eslint/no-unused-vars */
178
    const { type, ...selectProps } = getToggleButtonProps({
63✔
179
      tabIndex: props.disabled ? undefined : 0,
63✔
180
      onMouseEnter: composeEventHandlers(props.onMouseEnter, () => setIsHovered(true)),
7✔
181
      onMouseLeave: composeEventHandlers(props.onMouseLeave, () => setIsHovered(false)),
×
182
      onFocus: composeEventHandlers(props.onFocus, () => setIsFocused(true)),
19✔
183
      onBlur: composeEventHandlers(props.onBlur, () => setIsFocused(false)),
9✔
184
      ...props
185
    } as any);
186

187
    const isContainerHovered = isLabelHovered && !isOpen;
63✔
188
    const isContainerFocused = isFocused || isOpen;
63✔
189

190
    return (
63✔
191
      <Reference>
192
        {({ ref: popperReference }) => (
193
          <StyledFauxInput
54✔
194
            data-test-is-open={isOpen}
195
            data-test-is-hovered={isContainerHovered}
196
            data-test-is-focused={isOpen}
197
            isHovered={isContainerHovered}
198
            isFocused={isContainerFocused}
199
            {...selectProps}
200
            role="none"
201
            ref={selectRef => {
202
              // Pass ref to popperJS for positioning
203
              (popperReference as any)(selectRef);
108✔
204

205
              // Store ref locally to return focus on close
206
              // Apply Select ref to global Dropdown context
207
              mergeRefs([triggerRef, ref, popperReferenceElementRef])(selectRef);
108✔
208
            }}
209
          >
210
            {!!start && (
55✔
211
              <StyledFauxInput.StartIcon
212
                isHovered={isHovered || (isLabelHovered && !isOpen)}
2!
213
                isFocused={isContainerFocused}
214
                isDisabled={props.disabled}
215
              >
216
                {start}
217
              </StyledFauxInput.StartIcon>
218
            )}
219
            <StyledSelect>{children}</StyledSelect>
220
            <StyledInput
221
              {...getInputProps({
222
                readOnly: true,
223
                $isHidden: true,
224
                tabIndex: -1,
225
                ref: hiddenInputRef,
226
                value: '',
227
                onClick: (e: any) => {
228
                  if (isOpen) {
1!
229
                    (e.nativeEvent as any).preventDownshiftDefault = true;
×
230
                  }
231
                },
232
                onKeyDown: onInputKeyDown
233
              } as any)}
234
            />
235
            {!props.isBare && (
108✔
236
              <StyledFauxInput.EndIcon
237
                data-test-id="select-icon"
238
                isHovered={isHovered || (isLabelHovered && !isOpen)}
88✔
239
                isFocused={isContainerFocused}
240
                isDisabled={props.disabled}
241
                isRotated={isOpen}
242
              >
243
                <Chevron />
244
              </StyledFauxInput.EndIcon>
245
            )}
246
          </StyledFauxInput>
247
        )}
248
      </Reference>
249
    );
250
  }
251
);
252

253
Select.displayName = 'Select';
21✔
254

255
Select.propTypes = {
21✔
256
  isCompact: PropTypes.bool,
257
  isBare: PropTypes.bool,
258
  disabled: PropTypes.bool,
259
  focusInset: PropTypes.bool,
260
  validation: PropTypes.oneOf(VALIDATION),
261
  start: PropTypes.any
262
};
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

© 2025 Coveralls, Inc