Skip to content

Commit 2104b82

Browse files
authored
harden map accessibility and e2e stability (#84)
* stabilise initial map pin fetch * improve map accessibility
1 parent 3903284 commit 2104b82

9 files changed

Lines changed: 66 additions & 8 deletions

File tree

messages/de.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -500,6 +500,10 @@
500500
"Map": {
501501
"drawerTitle": "Eintragsdetails",
502502
"drawerDescription": "Details zum ausgewählten Eintrag.",
503+
"mapRegionLabel": "Kompostkarte",
504+
"controlsLabel": "Kartensteuerung",
505+
"zoomControlsLabel": "Zoomsteuerung der Karte",
506+
"markerLabel": "Kartenmarkierung",
503507
"emptyTitle": "Nichts gefunden",
504508
"emptyBody": "Der Eintrag, den du suchst, existiert nicht oder wurde entfernt. Tut uns leid.",
505509
"searchPlaceholder": "Strasse oder Ort suchen",

messages/en.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -500,6 +500,10 @@
500500
"Map": {
501501
"drawerTitle": "Listing details",
502502
"drawerDescription": "Selected listing details.",
503+
"mapRegionLabel": "Composting map",
504+
"controlsLabel": "Map controls",
505+
"zoomControlsLabel": "Map zoom controls",
506+
"markerLabel": "Map marker",
503507
"emptyTitle": "Coming up empty",
504508
"emptyBody": "The listing you’re looking for doesn’t exist or has been removed. Sorry to disappoint.",
505509
"searchPlaceholder": "Search for a street or place",

messages/es.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -500,6 +500,10 @@
500500
"Map": {
501501
"drawerTitle": "Detalles del anuncio",
502502
"drawerDescription": "Detalles del anuncio seleccionado.",
503+
"mapRegionLabel": "Mapa de compostaje",
504+
"controlsLabel": "Controles del mapa",
505+
"zoomControlsLabel": "Controles de zoom del mapa",
506+
"markerLabel": "Marcador del mapa",
503507
"emptyTitle": "No encontramos nada",
504508
"emptyBody": "El anuncio que buscas no existe o fue eliminado.",
505509
"searchPlaceholder": "Busca una calle o un lugar",

messages/fr.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -500,6 +500,10 @@
500500
"Map": {
501501
"drawerTitle": "Détails de l’annonce",
502502
"drawerDescription": "Détails de l’annonce sélectionnée.",
503+
"mapRegionLabel": "Carte du compostage",
504+
"controlsLabel": "Commandes de la carte",
505+
"zoomControlsLabel": "Commandes de zoom de la carte",
506+
"markerLabel": "Repère de carte",
503507
"emptyTitle": "Rien à l’horizon",
504508
"emptyBody": "L’annonce que vous cherchez n’existe pas ou a été supprimée. Désolé pour la déception.",
505509
"searchPlaceholder": "Rechercher une rue ou un lieu",

messages/pt-BR.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -500,6 +500,10 @@
500500
"Map": {
501501
"drawerTitle": "Detalhes do anúncio",
502502
"drawerDescription": "Detalhes do anúncio selecionado.",
503+
"mapRegionLabel": "Mapa de compostagem",
504+
"controlsLabel": "Controles do mapa",
505+
"zoomControlsLabel": "Controles de zoom do mapa",
506+
"markerLabel": "Marcador do mapa",
503507
"emptyTitle": "Nada por aqui",
504508
"emptyBody": "O anúncio que você procura não existe ou foi removido. Desculpe a decepção.",
505509
"searchPlaceholder": "Busque uma rua ou um lugar",

src/components/LocationSelect/LocationSelect.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,7 @@ export default function LocationSelect({
375375
<MapPin type={listingType} selected={true} />
376376
</Marker>
377377
<MapZoomControls
378+
controlsLabel={t("Map.zoomControlsLabel")}
378379
onZoomIn={() => mapRef.current?.getMap().zoomIn()}
379380
onZoomOut={() => mapRef.current?.getMap().zoomOut()}
380381
zoomInLabel={t("Map.zoomInControl")}

src/features/map/components/MapControls.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
import { theme } from "@/styles/theme.yak";
1212

1313
type MapControlClusterProps = {
14+
controlsLabel: string;
1415
locateActive?: boolean;
1516
locateLabel?: string;
1617
onLocate?: () => void;
@@ -21,9 +22,11 @@ type MapControlClusterProps = {
2122
zoomInDisabled?: boolean;
2223
zoomInLabel: string;
2324
zoomOutLabel: string;
25+
zoomControlsLabel: string;
2426
};
2527

2628
type MapZoomControlsProps = {
29+
controlsLabel: string;
2730
onZoomIn: () => void;
2831
onZoomOut: () => void;
2932
zoomInDisabled?: boolean;
@@ -139,6 +142,7 @@ const ZoomButton = styled.button`
139142
`;
140143

141144
export function MapZoomControls({
145+
controlsLabel,
142146
onZoomIn,
143147
onZoomOut,
144148
zoomInDisabled = false,
@@ -148,6 +152,7 @@ export function MapZoomControls({
148152
return (
149153
<ControlAnchor>
150154
<ZoomControlGroup
155+
controlsLabel={controlsLabel}
151156
onZoomIn={onZoomIn}
152157
onZoomOut={onZoomOut}
153158
zoomInDisabled={zoomInDisabled}
@@ -159,14 +164,15 @@ export function MapZoomControls({
159164
}
160165

161166
function ZoomControlGroup({
167+
controlsLabel,
162168
onZoomIn,
163169
onZoomOut,
164170
zoomInDisabled = false,
165171
zoomInLabel,
166172
zoomOutLabel,
167173
}: MapZoomControlsProps) {
168174
return (
169-
<ZoomGroup>
175+
<ZoomGroup role="group" aria-label={controlsLabel}>
170176
<ZoomButton
171177
type="button"
172178
aria-label={zoomInLabel}
@@ -191,6 +197,7 @@ function ZoomControlGroup({
191197
}
192198

193199
export default function MapControls({
200+
controlsLabel,
194201
locateActive = false,
195202
locateLabel,
196203
onLocate,
@@ -200,10 +207,11 @@ export default function MapControls({
200207
searchLabel,
201208
zoomInDisabled = false,
202209
zoomInLabel,
210+
zoomControlsLabel,
203211
zoomOutLabel,
204212
}: MapControlClusterProps) {
205213
return (
206-
<ControlAnchor $gap>
214+
<ControlAnchor $gap role="group" aria-label={controlsLabel}>
207215
{onSearch && searchLabel ? (
208216
<ControlButton
209217
type="button"
@@ -228,6 +236,7 @@ export default function MapControls({
228236
</ControlButton>
229237
) : null}
230238
<ZoomControlGroup
239+
controlsLabel={zoomControlsLabel}
231240
onZoomIn={onZoomIn}
232241
onZoomOut={onZoomOut}
233242
zoomInDisabled={zoomInDisabled}

src/features/map/components/MapPinLayer.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { hasValidCoordinates } from "../lib/mapUtils";
1515

1616
type MapPinLayerProps = {
1717
listings: ListingMarker[];
18+
markerLabel: string;
1819
selectedListingId: number | null;
1920
onMarkerClick: (listing: ListingMarker) => void;
2021
};
@@ -23,6 +24,7 @@ type ListingMapPinMarkerProps = {
2324
listing: ListingMarker;
2425
coords: ListingCoordinates;
2526
isSelected: boolean;
27+
markerLabel: string;
2628
onMarkerClick: MapPinLayerProps["onMarkerClick"];
2729
};
2830

@@ -36,6 +38,7 @@ function ListingMapPinMarker({
3638
listing,
3739
coords,
3840
isSelected,
41+
markerLabel,
3942
onMarkerClick,
4043
}: ListingMapPinMarkerProps) {
4144
const markerRef = useRef<MarkerInstance | null>(null);
@@ -51,6 +54,8 @@ function ListingMapPinMarker({
5154
if (!markerElement) return;
5255

5356
markerElement.tabIndex = 0;
57+
markerElement.setAttribute("role", "button");
58+
markerElement.setAttribute("aria-label", markerLabel);
5459

5560
const activateFromKeyboard = (event: KeyboardEvent) => {
5661
lastKeyboardActivationRef.current = event.timeStamp;
@@ -86,10 +91,13 @@ function ListingMapPinMarker({
8691

8792
return () => {
8893
isSpaceActivationPendingRef.current = false;
94+
markerElement.removeAttribute("aria-label");
95+
markerElement.removeAttribute("role");
96+
markerElement.removeAttribute("tabindex");
8997
markerElement.removeEventListener("keydown", handleMarkerKeyDown);
9098
markerElement.removeEventListener("keyup", handleMarkerKeyUp);
9199
};
92-
}, [listing, onMarkerClick]);
100+
}, [listing, markerLabel, onMarkerClick]);
93101

94102
const handlePinClick = (event: ReactMouseEvent<HTMLDivElement>) => {
95103
event.preventDefault();
@@ -146,6 +154,7 @@ function ListingMapPinMarker({
146154

147155
export default function MapPinLayer({
148156
listings,
157+
markerLabel,
149158
selectedListingId,
150159
onMarkerClick,
151160
}: MapPinLayerProps) {
@@ -163,6 +172,7 @@ export default function MapPinLayer({
163172
listing={listing}
164173
coords={coords}
165174
isSelected={isSelected}
175+
markerLabel={markerLabel}
166176
onMarkerClick={onMarkerClick}
167177
/>
168178
);

src/features/map/components/MapView.tsx

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ export default function MapView({
220220
const initialMapPinZoomStyleRef = useRef<MapPinZoomStyle | null>(null);
221221
const pendingPinZoomRef = useRef<number | null>(null);
222222
const pinZoomAnimationFrameRef = useRef<number | null>(null);
223+
const hasSyncedIdleBoundsRef = useRef(false);
223224
const saveMapViewTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(
224225
null
225226
);
@@ -350,10 +351,17 @@ export default function MapView({
350351

351352
const handleLoad = useCallback(() => {
352353
handleMapLoad();
353-
const map = syncCurrentMapState();
354-
map?.once("idle", syncCurrentMapState);
354+
hasSyncedIdleBoundsRef.current = false;
355+
syncCurrentMapState();
355356
}, [handleMapLoad, syncCurrentMapState]);
356357

358+
const handleIdle = useCallback(() => {
359+
if (hasSyncedIdleBoundsRef.current) return;
360+
361+
hasSyncedIdleBoundsRef.current = true;
362+
syncCurrentMapState();
363+
}, [syncCurrentMapState]);
364+
357365
useEffect(() => {
358366
if (initialCoordinates) {
359367
lastSavedMapViewRef.current = initialCoordinates;
@@ -485,6 +493,8 @@ export default function MapView({
485493
return (
486494
<MapContainer
487495
ref={mapContainerRef}
496+
role="region"
497+
aria-label={t("mapRegionLabel")}
488498
data-testid="map-view"
489499
style={initialMapPinZoomStyleRef.current ?? undefined}
490500
>
@@ -501,6 +511,7 @@ export default function MapView({
501511
onZoom={handleMove}
502512
onMoveEnd={handleMoveEnd}
503513
onLoad={handleLoad}
514+
onIdle={handleIdle}
504515
onError={handleMapError}
505516
onClick={handleMapClickInternal}
506517
>
@@ -515,6 +526,7 @@ export default function MapView({
515526

516527
<MapPinLayer
517528
listings={listings}
529+
markerLabel={t("markerLabel")}
518530
selectedListingId={selectedListingId}
519531
onMarkerClick={onMarkerClick}
520532
/>
@@ -524,12 +536,13 @@ export default function MapView({
524536
latitude={userCoordinates.latitude}
525537
anchor="center"
526538
>
527-
<UserLocationDot />
539+
<UserLocationDot aria-hidden="true" />
528540
</Marker>
529541
) : null}
530542
</Map>
531543

532544
<MapControls
545+
controlsLabel={t("controlsLabel")}
533546
locateActive={Boolean(userCoordinates)}
534547
locateLabel={t("locateControl")}
535548
onLocate={handleLocate}
@@ -539,6 +552,7 @@ export default function MapView({
539552
searchLabel={t("searchLabel")}
540553
zoomInDisabled={isZoomInDisabled}
541554
zoomInLabel={t("zoomInControl")}
555+
zoomControlsLabel={t("zoomControlsLabel")}
542556
zoomOutLabel={t("zoomOutControl")}
543557
/>
544558
<MapSearch
@@ -549,11 +563,15 @@ export default function MapView({
549563
/>
550564
</>
551565
) : (
552-
<LoadingChip>{t("loadingPins")}</LoadingChip>
566+
<LoadingChip role="status" aria-live="polite">
567+
{t("loadingPins")}
568+
</LoadingChip>
553569
)}
554570

555571
{hasInitialPosition && isFetching && (
556-
<LoadingChip>{t("loadingPins")}</LoadingChip>
572+
<LoadingChip role="status" aria-live="polite">
573+
{t("loadingPins")}
574+
</LoadingChip>
557575
)}
558576

559577
{showReturnButton && (

0 commit comments

Comments
 (0)