cerner / terra-core

Terra offers a set of configurable React components designed to help build scalable and modular application UIs. This UI library was created to solve real-world issues in projects we work on day to day.
http://terra-ui.com
Apache License 2.0
182 stars 167 forks source link

[terra-form-select] - Options are not read by VoiceOver on iOS #1887

Closed bjankord closed 5 years ago

bjankord commented 6 years ago

Bug Report

Description

When focusing on the terra-select and trying to interact with it, VoiceOver on iOS skips reading the options. It seems there is no detection that options exist when using VO.

Steps to Reproduce

  1. Open up https://engineering.cerner.com/terra-ui/#/components/terra-form-select/form-select/select on iOS device
  2. Tap on home button 3 times to turn on VoiceOver
  3. Shift focus to Select (swipe right until terra-select is focused)
  4. Double click to interact with select

Expected Behavior

terra-select should communicate and interact with the select options when using VoiceOver on iOS.

Environment

StephenEsser commented 6 years ago

This is likely a duplicate of https://github.com/cerner/terra-core/issues/1769

bjankord commented 5 years ago

Related: https://github.com/cerner/terra-core/issues/2023

bjankord commented 5 years ago

One of the issues I'm seeing with getting VoiceOver on iOS to read the options seems to be related to the markup used, specifically the placement of the aria attributes.

Currently, the aria-attributes are mainly placed on the wrapping div.

Moving these to the div with role="textbox" helps to enable moving VoiceOver's virtual indicator into the list of options when the list is rendered as a sibling to the div with role="textbox".

I'm going to look at the markup patterns from these 3 examples and see if there is some commonality with how they markup the component.

bjankord commented 5 years ago

Select

<div class="combo-wrap">
  <input 
    type="text" 
    class="combobox" 
    id="combobox-single" 
    role="combobox" 
    aria-owns="vlY4dhDQ" 
    aria-autocomplete="list" 
    aria-expanded="false" 
  >
  <i aria-hidden="true" class="fa trigger fa-caret-down" data-trigger="single"></i>
  <div class="listbox" id="vlY4dhDQ" role="listbox">
    <div class="option active" role="option" id="vd0mMU5u">1</div>
    <div class="option" role="option" id="0fxbY1Pn" style="">2</div>
    <div class="option" role="option" id="9MNqvGWK" style="">3</div>
    <div class="option" role="option" id="acAQyCO6" style="">4</div>
  </div>
</div>

Multi-Select

<div class="combo-wrap">
  <input 
    type="text" 
    class="combobox" 
    id="combobox-multiselect" 
    role="combobox" 
    aria-owns="dX6iZUoc" 
    aria-autocomplete="list" 
    aria-expanded="false"
  >
  <i aria-hidden="true" class="fa trigger fa-caret-down" data-trigger="multiselect"></i>
  <div class="listbox" id="dX6iZUoc" role="listbox">
    <div class="option" role="option" id="vd0mMU5u">1</div>
    <div class="option" role="option" id="0fxbY1Pn" style="">2</div>
    <div class="option" role="option" id="9MNqvGWK" style="">3</div>
    <div class="option" role="option" id="acAQyCO6" style="">4</div>
  </div>
</div>

The following is added to the input when the dropdown is opened / an option is selected.

data-active-option="vd0mMU5u" 
aria-activedescendant="okAhO6Lt"

It also looks like there is an aria-live region related to the select that updates when options are selected

<div 
  aria-live="assertive" 
  role="log" 
  aria-relevant="additions" 
  aria-atomic="false" 
  style="position: absolute; width: 1px; height: 1px; margin-top: -1px; clip: rect(1px, 1px, 1px, 1px); overflow: hidden;"></div>
bjankord commented 5 years ago
bjankord commented 5 years ago
bjankord commented 5 years ago

Looking into ways to allow VoiceOver on iOS to read through the list of options, one way that will work is to move the drop down to be a sibling DOM element next to the div[role="textboxt] element.

There are trade-offs to this though. This would require using relative positioning on the select wrapper and absolutely positioning the dropdown within the select wrapper. This can lead to the dropdown getting cut off inside of modals or triggering overflow inside of modals when the select opens. It may also be hard to make the dropdown wider than the select which is something the UX team wants to do as a future enhancement.

With the current select dropdown, it uses hookshot to position the dropdown specifically to work around the trade-offs noted above. There are some trade-offs with the hookshot approach though as well.

By rendering the dropdown DOM not as a sibling to the select div[role='textbox'] element, there is no easy way for VoiceOver users to get to the options using the flick guesture which is common to navigate with VO on iOS. The options are in the DOM and the VoiceOver user could get to them if they swipe through all of the tabbaple elements until they get to the dropdown.

At this point, I want to raise the issue with UX (cc/ @scottwilmarth @neilpfeiffer) and see if they can offer any insight on the issues.

bjankord commented 5 years ago

With the option of moving the drop down to be a sibling DOM element next to the div[role="textboxt] element which provides the neccesary DOM structure to enable usable navigation of the list options when using VoiceOver on iOS, the trade-off noted is that this will lead to the select dropdowns getting cut off inside of modals or triggering overflow inside of modals when the select opens.

Example of dropdown being cut off by modal overflow: hidden; style.

Dropdown confined by the bounds of the modal

I've made a codepen example to explore ways to get try and work around this and see if we can get the dropdown to display outside of the modal.

Example of what we're trying to achieve

Dropdown beyond the bounds of the modal While it is possible to get the dropdown to overflow beyond the modal by moving the position:relative; style higher up in the DOM, it would require that there is no use of position:relative throughout the DOM within the modal.

This seems like a very fragile solution. We already useposition: relative a few times within the modal manager DOM and would need to rework that to see if this could even work.

I'm continuing to look for ways to get this to work how we would want with the dropdown going beyond the bounds of the overflow: hidden; container but this may just be a restriction of the way these components have been constructed and combined.

bjankord commented 5 years ago

We currently position the dropdown in a portal that appends the dropdown to the end of the <body/> element. We do this to enable better postioning and avoid cases where the dropdown menu would be clipped by overflow: hidden of ancestor elements like modals or trigger ancestor elements to increase overlfow when the dropdown is open.

As noted in previous comment, by rendering the dropdown DOM not as a sibling to the select div[role='textbox'] element, there is no easy way for VoiceOver users to get to the options using the flick guesture which is common to navigate with VO on iOS.

I looked to see if we could listen for the flick/swipe event when performed on the select element and shift focus to the portaled dropdown, however, VoiceOver seems to prevent gestures from being detected by JavaScript. I tested this using react-swipeable and was unable to detect the swipe event when VoiceOver was enabled, however I am able to detect the swipe event when VoiceOver is not enabled. There is some more info on this stack overflow post about the issue: https://stackoverflow.com/questions/34712961/javascript-capture-swipe-gesture-using-ontouchstart-with-ios-voiceover-turned-o

bjankord commented 5 years ago

I've continued exploring ways to shift focus to the dropdown in the portal to enable navigating them with VoiceOver gestures. I've been exploring turning the caret icon in the select into a button. This would allow users a distinct trigger to interact with to shift focus to the dropdown.

While exploring this, I ran into some issues with VoiceOver on iOS with my first attempt at making the icon a toggle button. When a user clicks on the select (that is a variant not equal to default), we move focus to an input within the select. This triggers an onscreen keyboard to appear. Even though the onscreen keyboard is open, VoiceOver users can still flick through interactive elements on the page. So a user could flick to the toggle button, which I thought worked well for this case as then they could tap on the button and we would shift focus to the dropdown allowing them to navigate the select options.

However, there is some odd behavior with this. When opening the onscreen keyboard on iOS when focusing on an input, VoiceOver will shift focus back to the input when the keyboard is closed. This means we can't shift focus to the dropdown, while the onscreen keyboard is open. For example, when the user clicks on the select, we focus the input and this opens the onscreen keyboard. By allowing the user to flick to the toggle button, they can then click on the button. This triggers a blur event on the input which results in the onscreen keyboard closing.

So while we shift the focus to the dropdown on the toggle button click, VoiceOver has heuristics that will shift focus back the element that triggered the onscreen keyboard when the keyboard closes. This results in focus shifting to the dropdown for a brief moment and then VoiceOver taking over focus and shifting it back to the input, which prevents users from flicking/navigating through the select options.

bjankord commented 5 years ago

I've updated the code that I mentioned in the previous comment and got this in a working state. To work around the VoiceOver shifting focus weirdness, I don't render the toggle button when the input is focused. By not rendering the button, it avoids the scenario where users might try to shift to the toggle button while the onscreen keyboard is open. This makes the two actions in the select distinct from one another. The users will either type in an option via the input or interact with the toggle button to shift focus to the dropdown.

On a desktop, the experience doesn't change much from what it is currently. There is a change where when you click on the caret icon, the input will not be focused. However, on mobile, by having the two ways to select an option (either by typing on in or toggling to the dropdown) be distinct, it makes it easier to use one or the other while using VoiceOver on iOS.