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

code4recovery / tsml-ui / 17352278282

31 Aug 2025 04:01AM UTC coverage: 48.498% (-11.1%) from 59.622%
17352278282

Pull #457

github

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

379 of 988 branches covered (38.36%)

Branch coverage included in aggregate %.

171 of 346 new or added lines in 17 files covered. (49.42%)

100 existing lines in 10 files now uncovered.

622 of 1076 relevant lines covered (57.81%)

4.64 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 { 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
}) {
NEW
25
  const { indexes } = useData();
×
UNCOV
26
  const { strings } = useSettings();
×
NEW
27
  const { input, setInput, waitingForInput } = useInput();
×
NEW
28
  const options = indexes[filter];
×
29
  const values =
NEW
30
    filter === 'distance'
×
31
      ? input.distance
×
32
        ? [`${input.distance}`]
33
        : []
34
      : (input[filter as keyof typeof input] as string[]);
UNCOV
35
  const [expanded, setExpanded] = useState<string[]>([]);
×
36

37
  // handle expand toggle
UNCOV
38
  const toggleExpanded = (e: MouseEvent<HTMLButtonElement>, key: string) => {
×
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
UNCOV
49
  const setFilter = (
×
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

UNCOV
87
  const renderDropdownItem = (
×
88
    { key, name, slugs, children }: Index,
89
    parentExpanded: boolean = true
×
90
  ) => {
NEW
91
    return !slugs.length ? null : (
×
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}
×
98
          >
99
            <span>{name}</span>
100
            <span
101
              aria-label={
102
                slugs.length === 1
×
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 && (
×
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 && (
×
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
UNCOV
138
  const special = {
×
139
    type: ['active', 'in-person', 'online'],
140
  };
141

UNCOV
142
  return (
×
143
    <div css={dropdownCss}>
144
      <button
145
        aria-expanded={open}
146
        css={dropdownButtonCss}
147
        disabled={filter === 'distance' && waitingForInput}
×
148
        id={filter}
149
        onClick={e => {
UNCOV
150
          setDropdown(open ? undefined : filter);
×
UNCOV
151
          e.stopPropagation();
×
152
        }}
153
      >
154
        {values?.length && options?.length
×
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' }}
×
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 =>
UNCOV
174
              special[filter as keyof typeof special]?.includes(option.key)
×
175
            )
176
            .sort(
177
              (a, b) =>
UNCOV
178
                special[filter as keyof typeof special]?.indexOf(a.key) -
×
179
                special[filter as keyof typeof special]?.indexOf(b.key)
180
            ),
181
          options?.filter(
182
            option =>
UNCOV
183
              !special[filter as keyof typeof special]?.includes(option.key)
×
184
          ),
185
        ]
UNCOV
186
          .filter(e => e.length)
×
187
          .map((group, index) => (
UNCOV
188
            <Fragment key={index}>
×
189
              <hr />
UNCOV
190
              {group.map(option => renderDropdownItem(option))}
×
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