Splitting icon indicating copy to clipboard operation
Splitting copied to clipboard

Accessibility

Open shshaw opened this issue 6 years ago • 16 comments

Currently, some advanced effects for Splitting require using psuedo elements with the data-word and data-char attributes populating them. Unfortunately, screen readers read the psuedo elements populated with content: attr(data-word), effectively repeating every word.

For example: https://codepen.io/shshaw/pen/LBXZwW

In experimenting with VoiceOver, the best approach I found was to add an aria-label to the parent element. This still causes issues when they focus inside the element, but it's a much better experience on the main heading.

<h1 data-splitting aria-label="My Heading" ...>
  <span class="word" aria-role="presentation">...

Proposed solution:

  • Add an accessibility option that's true by default.
  • When true:
    • Add aria-label of the el.innerText when splitting text, if no aria-label already present.
    • Created elements ( <span>s ) get role="presentation".
  • Users can turn it off with accessibility: false if they want to handle accessibility on their own

We also should add this to the psuedo element CSS for browsers that do support:

  speak: never;
  speak: none;
  user-select: none;
  pointer-events: none;

Any other considerations?

shshaw avatar Aug 08 '18 19:08 shshaw

Hey,

What about using speech, aural and maybe reader at-media rules to hide the visibility of those pseudo-elements?

@media speech, aural, reader {
    [data-char]:after,
    [data-word]:after {
        display: none;
        visibility: hidden;
    }
}

Never tested yet :)

geoffreycrofte avatar Aug 17 '18 14:08 geoffreycrofte

I had tried that already and unfortunately did not make any difference for macOS Chrome & VoiceOver. Any idea what browsers / voice-to-text programs support it?

shshaw avatar Aug 17 '18 14:08 shshaw

Experimenting with VoiceOver, thanks to some guidance from https://axesslab.com/text-splitting/

https://codepen.io/shshaw/pen/YBeqRO

Splitting().forEach( s => {
  
  // Causes the entire block to be read as a whole with no distinction of headings
  // s.el.setAttribute('role', 'text');

  // Causes separation of the blocks, but loses some of the semantics between headings
  // [...s.el.childNodes].forEach( c => c.setAttribute('role', 'text') );
  
  // Works pretty well, but causes words to be read individually in paragraphs/inline elements
  s.words.forEach( word => {
    word.setAttribute('role', 'text');
  });
  
  // Might help prevent VoiceOver from treating the chars as individual elements to read.
  s.chars.forEach( char => {
    
    char.style.setProperty('--random', Math.random());
    char.setAttribute('role', 'presentation');
  });
  
});

shshaw avatar Feb 08 '19 16:02 shshaw

Could be worth copying the original DOM content somewhere hidden, and utilizing aria-labelledby.

shshaw avatar Feb 08 '19 16:02 shshaw

What about copying the textContent to an aria-label when doing a character split rather than using labeledby?

notoriousb1t avatar Feb 08 '19 17:02 notoriousb1t

Yeah; that's mentioned in the original post. The main issue is that loses some of the semantics if you had a list, headings, etc. within the main split component, and it would be a massive attribute in the DOM for paragraphs / long content.

shshaw avatar Feb 08 '19 19:02 shshaw

That is what happens when I try to respond to a 6 month+ thread without re-reading the whole thing :smile:

I am not opposed to keeping a copy of the original in the DOM for labeledby. How do you think we should handle re-splitting with that in mind? I wonder if it would get complicated...

notoriousb1t avatar Feb 08 '19 19:02 notoriousb1t

So glad I found this — was wondering how Splitting.js handles accessibility (and search indexing, which is a separate but related issue).

I'm curious about using Splitting to create custom-styled headings on a site, and it seems like for this use case the s.el.setAttribute('role', 'text'); example would definitely suffice — that is, it'll read the whole heading as one block element, vs. word-by-word. However, if I were to use it on multiple elements (like heading + body), I'd probably want to use [...s.el.childNodes].forEach( c => c.setAttribute('role', 'text') ); so that it breaks apart these elements.

Am I understanding that correctly?

thejohnnyhill avatar Feb 12 '19 03:02 thejohnnyhill

@thejohnnyhill No official recommendation yet. role="text" is a little heavy handed and reduces accessibility for headings, lists and other elements with semantic meanings. For now, this approach works well, but isn't ideal.

  // If the split element is only text, do this:
  s.el.setAttribute('aria-label', s.el.textContent);

  // If element has actual child elements ( `<div data-splitting> <h1>...</h1> <p>...</p>` etc ), do this:
  [...s.el.childNodes].forEach( c => c.setAttribute('aria-label', c.textContent) );
  
  // Prevents chars from appearing as individual elements, but is also preventing selecting text.
  s.chars.forEach( char => {
    char.setAttribute('aria-hidden', true);
  });

Still working through some approaches and seeking feedback on the best way to make screenreaders ignore these decorative elements since aria-role="presentation" doesn't seem to do it properly.

shshaw avatar Feb 13 '19 16:02 shshaw

Some experimentations: https://codepen.io/shshaw/pen/YBjjyN?editors=1010

shshaw avatar Feb 13 '19 16:02 shshaw

Here's how Lettering.js handles the issue: https://github.com/davatron5000/Lettering.js/pull/51

Basically like recommended above.

For automating this as a default option, we'll need to figure out a good way to apply the aria labels to non-Splitting created childNodes/parents in a sensible way.

shshaw avatar Feb 13 '19 21:02 shshaw

Another reference: https://github.com/ZeroSpree/CSSans.Pro#quick-start https://github.com/ZeroSpree/CSSans.Pro#accessibility

shshaw avatar Feb 13 '19 22:02 shshaw

@thejohnnyhill role="text" isn't appropriate, because:

  1. The role attribute overrides the native semantics of the element it is applied to. If splitting.js applies role="text" as a generic approach, it will be very damaging to the author's intended semantics. As a basic example, <h1 role="text">Main heading</h1> would not be reported as a heading to assistive technology. Screen reader users would not be able to use their heading navigation tools to find it.
  2. The text role is a non-standard role. It isn't included in the ARIA 1.0 or 1.1 recommendations, and it isn't in the current ARIA 1.2 working draft (18 Decemberr 2018) or editorial draft. AFAIK WebKit is the only engine which implements this role. OS-level accessibility APIs do have a text role, and the idea of a corresponding ARIA role has been discussed for a long time. However it hasn't achieved a consensus, and there are discussions about whether it should only be permitted on restricted set of native elements. At this stage, I wouldn't recommend it for use by any library code.

fuzzbomb avatar Sep 05 '19 06:09 fuzzbomb

@shshaw

Still working through some approaches and seeking feedback on the best way to make screenreaders ignore these decorative elements since aria-role="presentation" doesn't seem to do it properly.

You've misunderstood the meaning of role="presentation". It's purpose is to remove native semantics, but not remove content, from the accessibility tree. Child nodes will in the DOM will still be included in the accessibility tree with their roles unaffected. Many developers have been confused by this, so ARIA 1.1 introduced role="none" as a synonym for role="presentation".

If you want a generated character <span> to be ignored by assistive technology, use the aria-hidden="true" property.

fuzzbomb avatar Sep 05 '19 06:09 fuzzbomb

Using aria-hidden="true" on the generated spans is a good idea, so long as the text is presented by another method.

Using aria-label on the elements being split isn't going to be reliable in all cases; it may be inneffective on some element types.

I'm not sure this can ever be solved in JS library code alone. Rather, it may be preferable to provide accessibility guidance in documentation. It should ideally be a prominent section in the docs, like a top-level heading/chapter.

Idea: many use-cases for splitting.js are about providing a fancy graphical effect for text, such as logotypes. In some cases it may be reasonable to use an image role, e.g. <div role="img" aria-label="ACME">ACME</div>, which will convey the label to assistive tech. This is semantically equivalent to using an <img> for images of text, which is a well supported and widespread approach for fancy text/logos. However this is clearly a decision for authors, and we can't assume that splitting.js is always being used for fancy graphical effects. This is the sort of usage that can be covered in accessibility documentation, but could be dangerous to assume in library code.

fuzzbomb avatar Sep 06 '19 01:09 fuzzbomb

An alternative approach to aria-label is to use a visually-hidden span as the accessible text. This is quite a robust approach to providing accessible text alternatives.

e.g. <h1>ACME</h1> is transformed to

<h1>
  <span class="visually-hidden">ACME</span>
  <span aria-hidden="true">A</span>
  <span aria-hidden="true">C</span>
  <span aria-hidden="true">M</span>
  <span aria-hidden="true">E</span>
</h1>

PRO:

  • Doesn't alter the semantics of the target element, like role="text" or role="img" would.
  • Works equally well for different element types (h1, p, div, a, blockquote), unlike aria-label.

CON:

  • Assumes a visually hidden CSS utility class is available. These have different names across various CSS libraries: visually-hidden, visuallyhidden, sr-only, etc.. Perhaps this class name could be a configurable option?
  • When selecting text to cut/copy, it's likely this will catch the visually hidden span AND the individual character spans. So when you paste it somewhere else, you'll have something like ACME A C M E. Whilst annoying, this is a much lesser concern than presenting an accessible version of the text for the main use-case of reading the text.

fuzzbomb avatar Sep 06 '19 01:09 fuzzbomb