read-smore icon indicating copy to clipboard operation
read-smore copied to clipboard

Addressing accessibility

Open smastrom opened this issue 2 months ago • 0 comments

Hi @stephenscaff, first of all thanks for the library as it's very practical and has a demo I love.

I believe there are some accessiblity concerns (as it stands now) that should be addressed in order to make it perfect.

  • Once visually-impaired users navigate (and read) the "visible text" and then focus and click the Read more anchor, they kinda "get lost" because the focus remains on the anchor.
  • In case they directly tab to the anchor (not reading the text that is displayed by default), they also get lost because such link doesn't bring them anywhere and gives no hints on its purpose as role and aria-controls attributes are missing from the text region and the anchor respectively (and required if aria-expanded is present).

What comes to my mind are two different solutions which might both be valid:

Option 1 - Detect, mutate and focus the truncated node

On init(), get the node whose content will be truncated according to the limit:

<div 
  class="js-read-smore" 
  data-read-smore-words="80" 
>
  <p>{ "word ".repeat(30) }</p>
  <p>{ "word ".repeat(30) }</p>
  <p>{ "word ".repeat(30) }</p> {/* <-- Truncated node */}
  <p>{ "word ".repeat(30) }</p>
  <p>{ "word ".repeat(30) }</p>
</div>

Add id="some_id", role="region" and tabindex="0" to it. If not present add them to the wrapper. Add aria-controls="some_id" to the Read more button.

When users click on the Read more more button, update its aria-expanded value, and move the focus to that paragraph on the next tick.

Then if users click on the "Read less" button, set aria-expanded back to false without moving the focus and so on.

I think this approach makes sense, but it has one downside: developers may find annoying having to deal with :focus/:focus-visible via CSS once they notice the unexpected outline.

Option 2 - Display all the text to screen readers

Another option which comes to my mind but that's a bit more complex to architect is to create inner nodes with screen-reader-only styles applied:

border: 0;
clip: rect(0 0 0 0);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;

For example this markup:

<div 
  class="js-read-smore" 
  data-read-smore-words="15" 
>
  <p>{ "word ".repeat(10) }</p>
  <p>Hello Hello Hello Hello Hello Hello Hello Hello Hello Hello</p>
  <p>{ "word ".repeat(10) }</p>
  <p>{ "word ".repeat(10) }</p>
</div>

Could be mutated like this on init():

<div 
  class="js-read-smore" 
  data-read-smore-words="15" 
>
  <p>{ "word ".repeat(10) }</p>
  <p>Hello Hello Hello Hello Hello <span aria-hidden="true">...</span>
    <span style="border:0, clip..."> Hello Hello Hello Hello Hello</span>
  </p>

  <div style="border:0, clip...">
    <p>{ "word ".repeat(10) }</p>
    <p>{ "word ".repeat(10) }</p>
  </div>
</div>

And the Read more button could be completely hidden to screen readers via aria-hidden="true" along with the ellipse. This way the text will always be accessible to screen readers and that's it.

Then after clicking on Read more just revert the innerHTML to its original content.

This approach has one downside: setting aria-hidden="true" doesn't completely prevent focusing an element via Tab which can be confusing if visually-impaired users land on it but I can tell that on macOS VoiceOver those elements are totally ignored when navigating with arrows.

Thanks again for the library and let me know what you think! Cheers!

smastrom avatar Apr 25 '24 23:04 smastrom