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

code4recovery / tsml-ui / 19249631260

10 Nov 2025 11:35PM UTC coverage: 44.044% (+0.4%) from 43.641%
19249631260

Pull #487

github

web-flow
Merge a24a25ea9 into d06d75f3c
Pull Request #487: WIP: vite

418 of 1100 branches covered (38.0%)

Branch coverage included in aggregate %.

573 of 1150 relevant lines covered (49.83%)

4.26 hits per line

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

34.19
/src/components/Controls.tsx
1
import { FormEvent, MouseEvent, useEffect, useRef, useState } from 'react';
2

3
import { useNavigate } from 'react-router-dom';
4

5
import { analyticsEvent, formatSearch, formatUrl } from '../helpers';
6
import { useData, useInput, useSettings } from '../hooks';
7
import {
8
  controlsCss,
9
  controlsGroupFirstCss,
10
  controlsGroupLastCss,
11
  controlsInputCss,
12
  controlsInputFirstCss,
13
  controlsInputSearchSubmitCss,
14
  controlsSearchDropdownCss,
15
  dropdownButtonLastCss,
16
  dropdownCss,
17
} from '../styles';
18

19
import Dropdown from './Dropdown';
20

21
export default function Controls() {
22
  const { capabilities, meetings, slugs } = useData();
1✔
23
  const navigate = useNavigate();
1✔
24
  const { settings, strings } = useSettings();
1✔
25
  const [dropdown, setDropdown] = useState<string>();
1✔
26
  const { input } = useInput();
1✔
27
  const [search, setSearch] = useState(input.search);
1✔
28
  const searchInput = useRef<HTMLInputElement>(null);
1✔
29

30
  // get available search options based on capabilities
31
  const modes = settings.modes
1✔
32
    .filter(mode => mode !== 'location' || capabilities.coordinates)
6✔
33
    .filter(
34
      mode =>
35
        mode !== 'me' || (capabilities.coordinates && capabilities.geolocation)
4!
36
    );
37

38
  // get available dropdowns
39
  const dropdowns = settings.filters
1✔
40
    .filter(filter => capabilities[filter])
4✔
41
    .filter(filter => filter !== 'region' || input.mode === 'search');
×
42

43
  // get available views
44
  const views = settings.views.filter(
1✔
45
    view => view !== 'map' || capabilities.coordinates
4✔
46
  );
47

48
  // whether to show the views segmented button
49
  const canShowViews = views.length > 1;
1✔
50

51
  // add click listener for dropdowns (in lieu of including bootstrap js + jquery)
52
  useEffect(() => {
1✔
53
    document.body.addEventListener('click', closeDropdown);
1✔
54
    return () => {
1✔
55
      document.body.removeEventListener('click', closeDropdown);
×
56
    };
57
  }, [document]);
58

59
  // search effect
60
  useEffect(() => {
1✔
61
    const timer = setTimeout(() => {
1✔
62
      if (!input.search) return;
×
63
      analyticsEvent({
×
64
        category: 'search',
65
        action: input.mode,
66
        label: input.search,
67
      });
68
    }, 2000);
69
    return () => clearTimeout(timer);
1✔
70
  }, [input.search]);
71

72
  // update global state when search changes
73
  useEffect(() => {
1✔
74
    if (!searchInput.current) return;
1!
75

76
    if (input.mode !== 'search') return;
×
77

78
    const { value } = searchInput.current;
×
79

80
    if (value === input.search) return;
×
81

82
    navigate(formatUrl({ ...input, search: value }, settings));
×
83
  }, [searchInput.current?.value]);
84

85
  // update search when global state changes
86
  useEffect(() => {
1✔
87
    if (!searchInput.current || searchInput.current.value === input.search)
1!
88
      return;
1✔
89

90
    setSearch(input.search);
×
91
  }, [input.search]);
92

93
  // close current dropdown (on body click)
94
  const closeDropdown = () => {
1✔
95
    setDropdown(undefined);
×
96
  };
97

98
  // near location search
99
  const locationSearch = (e: FormEvent<HTMLFormElement>) => {
1✔
100
    e.preventDefault();
×
101

102
    if (input.mode !== 'location') return;
×
103

104
    if (!search) {
×
105
      slugs.forEach(slug => {
×
106
        meetings[slug].distance = undefined;
×
107
      });
108
    }
109

110
    navigate(formatUrl({ ...input, search }, settings));
×
111
  };
112

113
  // set search mode dropdown and reset distances
114
  const setMode = (e: MouseEvent, mode: 'search' | 'location' | 'me') => {
1✔
115
    e.preventDefault();
×
116

117
    slugs.forEach(slug => {
×
118
      meetings[slug].distance = undefined;
×
119
    });
120

121
    if (mode === 'me') {
×
122
      setSearch('');
×
123
    } else if (mode === 'location') {
×
124
      // sync local with state
125
      setSearch(input.search);
×
126
    }
127

128
    // focus after waiting for disabled to clear
129
    setTimeout(() => searchInput.current?.focus(), 100);
×
130

131
    const newInput = {
×
132
      ...input,
133
      distance:
134
        mode === 'search'
×
135
          ? undefined
136
          : input.distance ?? settings.distance_default,
×
137
      mode,
138
      region: mode === 'search' ? input.region : [],
×
139
      search,
140
    };
141

142
    navigate(formatUrl(newInput, settings));
×
143
  };
144

145
  return !slugs.length ? null : (
1!
146
    <div css={controlsCss}>
147
      <form onSubmit={locationSearch} css={dropdownCss}>
148
        <fieldset role="group">
149
          <input
150
            aria-label={strings.modes[input.mode]}
151
            css={modes.length > 1 ? controlsInputFirstCss : controlsInputCss}
×
152
            disabled={input.mode === 'me'}
153
            onChange={e => setSearch(formatSearch(e.target.value))}
×
154
            placeholder={strings.modes[input.mode]}
155
            ref={searchInput}
156
            spellCheck="false"
157
            type="search"
158
            value={search}
159
          />
160
          <input type="submit" hidden css={controlsInputSearchSubmitCss} />
161
          {modes.length > 1 && (
×
162
            <button
163
              aria-label={strings.modes[input.mode]}
164
              css={dropdownButtonLastCss}
165
              onClick={e => {
166
                setDropdown(dropdown === 'search' ? undefined : 'search');
×
167
                e.stopPropagation();
×
168
              }}
169
              type="button"
170
            />
171
          )}
172
        </fieldset>
173
        {modes.length > 1 && (
×
174
          <div
175
            css={controlsSearchDropdownCss}
176
            style={{
177
              display: dropdown === 'search' ? 'block' : 'none',
×
178
            }}
179
          >
180
            {modes.map(mode => (
181
              <div
×
182
                className="tsml-dropdown__item"
183
                data-active={input.mode === mode}
184
                key={mode}
185
              >
186
                <button
187
                  className="tsml-dropdown__button"
188
                  onClick={e => setMode(e, mode)}
×
189
                >
190
                  {strings.modes[mode]}
191
                </button>
192
              </div>
193
            ))}
194
          </div>
195
        )}
196
      </form>
197
      {input.mode !== 'search' && (
×
198
        <Dropdown
199
          defaultValue={strings.distance_any}
200
          filter="distance"
201
          open={dropdown === 'distance'}
202
          setDropdown={setDropdown}
203
        />
204
      )}
205
      {dropdowns.map(filter => (
206
        <div key={filter}>
×
207
          <Dropdown
208
            defaultValue={
209
              strings[`${filter}_any` as keyof typeof strings] as string
210
            }
211
            filter={filter}
212
            open={dropdown === filter}
213
            setDropdown={setDropdown}
214
          />
215
        </div>
216
      ))}
217
      {canShowViews && (
×
218
        <div role="group">
219
          {views.map((view, index) => (
220
            <button
×
221
              css={index ? controlsGroupLastCss : controlsGroupFirstCss}
×
222
              data-active={input.view === view}
223
              key={view}
224
              onClick={() => navigate(formatUrl({ ...input, view }, settings))}
×
225
              type="button"
226
            >
227
              {strings.views[view]}
228
            </button>
229
          ))}
230
        </div>
231
      )}
232
    </div>
233
  );
234
}
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