Skip to content

Commit 06a3971

Browse files
authored
Merge pull request #531 from ibi-group/location-field-error-a11y
Location field error a11y
2 parents d9038b2 + 92a081d commit 06a3971

File tree

12 files changed

+3054
-1162
lines changed

12 files changed

+3054
-1162
lines changed

__snapshots__/storybook.test.ts.snap

Lines changed: 2819 additions & 1093 deletions
Large diffs are not rendered by default.

packages/location-field/i18n/en-US.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ otpUi:
1111
LocationField:
1212
beingTypingPrompt: Begin typing to search for locations
1313
clearLocation: Clear location
14-
currentLocationUnavailable: Current location not available ({error})
14+
currentLocationUnavailable: Current location not available{error, select, undefined {} other { ({error})}}
1515
fetchingLocation: Fetching location...
1616
geocoderUnreachable: Could not reach geocoder{error, select, undefined {} other { ({error})}}
1717
homeLocation: Home
@@ -26,6 +26,9 @@ otpUi:
2626
# Note to translator: This is an implicit plural (as in "Other results").
2727
other: Other
2828
recentlySearched: Recently Searched
29+
resultsFound: >
30+
{count, plural, =0 {No results} =1 {# geocoder result} other {# geocoder results}}
31+
found for {input, select, null {your query} other {"{input}"}}
2932
suggestedLocations: Suggested locations
3033
suggestedLocationsLong: Toggle displaying the list of suggested locations
3134
stations: Stations

packages/location-field/i18n/es.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ otpUi:
22
LocationField:
33
useCurrentLocation: Usar ubicación actual
44
workLocation: Ubicación de trabajo
5-
currentLocationUnavailable: Ubicación actual no disponible ({error})
5+
currentLocationUnavailable: Ubicación actual no disponible{error, select, undefined {} other { ({error})}}
66
fetchingLocation: Buscando la ubicación...
77
beingTypingPrompt: Comience a escribir para buscar ubicaciones
88
clearLocation: Restablecer la ubicación
@@ -20,3 +20,6 @@ otpUi:
2020
noResultsFound:
2121
"No se han encontrado resultados para {input, select,\n null {su\
2222
\ consulta}\n other {{input}}\n }\n"
23+
resultsFound: >
24+
{count, plural, =0 {Ningún resultado} =1 {# resultado} other {# resultados} del
25+
geocodificador para {input, select, null {su consulta} other {"{input}"}}

packages/location-field/i18n/fr.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ otpUi:
1111
LocationField:
1212
beingTypingPrompt: Tapez pour rechercher des lieux
1313
clearLocation: Effacer le lieu
14-
currentLocationUnavailable: Position actuelle non disponible ({error})
14+
currentLocationUnavailable: Position actuelle non disponible{error, select, undefined {} other { ({error})}}
1515
fetchingLocation: Chargement du lieu...
1616
geocoderUnreachable: Géocodeur introuvable{error, select, undefined {} other { ({error})}}
1717
homeLocation: Domicile
@@ -26,6 +26,9 @@ otpUi:
2626
# Note to translator: This is an implicit plural (as in "Other results").
2727
other: Autres
2828
recentlySearched: Recherches récentes
29+
resultsFound: >
30+
{count, plural, =0 {Aucun résultat} =1 {# résultat} other {# résultats}} du
31+
géocodeur pour {input, select, null {votre requête} other {"{input}"}}
2932
stations: Stations
3033
stops: Arrêts
3134
useCurrentLocation: Utiliser ma position actuelle

packages/location-field/i18n/ko.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ otpUi:
99
workLocation: 근무지
1010
homeLocation:
1111
clearLocation: 위치 재설정
12-
currentLocationUnavailable: 현재 위치를 사용할 수 없습니다 ({error})
12+
currentLocationUnavailable: 현재 위치를 사용할 수 없습니다{error, select, undefined {} other { ({error})}}
1313
fetchingLocation: 페치 위치…
1414
geocoderUnreachable: 지오 코더에 도달 할 수 없습니다{error, select, undefined {} other { ({error})}}
1515
myPlaces: 내 장소

packages/location-field/i18n/vi.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@ otpUi:
44
noResultsFound:
55
"Không tìm thấy kết quả cho {input, select,\n null {vấn của bạn}\n\
66
\ other {{input}}\n }\n"
7+
resultsFound: >
8+
{count, plural, =0 {Không có}, other {#}} kết quả
9+
cho {input, select, null {vấn của bạn} other {"{input}"}}"
710
clearLocation: Vị trí rõ ràng
8-
currentLocationUnavailable: Vị trí hiện tại không có sẵn ({error})
11+
currentLocationUnavailable: Vị trí hiện tại không có sẵn{error, select, undefined {} other { ({error})}}
912
fetchingLocation: Tìm nạp vị trí…
1013
geocoderUnreachable:
1114
Không thể đến bộ định vị địa lý{error, select, undefined

packages/location-field/i18n/zh_Hans.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ otpUi:
55
homeLocation:
66
noResultsFound: "找不到与 {input, select,\n null {您的搜索}\n other {{input}}\n } 相关的结果\n"
77
stops: 车站
8-
currentLocationUnavailable: 当前位置不详 ({error})
8+
currentLocationUnavailable: 当前位置不详{error, select, undefined {} other { ({error})}}
99
fetchingLocation: 获取的位置…
1010
geocoderUnreachable: 无法到达地理编码器{error, select, undefined {} other { ({error})}}
1111
myPlaces: 我的地方

packages/location-field/src/index.tsx

Lines changed: 102 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,62 @@ function DefaultLocationIcon({
3838
return <LocationIcon size={13} type={locationType} />;
3939
}
4040

41+
/**
42+
* Helper function that includes or excludes features based om layers.
43+
*/
44+
function filter(list: any[], layers: string[], include: boolean, limit: number): any[] {
45+
return list
46+
.filter(feature => layers.includes(feature.properties.layer) === include)
47+
.slice(0, limit);
48+
}
49+
50+
/**
51+
* Puts the given geocoded features into several categories with upper bounds.
52+
*/
53+
function getFeaturesByCategoryWithLimit(
54+
geocodedFeatures: any[],
55+
suggestionCount: number,
56+
sortByDistance: boolean,
57+
preferredLayers: string[]
58+
) {
59+
// Split features into those we want to always show above others
60+
const { special, normal } = geocodedFeatures.reduce(
61+
(prev, cur) => {
62+
prev[
63+
preferredLayers.includes(cur?.properties?.layer)
64+
? "special"
65+
: "normal"
66+
].push(cur);
67+
return prev;
68+
},
69+
{ special: [], normal: [] }
70+
);
71+
72+
const sortedGeocodedFeatures = [
73+
...special,
74+
...normal.sort((a, b) => {
75+
if (!sortByDistance) return 0;
76+
return (
77+
(b.properties?.distance || Infinity) -
78+
(a.properties?.distance || Infinity)
79+
);
80+
})
81+
];
82+
83+
// Split out different types of transit results
84+
// To keep the list tidy, only include a subset of the responses for each category
85+
const stopFeatures = filter(sortedGeocodedFeatures, ["stops"], true, suggestionCount);
86+
const stationFeatures = filter(sortedGeocodedFeatures, ["stations"], true, suggestionCount);
87+
const otherFeatures = filter(sortedGeocodedFeatures, ["stops", "stations"], false, suggestionCount);
88+
89+
return {
90+
count: otherFeatures.length + stationFeatures.length + stopFeatures.length,
91+
otherFeatures,
92+
stationFeatures,
93+
stopFeatures
94+
}
95+
}
96+
4197
const LocationField = ({
4298
addLocationSearch = () => {},
4399
autoFocus = false,
@@ -114,6 +170,7 @@ const LocationField = ({
114170
const geocodeAutocomplete = useMemo(() => debounce(300, (text: string) => {
115171
if (!text) {
116172
console.warn("No text entry provided for geocode autocomplete search.");
173+
setMessage(null)
117174
return;
118175
}
119176
getGeocoder(geocoderConfig)
@@ -137,10 +194,14 @@ const LocationField = ({
137194
{ error: errorMessage }
138195
);
139196
geocodedFeatures = [];
140-
} else if (geocodedFeatures.length === 0) {
197+
} else {
198+
const { count } = getFeaturesByCategoryWithLimit(geocodedFeatures, suggestionCount, sortByDistance, preferredLayers);
141199
message = intl.formatMessage(
142-
{ id: "otpUi.LocationField.noResultsFound" },
143-
{ input: text }
200+
{ id: "otpUi.LocationField.resultsFound" },
201+
{
202+
count,
203+
input: text
204+
}
144205
);
145206
}
146207
setGeocodedFeatures(geocodedFeatures);
@@ -149,6 +210,11 @@ const LocationField = ({
149210
)
150211
.catch((err: unknown) => {
151212
console.error(err);
213+
const message = intl.formatMessage(
214+
{ id: "otpUi.LocationField.geocoderUnreachable" },
215+
{ error: err.toString() }
216+
);
217+
setMessage(message);
152218
});
153219
}), []);
154220

@@ -375,59 +441,22 @@ const LocationField = ({
375441
};
376442

377443
const message = stateMessage;
378-
let geocodedFeatures = stateGeocodedFeatures;
444+
const geocodedFeatures = stateGeocodedFeatures;
379445

380446
if (sessionSearches.length > 5) sessionSearches = sessionSearches.slice(0, 5);
381447

382448
// Assemble menu contents, to be displayed either as dropdown or static panel.
383449
// Menu items are created in four phases: (1) the current location, (2) any
384450
// geocoder search results; (3) nearby transit stops; and (4) saved searches
385451

452+
const statusMessages = [];
386453
let menuItems = []; // array of menu items for display (may include non-selectable items e.g. dividers/headings)
387454
let itemIndex = 0; // the index of the current location-associated menu item (excluding non-selectable items)
388455
const locationSelectedLookup = {}; // maps itemIndex to a location selection handler (for use by the onKeyDown method)
389456

390457
/* 1) Process geocode search result option(s) */
391458
if (geocodedFeatures.length > 0) {
392-
// Split features into those we want to always show above others
393-
const { special, normal } = geocodedFeatures.reduce(
394-
(prev, cur) => {
395-
prev[
396-
preferredLayers.includes(cur?.properties?.layer)
397-
? "special"
398-
: "normal"
399-
].push(cur);
400-
return prev;
401-
},
402-
{ special: [], normal: [] }
403-
);
404-
405-
geocodedFeatures = [
406-
...special,
407-
...normal.sort((a, b) => {
408-
if (!sortByDistance) return 0;
409-
return (
410-
(b.properties?.distance || Infinity) -
411-
(a.properties?.distance || Infinity)
412-
);
413-
})
414-
];
415-
416-
// Add the menu sub-heading (not a selectable item)
417-
// menuItems.push(<MenuItem header key='sr-header'>Search Results</MenuItem>)
418-
419-
// Split out different types of transit results
420-
// To keep the list tidy, only include a subset of the responses for each category
421-
const stopFeatures = geocodedFeatures
422-
.filter(feature => feature.properties.layer === "stops")
423-
.slice(0, suggestionCount);
424-
const stationFeatures = geocodedFeatures
425-
.filter(feature => feature.properties.layer === "stations")
426-
.slice(0, suggestionCount);
427-
const otherFeatures = geocodedFeatures
428-
.filter(feature => feature.properties.layer !== "stops")
429-
.filter(feature => feature.properties.layer !== "stations")
430-
.slice(0, suggestionCount);
459+
const { otherFeatures, stationFeatures, stopFeatures } = getFeaturesByCategoryWithLimit(geocodedFeatures, suggestionCount, sortByDistance, preferredLayers)
431460

432461
// If no categories of features are returned, this variable is used to
433462
// avoid displaying headers
@@ -645,18 +674,26 @@ const LocationField = ({
645674
} else {
646675
// error detecting current position
647676
optionIcon = currentPositionUnavailableIcon;
648-
optionTitle = intl.formatMessage({
649-
id: "otpUi.LocationField.currentLocationUnavailable"
650-
}, { error: typeof currentPosition.error === "string" ? currentPosition.error : currentPosition.error.message });
677+
optionTitle = intl.formatMessage(
678+
{
679+
id: "otpUi.LocationField.currentLocationUnavailable"
680+
},
681+
{
682+
error: !currentPosition
683+
? undefined
684+
: typeof currentPosition.error === "string" ? currentPosition.error : currentPosition.error.message
685+
}
686+
);
651687
positionUnavailable = true;
688+
statusMessages.push(optionTitle)
652689
}
653690

654691
// Add to the selection handler lookup (for use in onKeyDown)
655692
locationSelectedLookup[itemIndex] = locationSelected;
656693

657694
if (!suppressNearby) {
658695
// Create and add the option item to the menu items array
659-
const currentLocationOption = (
696+
menuItems.push(
660697
<Option
661698
disabled={positionUnavailable}
662699
icon={optionIcon}
@@ -666,19 +703,20 @@ const LocationField = ({
666703
title={optionTitle}
667704
/>
668705
);
669-
menuItems.push(currentLocationOption);
670-
itemIndex++;
706+
if (!positionUnavailable) itemIndex++;
671707
}
672708
if (message) {
673-
const messageItem = (
674-
<Option
675-
disabled
676-
icon={<ExclamationCircle size={20} />}
677-
key={optionKey++}
678-
title={message}
679-
/>
680-
);
681-
menuItems.unshift(messageItem);
709+
if (geocodedFeatures.length === 0) {
710+
menuItems.unshift(
711+
<Option
712+
disabled
713+
icon={<ExclamationCircle size={20} />}
714+
key={optionKey++}
715+
title={message}
716+
/>
717+
);
718+
}
719+
statusMessages.push(message)
682720
}
683721

684722
// Store the number of location-associated items for reference in the onKeyDown method
@@ -740,11 +778,14 @@ const LocationField = ({
740778
{clearButton}
741779
</S.InputGroup>
742780
</S.FormGroup>
781+
<S.HiddenContent role="status">{statusMessages?.join(", ")}</S.HiddenContent>
743782
<S.StaticMenuItemList
783+
// Hide the listbox from assistive technology if no valid items are shown.
784+
aria-hidden={menuItemCount === 0 || undefined}
744785
aria-label={intl.formatMessage({
745-
id: "otpUi.LocationField.suggestedLocations",
746786
defaultMessage: "Suggested locations",
747-
description: "Text to show as a label for the dropdown list of locations"
787+
description: "Text to show as a label for the dropdown list of locations",
788+
id: "otpUi.LocationField.suggestedLocations"
748789
})}
749790
id={listBoxId}
750791
>
@@ -773,6 +814,7 @@ const LocationField = ({
773814
listBoxIdentifier={listBoxId}
774815
onToggle={onDropdownToggle}
775816
open={menuVisible}
817+
status={statusMessages.join(", ")}
776818
title={<LocationIconComponent locationType={locationType} />}
777819
>
778820
{menuItems}

0 commit comments

Comments
 (0)