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

code4recovery / tsml-ui / 18451009991

12 Oct 2025 11:29PM UTC coverage: 43.532% (-19.9%) from 63.458%
18451009991

Pull #475

github

web-flow
Merge 9d0374e51 into 0a0ddf96f
Pull Request #475: pretty permalinks

369 of 1009 branches covered (36.57%)

Branch coverage included in aggregate %.

15 of 37 new or added lines in 5 files covered. (40.54%)

236 existing lines in 17 files now uncovered.

553 of 1109 relevant lines covered (49.86%)

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

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

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

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

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

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

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

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