Skip to content

Commit 388e040

Browse files
committed
Add configurable selectedIcon for InputMapFindPlace widget
- Add selectedIcon property to InputMapFindPlaceType interface - Allow customization of selected marker icon URL and size like placesIcon - Update render logic to use configurable selected icon instead of hardcoded value - Add selectedIcon to default configuration in inputMapFindPlaceBase - Include selectedIcon in comprehensive test coverage - Remove hardcoded selectedIconUrl instance variable from component This enables independent customization of both regular and selected marker appearances in map widgets.
1 parent 4d5c831 commit 388e040

File tree

4 files changed

+83
-48
lines changed

4 files changed

+83
-48
lines changed

packages/evolution-common/src/services/questionnaire/types/WidgetConfig.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -258,17 +258,32 @@ export type InputMapPointType = InputMapType &
258258
showSearchPlaceButton?: boolean | ParsingFunction<boolean>;
259259
};
260260

261+
/**
262+
* InputMapFindPlaceType is used to configure the mapFindPlace widget.
263+
* @param placesIcon: Icon used for all found places from geocoding search results
264+
* @param selectedIcon: Icon used when user selects/highlights a specific place from the results
265+
* @param maxGeocodingResultsBounds: Limits the search results to a specific area.
266+
* @param showSearchPlaceButton: Whether to show a button to search for a place.
267+
* @param searchPlaceButtonColor: Color of the search place button.
268+
* @param height: The height of the map container in css units: example: 28rem or 550px
269+
* @param coordinatesPrecision: Number of decimals to keep for latitute longitude coordinates.
270+
* @param invalidGeocodingResultTypes: Types of geocoding results from google to set as invalid (like 'city' or 'country', which are not precise enough for the address)
271+
* @param showPhoto: Whether to show a photo of the selected place, when available.
272+
* @param autoConfirmIfSingleResult: Whether to automatically confirm the selected place if there is only one result
273+
* @param updateDefaultValueWhenResponded: Whether to update the default value when the user responds
274+
*/
261275
export type InputMapFindPlaceType = InputMapType &
262276
BaseQuestionType & {
263277
inputType: 'mapFindPlace';
264278
showSearchPlaceButton?: boolean | ParsingFunction<boolean>;
265279
searchPlaceButtonColor?: string | ParsingFunction<string>;
266280
placesIcon?: IconData;
281+
selectedIcon?: IconData;
267282
maxGeocodingResultsBounds?: ParsingFunction<
268283
[{ lat: number; lng: number }, { lat: number; lng: number }] | undefined
269284
>;
270-
height?: string; // the height of the map container in css units: example: 28rem or 550px
271-
coordinatesPrecision?: number; // number of decimals to keep for latitute longitude coordinates.
285+
height?: string;
286+
coordinatesPrecision?: number;
272287
invalidGeocodingResultTypes?: string[];
273288
showPhoto?: boolean;
274289
autoConfirmIfSingleResult?: boolean;

packages/evolution-frontend/src/components/inputs/InputMapFindPlace.tsx

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,6 @@ export class InputMapFindPlace extends React.Component<
7373
> {
7474
private geocodeButtonRef: React.RefObject<HTMLButtonElement | null> = React.createRef();
7575
private autoConfirmIfSingleResult: boolean;
76-
private selectedIconUrl: string;
7776
private shouldFitBoundsIdx = 0;
7877
private currentBounds: [number, number, number, number] | undefined = undefined;
7978

@@ -92,8 +91,6 @@ export class InputMapFindPlace extends React.Component<
9291

9392
this.autoConfirmIfSingleResult = this.props.widgetConfig.autoConfirmIfSingleResult || false;
9493

95-
this.selectedIconUrl = defaultSelectedMarkerUrl;
96-
9794
this.state = {
9895
geocoding: false,
9996
defaultCenter,
@@ -382,6 +379,20 @@ export class InputMapFindPlace extends React.Component<
382379
? this.props.widgetConfig.placesIcon.size
383380
: defaultIconSize;
384381

382+
const selectedIconUrl = this.props.widgetConfig.selectedIcon
383+
? surveyHelper.parseString(
384+
this.props.widgetConfig.selectedIcon.url,
385+
this.props.interview,
386+
this.props.path,
387+
this.props.user
388+
) || defaultSelectedMarkerUrl
389+
: defaultSelectedMarkerUrl;
390+
391+
const selectedIconSize =
392+
this.props.widgetConfig.selectedIcon && this.props.widgetConfig.selectedIcon.size
393+
? this.props.widgetConfig.selectedIcon.size
394+
: defaultIconSize;
395+
385396
const iconUrl = this.props.widgetConfig.icon
386397
? surveyHelper.parseString(
387398
this.props.widgetConfig.icon.url,
@@ -412,8 +423,8 @@ export class InputMapFindPlace extends React.Component<
412423
markers.push({
413424
position: feature,
414425
icon: {
415-
url: feature === this.state.selectedPlace ? this.selectedIconUrl : placesIconUrl,
416-
size: placesIconSize
426+
url: feature === this.state.selectedPlace ? selectedIconUrl : placesIconUrl,
427+
size: feature === this.state.selectedPlace ? selectedIconSize : placesIconSize
417428
},
418429
draggable: this.state.selectedPlace ? true : false,
419430
onClick: () => this.onMarkerSelect(feature)

packages/evolution-frontend/src/components/inputs/__tests__/InputMapFindPlace.test.tsx

Lines changed: 45 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
*/
77
import React from 'react';
88
import { render, screen } from '@testing-library/react';
9-
import userEvent from '@testing-library/user-event'
9+
import userEvent from '@testing-library/user-event';
1010
import '@testing-library/jest-dom';
1111

1212
import { interviewAttributes } from './interviewData.test';
@@ -32,16 +32,16 @@ const userAttributes = {
3232
is_admin: false,
3333
pages: [],
3434
showUserInfo: true
35-
}
35+
};
3636

3737
const baseWidgetConfig = {
3838
type: 'question' as const,
3939
twoColumns: true,
4040
path: 'test.foo',
4141
containsHtml: true,
4242
label: {
43-
fr: `Texte en français`,
44-
en: `English text`
43+
fr: 'Texte en français',
44+
en: 'English text'
4545
},
4646
size: 'medium' as const,
4747
inputType: 'mapFindPlace' as const,
@@ -68,12 +68,12 @@ describe('Render InputMapPoint with various parameters', () => {
6868
});
6969

7070
test('Test with all parameters', () => {
71-
71+
7272
const testWidgetConfig = Object.assign({
7373
geocodingQueryString: jest.fn(),
7474
refreshGeocodingLabel: {
75-
fr: `Rafraîchir la carte`,
76-
en: `Refresh map`
75+
fr: 'Rafraîchir la carte',
76+
en: 'Refresh map'
7777
},
7878
icon: {
7979
url: 'path/to/icon',
@@ -83,14 +83,18 @@ describe('Render InputMapPoint with various parameters', () => {
8383
defaultZoom: 15,
8484
coordinatesPrecision: 6,
8585
placesIcon: {
86-
url: 'path/to/icon',
86+
url: 'path/to/places-icon',
8787
size: [85, 85] as [number, number]
8888
},
89+
selectedIcon: {
90+
url: 'path/to/selected-icon',
91+
size: [90, 90] as [number, number]
92+
},
8993
maxGeocodingResultsBounds: function (interview, path) {
90-
return [{lat: 45.2229, lng: -74.3230}, {lat: 46.1181, lng: -72.9215}] as [{ lat: number; lng: number; }, { lat: number; lng: number; }];
94+
return [{ lat: 45.2229, lng: -74.3230 }, { lat: 46.1181, lng: -72.9215 }] as [{ lat: number; lng: number; }, { lat: number; lng: number; }];
9195
},
9296
invalidGeocodingResultTypes: [
93-
'political',
97+
'political',
9498
'country',
9599
],
96100
updateDefaultValueWhenResponded: true
@@ -100,7 +104,7 @@ describe('Render InputMapPoint with various parameters', () => {
100104
id={'test'}
101105
onValueChange={() => { /* nothing to do */}}
102106
widgetConfig={testWidgetConfig}
103-
value={{ type: 'Feature' as const, properties: {}, geometry: { type: 'Point' as const, coordinates: [-73.1, 45.02]}}}
107+
value={{ type: 'Feature' as const, properties: {}, geometry: { type: 'Point' as const, coordinates: [-73.1, 45.02] } }}
104108
inputRef={React.createRef()}
105109
interview={interviewAttributes}
106110
user={userAttributes}
@@ -109,7 +113,7 @@ describe('Render InputMapPoint with various parameters', () => {
109113
);
110114
expect(container).toMatchSnapshot();
111115
});
112-
116+
113117
});
114118

115119
describe('Test geocoding requests', () => {
@@ -120,8 +124,8 @@ describe('Test geocoding requests', () => {
120124
const testWidgetConfig = Object.assign({
121125
geocodingQueryString: jest.fn().mockReturnValue(geocodingString),
122126
refreshGeocodingLabel: {
123-
fr: `Geocode`,
124-
en: `Geocode`
127+
fr: 'Geocode',
128+
en: 'Geocode'
125129
},
126130
icon: {
127131
url: 'path/to/icon',
@@ -135,18 +139,18 @@ describe('Test geocoding requests', () => {
135139
updateDefaultValueWhenResponded: true
136140
}, baseWidgetConfig);
137141

138-
const placeFeature1 = {
139-
type: 'Feature' as const,
142+
const placeFeature1 = {
143+
type: 'Feature' as const,
140144
geometry: { type: 'Point' as const, coordinates: [-73.2, 45.1] },
141145
properties: { placeData: { place_id: '1', formatted_address: '123 test street', name: 'Foo extra good restaurant' } }
142146
};
143147
const placeFeature2 = {
144-
type: 'Feature' as const,
148+
type: 'Feature' as const,
145149
geometry: { type: 'Point' as const, coordinates: [-73.2, 45.1] },
146150
properties: { placeData: { place_id: '2', formatted_address: '123 foo street', types: ['street_address'] } }
147151
};
148152
const placeFeature3 = {
149-
type: 'Feature' as const,
153+
type: 'Feature' as const,
150154
geometry: { type: 'Point' as const, coordinates: [ -73.5673919, 45.5018869] },
151155
properties: { placeData: { place_id: '3', formatted_address: 'Montreal, QC, Canada', types: ['locality', 'political'] } }
152156
};
@@ -157,12 +161,12 @@ describe('Test geocoding requests', () => {
157161
});
158162

159163
test('Geocode with multiple results', async () => {
160-
164+
161165
const { container } = render(<InputMapFindPlace
162166
id={testId}
163167
onValueChange={mockOnValueChange}
164168
widgetConfig={testWidgetConfig}
165-
value={{ type: 'Feature' as const, properties: {}, geometry: { type: 'Point' as const, coordinates: [-73.1, 45.02]}}}
169+
value={{ type: 'Feature' as const, properties: {}, geometry: { type: 'Point' as const, coordinates: [-73.1, 45.02] } }}
166170
inputRef={React.createRef()}
167171
interview={interviewAttributes}
168172
user={userAttributes}
@@ -189,27 +193,27 @@ describe('Test geocoding requests', () => {
189193

190194
// Make sure the value has not been changed
191195
expect(mockOnValueChange).not.toHaveBeenCalled();
192-
196+
193197
});
194198

195199
test('Geocode with single results, and confirm result', async () => {
196-
200+
197201
const { container } = render(<InputMapFindPlace
198202
id={testId}
199203
onValueChange={mockOnValueChange}
200204
widgetConfig={testWidgetConfig}
201-
value={{ type: 'Feature' as const, properties: {}, geometry: { type: 'Point' as const, coordinates: [-73.1, 45.02]}}}
205+
value={{ type: 'Feature' as const, properties: {}, geometry: { type: 'Point' as const, coordinates: [-73.1, 45.02] } }}
202206
inputRef={React.createRef()}
203207
interview={interviewAttributes}
204208
user={userAttributes}
205209
path='foo.test'
206210
loadingState={0}
207211
/>);
208-
212+
209213
const user = userEvent.setup();
210214

211215
// Find and click on the Geocode button, to return a single result
212-
mockedGeocode.mockResolvedValueOnce([placeFeature1]);
216+
mockedGeocode.mockResolvedValueOnce([placeFeature1]);
213217
await user.click(screen.getByText('Geocode'));
214218
expect(mockedGeocode).toHaveBeenCalledTimes(1);
215219
expect(mockedGeocode).toHaveBeenCalledWith(geocodingString, expect.anything());
@@ -225,7 +229,7 @@ describe('Test geocoding requests', () => {
225229
expect(mockOnValueChange).toHaveBeenCalledWith({ target: { value: {
226230
type: 'Feature' as const,
227231
geometry: placeFeature1.geometry,
228-
properties: {
232+
properties: {
229233
lastAction: 'findPlace',
230234
geocodingQueryString: geocodingString,
231235
geocodingResultsData: {
@@ -234,20 +238,20 @@ describe('Test geocoding requests', () => {
234238
types: undefined,
235239
}
236240
}
237-
}}})
241+
} } });
238242

239243
// There should not be any selection or confirm widgets anymore
240244
expect(container.querySelector('select')).not.toBeInTheDocument();
241245
expect(screen.queryByText('ConfirmLocation')).not.toBeInTheDocument();
242246
});
243247

244248
test('Geocode with single result, then re-query with undefined results', async () => {
245-
249+
246250
const { container } = render(<InputMapFindPlace
247251
id={testId}
248252
onValueChange={mockOnValueChange}
249253
widgetConfig={testWidgetConfig}
250-
value={{ type: 'Feature' as const, properties: {}, geometry: { type: 'Point' as const, coordinates: [-73.1, 45.02]}}}
254+
value={{ type: 'Feature' as const, properties: {}, geometry: { type: 'Point' as const, coordinates: [-73.1, 45.02] } }}
251255
inputRef={React.createRef()}
252256
interview={interviewAttributes}
253257
user={userAttributes}
@@ -270,7 +274,7 @@ describe('Test geocoding requests', () => {
270274
expect(selectionList).toBeInTheDocument();
271275

272276
expect(screen.getByText('ConfirmLocation')).toBeInTheDocument();
273-
277+
274278
// Click the geocode button again, but get undefined values
275279
mockedGeocode.mockResolvedValueOnce(undefined);
276280
const newGeocodingString = 'other string';
@@ -282,16 +286,16 @@ describe('Test geocoding requests', () => {
282286
expect(selectionList2).not.toBeInTheDocument();
283287

284288
expect(screen.queryByText('ConfirmLocation')).not.toBeInTheDocument();
285-
289+
286290
});
287291

288292
test('Geocode with single result, then re-query with rejection', async () => {
289-
293+
290294
const { container } = render(<InputMapFindPlace
291295
id={testId}
292296
onValueChange={mockOnValueChange}
293297
widgetConfig={testWidgetConfig}
294-
value={{ type: 'Feature' as const, properties: {}, geometry: { type: 'Point' as const, coordinates: [-73.1, 45.02]}}}
298+
value={{ type: 'Feature' as const, properties: {}, geometry: { type: 'Point' as const, coordinates: [-73.1, 45.02] } }}
295299
inputRef={React.createRef()}
296300
interview={interviewAttributes}
297301
user={userAttributes}
@@ -325,23 +329,23 @@ describe('Test geocoding requests', () => {
325329
expect(selectionList2).not.toBeInTheDocument();
326330

327331
expect(screen.queryByText('ConfirmLocation')).not.toBeInTheDocument();
328-
332+
329333
});
330334

331335
test('Click the geocode button, that triggers an update', async () => {
332-
336+
333337
const props = {
334338
id: testId,
335339
onValueChange: mockOnValueChange,
336340
widgetConfig: testWidgetConfig,
337-
value: { type: 'Feature' as const, properties: {}, geometry: { type: 'Point' as const, coordinates: [-73.1, 45.02]}},
341+
value: { type: 'Feature' as const, properties: {}, geometry: { type: 'Point' as const, coordinates: [-73.1, 45.02] } },
338342
inputRef: React.createRef() as React.LegacyRef<HTMLInputElement>,
339343
size: 'medium' as const,
340344
interview: interviewAttributes,
341345
user: userAttributes,
342346
path: 'foo.test',
343347
loadingState: 0,
344-
}
348+
};
345349

346350
const { container } = render(<InputMapFindPlace {...props} />);
347351
const user = userEvent.setup();
@@ -359,13 +363,13 @@ describe('Test geocoding requests', () => {
359363
expect(selectionList).toBeInTheDocument();
360364

361365
expect(screen.getByText('ConfirmLocation')).toBeInTheDocument();
362-
366+
363367
});
364368

365369
test('Geocode with single imprecise result', async () => {
366370
const widgetConfig = Object.assign({
367371
invalidGeocodingResultTypes: [
368-
'political',
372+
'political',
369373
'country',
370374
'administrative_area_level_1',
371375
'administrative_area_level_2',
@@ -411,7 +415,7 @@ describe('Test geocoding requests', () => {
411415
expect(mockOnValueChange).toHaveBeenCalledWith({ target: { value: {
412416
type: 'Feature' as const,
413417
geometry: placeFeature3.geometry,
414-
properties: {
418+
properties: {
415419
lastAction: 'findPlace',
416420
geocodingQueryString: geocodingString,
417421
geocodingResultsData: {
@@ -421,7 +425,7 @@ describe('Test geocoding requests', () => {
421425
},
422426
isGeocodingImprecise: true, // key part!
423427
}
424-
}}})
428+
} } });
425429

426430
// Select list and confirm button should not be present
427431
expect(container.querySelector('select')).not.toBeInTheDocument();

packages/evolution-frontend/src/components/inputs/defaultInputBase.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ export const inputMapFindPlaceBase: Pick<
128128
| 'containsHtml'
129129
| 'autoConfirmIfSingleResult'
130130
| 'placesIcon'
131+
| 'selectedIcon'
131132
| 'defaultCenter'
132133
| 'refreshGeocodingLabel'
133134
| 'showSearchPlaceButton'
@@ -143,6 +144,10 @@ export const inputMapFindPlaceBase: Pick<
143144
url: () => '/dist/images/activities_icons/default_marker.svg',
144145
size: [70, 70]
145146
},
147+
selectedIcon: {
148+
url: () => '/dist/images/activities_icons/default_selected_marker.svg',
149+
size: [70, 70]
150+
},
146151
defaultCenter: config.mapDefaultCenter,
147152
refreshGeocodingLabel: (t: TFunction) => t('customLabel:RefreshGeocodingLabel'),
148153
showSearchPlaceButton: () => true,

0 commit comments

Comments
 (0)