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

code4recovery / tsml-ui / 18624750993

19 Oct 2025 03:25AM UTC coverage: 42.456% (-0.6%) from 43.052%
18624750993

push

github

web-flow
Fix infinite spinner on bookmarked location searches (#482)

* centralize location services in new LocationProvider

centralize location services in new LocationProvider

rebase

fix type safety

* wrap calculateDistances in useCallback to prevent unnecessary re-renders

367 of 1028 branches covered (35.7%)

Branch coverage included in aggregate %.

9 of 79 new or added lines in 8 files covered. (11.39%)

5 existing lines in 4 files now uncovered.

556 of 1146 relevant lines covered (48.52%)

4.18 hits per line

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

0.0
/src/components/Dropdown.tsx
1
import {
2
  Dispatch,
3
  Fragment,
4
  MouseEvent,
5
  SetStateAction,
6
  useState,
7
} from 'react';
8

9
import { useNavigate } from 'react-router-dom';
10
import { formatUrl, getIndexByKey, formatString as i18n } from '../helpers';
11
import { type Data, useData, useInput, useLocation, useSettings } from '../hooks';
12
import { dropdownButtonCss, dropdownCss } from '../styles';
13
import type { Index } from '../types';
14

15
export default function Dropdown({
16
  defaultValue,
17
  filter,
18
  open,
19
  setDropdown,
20
}: {
21
  defaultValue: string;
22
  filter: keyof Data['indexes'];
23
  open: boolean;
24
  setDropdown: Dispatch<SetStateAction<string | undefined>>;
25
}) {
26
  const { indexes } = useData();
×
27
  const navigate = useNavigate();
×
28
  const { settings, strings } = useSettings();
×
NEW
29
  const { input } = useInput();
×
NEW
30
  const { waitingForLocation } = useLocation();
×
UNCOV
31
  const options = indexes[filter];
×
32
  const values =
33
    filter === 'distance'
×
34
      ? input.distance
×
35
        ? [`${input.distance}`]
36
        : []
37
      : (input[filter as keyof typeof input] as string[]);
38
  const [expanded, setExpanded] = useState<string[]>([]);
×
39

40
  // handle expand toggle
41
  const toggleExpanded = (e: MouseEvent<HTMLButtonElement>, key: string) => {
×
42
    e.preventDefault();
×
43
    e.stopPropagation();
×
44
    if (!expanded.includes(key)) {
×
45
      setExpanded(expanded.concat(key));
×
46
    } else {
47
      setExpanded(expanded.filter(item => item !== key));
×
48
    }
49
  };
50

51
  // set filter: pass it up to parent
52
  const setFilter = (
×
53
    e: MouseEvent<HTMLButtonElement>,
54
    filter: keyof typeof indexes,
55
    value?: string
56
  ) => {
57
    e.preventDefault();
×
58

59
    if (filter === 'distance') {
×
60
      navigate(
×
61
        formatUrl(
62
          { ...input, distance: value ? parseInt(value) : undefined },
×
63
          settings
64
        )
65
      );
66
    } else {
67
      // add or remove from filters
68
      let currentValues = input[filter] as string[];
×
69

70
      if (value) {
×
71
        const index = currentValues.indexOf(value);
×
72
        if (e.metaKey || e.ctrlKey) {
×
73
          if (index === -1) {
×
74
            currentValues.push(value);
×
75
            currentValues.sort();
×
76
          } else {
77
            // Remove the value
78
            currentValues.splice(index, 1);
×
79
          }
80
        } else {
81
          // Single value, directly set the value
82
          currentValues = [value];
×
83
        }
84
      } else {
85
        // Remove the filter from search params if no value is provided
86
        currentValues = [];
×
87
      }
88
      navigate(formatUrl({ ...input, [filter]: currentValues }, settings));
×
89
    }
90
  };
91

92
  const renderDropdownItem = (
×
93
    { key, name, slugs, children }: Index,
94
    parentExpanded: boolean = true
×
95
  ) => {
96
    return !slugs.length ? null : (
×
97
      <Fragment key={key}>
98
        <div className="tsml-dropdown__item" data-active={values.includes(key)}>
99
          <button
100
            className="tsml-dropdown__button"
101
            onClick={e => setFilter(e, filter, key)}
×
102
            tabIndex={parentExpanded ? 0 : -1}
×
103
          >
104
            <span>{name}</span>
105
            <span
106
              aria-label={
107
                slugs.length === 1
×
108
                  ? strings.match_single
109
                  : i18n(strings.match_multiple, {
110
                      count: slugs.length,
111
                    })
112
              }
113
            >
114
              {slugs.length}
115
            </span>
116
          </button>
117
          {!!children?.length && (
×
118
            <button
119
              className="tsml-dropdown__expand"
120
              data-expanded={expanded.includes(key)}
121
              onClick={e => toggleExpanded(e, key)}
×
122
              aria-label={
123
                expanded.includes(key) ? strings.collapse : strings.expand
×
124
              }
125
            />
126
          )}
127
        </div>
128
        {!!children?.length && (
×
129
          <div
130
            className="tsml-dropdown__children"
131
            data-expanded={expanded.includes(key)}
132
          >
133
            {children.map(child =>
134
              renderDropdownItem(child, expanded.includes(key))
×
135
            )}
136
          </div>
137
        )}
138
      </Fragment>
139
    );
140
  };
141

142
  // separate section above the other items
143
  const special = {
×
144
    type: ['active', 'in-person', 'online'],
145
  };
146

147
  return (
×
148
    <div css={dropdownCss}>
149
      <button
150
        aria-expanded={open}
151
        css={dropdownButtonCss}
152
        disabled={filter === 'distance' && waitingForLocation}
×
153
        id={filter}
154
        onClick={e => {
155
          setDropdown(open ? undefined : filter);
×
156
          e.stopPropagation();
×
157
        }}
158
      >
159
        {values?.length && options?.length
×
160
          ? values.map(value => getIndexByKey(options, value)?.name).join(' + ')
×
161
          : defaultValue}
162
      </button>
163
      <div
164
        aria-labelledby={filter}
165
        className="tsml-dropdown"
166
        style={{ display: open ? 'block' : 'none' }}
×
167
      >
168
        <div data-active={!values.length} className="tsml-dropdown__item">
169
          <button
170
            className="tsml-dropdown__button"
171
            onClick={e => setFilter(e, filter, undefined)}
×
172
          >
173
            {defaultValue}
174
          </button>
175
        </div>
176
        {[
177
          options
178
            ?.filter(option =>
179
              special[filter as keyof typeof special]?.includes(option.key)
×
180
            )
181
            .sort(
182
              (a, b) =>
183
                special[filter as keyof typeof special]?.indexOf(a.key) -
×
184
                special[filter as keyof typeof special]?.indexOf(b.key)
185
            ),
186
          options?.filter(
187
            option =>
188
              !special[filter as keyof typeof special]?.includes(option.key)
×
189
          ),
190
        ]
191
          .filter(e => e.length)
×
192
          .map((group, index) => (
193
            <Fragment key={index}>
×
194
              <hr />
195
              {group.map(option => renderDropdownItem(option))}
×
196
            </Fragment>
197
          ))}
198
      </div>
199
    </div>
200
  );
201
}
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