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

yext / search-ui-react / 14384223156

10 Apr 2025 03:21PM UTC coverage: 87.746% (+2.6%) from 85.189%
14384223156

push

github

web-flow
Merge main (v1.8.2) into develop (#504)

* Version 1.8.0

This PR adds Generative Direct Answers (GDA) and GDA analytics. Full merge thread can be found below - many of the changes were already in main and were just merged into the develop branch while developing.


* add generativeDirectAnswer support (#441)

J=CLIP-1226
TEST=auto,manual

added unit and visual regression test, ran and saw tests passed
added GDA to test-site App and verified that the appropriate gda content shows up and the UI looks as expected

* Added storybook stories for generative direct answer component (#442)

J=CLIP-1226
TEST=manual

ran `npm run storybook`

* update search-headless-react dependencies version to 2.5.0-beta.3 (#443)

New version has `queryDurationMillis` as an optional field of VerticalResults

J=CLIP-1226
TEST=auto,manual

ran `npm run test`
temporarily added GDA to ProductsPage in test site and verified that the appropriate gda content shows up after a vertical search.

* support clickable citation card with link (#444)

J=CLIP-1332
TEST=auto,manual

ran `npm run test` and test manually on test-site

* GDA: citations component override  (#451)

Changes:
- Added `CitationsContainer` prop to `<GenerativeDirectAnswer>` to allow custom components to be passed in
- Updated test cases and test-site with new component passed through

J=CLIP-1369
TEST=manual|auto

Ran `npm run build`, `npm run test`, and `npm run wcag` and did manual testing on test-site

* Check for search id before executing GDA (#454)

J=CLIP-1461
TEST=auto,manual

ran `npm run test` and tested manually on test-site to verified gda is executed as expected

* Automated update to THIRD-PARTY-NOTICES from github action's 3rd party notices check

* increment versions

* Automated update to THIRD-PARTY-NOTICES from github action's 3rd party notices check

* Update snapshots

* decrement package

* Function Vertical Support via search-core/headless upgrade (#459)

This change upgrades the version for se... (continued)

1296 of 1590 branches covered (81.51%)

Branch coverage included in aggregate %.

18 of 21 new or added lines in 2 files covered. (85.71%)

2 existing lines in 1 file now uncovered.

1955 of 2115 relevant lines covered (92.43%)

149.58 hits per line

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

92.31
/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
   * The window object of the iframe where the map should rendered. Must have mapboxgl loaded.
92
   * If not provided or mapboxgl not loaded, the map will be rendered in the parent window.
93
   */
94
  iframeWindow?: Window,
95
  /**
96
   * If set to true, the map will update its options when the mapboxOptions prop changes.
97
   * Otherwise, the map will not update its options once initially set.
98
   */
99
  allowUpdates?: boolean
100
}
101

102
/**
103
 * A component that renders a map with markers to show result locations using Mapbox GL.
104
 *
105
 * @remarks
106
 * For the map to work properly, be sure to include Mapbox GL stylesheet in the application.
107
 *
108
 * @example
109
 * For instance, user may add the following import statement in their application's index file
110
 * or in the file where `MapboxMap` is used:
111
 * `import 'mapbox-gl/dist/mapbox-gl.css';`
112
 *
113
 * Or, user may add a stylesheet link in their html page:
114
 * `<link href="https://api.mapbox.com/mapbox-gl-js/v2.9.2/mapbox-gl.css" rel="stylesheet" />`
115
 *
116
 * @param props - {@link MapboxMapProps}
117
 * @returns A React element containing a Mapbox Map
118
 *
119
 * @public
120
 */
121
export function MapboxMap<T>({
6✔
122
  mapboxAccessToken,
123
  mapboxOptions,
124
  PinComponent,
125
  renderPin,
126
  getCoordinate = getDefaultCoordinate,
17✔
127
  onDrag,
128
  iframeWindow,
129
  allowUpdates = false,
18✔
130
}: MapboxMapProps<T>): JSX.Element {
131
  const mapboxInstance = (iframeWindow as Window & { mapboxgl?: typeof mapboxgl })?.mapboxgl ?? mapboxgl;
18✔
132
  useEffect(() => {
18✔
133
    mapboxInstance.accessToken = mapboxAccessToken;
16✔
134
  }, [mapboxAccessToken]);
135

136
  const mapContainer = useRef<HTMLDivElement>(null);
18✔
137
  const map = useRef<mapboxgl.Map | null>(null);
18✔
138
  const markers = useRef<mapboxgl.Marker[]>([]);
18✔
139

140
  const locationResults = useSearchState(state => state.vertical.results) as Result<T>[];
27✔
141
  const onDragDebounced = useDebouncedFunction(onDrag, 100);
18✔
142

143
  useEffect(() => {
18✔
144
    if (mapContainer.current) {
18✔
145
      if (map.current && allowUpdates) {
18!
146
        // Update to existing Map
NEW
147
        handleMapboxOptionsUpdates(mapboxOptions, map.current);
×
148
      } else if (!map.current && mapboxInstance) {
18✔
149
        const options: mapboxgl.MapboxOptions = {
16✔
150
          container: mapContainer.current,
151
          style: 'mapbox://styles/mapbox/streets-v11',
152
          center: [-74.005371, 40.741611],
153
          zoom: 9,
154
          ...mapboxOptions
155
        };
156
        map.current = new mapboxInstance.Map(options);
16✔
157
        const mapbox = map.current;
16✔
158
        mapbox.resize();
16✔
159
        const nav = new mapboxInstance.NavigationControl({
16✔
160
          showCompass: false,
161
          showZoom: true,
162
          visualizePitch: false
163
        });
164
        mapbox.addControl(nav, 'top-right');
16✔
165
        if (onDragDebounced) {
16✔
166
          mapbox.on('drag', () => {
11✔
167
            onDragDebounced(mapbox.getCenter(), mapbox.getBounds());
1✔
168
          });
169
        }
170
      }
171
    }
172
  }, [mapboxOptions, onDragDebounced]);
173

174
  useEffect(() => {
18✔
175
    markers.current.forEach(marker => marker.remove());
18✔
176
    markers.current = [];
18✔
177
    const mapbox = map.current;
18✔
178
    if (mapbox && locationResults?.length > 0) {
18✔
179
      const bounds = new mapboxInstance.LngLatBounds();
17✔
180
      locationResults.forEach((result, i) => {
17✔
181
        const markerLocation = getCoordinate(result);
23✔
182
        if (markerLocation) {
23✔
183
          const { latitude, longitude } = markerLocation;
21✔
184
          const el = document.createElement('div');
21✔
185
          const markerOptions: mapboxgl.MarkerOptions = {};
21✔
186
          if (PinComponent) {
21✔
187
            if (renderPin) {
5✔
188
              console.warn(
1✔
189
                'Found both PinComponent and renderPin props. Using PinComponent.'
190
              );
191
            }
192
            ReactDOM.render(<PinComponent
5✔
193
              index={i}
194
              mapbox={mapbox}
195
              result={result}
196
            />, el);
197
            markerOptions.element = el;
5✔
198
          } else if (renderPin) {
16✔
199
            renderPin({ index: i, mapbox, result, container: el });
1✔
200
            markerOptions.element = el;
1✔
201
          }
202
          const marker = new mapboxInstance.Marker(markerOptions)
21✔
203
            .setLngLat({ lat: latitude, lng: longitude })
204
            .addTo(mapbox);
205
          markers.current.push(marker);
21✔
206
          bounds.extend([longitude, latitude]);
21✔
207
        }
208
      });
209

210
      if (!bounds.isEmpty()){
17✔
211
        mapbox.fitBounds(bounds, {
17✔
212
          padding: { top: 50, bottom: 50, left: 50, right: 50 },
213
          maxZoom: 15
214
        });
215
      }
216
    }
217
  }, [PinComponent, getCoordinate, locationResults]);
218

219
  return (
18✔
220
    <div ref={mapContainer} className='h-full w-full' />
221
  );
222
}
223

224
function handleMapboxOptionsUpdates(mapboxOptions: Omit<mapboxgl.MapboxOptions, 'container'> | undefined, currentMap: mapboxgl.Map) {
NEW
225
  if (mapboxOptions?.style) {
×
NEW
226
    currentMap.setStyle(mapboxOptions.style);
×
227
  }
228
  // Add more options to update as needed
229
}
230

231
function isCoordinate(data: unknown): data is Coordinate {
232
  return typeof data == 'object'
21✔
233
    && typeof data?.['latitude'] === 'number'
81✔
234
    && typeof data?.['longitude'] === 'number';
78✔
235
}
236

237
function getDefaultCoordinate<T>(result: Result<T>): Coordinate | undefined {
238
  const yextDisplayCoordinate = result.rawData['yextDisplayCoordinate'];
22✔
239
  if (!yextDisplayCoordinate) {
22✔
240
    console.error('Unable to use the default "yextDisplayCoordinate" field as the result\'s coordinate to display on map.'
1✔
241
    + '\nConsider providing the "getCoordinate" prop to MapboxMap component to fetch the desire coordinate from result.');
242
    return undefined;
1✔
243
  }
244
  if (!isCoordinate(yextDisplayCoordinate)) {
21✔
245
    console.error('The default `yextDisplayCoordinate` field from result is not of type "Coordinate".');
1✔
246
    return undefined;
1✔
247
  }
248
  return yextDisplayCoordinate;
20✔
249
}
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