From 7a10378ba823cf0361580da9ad108225bcc19d21 Mon Sep 17 00:00:00 2001 From: Vladyslav Bondarenko Date: Mon, 7 Nov 2022 15:03:27 +0200 Subject: [PATCH] Improve screen reader accessibility Ramp task: https://ramp.accessibleweb.com/app/websites/83ca83c6265c/tasks/2787246f7ae2 This allows following WAI-ARIA best practices for combobox elements - https://www.w3.org/WAI/ARIA/apg/patterns/combobox/. Changes in this PR mostly correspond to the https://github.com/patw0929/react-intl-tel-input/pull/405 Although most of the best practices to improve accessibility were implemented, the role="combobox" was not added neither the to the number input (because the input doesn't show/hide the dropdown), nor to the country code button. Looks like OSx VoiceOver announces everything correctly. Changes: - Add role="listbox" to the countries list - Refer the countries list by its Id in the country code button via aria-controls and aria-owns - Add role="option" to each country list option - Use aria-expanded on the country code button element - Refer focused option by its Id in the country code button element via aria-activedescendant to read focused option while navigating by keyboard - Add aria-selected="true" to the selected list option - Set aria-autocomplete="none" to the country code button element since displayed options do not depend on the number input value - Allow passing arbitrary props to the country code button element via flagDropdownProps prop (for example, "aria-labelledby" property can be passed) --- src/components/CountryList.js | 10 ++++++++++ src/components/FlagDropDown.js | 18 ++++++++++++++++++ src/components/IntlTelInputApp.js | 7 +++++++ src/styles/intlTelInput.scss | 2 +- 4 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/components/CountryList.js b/src/components/CountryList.js index c0cab7d63..8d93f143b 100644 --- a/src/components/CountryList.js +++ b/src/components/CountryList.js @@ -66,6 +66,7 @@ class CountryList extends Component { }; const countryClass = classNames(countryClassObj); const keyPrefix = isPreferred ? 'pref-' : ''; + const isSelected = this.props.selectedCountryCode === country.iso2; return (
  • { this.selectedFlag = selectedFlag; } } @@ -125,6 +130,8 @@ class CountryList extends Component {
      { this.listElement = listElement; } } className={ className } + id={ this.props.countryListId } + role="listbox" > { preferredOptions } { divider } @@ -144,6 +151,9 @@ CountryList.propTypes = { changeHighlightCountry: PropTypes.func, showDropdown: PropTypes.bool, isMobile: PropTypes.bool, + selectedCountryCode: PropTypes.string.isRequired, + countryListId: PropTypes.string.isRequired, + getItemId: PropTypes.func.isRequired, }; export default CountryList; diff --git a/src/components/FlagDropDown.js b/src/components/FlagDropDown.js index 16ec53d83..ba738a004 100644 --- a/src/components/FlagDropDown.js +++ b/src/components/FlagDropDown.js @@ -5,6 +5,8 @@ import CountryList from './CountryList'; import RootModal from './RootModal'; class FlagDropDown extends Component { + getItemId = (id) => `intl-tel-item-${this.props.uniqueId}-${id}` + render() { const flagClassObj = { 'iti-flag': true, @@ -35,6 +37,8 @@ class FlagDropDown extends Component { let genCountryList = () => ''; + const countryListId = `intl-tel-countries-list-${this.props.uniqueId}`; + if (this.props.dropdownContainer) { if (this.props.showDropdown) { genCountryList = () => @@ -51,6 +55,9 @@ class FlagDropDown extends Component { preferredCountries={ this.props.preferredCountries } highlightedCountry={ this.props.highlightedCountry } changeHighlightCountry={ this.props.changeHighlightCountry } + selectedCountryCode={ this.props.countryCode } + countryListId={ countryListId } + getItemId={ this.getItemId } /> ; } @@ -68,6 +75,9 @@ class FlagDropDown extends Component { preferredCountries={ this.props.preferredCountries } highlightedCountry={ this.props.highlightedCountry } changeHighlightCountry={ this.props.changeHighlightCountry } + selectedCountryCode={ this.props.countryCode } + countryListId={ countryListId } + getItemId={ this.getItemId } />; } @@ -77,11 +87,17 @@ class FlagDropDown extends Component { className="flag-container" >
      { genSelectedDialCode() } @@ -112,6 +128,8 @@ FlagDropDown.propTypes = { changeHighlightCountry: PropTypes.func, titleTip: PropTypes.string, refCallback: PropTypes.func.isRequired, + uniqueId: PropTypes.string.isRequired, + flagDropdownProps: PropTypes.object, // eslint-disable-line react/forbid-prop-types }; export default FlagDropDown; diff --git a/src/components/IntlTelInputApp.js b/src/components/IntlTelInputApp.js index ad0e893f3..5b8781ffb 100644 --- a/src/components/IntlTelInputApp.js +++ b/src/components/IntlTelInputApp.js @@ -100,6 +100,8 @@ class IntlTelInputApp extends Component { this.handleInputChange = this.handleInputChange.bind(this); this.changeHighlightCountry = this.changeHighlightCountry.bind(this); this.formatNumber = this.formatNumber.bind(this); + // using a unique Id not to interfere with other instances of the tel input + this.uniqueId = Math.random().toString(36).substring(2); } componentDidMount() { @@ -1151,6 +1153,8 @@ class IntlTelInputApp extends Component { preferredCountries={ this.preferredCountries } highlightedCountry={ this.state.highlightedCountry } titleTip={ titleTip } + flagDropdownProps={ this.props.flagDropdownProps } + uniqueId={ this.uniqueId } />