read-smore
read-smore copied to clipboard
Addressing accessibility
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
andaria-controls
attributes are missing from the text region and the anchor respectively (and required ifaria-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!