diff --git a/app/public/locales/main-en.json b/app/public/locales/main-en.json index 52ebcb1..3d20df7 100644 --- a/app/public/locales/main-en.json +++ b/app/public/locales/main-en.json @@ -23,8 +23,8 @@ "label": "Street Address", "placeholder": "Your NYC street address", "val_errors": { - "not_found": "* Sorry, we don't recognize that NYC address, please try again!", - "address": "* Enter your building number, street name, and borough.", + "not_found": "Sorry, we don't recognize that NYC address, please try again!", + "address": "Enter your building number, street name, and borough.", "generic": "Something went wrong, please try again." } }, diff --git a/app/public/locales/main-es.json b/app/public/locales/main-es.json index f2721df..4df88bd 100644 --- a/app/public/locales/main-es.json +++ b/app/public/locales/main-es.json @@ -23,8 +23,8 @@ "label": "Dirección", "placeholder": "Su dirección postal de NYC", "val_errors": { - "not_found": "* Lo sentimos, no reconocemos ésta dirección, favor de intentar de nuevo.", - "address": "* Ingrese el número de su edificio, el nombre de la calle y el distrito.", + "not_found": "Lo sentimos, no reconocemos ésta dirección, favor de intentar de nuevo.", + "address": "Ingrese el número de su edificio, el nombre de la calle y el distrito.", "generic": "Algo salió mal. Por favor, vuelva a intentarlo." } }, diff --git a/app/setupJest.js b/app/setupJest.js index e125cc2..93e7311 100644 --- a/app/setupJest.js +++ b/app/setupJest.js @@ -40,7 +40,6 @@ function registerHbsPartials() { "language_toggle", "progress_indicator", "address_search_form", - "search_validation_errors", "search_result_map", "add_to_calendar", "tenants_rights_modal", diff --git a/app/src/components/addressSearchForm.js b/app/src/components/addressSearchForm.js index 631ce2c..c71ade1 100644 --- a/app/src/components/addressSearchForm.js +++ b/app/src/components/addressSearchForm.js @@ -35,7 +35,7 @@ export class AddressSearchForm extends Component { this.cached.searchResult = undefined; this.validationErrors = new SearchValidationErrors({ - element: this.element.querySelector("ul"), + element: this.element.querySelector(".search-validation-errors"), searchForm: this, }); @@ -87,9 +87,10 @@ export class AddressSearchForm extends Component { if (this.inputAddress.value.length) { this.searchRentStabilized(this.inputAddress.value); logAddressSearch(this.inputAddress.value); - this.inputAddress.value = ""; } else { + this.validationErrors.hideAll(); this.validationErrors.showNoInput(); + this.inputAddress.focus(); } } @@ -164,6 +165,7 @@ export class AddressSearchForm extends Component { this.store.dispatch(goToSlideIdx(2)); } else { this.validationErrors.showNotFound(); + this.inputAddress.focus(); logAddressNF(this.inputAddress.value); } } @@ -178,6 +180,7 @@ export class AddressSearchForm extends Component { handleFetchError(error) { this.validationErrors.showGeneric(); + this.inputAddress.focus(); // TODO: if exceptions are being logged from Redux middleware correctly, // then the following logException call may be redundant logException(handleErrorObj("AddressSearchForm.handleFetchError", error)); diff --git a/app/src/components/addressSearchForm.spec.js b/app/src/components/addressSearchForm.spec.js index 6bd1173..aaa5d85 100644 --- a/app/src/components/addressSearchForm.spec.js +++ b/app/src/components/addressSearchForm.spec.js @@ -332,6 +332,8 @@ describe("AddressSearchForm", () => { expect(logAddressNF).toHaveBeenCalledWith( "444 Unknown Street, Staten Island" ); + // NOTE: although the searchValidationErrors handles updating the DOM when an error occurs, the addressSearchForm focuses the input so that the error help text is announced to screen readers. + expect(document.activeElement).toEqual(addressSearchForm.inputAddress); }); test("handleSubmit", () => { @@ -352,7 +354,7 @@ describe("AddressSearchForm", () => { expect(logAddressSearch).toHaveBeenCalledWith("999 Main Street"); expect(event.preventDefault).toHaveBeenCalled(); expect(spy2).toHaveBeenCalledWith("999 Main Street"); - expect(addressSearchForm.inputAddress.value).toEqual(""); + expect(addressSearchForm.inputAddress.value).toEqual("999 Main Street"); }); test("handleSubmit no user input", () => { @@ -371,6 +373,8 @@ describe("AddressSearchForm", () => { addressSearchForm.element.dispatchEvent(event); expect(spy).toHaveBeenCalled(); expect(spy2).not.toHaveBeenCalled(); + // NOTE: although the searchValidationErrors handles updating the DOM when an error occurs, the addressSearchForm focuses the input so that the error help text is announced to screen readers. + expect(document.activeElement).toEqual(addressSearchForm.inputAddress); }); test("searchRentStabilized", () => { @@ -436,6 +440,8 @@ describe("AddressSearchForm", () => { addressSearchForm.handleFetchError(new Error("Something went wrong")); expect(spy).toHaveBeenCalled(); expect(logException).toHaveBeenCalled(); + // NOTE: although the searchValidationErrors handles updating the DOM when an error occurs, the addressSearchForm focuses the input so that the error help text is announced to screen readers. + expect(document.activeElement).toEqual(addressSearchForm.inputAddress); }); test("cleanUp", () => { diff --git a/app/src/components/searchValidationErrors.js b/app/src/components/searchValidationErrors.js index 51c8d3b..172f454 100644 --- a/app/src/components/searchValidationErrors.js +++ b/app/src/components/searchValidationErrors.js @@ -1,17 +1,24 @@ import { Component } from "./_componentBase"; +/** HTML id attribute values for validation text */ +export const VALIDATION_TEXT_ID = { + NOT_FOUND: "error-not-found", + NO_INPUT: "error-address", + GENERIC: "error-generic", +}; + export class SearchValidationErrors extends Component { init({ searchForm }) { this._searchForm = searchForm; - this.errorNotFound = this.element.querySelector("#error-not-found"); - this.errorNoInput = this.element.querySelector("#error-address"); - this.errorGeneric = this.element.querySelector("#error-generic"); - } - - showAll() { - this.showNotFound(); - this.showNoInput(); - this.showGeneric(); + this.errorNotFound = this.element.querySelector( + `#${VALIDATION_TEXT_ID.NOT_FOUND}` + ); + this.errorNoInput = this.element.querySelector( + `#${VALIDATION_TEXT_ID.NO_INPUT}` + ); + this.errorGeneric = this.element.querySelector( + `#${VALIDATION_TEXT_ID.GENERIC}` + ); } hideAll() { @@ -22,28 +29,53 @@ export class SearchValidationErrors extends Component { showNotFound() { this.errorNotFound.classList.remove("hidden"); + this.setInputAriaDescribedby(VALIDATION_TEXT_ID.NOT_FOUND); + this.setInputInvalid(true); } hideNotFound() { this.errorNotFound.classList.add("hidden"); + this.setInputAriaDescribedby(); + this.setInputInvalid(false); } showNoInput() { this.errorNoInput.classList.remove("hidden"); - this._searchForm.inputAddress.classList.add("invalid"); + this.setInputAriaDescribedby(VALIDATION_TEXT_ID.NO_INPUT); + this.setInputInvalid(true); } hideNoInput() { this.errorNoInput.classList.add("hidden"); - this._searchForm.inputAddress.classList.remove("invalid"); + this.setInputAriaDescribedby(); + this.setInputInvalid(false); } showGeneric() { this.errorGeneric.classList.remove("hidden"); + this.setInputAriaDescribedby(VALIDATION_TEXT_ID.GENERIC); } hideGeneric() { this.errorGeneric.classList.add("hidden"); + this.setInputAriaDescribedby(); + } + + setInputAriaDescribedby(value) { + if (typeof value === "string") { + this._searchForm.inputAddress.setAttribute("aria-describedby", value); + } else { + this._searchForm.inputAddress.removeAttribute("aria-describedby"); + } + } + + setInputInvalid(value) { + if (value) { + this._searchForm.inputAddress.classList.add("invalid"); + } else { + this._searchForm.inputAddress.classList.remove("invalid"); + } + this._searchForm.inputAddress.setAttribute("aria-invalid", value); } get noInputIsHidden() { diff --git a/app/src/components/searchValidationErrors.spec.js b/app/src/components/searchValidationErrors.spec.js index b266e02..daaf171 100644 --- a/app/src/components/searchValidationErrors.spec.js +++ b/app/src/components/searchValidationErrors.spec.js @@ -1,4 +1,7 @@ -import { SearchValidationErrors } from "./searchValidationErrors"; +import { + SearchValidationErrors, + VALIDATION_TEXT_ID, +} from "./searchValidationErrors"; import { AddressSearchForm } from "./addressSearchForm"; import { store } from "../store"; @@ -44,7 +47,7 @@ describe("SearchValidationErrors", () => { setDocumentHtml(getMainHtml()); // eslint-disable-line no-undef - element = document.querySelector("#address-form ul"); + element = document.querySelector(".search-validation-errors"); }); beforeEach(() => { @@ -79,6 +82,12 @@ describe("SearchValidationErrors", () => { expect( searchValidationErrors.errorNotFound.classList.contains("hidden") ).toBe(false); + expect(addressSearchForm.inputAddress.getAttribute("aria-invalid")).toBe( + "true" + ); + expect( + addressSearchForm.inputAddress.getAttribute("aria-describedby") + ).toBe(VALIDATION_TEXT_ID.NOT_FOUND); }); test("hideNotFound", () => { @@ -86,6 +95,12 @@ describe("SearchValidationErrors", () => { expect( searchValidationErrors.errorNotFound.classList.contains("hidden") ).toBe(true); + expect(addressSearchForm.inputAddress.getAttribute("aria-invalid")).toBe( + "false" + ); + expect( + addressSearchForm.inputAddress.getAttribute("aria-describedby") + ).toBeNull(); }); test("showNoInput", () => { @@ -93,6 +108,12 @@ describe("SearchValidationErrors", () => { expect( searchValidationErrors.errorNoInput.classList.contains("hidden") ).toBe(false); + expect(addressSearchForm.inputAddress.getAttribute("aria-invalid")).toBe( + "true" + ); + expect( + addressSearchForm.inputAddress.getAttribute("aria-describedby") + ).toBe(VALIDATION_TEXT_ID.NO_INPUT); }); test("hideNoInput", () => { @@ -100,6 +121,12 @@ describe("SearchValidationErrors", () => { expect( searchValidationErrors.errorNoInput.classList.contains("hidden") ).toBe(true); + expect(addressSearchForm.inputAddress.getAttribute("aria-invalid")).toBe( + "false" + ); + expect( + addressSearchForm.inputAddress.getAttribute("aria-describedby") + ).toBeNull(); }); test("showGeneric", () => { @@ -107,6 +134,9 @@ describe("SearchValidationErrors", () => { expect( searchValidationErrors.errorGeneric.classList.contains("hidden") ).toBe(false); + expect( + addressSearchForm.inputAddress.getAttribute("aria-describedby") + ).toBe(VALIDATION_TEXT_ID.GENERIC); }); test("hideGeneric", () => { @@ -114,13 +144,9 @@ describe("SearchValidationErrors", () => { expect( searchValidationErrors.errorGeneric.classList.contains("hidden") ).toBe(true); - }); - - test("showAll", () => { - searchValidationErrors.showAll(); - expect(spyShowNoInput).toHaveBeenCalledTimes(1); - expect(spyShowNotFound).toHaveBeenCalledTimes(1); - expect(spyShowGeneric).toHaveBeenCalledTimes(1); + expect( + addressSearchForm.inputAddress.getAttribute("aria-describedby") + ).toBeNull(); }); test("hideAll", () => { @@ -128,5 +154,11 @@ describe("SearchValidationErrors", () => { expect(spyHideNoInput).toHaveBeenCalledTimes(1); expect(spyHideNotFound).toHaveBeenCalledTimes(1); expect(spyHideGeneric).toHaveBeenCalledTimes(1); + expect( + addressSearchForm.inputAddress.getAttribute("aria-describedby") + ).toBeNull(); + expect(addressSearchForm.inputAddress.getAttribute("aria-invalid")).toBe( + "false" + ); }); }); diff --git a/app/src/hbs_partials/address_search_form.hbs b/app/src/hbs_partials/address_search_form.hbs index 4caab08..6f5fab0 100644 --- a/app/src/hbs_partials/address_search_form.hbs +++ b/app/src/hbs_partials/address_search_form.hbs @@ -2,10 +2,30 @@
\ No newline at end of file +