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

yext / search-ui-react / 23266474714

18 Mar 2026 08:52PM UTC coverage: 84.709% (+0.1%) from 84.61%
23266474714

Pull #654

github

web-flow
Merge cc7968390 into 2b501425b
Pull Request #654: chore: upgrade mapbox versions (v3.0.0)

1041 of 1446 branches covered (71.99%)

Branch coverage included in aggregate %.

38 of 58 new or added lines in 4 files covered. (65.52%)

28 existing lines in 2 files now uncovered.

2294 of 2491 relevant lines covered (92.09%)

127.53 hits per line

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

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

7
import ReactDOM from 'react-dom';
7✔
8
// Try to statically import createRoot, will be undefined for <18.
9
import * as ReactDomClient from 'react-dom/client';
7✔
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;
11✔
22
const reactMajorVersion = Number(React.version.split('.')[0]);
11✔
23
const supportsCreateRoot = !Number.isNaN(reactMajorVersion) && reactMajorVersion >= 18;
11✔
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/v3.20.0/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>({
7✔
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
  // keep the mapbox access token in sync with prop changes.
159
  useEffect(() => {
13✔
160
    mapboxInstance.accessToken = mapboxAccessToken;
10✔
161
  }, [mapboxAccessToken, mapboxInstance]);
162

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

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

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

179
  // notify consumers when the selected pin changes.
180
  useEffect(() => {
13✔
181
    onPinClick?.(selectedResult);
13✔
182
  }, [onPinClick, selectedResult]);
183

184
  const scheduleRootUnmount = useCallback((root: RootHandle) => {
19✔
185
    if (typeof queueMicrotask === 'function') {
13!
186
      queueMicrotask(() => root.unmount());
13✔
187
    } else {
188
      setTimeout(() => root.unmount(), 0);
13✔
189
    }
190
  }, []);
191

192
  const cleanupPinComponent = useCallback((element: HTMLElement) => {
19✔
193
    activeMarkerElements.current.delete(element);
13✔
194
    if (supportsCreateRoot) {
13!
195
      const root = markerRoots.current.get(element);
10✔
196
      if (root) {
13!
197
        // unmount must be called after the current render finishes, so schedule it for the next
198
        // microtask
199
        scheduleRootUnmount(root);
13✔
200
        markerRoots.current.delete(element);
13✔
201
      }
202
    } else {
203
      legacyReactDOM.unmountComponentAtNode?.(element);
13✔
204
    }
205
  }, [scheduleRootUnmount]);
206

207
  const attachPinComponent = useCallback((element: HTMLElement, component: React.JSX.Element) => {
19✔
208
    if (supportsCreateRoot && typeof ReactDomClient.createRoot === 'function') {
14!
209
      // Use React 18+ API
210
      let root = markerRoots.current.get(element);
7✔
211
      if (!root) {
14✔
212
        root = ReactDomClient.createRoot(element);
14✔
213
        markerRoots.current.set(element, root);
14✔
214
      }
215
      root.render(component);
14✔
216
    } else if (typeof legacyReactDOM.render === 'function') {
13!
217
      // Fallback for React <18
218
      legacyReactDOM.render(component, element);
13✔
219
    }
220
  }, []);
221

222
  // builds and attaches a single marker to the mapbox map
223
  const createMarker = useCallback((
19✔
224
    mapbox: mapboxgl.Map,
225
    result: Result<T>,
226
    index: number,
227
    selected: boolean
228
  ) => {
229
    const markerLocation = getCoordinate(result);
21✔
230
    if (!markerLocation) {
18!
231
      return null;
15✔
232
    }
233
    const { latitude, longitude } = markerLocation;
19✔
234
    if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) {
16!
235
      return null;
13✔
236
    }
237

238
    const el = document.createElement('div');
19✔
239
    let markerOptions: mapboxgl.MarkerOptions = {};
19✔
240
    if (PinComponent) {
16✔
241
      if (renderPin) {
14!
242
        console.warn(
14✔
243
          'Found both PinComponent and renderPin props. Using PinComponent.'
244
        );
245
      }
246
      attachPinComponent(el, (
14✔
247
        <PinComponent
248
          index={index}
249
          mapbox={mapbox}
250
          result={result}
251
          selected={selected}
252
        />
253
      ));
254
      markerOptions.element = el;
14✔
255
    } else if (renderPin) {
15!
256
      renderPin({ index, mapbox, result, container: el });
13✔
257
      markerOptions.element = el;
13✔
258
    }
259

260
    if (markerOptionsOverride) {
16!
261
      markerOptions = {
13✔
262
        ...markerOptions,
263
        ...markerOptionsOverride(selected)
264
      };
265
    }
266

267
    const marker = new mapboxInstance.Marker(markerOptions)
19✔
268
      .setLngLat({ lat: latitude, lng: longitude })
269
      .addTo(mapbox);
270

271
    marker?.getElement().addEventListener('click', () => handlePinClick(result));
16✔
272

273
    return { marker, location: markerLocation };
16✔
274
  }, [
275
    PinComponent,
276
    attachPinComponent,
277
    getCoordinate,
278
    handlePinClick,
279
    mapboxInstance,
280
    markerOptionsOverride,
281
    renderPin
282
  ]);
283

284
  const removeMarkers = useCallback(() => {
19✔
285
    markers.current.forEach(marker => {
24✔
286
      if (!marker) {
16!
287
        return;
16✔
288
      }
289
      const element = marker?.getElement?.();
10✔
290
      if (element) {
13!
291
        cleanupPinComponent(element);
13✔
292
      }
293
      if (typeof marker.remove === 'function') {
13!
294
        marker.remove();
13✔
295
      }
296
    });
297
    markers.current = [];
24✔
298
    markerData.current = [];
24✔
299
  }, [cleanupPinComponent]);
300

301
  const locale = useSearchState(state => state.meta?.locale);
19✔
302
  // keep track of the previous value of mapboxOptions across renders
303
  const prevMapboxOptions = useRef(mapboxOptions);
19✔
304

305
  /**
306
   * Localizes Mapbox label text to a specific locale.
307
   *
308
   * Updates symbol layers that are place names such that labels prefer `name_<lang>`
309
   * (e.g. `name_fr`) and fall back to `name` when unavailable.
310
   *
311
   * Note:
312
   * - Symbol layers that are place names would have `text-field` properties that includes
313
   *   'name', which are localized.
314
   * - Other symbol layers (e.g. road shields, transit, icons) are left unchanged.
315
   */
316
  const localizeMap = useCallback(() => {
19✔
317
    const mapbox = map.current;
8✔
318
    if (!mapbox || !locale) return;
18✔
319

UNCOV
320
    const localizeLabels = () => {
×
UNCOV
321
      mapbox.getStyle().layers.forEach(layer => {
×
UNCOV
322
        if (layer.type !== 'symbol') {
×
UNCOV
323
          return;
×
324
        }
UNCOV
325
        const textField = layer.layout?.['text-field'];
×
UNCOV
326
        if (typeof textField === 'string'
×
327
          ? textField.includes('name')
328
          : (Array.isArray(textField) && JSON.stringify(textField).includes('name'))) {
×
UNCOV
329
          mapbox.setLayoutProperty(
×
330
            layer.id,
331
            'text-field',
332
            [
333
              'coalesce',
334
              ['get', `name_${getMapboxLanguage(locale)}`],
335
              ['get', 'name']
336
            ]
337
          );
338
        }
339
      });
340
    };
341

342
    if (mapbox.isStyleLoaded()) {
13!
343
      localizeLabels();
13✔
344
    } else {
345
      mapbox.once('styledata', () => localizeLabels());
13✔
346
    }
347
  }, [locale]);
348

349
  // initialize the map once and update mapbox options when allowUpdates is true.
350
  useEffect(() => {
13✔
351
    if (mapContainer.current) {
13✔
352
      if (map.current && allowUpdates) {
13!
353
        // Compare current and previous mapboxOptions using deep equality
UNCOV
354
        if (!_.isEqual(prevMapboxOptions.current, mapboxOptions)) {
×
355
          // Update to existing Map
UNCOV
356
          handleMapboxOptionsUpdates(mapboxOptions, map.current);
×
UNCOV
357
          prevMapboxOptions.current = (mapboxOptions);
×
358
        }
359
      } else if (!map.current && mapboxInstance) {
13✔
360
        const options: mapboxgl.MapboxOptions = {
16✔
361
          container: mapContainer.current,
362
          style: 'mapbox://styles/mapbox/streets-v11',
363
          center: [-74.005371, 40.741611],
364
          zoom: 9,
365
          ...mapboxOptions
366
        };
367
        map.current = new mapboxInstance.Map(options);
10✔
368
        const mapbox = map.current;
16✔
369
        mapbox.resize();
10✔
370
        const nav = new mapboxInstance.NavigationControl({
16✔
371
          showCompass: false,
372
          showZoom: true,
373
          visualizePitch: false
374
        });
375
        mapbox.addControl(nav, 'top-right');
10✔
376
        if (onDragDebounced) {
10✔
377
          const dispatchDrag = () => {
11✔
378
            const bounds = mapbox.getBounds();
1✔
379
            if (!bounds) {
11!
380
              return;
10✔
381
            }
382
            onDragDebounced(mapbox.getCenter(), bounds);
11✔
383
          };
384
          const onDrag = () => {
11✔
385
            dispatchDrag();
11✔
386
          };
387
          const onZoom = (e: mapboxgl.MapboxEvent) => {
11✔
388
            if ('originalEvent' in e && e.originalEvent) {
59!
389
              // only trigger on user zoom, not programmatic zoom (e.g. from fitBounds)
390
              dispatchDrag();
10✔
391
            }
392
          };
393
          mapbox.on('drag', onDrag);
10✔
394
          mapbox.on('zoom', onZoom);
10✔
395
          return () => {
10✔
396
            mapbox.off('drag', onDrag);
8✔
397
            mapbox.off('zoom', onZoom);
8✔
398
          };
399
        }
400
      }
401
      localizeMap();
5✔
402
    }
403
  }, [allowUpdates, mapboxInstance, mapboxOptions, onDragDebounced, localizeMap]);
404

405
  // resize the map when its iframe container changes size.
406
  useEffect(() => {
13✔
407
    if (iframeWindow && map.current) {
10!
NEW
408
      map.current.resize();
×
409
    }
410
  }, [iframeWindow]);
411

412
  // create and place markers when results change, then cleanup on teardown
413
  useEffect(() => {
13✔
414
    removeMarkers();
10✔
415
    const mapbox = map.current;
16✔
416
    if (mapbox && locationResults) {
10✔
417
      if (locationResults.length > 0) {
10!
418
        const bounds = new mapboxInstance.LngLatBounds();
15✔
419
        // create a marker for each result
420
        locationResults.forEach((result, i) => {
10✔
421
          const created = createMarker(mapbox, result, i, false);
21✔
422
          if (!created) {
16!
423
            return;
2✔
424
          }
425
          markers.current.push(created.marker);
16✔
426
          markerData.current.push({ marker: created.marker, result, index: i });
16✔
427
          bounds.extend([created.location.longitude, created.location.latitude]);
16✔
428
        });
429

430
        // fit the map to the markers
431
        mapbox.resize();
10✔
432
        const canvas = mapbox.getCanvas();
15✔
433

434
        // add padding to map
435
        if (!bounds.isEmpty()
10!
436
            && !!canvas
437
            && canvas.clientHeight > 0
438
            && canvas.clientWidth > 0
439
        ) {
440
          const resolvedOptions = {
10✔
441
            // these settings are defaults and will be overriden if present on fitBoundsOptions
442
            padding: { top: 50, bottom: 50, left: 50, right: 50 },
443
            maxZoom: mapboxOptions?.maxZoom ?? 15,
20✔
444
            ...mapboxOptions?.fitBoundsOptions,
445
          };
446

447
          let resolvedPadding;
448
          if (typeof resolvedOptions.padding === 'number') {
10!
UNCOV
449
            resolvedPadding = {
×
450
              top: resolvedOptions.padding,
451
              bottom: resolvedOptions.padding,
452
              left: resolvedOptions.padding,
453
              right: resolvedOptions.padding
454
            };
455
          } else {
456
            resolvedPadding = {
10✔
457
              top: resolvedOptions.padding?.top ?? 0,
10!
458
              bottom: resolvedOptions.padding?.bottom ?? 0,
10!
459
              left: resolvedOptions.padding?.left ?? 0,
10!
460
              right: resolvedOptions.padding?.right ?? 0
10!
461
            };
462
          }
463

464
          // Padding must not exceed the map's canvas dimensions
465
          const verticalPaddingSum = resolvedPadding.top + resolvedPadding.bottom;
10✔
466
          if (verticalPaddingSum >= canvas.clientHeight) {
10!
UNCOV
467
            const ratio = canvas.clientHeight / (verticalPaddingSum || 1);
×
468
            resolvedPadding.top = Math.max(0, resolvedPadding.top * ratio - 1);
×
UNCOV
469
            resolvedPadding.bottom = Math.max(0, resolvedPadding.bottom * ratio - 1);
×
470
          }
471
          const horizontalPaddingSum = resolvedPadding.left + resolvedPadding.right;
10✔
472
          if (horizontalPaddingSum >= canvas.clientWidth) {
10!
UNCOV
473
            const ratio = canvas.clientWidth / (horizontalPaddingSum || 1);
×
UNCOV
474
            resolvedPadding.left = Math.max(0, resolvedPadding.left * ratio - 1);
×
UNCOV
475
            resolvedPadding.right = Math.max(0, resolvedPadding.right * ratio - 1);
×
476
          }
477
          resolvedOptions.padding = resolvedPadding;
10✔
478
          mapbox.fitBounds(bounds, resolvedOptions);
10✔
479
        }
480

481
        // return a cleanup function to remove markers when the map component unmounts
482
        return () => {
10✔
483
          markers.current.forEach((marker, i) => {
6✔
484
            marker?.getElement().removeEventListener('click', () => handlePinClick(locationResults[i]));
10✔
485
          });
486
          removeMarkers();
6✔
487
        };
UNCOV
488
      } else if (staticFilters?.length) {
×
UNCOV
489
        const locationFilterValue = getLocationFilterValue(staticFilters);
×
UNCOV
490
        if (locationFilterValue) {
×
UNCOV
491
          mapbox.flyTo({
×
492
            center: locationFilterValue
493
          });
494
        }
495
      }
496
    }
497
  }, [
498
    createMarker,
499
    handlePinClick,
500
    locationResults,
501
    mapboxInstance,
502
    mapboxOptions,
503
    removeMarkers,
504
    staticFilters
505
  ]);
506

507
  const previousSelectedResult = useRef<Result<T> | undefined>(undefined);
19✔
508

509
  // update marker options when markerOptionsOverride changes or selectedResult changes
510
  useEffect(() => {
13✔
511
    const mapbox = map.current;
19✔
512
    if (!mapbox || !markerOptionsOverride) {
13✔
513
      previousSelectedResult.current = selectedResult;
13✔
514
      return;
13✔
515
    }
516

UNCOV
517
    const prevSelected = previousSelectedResult.current;
×
UNCOV
518
    previousSelectedResult.current = selectedResult;
×
519

520
    // markerOptionsOverride is applied at creation time, so we recreate only the affected
521
    // markers to reflect selection changes without tearing down all pins.
NEW
522
    const resultsToUpdate = new Set<Result<T>>();
×
NEW
523
    if (prevSelected) {
×
NEW
524
      resultsToUpdate.add(prevSelected);
×
525
    }
NEW
526
    if (selectedResult) {
×
NEW
527
      resultsToUpdate.add(selectedResult);
×
528
    }
529

NEW
530
    resultsToUpdate.forEach((result) => {
×
NEW
531
      const markerEntry = markerData.current.find(entry => entry.result === result);
×
NEW
532
      if (!markerEntry) {
×
NEW
533
        return;
×
534
      }
535
      // recreate the marker to apply new markerOptionsOverride (e.g. color/scale).
NEW
536
      const oldMarker = markerEntry.marker;
×
NEW
537
      const element = oldMarker?.getElement?.();
×
NEW
538
      if (element) {
×
NEW
539
        cleanupPinComponent(element);
×
540
      }
NEW
541
      oldMarker?.remove?.();
×
542

NEW
543
      const created = createMarker(mapbox, result, markerEntry.index, selectedResult === result);
×
NEW
544
      if (!created) {
×
NEW
545
        return;
×
546
      }
NEW
547
      markerEntry.marker = created.marker;
×
NEW
548
      markers.current[markerEntry.index] = created.marker;
×
549
    });
550
  }, [cleanupPinComponent, createMarker, markerOptionsOverride, selectedResult]);
551

552
  // re-render custom PinComponent on selection changes to update the visual state
553
  useEffect(() => {
13✔
554
    const mapbox = map.current;
19✔
555
    if (!mapbox || !PinComponent) {
13✔
556
      return;
9✔
557
    }
558
    markerData.current.forEach(({ marker, result, index }) => {
4✔
559
      const element = marker?.getElement?.();
5✔
560
      if (!element) {
4!
561
        return;
1✔
562
      }
563
      attachPinComponent(element, (
4✔
564
        <PinComponent
565
          index={index}
566
          mapbox={mapbox}
567
          result={result}
568
          selected={selectedResult === result}
569
        />
570
      ));
571
    });
572
  }, [attachPinComponent, PinComponent, selectedResult]);
573

574
  return (
13✔
575
    <div ref={mapContainer} className='h-full w-full' />
576
  );
577
}
578

579
function handleMapboxOptionsUpdates(mapboxOptions: Omit<mapboxgl.MapboxOptions, 'container'> | undefined, currentMap: mapboxgl.Map) {
UNCOV
580
  if (mapboxOptions?.style) {
×
UNCOV
581
    currentMap.setStyle(mapboxOptions.style);
×
582
  }
583
  // Add more options to update as needed
584
}
585

586
function isCoordinate(data: unknown): data is Coordinate {
587
  return typeof data == 'object'
16✔
588
    && typeof (data as any)?.['latitude'] === 'number'
589
    && typeof (data as any)?.['longitude'] === 'number';
590
}
591

592
function getDefaultCoordinate<T>(result: Result<T>): Coordinate | undefined {
593
  const yextDisplayCoordinate: Coordinate = (result.rawData as any)['yextDisplayCoordinate'];
20✔
594
  if (!yextDisplayCoordinate) {
16!
595
    console.error('Unable to use the default "yextDisplayCoordinate" field as the result\'s coordinate to display on map.'
1✔
596
    + '\nConsider providing the "getCoordinate" prop to MapboxMap component to fetch the desire coordinate from result.');
597
    return undefined;
1✔
598
  }
599
  if (!isCoordinate(yextDisplayCoordinate)) {
16!
600
    console.error('The default `yextDisplayCoordinate` field from result is not of type "Coordinate".');
1✔
601
    return undefined;
1✔
602
  }
603
  return yextDisplayCoordinate;
16✔
604
}
605

606
export function getMapboxLanguage(locale: string) {
7✔
607
  try {
22✔
608
    const localeOptions = new Intl.Locale(locale.replaceAll('_', '-'));
22✔
609
    return localeOptions.script ? `${localeOptions.language}-${localeOptions.script}` : localeOptions.language;
22✔
610
  } catch (e) {
UNCOV
611
    console.warn(`Locale "${locale}" is not supported.`);
×
612
  }
UNCOV
613
  return 'en';
×
614
}
615

616
function getLocationFilterValue(staticFilters: SelectableStaticFilter[]): [number, number] | undefined {
UNCOV
617
  const locationFilter = staticFilters.find(f => (f.filter as any)['fieldId'] === 'builtin.location' && (f.filter as any)['value'])?.filter;
×
618
  if (locationFilter) {
×
619
    const { lat, lng } = (locationFilter as any)['value'];
×
620
    return [lng, lat];
×
621
  }
622
}
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