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

code4recovery / tsml-ui / 17359326775

31 Aug 2025 03:56PM UTC coverage: 62.306% (+2.7%) from 59.622%
17359326775

Pull #457

github

web-flow
Merge 68c6c3bc4 into 4ec9a9232
Pull Request #457: Refactor to use hooks

528 of 988 branches covered (53.44%)

Branch coverage included in aggregate %.

218 of 346 new or added lines in 17 files covered. (63.01%)

59 existing lines in 9 files now uncovered.

758 of 1076 relevant lines covered (70.45%)

6.32 hits per line

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

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

9
import { getIndexByKey, formatString as i18n } from '../helpers';
10
import { type Data, useData, useInput, useSettings } from '../hooks';
11
import { dropdownButtonCss, dropdownCss } from '../styles';
12
import type { Index } from '../types';
13

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

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

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

NEW
56
    if (filter === 'distance') {
×
NEW
57
      setInput(input => ({
×
58
        ...input,
59
        distance: value ? parseInt(value) : undefined,
×
60
      }));
61
    } else {
62
      // add or remove from filters
NEW
63
      let currentValues = input[filter] as string[];
×
64

NEW
65
      if (value) {
×
NEW
66
        const index = currentValues.indexOf(value);
×
NEW
67
        if (e.metaKey || e.ctrlKey) {
×
NEW
68
          if (index === -1) {
×
NEW
69
            currentValues.push(value);
×
NEW
70
            currentValues.sort();
×
71
          } else {
72
            // Remove the value
NEW
73
            currentValues.splice(index, 1);
×
74
          }
75
        } else {
76
          // Single value, directly set the value
NEW
77
          currentValues = [value];
×
78
        }
79
      } else {
80
        // Remove the filter from search params if no value is provided
NEW
81
        currentValues = [];
×
82
      }
NEW
83
      setInput(input => ({ ...input, [filter]: currentValues }));
×
84
    }
85
  };
86

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

137
  // separate section above the other items
138
  const special = {
8✔
139
    type: ['active', 'in-person', 'online'],
140
  };
141

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