Skip to content

ERA-11237: Conditional display of event detail location markers on map #1258

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Mar 24, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/CursorGpsDisplay/MenuPopover/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ const MenuPopover = ({ buttonRef, className, onClose, ...otherProps }, ref) => {
}
};

const onGpsInputButtonClick = (event) => {
event.stopPropagation();

onJumpToCoordinates();
};

useEffect(() => {
// Select the GPS input on mount so user can type away or navigate.
gpsInputRef.current.select();
Expand Down Expand Up @@ -115,7 +121,7 @@ const MenuPopover = ({ buttonRef, className, onClose, ...otherProps }, ref) => {
aria-label={t('gpsInputButtonLabel')}
className={styles.gpsInputButton}
disabled={!gpsInputValue}
onClick={() => onJumpToCoordinates()}
onClick={onGpsInputButtonClick}
ref={gpsInputButtonRef}
title={t('gpsInputButtonLabel')}
type="button"
Expand Down
1 change: 1 addition & 0 deletions src/LocationPicker/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ const LocationPicker = ({
className={`${styles.input} ${readOnly ? styles.readOnly : ''}`}
disabled={disabled}
id={id}
onFocus={() => setLocationButtonRef.current.focus()}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For form data management, the id must be appended to the <input> but we forward the focusing to the button so when we programmatically focus the element by the id (like when we click a location marker) we see the location picker button being focused.

placeholder={placeholder || t('defaultPlaceholder')}
readOnly
required={required}
Expand Down
2 changes: 1 addition & 1 deletion src/LocationPicker/styles.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
background-color: colors.$disabled-field-gray;
}

&:focus-visible:not(:disabled) {
&:focus:not(:disabled) {
border: 2px solid colors.$bright-blue;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const FormModal = ({
breadcrumbs,
columns,
errors,
focusLocationMarker,
formData,
isOpen,
itemName,
Expand Down Expand Up @@ -74,6 +75,7 @@ const FormModal = ({
formData[fieldId],
onFieldChange,
errors?.[fieldId],
focusLocationMarker,
[...breadcrumbs, { display: title, id: fieldId }]
))}
</div>
Expand All @@ -87,6 +89,7 @@ const FormModal = ({
formData[fieldId],
onFieldChange,
errors?.[fieldId],
focusLocationMarker,
[...breadcrumbs, { display: title, id: fieldId }]
))}
</div>}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const Item = ({
collectionDetails,
errors,
fields,
focusLocationMarker,
formData,
id,
isDragging = false,
Expand Down Expand Up @@ -118,7 +119,13 @@ const Item = ({
+ (isDragging ? ` ${styles.isDragging}` : '')
+ (isDragOverlay ? ` ${styles.dragOverlay}` : '')
+ (hasError ? ` ${styles.error}` : '');
return <li className={itemClassName} data-testid="schema-form-collection-item" ref={ref} {...otherProps}>
return <li
className={itemClassName}
data-testid="schema-form-collection-item"
id={`${collectionDetails.value}.${id}`}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add an id to the collection items so we can focus them programmatically when a user clicks a location marker which has its corresponding location field inside a collection item.

ref={ref}
{...otherProps}
>
<div className={styles.header}>
<div
aria-controls={`collectionForm-${title}`}
Expand Down Expand Up @@ -189,6 +196,7 @@ const Item = ({
{!isDragOverlay && <FormModal
breadcrumbs={breadcrumbs}
columns={collectionDetails.columns}
focusLocationMarker={focusLocationMarker}
formData={formData}
errors={errors}
isOpen={isFormModalOpen}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
}
}

&:focus-visible:not(.isDragging) {
&:focus:not(.isDragging) {
outline: colors.$bright-blue auto 1px;

.header {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const SortableList = ({
breadcrumbs,
collectionDetails,
fields,
focusLocationMarker,
items,
onItemChange,
onItemDelete,
Expand Down Expand Up @@ -144,6 +145,7 @@ const SortableList = ({
collectionDetails={collectionDetails}
errors={item.error}
fields={fields}
focusLocationMarker={focusLocationMarker(index)}
formData={item.formData}
id={item.id}
isFormModalOpen={item.isFormModalOpen}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const Collection = ({
details,
error,
fields,
focusLocationMarker,
id,
onFieldChange,
renderField,
Expand Down Expand Up @@ -131,6 +132,11 @@ const Collection = ({
setItems([...items, { id: lastAddedItemIdRef.current, isFormModalOpen: true, isFormPreviewOpen: false }]);
};

// If a location field from an item requests to focus its location marker, prefix the marker id with the collection
// id and the item index.
const focusLocationMarkerFromItem = (itemIndex) => (markerId) =>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Location fields inside collection items need to have prefixed ids to make them unique, otherwise several items may have the location field set and have their ids clashing, so we propagate the callbacks prefixing the ids like: collection1.2.nested-collection.1.item. Notice that, since we decided to allow every single character in the values of a field in the EFB, there may be scenarios where this is not enough to make ids unique (a user could decide to set the value of a field like: collection.1.field, the chances are extremely low, but its possible). We should have prohibited some characters 😅

focusLocationMarker(`${id}.${itemIndex}.${markerId}`);

return <div
aria-errormessage={hasError ? `${id}-description` : undefined}
aria-labelledby={`${id}-label`}
Expand Down Expand Up @@ -168,6 +174,7 @@ const Collection = ({
breadcrumbs={breadcrumbs}
collectionDetails={details}
fields={fields}
focusLocationMarker={focusLocationMarkerFromItem}
// Merge the value, error and items array into a single array of item objects.
items={items
.filter((_, index) => !!value[index])
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import React, { memo } from 'react';
import React, { memo, useEffect } from 'react';

import LocationPicker from '../../../../../LocationPicker';

import styles from './styles.module.scss';

const Location = ({
autofillDefaultInput: _autofillDefaultInput,
blurLocationMarker,
details,
error,
focusLocationMarker,
id,
onFieldChange,
value = null,
Expand All @@ -16,6 +17,10 @@ const Location = ({
const hasDescription = !!details.description && !hasError;
const label = details.isRequired ? `${details.label} *` : details.label;

// When closing a collection item form modal, the location fields get unmounted without triggering the blur event, so
// we need to blur the location markers manually.
useEffect(() => () => blurLocationMarker(), [blurLocationMarker]);

return <div className={styles.text} data-testid={`schema-form-location-field-${id}`}>
<label className={`${styles.label} ${hasError ? styles.error : ''}`} htmlFor={id}>{label}</label>

Expand All @@ -27,7 +32,9 @@ const Location = ({
'aria-invalid': hasError,
'aria-required': details.isRequired,
}}
onBlur={() => blurLocationMarker()}
onChange={(newLocation) => onFieldChange(id, newLocation || undefined)}
onFocus={() => focusLocationMarker(id)}
value={value}
/>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,16 @@ import styles from './styles.module.scss';

// Sections are just visual elements that are not present in the form data structure. Thus, fields contained by
// sections are in the root objects of form data and field errors.
const Section = ({ details, fieldErrors, formData, id, onFieldChange, onFieldErrorsChange, renderField }) => {
const Section = ({
details,
fieldErrors,
focusLocationMarker,
formData,
id,
onFieldChange,
onFieldErrorsChange,
renderField,
}) => {
const onColumnFieldChange = (fieldId, value, error) => {
onFieldChange(fieldId, value);
onFieldErrorsChange({ ...fieldErrors, [fieldId]: error });
Expand All @@ -25,7 +34,8 @@ const Section = ({ details, fieldErrors, formData, id, onFieldChange, onFieldErr
fieldId,
formData[fieldId],
onColumnFieldChange,
fieldErrors[fieldId]
fieldErrors[fieldId],
focusLocationMarker
))}
</div>

Expand All @@ -37,7 +47,8 @@ const Section = ({ details, fieldErrors, formData, id, onFieldChange, onFieldErr
fieldId,
formData[fieldId],
onColumnFieldChange,
fieldErrors[fieldId]
fieldErrors[fieldId],
focusLocationMarker
))}
</div>}
</div>
Expand Down
66 changes: 63 additions & 3 deletions src/ReportManager/DetailsSection/SchemaForm/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'

import { FORM_ELEMENT_TYPES, ROOT_CANVAS_ID } from './constants';
import makeFieldsFromSchema from './utils/makeFieldsFromSchema';
import useLocationMarkersLayer from './utils/useLocationMarkersLayer';
import useSchemaValidations from './utils/useSchemaValidations';

import Collection from './fields/Collection';
Expand All @@ -16,13 +17,13 @@ import Text from './fields/Text';
export const FIELDS = {
[FORM_ELEMENT_TYPES.CHOICE_LIST]: ChoiceList,
[FORM_ELEMENT_TYPES.DATE_TIME]: DateTime,
[FORM_ELEMENT_TYPES.LOCATION]: Location,
[FORM_ELEMENT_TYPES.NUMERIC]: Numeric,
[FORM_ELEMENT_TYPES.TEXT]: Text,
};

const SchemaForm = ({
autofillDefaultInputs,
eventLocation,
initialFormData,
onFormDataChange,
onFormSubmit,
Expand All @@ -31,6 +32,27 @@ const SchemaForm = ({
}) => {
const runValidations = useSchemaValidations(schema);

const onLocationMarkerClick = useCallback((markerId) => {
const locationField = document.getElementById(markerId);
if (locationField) {
// If the location field that corresponds to the clicked marker is contained directly by a section, its element
// will be focusable.
locationField.focus();
} else {
// If the location field is nested in a collection, we try to calculate the id of the collection item that
// contains it to focus it.
const markerIdPathParts = markerId.split('.');
const collectionItemId = `${markerIdPathParts[0]}.${markerIdPathParts[1]}`;
document.getElementById(collectionItemId)?.focus();
}
}, []);

const {
blurLocationMarker,
focusLocationMarker,
updateLocationMarkers
} = useLocationMarkersLayer(eventLocation, onLocationMarkerClick);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we use the new hook, we pass the event location and the callback to focus the corresponding fields when clicking a marker, and receive the methods to focus, blur and update the markers.


// This ref works as a flag to trigger a useEffect and call onFormDataChange asynchronously when there are changes in
// the form data, so we can keep the onSectionFieldChange dependency array empty.
const shouldSendFormDataChangeRef = useRef(false);
Expand Down Expand Up @@ -65,8 +87,9 @@ const SchemaForm = ({
};

// This method is designed to render fields inside sections and collections. In order to support recursion we let the
// parents handle the propagation of values, change callbacks, errors, breadcrumbs (only for collections), etc...
const renderField = (id, value, onChange, error, breadcrumbs = []) => {
// parents handle the propagation of values, change callbacks, errors, focusing of location markers and breadcrumbs
// (only for collections).
const renderField = (id, value, onChange, error, focusLocationMarker, breadcrumbs = []) => {
switch (fields[id].type) {
case FORM_ELEMENT_TYPES.HEADER:
return <Header details={fields[id].details} id={id} key={id} />;
Expand All @@ -77,13 +100,26 @@ const SchemaForm = ({
details={fields[id].details}
error={error}
fields={fields}
focusLocationMarker={focusLocationMarker}
id={id}
key={id}
onFieldChange={onChange}
renderField={renderField}
value={value}
/>;

case FORM_ELEMENT_TYPES.LOCATION:
return <Location
blurLocationMarker={blurLocationMarker}
details={fields[id].details}
error={error}
focusLocationMarker={focusLocationMarker}
id={id}
key={id}
onFieldChange={onChange}
value={value}
/>;

default:
const Field = FIELDS[fields[id].type];
return <Field
Expand All @@ -106,10 +142,34 @@ const SchemaForm = ({
}
}, [formData, onFormDataChange]);

useEffect(() => {
// Update the location markers whenever there is a change.
const locationMarkers = {};
const addLocationMarkersFromFormDataRecursively = (formData, idPrefix = '') => {
// Iterate the fields.
Object.entries(formData).forEach(([fieldId, fieldValue]) => {
if (fields[fieldId]?.type === FORM_ELEMENT_TYPES.LOCATION && fieldValue) {
// If the field is a location with a value, add it to the location markers.
locationMarkers[`${idPrefix}${fieldId}`] = fieldValue;
} else if (fields[fieldId]?.type === FORM_ELEMENT_TYPES.COLLECTION) {
// If the field is a collection, add the location markers for each collection item.
fieldValue.forEach((itemFormData, index) => addLocationMarkersFromFormDataRecursively(
itemFormData,
`${idPrefix}${fieldId}.${index}.`
));
}
});
};
addLocationMarkersFromFormDataRecursively(formData);

updateLocationMarkers(locationMarkers);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we update the markers every time the form data changes.

}, [fields, formData, updateLocationMarkers]);

return <form onSubmit={onSubmit}>
{fields[ROOT_CANVAS_ID]?.details.fields.map((sectionId) => <Section
details={fields[sectionId].details}
fieldErrors={fieldErrors}
focusLocationMarker={focusLocationMarker}
formData={formData}
id={sectionId}
key={sectionId}
Expand Down
Loading