@@ -38,6 +38,62 @@ function DefaultLocationIcon({
38
38
return < LocationIcon size = { 13 } type = { locationType } /> ;
39
39
}
40
40
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
+
41
97
const LocationField = ( {
42
98
addLocationSearch = ( ) => { } ,
43
99
autoFocus = false ,
@@ -114,6 +170,7 @@ const LocationField = ({
114
170
const geocodeAutocomplete = useMemo ( ( ) => debounce ( 300 , ( text : string ) => {
115
171
if ( ! text ) {
116
172
console . warn ( "No text entry provided for geocode autocomplete search." ) ;
173
+ setMessage ( null )
117
174
return ;
118
175
}
119
176
getGeocoder ( geocoderConfig )
@@ -137,10 +194,14 @@ const LocationField = ({
137
194
{ error : errorMessage }
138
195
) ;
139
196
geocodedFeatures = [ ] ;
140
- } else if ( geocodedFeatures . length === 0 ) {
197
+ } else {
198
+ const { count } = getFeaturesByCategoryWithLimit ( geocodedFeatures , suggestionCount , sortByDistance , preferredLayers ) ;
141
199
message = intl . formatMessage (
142
- { id : "otpUi.LocationField.noResultsFound" } ,
143
- { input : text }
200
+ { id : "otpUi.LocationField.resultsFound" } ,
201
+ {
202
+ count,
203
+ input : text
204
+ }
144
205
) ;
145
206
}
146
207
setGeocodedFeatures ( geocodedFeatures ) ;
@@ -149,6 +210,11 @@ const LocationField = ({
149
210
)
150
211
. catch ( ( err : unknown ) => {
151
212
console . error ( err ) ;
213
+ const message = intl . formatMessage (
214
+ { id : "otpUi.LocationField.geocoderUnreachable" } ,
215
+ { error : err . toString ( ) }
216
+ ) ;
217
+ setMessage ( message ) ;
152
218
} ) ;
153
219
} ) , [ ] ) ;
154
220
@@ -375,59 +441,22 @@ const LocationField = ({
375
441
} ;
376
442
377
443
const message = stateMessage ;
378
- let geocodedFeatures = stateGeocodedFeatures ;
444
+ const geocodedFeatures = stateGeocodedFeatures ;
379
445
380
446
if ( sessionSearches . length > 5 ) sessionSearches = sessionSearches . slice ( 0 , 5 ) ;
381
447
382
448
// Assemble menu contents, to be displayed either as dropdown or static panel.
383
449
// Menu items are created in four phases: (1) the current location, (2) any
384
450
// geocoder search results; (3) nearby transit stops; and (4) saved searches
385
451
452
+ const statusMessages = [ ] ;
386
453
let menuItems = [ ] ; // array of menu items for display (may include non-selectable items e.g. dividers/headings)
387
454
let itemIndex = 0 ; // the index of the current location-associated menu item (excluding non-selectable items)
388
455
const locationSelectedLookup = { } ; // maps itemIndex to a location selection handler (for use by the onKeyDown method)
389
456
390
457
/* 1) Process geocode search result option(s) */
391
458
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 )
431
460
432
461
// If no categories of features are returned, this variable is used to
433
462
// avoid displaying headers
@@ -645,18 +674,26 @@ const LocationField = ({
645
674
} else {
646
675
// error detecting current position
647
676
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
+ ) ;
651
687
positionUnavailable = true ;
688
+ statusMessages . push ( optionTitle )
652
689
}
653
690
654
691
// Add to the selection handler lookup (for use in onKeyDown)
655
692
locationSelectedLookup [ itemIndex ] = locationSelected ;
656
693
657
694
if ( ! suppressNearby ) {
658
695
// Create and add the option item to the menu items array
659
- const currentLocationOption = (
696
+ menuItems . push (
660
697
< Option
661
698
disabled = { positionUnavailable }
662
699
icon = { optionIcon }
@@ -666,19 +703,20 @@ const LocationField = ({
666
703
title = { optionTitle }
667
704
/>
668
705
) ;
669
- menuItems . push ( currentLocationOption ) ;
670
- itemIndex ++ ;
706
+ if ( ! positionUnavailable ) itemIndex ++ ;
671
707
}
672
708
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 )
682
720
}
683
721
684
722
// Store the number of location-associated items for reference in the onKeyDown method
@@ -740,11 +778,14 @@ const LocationField = ({
740
778
{ clearButton }
741
779
</ S . InputGroup >
742
780
</ S . FormGroup >
781
+ < S . HiddenContent role = "status" > { statusMessages ?. join ( ", " ) } </ S . HiddenContent >
743
782
< S . StaticMenuItemList
783
+ // Hide the listbox from assistive technology if no valid items are shown.
784
+ aria-hidden = { menuItemCount === 0 || undefined }
744
785
aria-label = { intl . formatMessage ( {
745
- id : "otpUi.LocationField.suggestedLocations" ,
746
786
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"
748
789
} ) }
749
790
id = { listBoxId }
750
791
>
@@ -773,6 +814,7 @@ const LocationField = ({
773
814
listBoxIdentifier = { listBoxId }
774
815
onToggle = { onDropdownToggle }
775
816
open = { menuVisible }
817
+ status = { statusMessages . join ( ", " ) }
776
818
title = { < LocationIconComponent locationType = { locationType } /> }
777
819
>
778
820
{ menuItems }
0 commit comments