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

yext / search-ui-react / 23466000706

23 Mar 2026 11:52PM UTC coverage: 84.783% (-0.003%) from 84.786%
23466000706

push

github

anguyen-yext2
preserve all MapCenter and MaoBounds instance methods

1066 of 1474 branches covered (72.32%)

Branch coverage included in aggregate %.

2327 of 2528 relevant lines covered (92.05%)

126.81 hits per line

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

88.44
/src/components/Dropdown/Dropdown.tsx
1
import { useTranslation } from 'react-i18next';
8✔
2
import React, {
8✔
3
  createElement,
4
  isValidElement,
5
  KeyboardEvent,
6
  PropsWithChildren,
7
  ReactNode,
8
  useEffect,
9
  useMemo,
10
  useRef,
11
  useState
12
} from 'react';
13
import { DropdownContext, DropdownContextType } from './DropdownContext';
8✔
14
import { InputContext, InputContextType } from './InputContext';
8✔
15
import useRootClosePkg from '@restart/ui/useRootClose';
8✔
16
import { FocusContext, FocusContextType } from './FocusContext';
8✔
17
import { ScreenReader } from '../ScreenReader';
8✔
18
import { recursivelyMapChildren } from '../utils/recursivelyMapChildren';
8✔
19
import { DropdownItem, DropdownItemProps, DropdownItemWithIndex } from './DropdownItem';
8✔
20
import { useLayoutEffect } from '../../hooks/useLayoutEffect';
8✔
21
import { useId } from '../../hooks/useId';
8✔
22

23
const useRootClose = typeof useRootClosePkg === 'function' ? useRootClosePkg : useRootClosePkg['default'];
20!
24

25
interface DropdownItemData {
26
  value: string,
27
  itemData?: Record<string, unknown>
28
}
29

30
export interface DropdownProps {
31
  screenReaderText: string,
32
  screenReaderInstructions?: string,
33
  parentQuery?: string,
34
  onSelect?: (value: string, index: number, focusedItemData: Record<string, unknown> | undefined) => void,
35
  onToggle?: (
36
    isActive: boolean,
37
    prevValue: string,
38
    value: string,
39
    index: number,
40
    focusedItemData: Record<string, unknown> | undefined
41
  ) => void,
42
  className?: string,
43
  activeClassName?: string,
44
  alwaysSelectOption?: boolean
45
}
46

47
/**
48
 * Dropdown is the parent component for a set of Dropdown-related components.
49
 *
50
 * @remarks
51
 * It provides multiple shared contexts, which are consumed by its child components,
52
 * and also registers some global event listeners.
53
 */
54
export function Dropdown(props: PropsWithChildren<DropdownProps>): React.JSX.Element {
8✔
55
  const { t } = useTranslation();
989✔
56
  const {
57
    children,
58
    screenReaderText,
59
    screenReaderInstructions,
60
    onSelect,
61
    onToggle,
62
    className,
63
    activeClassName,
64
    parentQuery,
65
    alwaysSelectOption = false
527✔
66
  } = props;
989✔
67

68
  const containerRef = useRef<HTMLDivElement>(null);
989✔
69
  const screenReaderUUID = useId('dropdown');
989✔
70
  const dropdownListUUID = useId('dropdown-list');
989✔
71
  const [screenReaderKey, setScreenReaderKey] = useState<number>(0);
989✔
72
  const [hasTyped, setHasTyped] = useState<boolean>(false);
989✔
73
  const [childrenWithDropdownItemsTransformed, items] = useMemo(() => {
989✔
74
    return getTransformedChildrenAndItemData(children);
694✔
75
  }, [children]);
76

77
  const inputContext = useInputContextInstance();
989✔
78
  const { value, setValue, lastTypedOrSubmittedValue, setLastTypedOrSubmittedValue } = inputContext;
989✔
79

80
  const focusContext = useFocusContextInstance(
626✔
81
    items,
82
    lastTypedOrSubmittedValue,
83
    setValue,
84
    setScreenReaderKey,
85
    alwaysSelectOption
86
  );
87
  const { focusedIndex, focusedItemData, updateFocusedItem } = focusContext;
989✔
88

89
  const dropdownContext = useDropdownContextInstance(
626✔
90
    lastTypedOrSubmittedValue,
91
    value,
92
    focusedIndex,
93
    focusedItemData,
94
    screenReaderUUID,
95
    dropdownListUUID,
96
    setHasTyped,
97
    onToggle,
98
    onSelect
99
  );
100
  const { toggleDropdown, isActive } = dropdownContext;
989✔
101

102
  useLayoutEffect(() => {
626✔
103
    if (parentQuery !== undefined && parentQuery !== lastTypedOrSubmittedValue) {
564!
104
      setLastTypedOrSubmittedValue(parentQuery);
22✔
105
      updateFocusedItem(-1, parentQuery);
22✔
106
    }
107
  }, [
108
    parentQuery,
109
    lastTypedOrSubmittedValue,
110
    updateFocusedItem,
111
    setLastTypedOrSubmittedValue
112
  ]);
113

114
  useRootClose(containerRef as React.RefObject<Element>, () => {
626✔
115
    toggleDropdown(false);
4✔
116
  }, { disabled: !isActive });
117

118
  function handleKeyDown(e: KeyboardEvent<HTMLDivElement>) {
119
    if (!isActive) {
224!
120
      return;
×
121
    }
122

123
    if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
224✔
124
      e.preventDefault();
10✔
125
    }
126

127
    if (e.key === 'ArrowDown') {
224✔
128
      if (alwaysSelectOption && focusedIndex === items.length - 1) {
10!
129
        updateFocusedItem(0);
×
130
      } else {
131
        updateFocusedItem(focusedIndex + 1);
10✔
132
      }
133
    } else if (e.key === 'ArrowUp') {
214!
134
      if (alwaysSelectOption && focusedIndex === 0) {
1!
135
        updateFocusedItem(items.length - 1);
×
136
      } else {
137
        updateFocusedItem(focusedIndex - 1);
1✔
138
      }
139
    } else if (e.key === 'Tab' && !e.shiftKey) {
214!
140
      updateFocusedItem(-1);
1✔
141
      toggleDropdown(false);
1✔
142
    } else if (!hasTyped) {
214✔
143
      setHasTyped(true);
43✔
144
    }
145
  }
146

147
  return (
626✔
148
    <div ref={containerRef} className={isActive ? activeClassName : className} onKeyDown={handleKeyDown}>
989✔
149
      <DropdownContext.Provider value={dropdownContext}>
150
        <InputContext.Provider value={inputContext}>
151
          <FocusContext.Provider value={focusContext}>
152
            {childrenWithDropdownItemsTransformed}
153
          </FocusContext.Provider>
154
        </InputContext.Provider>
155
      </DropdownContext.Provider>
156

157
      <ScreenReader
158
        announcementKey={screenReaderKey}
159
        announcementText={isActive && (hasTyped || items.length || value) ? screenReaderText : ''}
2,942✔
160
        instructionsId={screenReaderUUID}
161
        instructions={screenReaderInstructions ?? t('dropDownScreenReaderInstructions')}
1,978✔
162
      />
163
    </div>
164
  );
165
}
166

167
function useInputContextInstance(): InputContextType {
168
  const [value, setValue] = useState('');
989✔
169
  const [lastTypedOrSubmittedValue, setLastTypedOrSubmittedValue] = useState('');
989✔
170
  return {
626✔
171
    value,
172
    setValue,
173
    lastTypedOrSubmittedValue,
174
    setLastTypedOrSubmittedValue
175
  };
176
}
177

178
function useFocusContextInstance(
179
  items: DropdownItemData[],
180
  lastTypedOrSubmittedValue: string,
181
  setValue: (newValue: string) => void,
182
  setScreenReaderKey: React.Dispatch<React.SetStateAction<number>>,
183
  alwaysSelectOption: boolean
184
): FocusContextType {
185
  const [focusedIndex, setFocusedIndex] = useState(-1);
989✔
186
  const [focusedValue, setFocusedValue] = useState<string | null>(null);
989✔
187
  const [focusedItemData, setFocusedItemData] = useState<Record<string, unknown> | undefined>(undefined);
989✔
188
  useEffect(() => {
626✔
189
    if (alwaysSelectOption) {
363✔
190
      if (items.length > 0) {
187✔
191
        const index = focusedIndex === -1 || focusedIndex >= items.length ? 0 : focusedIndex;
67✔
192
        setFocusedIndex(index);
56✔
193
        setFocusedValue(items[index].value);
56✔
194
        setFocusedItemData(items[index].itemData);
56✔
195
      } else {
196
        setFocusedIndex(-1);
131✔
197
        setFocusedValue(null);
131✔
198
        setFocusedItemData(undefined);
131✔
199
      }
200
    }
201
  }, [alwaysSelectOption, focusedIndex, items]);
202

203
  function updateFocusedItem(updatedFocusedIndex: number, value?: string) {
204
    const numItems = items.length;
546✔
205
    let updatedValue;
206
    if (updatedFocusedIndex === -1 || updatedFocusedIndex >= numItems || numItems === 0) {
273✔
207
      updatedValue = value ?? lastTypedOrSubmittedValue;
261✔
208
      if (alwaysSelectOption && numItems !== 0) {
261✔
209
        setFocusedIndex(0);
27✔
210
        setFocusedItemData(items[0].itemData);
27✔
211
        setScreenReaderKey(prev => prev + 1);
28✔
212
      } else {
213
        setFocusedIndex(-1);
260✔
214
        setFocusedItemData(undefined);
260✔
215
        setScreenReaderKey(prev => prev + 1);
482✔
216
      }
217
    } else if (updatedFocusedIndex < -1) {
24!
218
      const loopedAroundIndex = (numItems + updatedFocusedIndex + 1) % numItems;
×
219
      updatedValue = value ?? items[loopedAroundIndex].value;
×
220
      setFocusedIndex(loopedAroundIndex);
×
221
      setFocusedItemData(items[loopedAroundIndex].itemData);
×
222
    } else {
223
      updatedValue = value ?? items[updatedFocusedIndex].value;
24✔
224
      setFocusedIndex(updatedFocusedIndex);
24✔
225
      setFocusedItemData(items[updatedFocusedIndex].itemData);
24✔
226
    }
227
    setFocusedValue(updatedValue);
273✔
228
    setValue(alwaysSelectOption ? (value ?? lastTypedOrSubmittedValue) : updatedValue);
273✔
229
  }
230

231
  return {
626✔
232
    focusedIndex,
233
    focusedValue,
234
    focusedItemData,
235
    updateFocusedItem
236
  };
237
}
238

239
function useDropdownContextInstance(
240
  prevValue: string,
241
  value: string,
242
  index: number,
243
  focusedItemData: Record<string, unknown> | undefined,
244
  screenReaderUUID: string | undefined,
245
  dropdownListUUID: string | undefined,
246
  setHasTyped: (hasTyped: boolean) => void,
247
  onToggle?: (
248
    isActive: boolean,
249
    prevValue: string,
250
    value: string,
251
    index: number,
252
    focusedItemData: Record<string, unknown> | undefined
253
  ) => void,
254
  onSelect?: (value: string, index: number, focusedItemData: Record<string, unknown> | undefined) => void
255
): DropdownContextType {
256
  const [isActive, _toggleDropdown] = useState(false);
989✔
257
  const toggleDropdown = (willBeOpen: boolean) => {
989✔
258
    if (!willBeOpen) {
590✔
259
      setHasTyped(false);
397✔
260
    }
261
    _toggleDropdown(willBeOpen);
590✔
262
    onToggle?.(willBeOpen, prevValue, value, index, focusedItemData);
590✔
263
  };
264
  return {
626✔
265
    isActive,
266
    toggleDropdown,
267
    onSelect,
268
    screenReaderUUID,
269
    dropdownListUUID
270
  };
271
}
272

273
function getTransformedChildrenAndItemData(children: ReactNode): [ReactNode, DropdownItemData[]] {
274
  const items: DropdownItemData [] = [];
604✔
275
  const childrenWithDropdownItemsTransformed = recursivelyMapChildren(children, (child => {
604✔
276
    if (!(isValidElement(child) && child.type === DropdownItem)) {
3,498✔
277
      return child;
3,185✔
278
    }
279
    const props = child.props as DropdownItemProps;
481✔
280
    items.push({
441✔
281
      value: props.value,
282
      itemData: props.itemData
283
    });
284
    return createElement(DropdownItemWithIndex, { ...props, index: items.length - 1 });
441✔
285
  }));
286
  return [childrenWithDropdownItemsTransformed, items];
331✔
287
}
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