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

yext / search-ui-react / 13243411771

10 Feb 2025 02:40PM UTC coverage: 84.923% (-0.4%) from 85.327%
13243411771

Pull #485

github

web-flow
Merge c25d5370a into 6a7ae5ac1
Pull Request #485: Mark all icons as hidden for screen readers.

1290 of 1699 branches covered (75.93%)

Branch coverage included in aggregate %.

1870 of 2022 relevant lines covered (92.48%)

149.12 hits per line

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

76.85
/src/components/MapboxMap.tsx
1
import React, { useRef, useEffect } from 'react';
6✔
2
import mapboxgl from 'mapbox-gl';
6✔
3
import { Result, useSearchState } from '@yext/search-headless-react';
6✔
4
import { useDebouncedFunction } from '../hooks/useDebouncedFunction';
6✔
5
import ReactDOM from 'react-dom';
6✔
6

7
/**
8
 * Props for rendering a custom marker on the map.
9
 *
10
 * @public
11
 */
12
export type PinComponentProps<T> = {
13
  /** The index of the pin. */
14
  index: number,
15
  /** The Mapbox map. */
16
  mapbox: mapboxgl.Map,
17
  /** The search result corresponding to the pin. */
18
  result: Result<T>
19
};
20

21
/**
22
 * A functional component that can be used to render a custom marker on the map.
23
 *
24
 * @public
25
 */
26
export type PinComponent<T> = (props: PinComponentProps<T>) => JSX.Element;
27

28
/**
29
 * A function use to derive a result's coordinate.
30
 *
31
 * @public
32
 */
33
export type CoordinateGetter<T> = (result: Result<T>) => Coordinate | undefined;
34

35
/**
36
 * Coordinate use to represent the result's location on a map.
37
 *
38
 * @public
39
 */
40
export interface Coordinate {
41
  /** The latitude of the location. */
42
  latitude: number,
43
  /** The longitude of the location. */
44
  longitude: number
45
}
46

47
/**
48
 * A function which is called when user drag the map.
49
 *
50
 * @public
51
 */
52
export type OnDragHandler = (center: mapboxgl.LngLat, bounds: mapboxgl.LngLatBounds) => void;
53

54
/**
55
 * Props for the {@link MapboxMap} component.
56
 * The type param "T" represents the type of "rawData" field of the results use in the map.
57
 *
58
 * @public
59
 */
60
export interface MapboxMapProps<T> {
61
  /** Mapbox access token. */
62
  mapboxAccessToken: string,
63
  /** Interface for map customization derived from Mapbox GL's Map options. */
64
  mapboxOptions?: Omit<mapboxgl.MapboxOptions, 'container'>,
65
  /**
66
   * Custom Pin component to render for markers on the map.
67
   * By default, the built-in marker image from Mapbox GL is used.
68
   * This prop should not be used with
69
   * {@link MapboxMapProps.renderPin | renderPin}. If both are provided,
70
   * only PinComponent will be used.
71
   */
72
  PinComponent?: PinComponent<T>,
73
  /**
74
   * Render function for a custom marker on the map. This function takes in an
75
   * HTML element and is responible for rendering the pin into that element,
76
   * which will be used as the marker.
77
   * By default, the built-in marker image from Mapbox GL is used.
78
   * This prop should not be used with
79
   * {@link MapboxMapProps.PinComponent | PinComponent}. If both are provided,
80
   * only PinComponent will be used.
81
   */
82
  renderPin?: (props: PinComponentProps<T> & { container: HTMLElement }) => void,
83
  /**
84
   * A function to derive a result's coordinate for the corresponding marker's location on the map.
85
   * By default, "yextDisplayCoordinate" field is used as the result's display coordinate.
86
   */
87
  getCoordinate?: CoordinateGetter<T>,
88
  /** {@inheritDoc OnDragHandler} */
89
  onDrag?: OnDragHandler
90
}
91

92
/**
93
 * A component that renders a map with markers to show result locations using Mapbox GL.
94
 *
95
 * @remarks
96
 * For the map to work properly, be sure to include Mapbox GL stylesheet in the application.
97
 *
98
 * @example
99
 * For instance, user may add the following import statement in their application's index file
100
 * or in the file where `MapboxMap` is used:
101
 * `import 'mapbox-gl/dist/mapbox-gl.css';`
102
 *
103
 * Or, user may add a stylesheet link in their html page:
104
 * `<link href="https://api.mapbox.com/mapbox-gl-js/v2.9.2/mapbox-gl.css" rel="stylesheet" />`
105
 *
106
 * @param props - {@link MapboxMapProps}
107
 * @returns A React element containing a Mapbox Map
108
 *
109
 * @public
110
 */
111
export function MapboxMap<T>({
6✔
112
  mapboxAccessToken,
113
  mapboxOptions,
114
  PinComponent,
115
  renderPin,
116
  getCoordinate = getDefaultCoordinate,
5✔
117
  onDrag
118
}: MapboxMapProps<T>): JSX.Element {
119
  useEffect(() => {
6✔
120
    mapboxgl.accessToken = mapboxAccessToken;
6✔
121
  }, [mapboxAccessToken]);
122

123
  const mapContainer = useRef<HTMLDivElement>(null);
6✔
124
  const map = useRef<mapboxgl.Map | null>(null);
6✔
125
  const markers = useRef<mapboxgl.Marker[]>([]);
6✔
126

127
  const locationResults = useSearchState(state => state.vertical.results) as Result<T>[];
6✔
128
  const onDragDebounced = useDebouncedFunction(onDrag, 100);
6✔
129

130
  useEffect(() => {
6✔
131
    if (mapContainer.current && !map.current) {
6!
132
      const options: mapboxgl.MapboxOptions = {
6✔
133
        container: mapContainer.current,
134
        style: 'mapbox://styles/mapbox/streets-v11',
135
        center: [-74.005371, 40.741611],
136
        zoom: 9,
137
        ...mapboxOptions
138
      };
139
      map.current = new mapboxgl.Map(options);
6✔
140
      const mapbox = map.current;
6✔
141
      mapbox.resize();
6✔
142
      if (onDragDebounced) {
6!
143
        mapbox.on('drag', () => {
1✔
144
          onDragDebounced(mapbox.getCenter(), mapbox.getBounds());
1✔
145
        });
146
      }
147
    }
148
  }, [mapboxOptions, onDragDebounced]);
149

150
  useEffect(() => {
6✔
151
    markers.current.forEach(marker => marker.remove());
6✔
152
    markers.current = [];
6✔
153
    const mapbox = map.current;
6✔
154
    if (mapbox && locationResults?.length > 0) {
6!
155
      const bounds = new mapboxgl.LngLatBounds();
5✔
156
      locationResults.forEach((result, i) => {
5✔
157
        const markerLocation = getCoordinate(result);
5✔
158
        if (markerLocation) {
5!
159
          const { latitude, longitude } = markerLocation;
3✔
160
          const el = document.createElement('div');
3✔
161
          const markerOptions: mapboxgl.MarkerOptions = {};
3✔
162
          if (PinComponent) {
3!
163
            if (renderPin) {
1!
164
              console.warn(
1✔
165
                'Found both PinComponent and renderPin props. Using PinComponent.'
166
              );
167
            }
168
            ReactDOM.render(<PinComponent
1✔
169
              index={i}
170
              mapbox={mapbox}
171
              result={result}
172
            />, el);
173
            markerOptions.element = el;
1✔
174
          } else if (renderPin) {
2!
175
            renderPin({ index: i, mapbox, result, container: el });
×
176
            markerOptions.element = el;
×
177
          }
178
          const marker = new mapboxgl.Marker(markerOptions)
3✔
179
            .setLngLat({ lat: latitude, lng: longitude })
180
            .addTo(mapbox);
181
          markers.current.push(marker);
3✔
182
          bounds.extend([longitude, latitude]);
3✔
183
        }
184
      });
185

186
      if (!bounds.isEmpty()){
5!
187
        mapbox.fitBounds(bounds, {
5✔
188
          padding: { top: 50, bottom: 50, left: 50, right: 50 },
189
          maxZoom: 15
190
        });
191
      }
192
    }
193
  }, [PinComponent, getCoordinate, locationResults]);
194

195
  return (
6✔
196
    <div ref={mapContainer} className='h-full w-full' />
197
  );
198
}
199

200
function isCoordinate(data: unknown): data is Coordinate {
201
  return typeof data == 'object'
3✔
202
    && typeof data?.['latitude'] === 'number'
9!
203
    && typeof data?.['longitude'] === 'number';
6!
204
}
205

206
function getDefaultCoordinate<T>(result: Result<T>): Coordinate | undefined {
207
  const yextDisplayCoordinate = result.rawData['yextDisplayCoordinate'];
4✔
208
  if (!yextDisplayCoordinate) {
4!
209
    console.error('Unable to use the default "yextDisplayCoordinate" field as the result\'s coordinate to display on map.'
1✔
210
    + '\nConsider providing the "getCoordinate" prop to MapboxMap component to fetch the desire coordinate from result.');
211
    return undefined;
1✔
212
  }
213
  if (!isCoordinate(yextDisplayCoordinate)) {
3!
214
    console.error('The default `yextDisplayCoordinate` field from result is not of type "Coordinate".');
1✔
215
    return undefined;
1✔
216
  }
217
  return yextDisplayCoordinate;
2✔
218
}
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