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

yext / search-ui-react / 21048748271

15 Jan 2026 10:34PM UTC coverage: 85.208% (-0.08%) from 85.288%
21048748271

push

github

web-flow
Merge main (v2.0.2) into develop (#616)

* Support trusted publishing with OIDC (#558)

* Bump search-headless-react version (to be published as v1.10.0) (#561)

which includes change that supports facetAllowlist query parameter for vertical searches

* release: v1.10.0-beta.0

* release: v1.10.0

* drop node 16 from github workflows (#567)

* drop node 16 from github workflows

We are dropping node 16 support, and have removed it from
the related slapshot reusable workflows already. This PR
drops it from the github workflows for this repo
J=none
TEST=none

* Update snapshots

* Update snapshots

* Update snapshots

* Update snapshots

* Update snapshots

* Update snapshots

---------

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>

* v1.10.1 (#569)

* v1.10.1 (#565)

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

* feat: upgrade playwright version to avoid vuln

This PR ups the playwright version used to avoid a vuln

J=VULN-40423
TEST=compile

* Update snapshots

* Update snapshots

* Update snapshots

* Update snapshots

* Update snapshots

* remove version upgrade workflow

* remove version change

* Update snapshots

* Update snapshots

* Update snapshots

* Update snapshots

* Update snapshots

* Update snapshots

* Update snapshots

* Update snapshots

* Update snapshots

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

* release: v1.10.1

* Move map to search location when there are no results (v1.10.2) (#570)

J=WAT-5112
TEST=manual

---------

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>

* release: v1.10.2

* Fix orphaned ARIA references when collapsible=false (#579)

Fix originally made in #576 by @chrissnyder2337

When collapsible={false}, FilterGroup now renders a plain div instead of
... (continued)

1002 of 1391 branches covered (72.03%)

Branch coverage included in aggregate %.

86 of 97 new or added lines in 24 files covered. (88.66%)

8 existing lines in 1 file now uncovered.

2247 of 2422 relevant lines covered (92.77%)

149.47 hits per line

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

66.38
/src/components/MapboxMap.tsx
1
import React, { useRef, useEffect, useState, useCallback } from 'react';
6✔
2
import mapboxgl, { MarkerOptions } from 'mapbox-gl';
6✔
3
import { Result, useSearchState, SelectableStaticFilter } from '@yext/search-headless-react';
6✔
4
import { useDebouncedFunction } from '../hooks/useDebouncedFunction';
6✔
5
import _ from 'lodash';
6✔
6

7
import ReactDOM from 'react-dom';
6✔
8
// Try to statically import createRoot, will be undefined for <18.
9
import * as ReactDomClient from 'react-dom/client';
6✔
10

11
type LegacyReactDOM = {
12
  render?: (element: React.ReactElement, container: Element) => void,
13
  unmountComponentAtNode?: (container: Element | DocumentFragment) => boolean
14
};
15

16
type RootHandle = {
17
  render: (children: React.ReactNode) => void,
18
  unmount: () => void
19
};
20

21
const legacyReactDOM = ReactDOM as LegacyReactDOM;
10✔
22
const reactMajorVersion = Number(React.version.split('.')[0]);
10✔
23
const supportsCreateRoot = !Number.isNaN(reactMajorVersion) && reactMajorVersion >= 18;
10✔
24

25
/**
26
 * Props for rendering a custom marker on the map.
27
 *
28
 * @public
29
 */
30
export type PinComponentProps<T> = {
31
  /** The index of the pin. */
32
  index: number,
33
  /** The Mapbox map. */
34
  mapbox: mapboxgl.Map,
35
  /** The search result corresponding to the pin. */
36
  result: Result<T>,
37
  /** Where the pin is selected. */
38
  selected?: boolean
39
};
40

41
/**
42
 * A functional component that can be used to render a custom marker on the map.
43
 *
44
 * @public
45
 */
46
export type PinComponent<T> = (props: PinComponentProps<T>) => React.JSX.Element;
47

48
/**
49
 * A function use to derive a result's coordinate.
50
 *
51
 * @public
52
 */
53
export type CoordinateGetter<T> = (result: Result<T>) => Coordinate | undefined;
54

55
/**
56
 * Coordinate use to represent the result's location on a map.
57
 *
58
 * @public
59
 */
60
export interface Coordinate {
61
  /** The latitude of the location. */
62
  latitude: number,
63
  /** The longitude of the location. */
64
  longitude: number
65
}
66

67
/**
68
 * A function which is called when user drags or zooms the map.
69
 *
70
 * @public
71
 */
72
export type OnDragHandler = (center: mapboxgl.LngLat, bounds: mapboxgl.LngLatBounds) => void;
73

74
/**
75
 * Props for the {@link MapboxMap} component.
76
 * The type param "T" represents the type of "rawData" field of the results use in the map.
77
 *
78
 * @public
79
 */
80
export interface MapboxMapProps<T> {
81
  /** Mapbox access token. */
82
  mapboxAccessToken: string,
83
  /** Interface for map customization derived from Mapbox GL's Map options. */
84
  mapboxOptions?: Omit<mapboxgl.MapboxOptions, 'container'>,
85
  /**
86
   * Custom Pin component to render for markers on the map.
87
   * By default, the built-in marker image from Mapbox GL is used.
88
   * This prop should not be used with
89
   * {@link MapboxMapProps.renderPin | renderPin}. If both are provided,
90
   * only PinComponent will be used.
91
   */
92
  PinComponent?: PinComponent<T>,
93
  /**
94
   * Render function for a custom marker on the map. This function takes in an
95
   * HTML element and is responible for rendering the pin into that element,
96
   * which will be used as the marker.
97
   * By default, the built-in marker image from Mapbox GL is used.
98
   * This prop should not be used with
99
   * {@link MapboxMapProps.PinComponent | PinComponent}. If both are provided,
100
   * only PinComponent will be used.
101
   */
102
  renderPin?: (props: PinComponentProps<T> & { container: HTMLElement }) => void,
103
  /**
104
   * A function to derive a result's coordinate for the corresponding marker's location on the map.
105
   * By default, "yextDisplayCoordinate" field is used as the result's display coordinate.
106
   */
107
  getCoordinate?: CoordinateGetter<T>,
108
  /** {@inheritDoc OnDragHandler} */
109
  onDrag?: OnDragHandler,
110
  /**
111
   * The window object of the iframe where the map should rendered. Must have mapboxgl loaded.
112
   * If not provided or mapboxgl not loaded, the map will be rendered in the parent window.
113
   */
114
  iframeWindow?: Window,
115
  /**
116
   * If set to true, the map will update its options when the mapboxOptions prop changes.
117
   * Otherwise, the map will not update its options once initially set.
118
   */
119
  allowUpdates?: boolean,
120
  /** A function that handles a pin click event. */
121
  onPinClick?: (result: Result<T> | undefined) => void,
122
  /** The options to apply to the map markers based on whether it is selected. */
123
  markerOptionsOverride?: (selected: boolean) => MarkerOptions
124
}
125

126
/**
127
 * A component that renders a map with markers to show result locations using Mapbox GL.
128
 *
129
 * @remarks
130
 * For the map to work properly, be sure to include Mapbox GL stylesheet in the application.
131
 *
132
 * @example
133
 * For instance, user may add the following import statement in their application's index file
134
 * or in the file where `MapboxMap` is used:
135
 * `import 'mapbox-gl/dist/mapbox-gl.css';`
136
 *
137
 * Or, user may add a stylesheet link in their html page:
138
 * `<link href="https://api.mapbox.com/mapbox-gl-js/v2.9.2/mapbox-gl.css" rel="stylesheet" />`
139
 *
140
 * @param props - {@link MapboxMapProps}
141
 * @returns A React element containing a Mapbox Map
142
 *
143
 * @public
144
 */
145
export function MapboxMap<T>({
6✔
146
  mapboxAccessToken,
147
  mapboxOptions,
148
  PinComponent,
149
  renderPin,
150
  getCoordinate = getDefaultCoordinate,
18✔
151
  onDrag,
152
  iframeWindow,
153
  allowUpdates = false,
19✔
154
  onPinClick,
155
  markerOptionsOverride,
156
}: MapboxMapProps<T>): React.JSX.Element {
157
  const mapboxInstance = (iframeWindow as Window & { mapboxgl?: typeof mapboxgl })?.mapboxgl ?? mapboxgl;
19✔
158
  useEffect(() => {
13✔
159
    mapboxInstance.accessToken = mapboxAccessToken;
10✔
160
  }, [mapboxAccessToken, mapboxInstance]);
161

162
  const mapContainer = useRef<HTMLDivElement>(null);
19✔
163
  const map = useRef<mapboxgl.Map | null>(null);
19✔
164
  const markers = useRef<mapboxgl.Marker[]>([]);
19✔
165
  const markerRoots = useRef(new Map<HTMLElement, RootHandle>());
19✔
166
  const activeMarkerElements = useRef(new Set<HTMLElement>());
19✔
167
  const markerData = useRef<Array<{ marker: mapboxgl.Marker, result: Result<T>, index: number }>>([]);
19✔
168

169
  const locationResults = useSearchState(state => state.vertical.results) as Result<T>[];
19✔
170
  const staticFilters = useSearchState(state => state.filters?.static);
19✔
171
  const onDragDebounced = useDebouncedFunction(onDrag, 100);
19✔
172
  const [selectedResult, setSelectedResult] = useState<Result<T> | undefined>(undefined);
19✔
173

174
  const handlePinClick = useCallback((result: Result<T>) => {
19✔
175
    setSelectedResult(prev => prev === result ? undefined : result);
13!
176
  }, []);
177

178
  useEffect(() => {
13✔
179
    onPinClick?.(selectedResult);
13✔
180
  }, [onPinClick, selectedResult]);
181

182
  const cleanupPinComponent = useCallback((element: HTMLElement) => {
19✔
183
    activeMarkerElements.current.delete(element);
13✔
184
    if (supportsCreateRoot) {
13!
185
      const root = markerRoots.current.get(element);
13✔
186
      if (root) {
13!
187
        root.unmount();
13✔
188
        markerRoots.current.delete(element);
13✔
189
      }
190
    } else {
191
      legacyReactDOM.unmountComponentAtNode?.(element);
13✔
192
    }
193
  }, []);
194

195
  const attachPinComponent = useCallback((element: HTMLElement, component: React.JSX.Element) => {
19✔
196
    if (supportsCreateRoot && typeof ReactDomClient.createRoot === 'function') {
14!
197
      // Use React 18+ API
198
      let root = markerRoots.current.get(element);
9✔
199
      if (!root) {
14✔
200
        root = ReactDomClient.createRoot(element);
14✔
201
        markerRoots.current.set(element, root);
14✔
202
      }
203
      root.render(component);
14✔
204
    } else if (typeof legacyReactDOM.render === 'function') {
13!
205
      // Fallback for React <18
206
      legacyReactDOM.render(component, element);
13✔
207
    }
208
  }, []);
209

210
  const removeMarkers = useCallback(() => {
19✔
211
    markers.current.forEach(marker => {
24✔
212
      if (!marker) {
16!
213
        return;
16✔
214
      }
215
      const element = typeof marker.getElement === 'function' ? marker.getElement() : null;
13!
216
      if (element) {
13!
217
        cleanupPinComponent(element);
13✔
218
      }
219
      if (typeof marker.remove === 'function') {
13!
220
        marker.remove();
13✔
221
      }
222
    });
223
    markers.current = [];
24✔
224
    markerData.current = [];
24✔
225
  }, [cleanupPinComponent]);
226

227
  const locale = useSearchState(state => state.meta?.locale);
19✔
228
  // keep track of the previous value of mapboxOptions across renders
229
  const prevMapboxOptions = useRef(mapboxOptions);
19✔
230

231
  /**
232
   * Localizes Mapbox label text to a specific locale.
233
   *
234
   * Updates symbol layers that are place names such that labels prefer `name_<lang>`
235
   * (e.g. `name_fr`) and fall back to `name` when unavailable.
236
   *
237
   * Note:
238
   * - Symbol layers that are place names would have `text-field` properties that includes
239
   *   'name', which are localized.
240
   * - Other symbol layers (e.g. road shields, transit, icons) are left unchanged.
241
   */
242
  const localizeMap = useCallback(() => {
19✔
243
    const mapbox = map.current;
8✔
244
    if (!mapbox || !locale) return;
18✔
245

UNCOV
246
    const localizeLabels = () => {
×
UNCOV
247
      mapbox.getStyle().layers.forEach(layer => {
×
NEW
248
        if (layer.type !== 'symbol') {
×
UNCOV
249
          return;
×
250
        }
NEW
251
        const textField = layer.layout?.['text-field'];
×
NEW
252
        if (typeof textField === 'string'
×
253
          ? textField.includes('name')
254
          : (Array.isArray(textField) && JSON.stringify(textField).includes('name'))) {
×
UNCOV
255
          mapbox.setLayoutProperty(
×
256
            layer.id,
257
            'text-field',
258
            [
259
              'coalesce',
260
              ['get', `name_${getMapboxLanguage(locale)}`],
261
              ['get', 'name']
262
            ]
263
          );
264
        }
265
      });
266
    };
267

268
    if (mapbox.isStyleLoaded()) {
13!
269
      localizeLabels();
13✔
270
    } else {
271
      mapbox.once('styledata', () => localizeLabels());
13✔
272
    }
273
  }, [locale]);
274

275
  useEffect(() => {
13✔
276
    if (mapContainer.current) {
13✔
277
      if (map.current && allowUpdates) {
13!
278
        // Compare current and previous mapboxOptions using deep equality
279
        if (!_.isEqual(prevMapboxOptions.current, mapboxOptions)) {
×
280
          // Update to existing Map
281
          handleMapboxOptionsUpdates(mapboxOptions, map.current);
×
282
          prevMapboxOptions.current = (mapboxOptions);
×
283
        }
284
      } else if (!map.current && mapboxInstance) {
13✔
285
        const options: mapboxgl.MapboxOptions = {
16✔
286
          container: mapContainer.current,
287
          style: 'mapbox://styles/mapbox/streets-v11',
288
          center: [-74.005371, 40.741611],
289
          zoom: 9,
290
          ...mapboxOptions
291
        };
292
        map.current = new mapboxInstance.Map(options);
10✔
293
        const mapbox = map.current;
16✔
294
        mapbox.resize();
10✔
295
        const nav = new mapboxInstance.NavigationControl({
16✔
296
          showCompass: false,
297
          showZoom: true,
298
          visualizePitch: false
299
        });
300
        mapbox.addControl(nav, 'top-right');
10✔
301
        if (onDragDebounced) {
10✔
302
          mapbox.on('drag', () => {
10✔
303
            onDragDebounced(mapbox.getCenter(), mapbox.getBounds());
1✔
304
          });
305
          mapbox.on('zoom', (e) => {
10✔
306
            if (e.originalEvent) {
102!
307
              // only trigger on user zoom, not programmatic zoom (e.g. from fitBounds)
308
              onDragDebounced(mapbox.getCenter(), mapbox.getBounds());
×
309
            }
310
          });
311
          return () => {
10✔
312
            mapbox.off('drag', () => {
8✔
313
              onDragDebounced(mapbox.getCenter(), mapbox.getBounds());
×
314
            });
315
            mapbox.off('zoom', (e) => {
8✔
316
              if (e.originalEvent) {
×
317
                onDragDebounced(mapbox.getCenter(), mapbox.getBounds());
×
318
              }
319
            });
320
          };
321
        }
322
      }
323
      localizeMap();
5✔
324
    }
325
  }, [allowUpdates, mapboxInstance, mapboxOptions, onDragDebounced, localizeMap]);
326

327
  useEffect(() => {
13✔
328
    if (iframeWindow && map.current) {
10!
329
      map.current.resize();
×
330
    }
331
  }, [iframeWindow]);
332

333
  useEffect(() => {
13✔
334
    removeMarkers();
13✔
335
    const mapbox = map.current;
19✔
336
    if (mapbox && locationResults) {
13✔
337
      if (locationResults.length > 0) {
13!
338
        const bounds = new mapboxInstance.LngLatBounds();
18✔
339
        locationResults.forEach((result, i) => {
13✔
340
          const markerLocation = getCoordinate(result);
24✔
341
          if (markerLocation) {
19✔
342
            const { latitude, longitude } = markerLocation;
22✔
343
            if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) {
19!
NEW
344
              return;
×
345
            }
346
            const el = document.createElement('div');
22✔
347
            let markerOptions: mapboxgl.MarkerOptions = {};
22✔
348
            if (PinComponent) {
19✔
349
              if (renderPin) {
4!
350
                console.warn(
1✔
351
                  'Found both PinComponent and renderPin props. Using PinComponent.'
352
                );
353
              }
354
              attachPinComponent(el, (
4✔
355
                <PinComponent
356
                  index={i}
357
                  mapbox={mapbox}
358
                  result={result}
359
                  selected={selectedResult === result}
360
                />
361
              ));
362
              markerOptions.element = el;
4✔
363
            } else if (renderPin) {
15!
364
              renderPin({ index: i, mapbox, result, container: el });
2✔
365
              markerOptions.element = el;
2✔
366
            }
367

368
            if (markerOptionsOverride) {
19!
369
              markerOptions = {
×
370
                ...markerOptions,
371
                ...markerOptionsOverride(selectedResult === result)
372
              };
373
            }
374

375
            const marker = new mapboxInstance.Marker(markerOptions)
22✔
376
              .setLngLat({ lat: latitude, lng: longitude })
377
              .addTo(mapbox);
378

379
            marker?.getElement().addEventListener('click', () => handlePinClick(result));
19✔
380
            markers.current.push(marker);
19✔
381
            if (marker) {
19!
382
              markerData.current.push({ marker, result, index: i });
19✔
383
            }
384
            bounds.extend([longitude, latitude]);
19✔
385
          }
386
        });
387

388
        mapbox.resize();
13✔
389
        const canvas = mapbox.getCanvas();
18✔
390

391
        if (!bounds.isEmpty()
13!
392
            && !!canvas
393
            && canvas.clientHeight > 0
394
            && canvas.clientWidth > 0
395
        ) {
396
          const resolvedOptions = {
13✔
397
            // these settings are defaults and will be overriden if present on fitBoundsOptions
398
            padding: { top: 50, bottom: 50, left: 50, right: 50 },
399
            maxZoom: mapboxOptions?.maxZoom ?? 15,
26✔
400
            ...mapboxOptions?.fitBoundsOptions,
401
          };
402

403
          let resolvedPadding;
404
          if (typeof resolvedOptions.padding === 'number') {
13!
NEW
405
            resolvedPadding = {
×
406
              top: resolvedOptions.padding,
407
              bottom: resolvedOptions.padding,
408
              left: resolvedOptions.padding,
409
              right: resolvedOptions.padding
410
            };
411
          } else {
412
            resolvedPadding = {
13✔
413
              top: resolvedOptions.padding?.top ?? 0,
13!
414
              bottom: resolvedOptions.padding?.bottom ?? 0,
13!
415
              left: resolvedOptions.padding?.left ?? 0,
13!
416
              right: resolvedOptions.padding?.right ?? 0
13!
417
            };
418
          }
419

420
          // Padding must not exceed the map's canvas dimensions
421
          const verticalPaddingSum = resolvedPadding.top + resolvedPadding.bottom;
13✔
422
          if (verticalPaddingSum >= canvas.clientHeight) {
13!
NEW
423
            const ratio = canvas.clientHeight / (verticalPaddingSum || 1);
×
UNCOV
424
            resolvedPadding.top = Math.max(0, resolvedPadding.top * ratio - 1);
×
UNCOV
425
            resolvedPadding.bottom = Math.max(0, resolvedPadding.bottom * ratio - 1);
×
426
          }
427
          const horizontalPaddingSum = resolvedPadding.left + resolvedPadding.right;
13✔
428
          if (horizontalPaddingSum >= canvas.clientWidth) {
13!
NEW
429
            const ratio = canvas.clientWidth / (horizontalPaddingSum || 1);
×
UNCOV
430
            resolvedPadding.left = Math.max(0, resolvedPadding.left * ratio - 1);
×
UNCOV
431
            resolvedPadding.right = Math.max(0, resolvedPadding.right * ratio - 1);
×
432
          }
433
          resolvedOptions.padding = resolvedPadding;
13✔
434
          mapbox.fitBounds(bounds, resolvedOptions);
13✔
435
        }
436

437
        return () => {
13✔
438
          markers.current.forEach((marker, i) => {
9✔
439
            marker?.getElement().removeEventListener('click', () => handlePinClick(locationResults[i]));
13✔
440
          });
441
          removeMarkers();
9✔
442
        };
443
      } else if (staticFilters?.length) {
×
444
        const locationFilterValue = getLocationFilterValue(staticFilters);
×
445
        if (locationFilterValue) {
×
446
          mapbox.flyTo({
×
447
            center: locationFilterValue
448
          });
449
        }
450
      }
451
    }
452
  }, [
453
    PinComponent,
454
    attachPinComponent,
455
    getCoordinate,
456
    handlePinClick,
457
    locationResults,
458
    mapboxInstance,
459
    mapboxOptions,
460
    markerOptionsOverride,
461
    removeMarkers,
462
    renderPin,
463
    selectedResult,
464
    staticFilters
465
  ]);
466

467
  useEffect(() => {
13✔
468
    const mapbox = map.current;
19✔
469
    if (!mapbox || !PinComponent) {
13✔
470
      return;
9✔
471
    }
472
    markerData.current.forEach(({ marker, result, index }) => {
4✔
473
      const element = typeof marker.getElement === 'function' ? marker.getElement() : null;
4!
474
      if (!element) {
4!
475
        return;
×
476
      }
477
      attachPinComponent(element, (
4✔
478
        <PinComponent
479
          index={index}
480
          mapbox={mapbox}
481
          result={result}
482
          selected={selectedResult === result}
483
        />
484
      ));
485
    });
486
  }, [attachPinComponent, PinComponent, selectedResult]);
487

488
  return (
13✔
489
    <div ref={mapContainer} className='h-full w-full' />
490
  );
491
}
492

493
function handleMapboxOptionsUpdates(mapboxOptions: Omit<mapboxgl.MapboxOptions, 'container'> | undefined, currentMap: mapboxgl.Map) {
494
  if (mapboxOptions?.style) {
×
495
    currentMap.setStyle(mapboxOptions.style);
×
496
  }
497
  // Add more options to update as needed
498
}
499

500
function isCoordinate(data: unknown): data is Coordinate {
501
  return typeof data == 'object'
19✔
502
    && typeof (data as any)?.['latitude'] === 'number'
503
    && typeof (data as any)?.['longitude'] === 'number';
504
}
505

506
function getDefaultCoordinate<T>(result: Result<T>): Coordinate | undefined {
507
  const yextDisplayCoordinate: Coordinate = (result.rawData as any)['yextDisplayCoordinate'];
23✔
508
  if (!yextDisplayCoordinate) {
19!
509
    console.error('Unable to use the default "yextDisplayCoordinate" field as the result\'s coordinate to display on map.'
1✔
510
    + '\nConsider providing the "getCoordinate" prop to MapboxMap component to fetch the desire coordinate from result.');
511
    return undefined;
1✔
512
  }
513
  if (!isCoordinate(yextDisplayCoordinate)) {
19!
514
    console.error('The default `yextDisplayCoordinate` field from result is not of type "Coordinate".');
1✔
515
    return undefined;
1✔
516
  }
517
  return yextDisplayCoordinate;
19✔
518
}
519

520
export function getMapboxLanguage(locale: string) {
6✔
521
  try {
22✔
522
    const localeOptions = new Intl.Locale(locale.replaceAll('_', '-'));
22✔
523
    return localeOptions.script ? `${localeOptions.language}-${localeOptions.script}` : localeOptions.language;
22✔
524
  } catch (e) {
NEW
525
    console.warn(`Locale "${locale}" is not supported.`);
×
526
  }
527
  return 'en';
×
528
}
529

530
function getLocationFilterValue(staticFilters: SelectableStaticFilter[]): [number, number] | undefined {
531
  const locationFilter = staticFilters.find(f => (f.filter as any)['fieldId'] === 'builtin.location' && (f.filter as any)['value'])?.filter;
×
532
  if (locationFilter) {
×
NEW
533
    const { lat, lng } = (locationFilter as any)['value'];
×
534
    return [lng, lat];
×
535
  }
536
}
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