diff --git a/README.md b/README.md
index e0698587..8b5e687a 100644
--- a/README.md
+++ b/README.md
@@ -56,7 +56,7 @@ Radar.initialize('prj_test_pk_...', { /* options */ });
Add the following script in your `html` file
```html
-
+
```
Then initialize the Radar SDK
@@ -73,8 +73,8 @@ To create a map, first initialize the Radar SDK with your publishable key. Then
```html
-
-
+
+
@@ -98,8 +98,8 @@ To create an autocomplete input, first initialize the Radar SDK with your publis
```html
-
-
+
+
@@ -130,8 +130,8 @@ To power [geofencing](https://radar.com/documentation/geofencing/overview) exper
```html
-
-
+
+
diff --git a/package-lock.json b/package-lock.json
index 6f2b7bf3..cd681c09 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "radar-sdk-js",
- "version": "4.4.10",
+ "version": "4.5.0-beta.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
diff --git a/package.json b/package.json
index df56c923..6062efa2 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "radar-sdk-js",
- "version": "4.4.10",
+ "version": "4.5.0-beta.1",
"description": "Web Javascript SDK for Radar, location infrastructure for mobile and web apps.",
"homepage": "https://radar.com",
"type": "module",
diff --git a/src/types.ts b/src/types.ts
index 6491a86c..8bae28f3 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -578,6 +578,9 @@ export interface RadarAutocompleteUIOptions extends Omit {
const fill = color.replace('#', '%23');
- const svg = `
+ const svg = `
`.trim();
return `data:image/svg+xml;charset=utf-8,${svg}`;
@@ -178,6 +183,16 @@ class AutocompleteUI {
this.inputField.setAttribute('aria-haspopup', 'listbox');
this.inputField.setAttribute('aria-autocomplete', 'list');
this.inputField.setAttribute('aria-activedescendant', '');
+ this.inputField.setAttribute('aria-owns', CLASSNAMES.RESULTS_LIST);
+ this.inputField.setAttribute('aria-describedby', 'autocomplete-instructions');
+
+ // screen reader instructions
+ const srInstructions = document.createElement('div');
+ srInstructions.id = 'autocomplete-instructions';
+ srInstructions.className = 'sr-only'; // screen reader only class
+ srInstructions.textContent = 'When results appear, use up and down arrow keys to navigate and Enter to select';
+ this.wrapper.appendChild(srInstructions);
+
// setup event listeners
this.inputField.addEventListener('input', this.handleInput.bind(this));
@@ -186,6 +201,18 @@ class AutocompleteUI {
this.inputField.addEventListener('blur', this.close.bind(this), true);
}
+ // mobile accessibility - touch support
+ this.resultsList.addEventListener('touchstart', (event) => {
+ const target = event.target as HTMLElement;
+ const li = target.closest('li');
+ if (li) {
+ const index = Array.from(this.resultsList.children).indexOf(li);
+ if (index !== -1) {
+ this.select(index);
+ }
+ }
+ });
+
Logger.debug('AutocompleteUI initialized with options', this.config);
}
@@ -273,15 +300,33 @@ class AutocompleteUI {
}
public displayResults(results: any[]) {
- // Clear the previous results
+ // clear the previous results
this.clearResultsList();
this.results = results;
+ // create status announcement for screen readers
+ const statusAnnouncement = document.createElement('div');
+ statusAnnouncement.setAttribute('role', 'status');
+ statusAnnouncement.className = 'sr-only';
+ if (results.length > 0) {
+ statusAnnouncement.textContent = `${results.length} results available. Use arrow keys to navigate.`;
+ } else {
+ statusAnnouncement.textContent = 'No results found.';
+ }
+ this.wrapper.appendChild(statusAnnouncement);
+
+ // Remove the announcement after it's been read
+ setTimeout(() => {
+ statusAnnouncement.remove();
+ }, 2000);
+
let marker: HTMLElement;
if (this.config.showMarkers) {
marker = document.createElement('img');
marker.classList.add(CLASSNAMES.RESULTS_MARKER);
marker.setAttribute('src', getMarkerIcon(this.config.markerColor));
+ marker.setAttribute('alt', 'Location marker'); // add alt text for screen readers
+ marker.setAttribute('aria-hidden', 'true'); // hide from screen readers when decorative
}
// Create and append list items for each result
@@ -290,6 +335,9 @@ class AutocompleteUI {
li.classList.add(CLASSNAMES.RESULTS_ITEM);
li.setAttribute('role', 'option');
li.setAttribute('id', `${CLASSNAMES.RESULTS_ITEM}}-${index}`);
+ li.setAttribute('aria-selected', 'false'); // default state
+
+ li.setAttribute('tabindex', '-1'); // make focusable but not in tab order
// construct result with bolded label
let listContent;
@@ -314,6 +362,14 @@ class AutocompleteUI {
this.select(index);
});
+ // add keyboard event for each item for better interaction
+ li.addEventListener('keydown', (event) => {
+ if (event.key === 'Enter' || event.key === ' ') {
+ event.preventDefault();
+ this.select(index);
+ }
+ });
+
this.resultsList.appendChild(li);
});
@@ -392,14 +448,19 @@ class AutocompleteUI {
if (this.highlightedIndex > -1) {
// clear class names on previously highlighted item
resultItems[this.highlightedIndex].classList.remove(CLASSNAMES.SELECTED_ITEM);
+ resultItems[this.highlightedIndex].setAttribute('aria-selected', 'false');
}
// add class name to newly highlighted item
resultItems[index].classList.add(CLASSNAMES.SELECTED_ITEM);
+ resultItems[index].setAttribute('aria-selected', 'true');
// set aria active descendant
this.inputField.setAttribute('aria-activedescendant', `${CLASSNAMES.RESULTS_ITEM}-${index}`);
+ // Make sure the selected item is visible (scroll into view if needed)
+ resultItems[index].scrollIntoView({ block: 'nearest' });
+
this.highlightedIndex = index;
}
@@ -436,8 +497,11 @@ class AutocompleteUI {
break;
// Close
+ case 'Escape':
case 'Esc':
+ event.preventDefault();
this.close();
+ setTimeout(() => this.inputField.focus(), 0); // return focus to input
break;
}
}
@@ -458,11 +522,26 @@ class AutocompleteUI {
}
this.inputField.value = inputValue;
+ // announce selection to screen readers
+ const announcement = document.createElement('div');
+ announcement.setAttribute('role', 'status');
+ announcement.className = 'sr-only';
+ announcement.textContent = `Selected: ${inputValue}`;
+ this.wrapper.appendChild(announcement);
+
+ // remove announcement after it's been read
+ setTimeout(() => {
+ announcement.remove();
+ }, 1000);
+
const onSelection = this.config.onSelection;
if (onSelection) {
onSelection(result);
}
+ // Return focus to input after selection
+ this.inputField.focus();
+
// clear results list
this.close();
}
diff --git a/src/version.ts b/src/version.ts
index 6fa3ea42..0fce909d 100644
--- a/src/version.ts
+++ b/src/version.ts
@@ -1 +1 @@
-export default '4.4.10';
\ No newline at end of file
+export default '4.5.0-beta.1';
diff --git a/styles/radar.css b/styles/radar.css
index 2e958872..1c5d25de 100644
--- a/styles/radar.css
+++ b/styles/radar.css
@@ -94,6 +94,8 @@
opacity: 1;
border: 1px solid var(--radar-gray3);
box-shadow: 0px 0px 4px #81BEFF;
+ outline: 2px solid #4d90fe;
+ outline-offset: 2px;
}
.radar-autocomplete-search-icon {
@@ -163,6 +165,11 @@
background-color: var(--radar-gray1);
}
+.radar-autocomplete-results-item:focus {
+ outline: 2px solid #4d90fe;
+ outline-offset: -2px;
+}
+
.radar-autocomplete-results-item[aria-selected="true"] {
background-color: var(--radar-gray2);
}
@@ -206,6 +213,19 @@
padding: 8px 16px;
}
+/* screen reader only class for autocomplete */
+.sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border-width: 0;
+}
+
.maplibregl-marker:hover {
cursor: pointer;
}