Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Accessibility #19

Open
shshaw opened this issue Aug 8, 2018 · 16 comments

Comments

@shshaw
Copy link
Owner

commented Aug 8, 2018

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 shshaw added the enhancement label Aug 8, 2018

@geoffreycrofte

This comment has been minimized.

Copy link

commented Aug 17, 2018

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 :)

@shshaw

This comment has been minimized.

Copy link
Owner Author

commented Aug 17, 2018

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 shshaw added this to the 1.1.0 milestone Aug 28, 2018

@shshaw shshaw added the help wanted label Aug 30, 2018

@shshaw

This comment has been minimized.

Copy link
Owner Author

commented Feb 8, 2019

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

This comment has been minimized.

Copy link
Owner Author

commented Feb 8, 2019

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

@shshaw shshaw modified the milestones: 1.1.0, 2.0.0 Feb 8, 2019

@notoriousb1t

This comment has been minimized.

Copy link
Collaborator

commented Feb 8, 2019

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

@shshaw

This comment has been minimized.

Copy link
Owner Author

commented Feb 8, 2019

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.

@notoriousb1t

This comment has been minimized.

Copy link
Collaborator

commented Feb 8, 2019

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

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...

@thejohnnyhill

This comment has been minimized.

Copy link

commented Feb 12, 2019

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?

@shshaw

This comment has been minimized.

Copy link
Owner Author

commented Feb 13, 2019

@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

This comment has been minimized.

Copy link
Owner Author

commented Feb 13, 2019

@shshaw

This comment has been minimized.

Copy link
Owner Author

commented Feb 13, 2019

Here's how Lettering.js handles the issue: davatron5000/Lettering.js#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

This comment has been minimized.

@fuzzbomb

This comment has been minimized.

Copy link

commented Sep 5, 2019

@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

This comment has been minimized.

Copy link

commented Sep 5, 2019

@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

This comment has been minimized.

Copy link

commented Sep 6, 2019

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

This comment has been minimized.

Copy link

commented Sep 6, 2019

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
5 participants
You can’t perform that action at this time.