plotly / plotly.js

Open-source JavaScript charting library behind Plotly and Dash
https://plotly.com/javascript/
MIT License
16.92k stars 1.85k forks source link

modebar tools are not accessible #6645

Open warrickball opened 1 year ago

warrickball commented 1 year ago

I'm currently working in a small team on a Django website that has an embedded Plotly Dash. We're working through accessibility issues raised by pa11y-ci and have identified a few that are arising from within Plotly.js itself.

Here's the minimal example I set up based on the "getting started" example code on the Plotly.js website:

<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Plotly.js Minimal Working Example</title>
    <script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
  </head>
  <body>
    <div id="tester" style="width:600px;height:250px;"></div>
    <script>
      TESTER = document.getElementById('tester');

      Plotly.newPlot( TESTER, [{
          x: [1, 2, 3, 4, 5],
          y: [1, 2, 4, 8, 16] }]);
    </script>
  </body>
</html>

I served this using a simple Python server:

$ python3 -m http.server 8500 --bind 127.0.0.1 &

and run pa11y-ci with this configuration:

$ cat .pa11yci
{
    "defaults": {
        "runners": ["axe", "htmlcs"],
    "chromeLaunchConfig": {
            "args": ["--no-sandbox"]
        }
    },
    "urls": [
    "http://127.0.0.1:8500/plotly-mwe.html"
    ]
}

which produces a number of issues (full log collapsed below), several of which look like this:

$ npx pa11y-ci
Running Pa11y on 1 URLs:
127.0.0.1 - - [15/Jun/2023 15:40:27] "GET /plotly-mwe.html HTTP/1.1" 200 -
 > http://127.0.0.1:8500/plotly-mwe.html - 22 errors

Errors in http://127.0.0.1:8500/plotly-mwe.html:
...
 • Anchor element found with no link content and no name and/or ID attribute.

   (#modebar-c74f48 > div:nth-child(1) > a)

   <a rel="tooltip" class="modebar-btn" data-title="Download plot as a png" data-toggle="false" data-gravity="n"><svg viewBox="0 0 1000 1000" cl...</a>
...
✘ 0/1 URLs passed

There's one of these for each of the tooltips, presumably because they have neither id nor name attributes.

I'm not sure what the most appopriate id or name attribute would be.

Full error log ``` $ npx pa11y-ci Running Pa11y on 1 URLs: 127.0.0.1 - - [15/Jun/2023 15:40:27] "GET /plotly-mwe.html HTTP/1.1" 200 - > http://127.0.0.1:8500/plotly-mwe.html - 22 errors Errors in http://127.0.0.1:8500/plotly-mwe.html: • Page must have means to bypass repeated blocks (https://dequeuniversity.com/rules/axe/4.7/bypass?application=axeAPI) (html) Plotly.js Min...</html> • Elements must meet minimum color contrast ratio thresholds (https://dequeuniversity.com/rules/axe/4.7/color-contrast?application=axeAPI) (#tester > div > div > svg:nth-child(1) > g:nth-child(5) > g > g:nth-child(15) > g:nth-child(1) > text) <text text-anchor="middle" x="0" y="183" data-unformatted="1" data-math="N" transform="translate(105.75,0)" style="font-family: "Open Sans", verdana, arial, sans-serif; font-size: 12px; fill: rgb(68, 68, 68); fill-opacity: 1; white-space: p... • Elements must meet minimum color contrast ratio thresholds (https://dequeuniversity.com/rules/axe/4.7/color-contrast?application=axeAPI) (#tester > div > div > svg:nth-child(1) > g:nth-child(5) > g > g:nth-child(15) > g:nth-child(2) > text) <text text-anchor="middle" x="0" y="183" data-unformatted="2" data-math="N" transform="translate(202.87,0)" style="font-family: "Open Sans", verdana, arial, sans-serif; font-size: 12px; fill: rgb(68, 68, 68); fill-opacity: 1; white-space: p... • Elements must meet minimum color contrast ratio thresholds (https://dequeuniversity.com/rules/axe/4.7/color-contrast?application=axeAPI) (#tester > div > div > svg:nth-child(1) > g:nth-child(5) > g > g:nth-child(15) > g:nth-child(3) > text) <text text-anchor="middle" x="0" y="183" data-unformatted="3" data-math="N" transform="translate(300,0)" style="font-family: "Open Sans", verdana, arial, sans-serif; font-size: 12px; fill: rgb(68, 68, 68); fill-opacity: 1; white-space: pre;... • Elements must meet minimum color contrast ratio thresholds (https://dequeuniversity.com/rules/axe/4.7/color-contrast?application=axeAPI) (#tester > div > div > svg:nth-child(1) > g:nth-child(5) > g > g:nth-child(15) > g:nth-child(4) > text) <text text-anchor="middle" x="0" y="183" data-unformatted="4" data-math="N" transform="translate(397.12,0)" style="font-family: "Open Sans", verdana, arial, sans-serif; font-size: 12px; fill: rgb(68, 68, 68); fill-opacity: 1; white-space: p... • Elements must meet minimum color contrast ratio thresholds (https://dequeuniversity.com/rules/axe/4.7/color-contrast?application=axeAPI) (#tester > div > div > svg:nth-child(1) > g:nth-child(5) > g > g:nth-child(15) > g:nth-child(5) > text) <text text-anchor="middle" x="0" y="183" data-unformatted="5" data-math="N" transform="translate(494.25,0)" style="font-family: "Open Sans", verdana, arial, sans-serif; font-size: 12px; fill: rgb(68, 68, 68); fill-opacity: 1; white-space: p... • Elements must meet minimum color contrast ratio thresholds (https://dequeuniversity.com/rules/axe/4.7/color-contrast?application=axeAPI) (#tester > div > div > svg:nth-child(1) > g:nth-child(5) > g > g:nth-child(16) > g:nth-child(1) > text) <text text-anchor="end" x="79" y="4.199999999999999" data-unformatted="0" data-math="N" transform="translate(0,166.45)" style="font-family: "Open Sans", verdana, arial, sans-serif; font-size: 12px; fill: rgb(68, 68, 68); fill-opacity: 1; wh... • Elements must meet minimum color contrast ratio thresholds (https://dequeuniversity.com/rules/axe/4.7/color-contrast?application=axeAPI) (#tester > div > div > svg:nth-child(1) > g:nth-child(5) > g > g:nth-child(16) > g:nth-child(2) > text) <text text-anchor="end" x="79" y="4.199999999999999" data-unformatted="5" data-math="N" transform="translate(0,147.95)" style="font-family: "Open Sans", verdana, arial, sans-serif; font-size: 12px; fill: rgb(68, 68, 68); fill-opacity: 1; wh... • Elements must meet minimum color contrast ratio thresholds (https://dequeuniversity.com/rules/axe/4.7/color-contrast?application=axeAPI) (#tester > div > div > svg:nth-child(1) > g:nth-child(5) > g > g:nth-child(16) > g:nth-child(3) > text) <text text-anchor="end" x="79" y="4.199999999999999" data-unformatted="10" data-math="N" transform="translate(0,129.45)" style="font-family: "Open Sans", verdana, arial, sans-serif; font-size: 12px; fill: rgb(68, 68, 68); fill-opacity: 1; w... • Elements must meet minimum color contrast ratio thresholds (https://dequeuniversity.com/rules/axe/4.7/color-contrast?application=axeAPI) (#tester > div > div > svg:nth-child(1) > g:nth-child(5) > g > g:nth-child(16) > g:nth-child(4) > text) <text text-anchor="end" x="79" y="4.199999999999999" data-unformatted="15" data-math="N" transform="translate(0,110.95)" style="font-family: "Open Sans", verdana, arial, sans-serif; font-size: 12px; fill: rgb(68, 68, 68); fill-opacity: 1; w... • Anchor element found with no link content and no name and/or ID attribute. (#modebar-c74f48 > div:nth-child(1) > a) <a rel="tooltip" class="modebar-btn" data-title="Download plot as a png" data-toggle="false" data-gravity="n"><svg viewBox="0 0 1000 1000" cl...</a> • Anchor element found with no link content and no name and/or ID attribute. (#modebar-c74f48 > div:nth-child(2) > a:nth-child(1)) <a rel="tooltip" class="modebar-btn active" data-title="Zoom" data-attr="dragmode" data-val="zoom" data-toggle="false" data-gravity="n"><svg viewBox="0 0 1000 1000" cl...</a> • Anchor element found with no link content and no name and/or ID attribute. (#modebar-c74f48 > div:nth-child(2) > a:nth-child(2)) <a rel="tooltip" class="modebar-btn" data-title="Pan" data-attr="dragmode" data-val="pan" data-toggle="false" data-gravity="n"><svg viewBox="0 0 1000 1000" cl...</a> • Anchor element found with no link content and no name and/or ID attribute. (#modebar-c74f48 > div:nth-child(2) > a:nth-child(3)) <a rel="tooltip" class="modebar-btn" data-title="Box Select" data-attr="dragmode" data-val="select" data-toggle="false" data-gravity="n"><svg viewBox="0 0 1000 1000" cl...</a> • Anchor element found with no link content and no name and/or ID attribute. (#modebar-c74f48 > div:nth-child(2) > a:nth-child(4)) <a rel="tooltip" class="modebar-btn" data-title="Lasso Select" data-attr="dragmode" data-val="lasso" data-toggle="false" data-gravity="n"><svg viewBox="0 0 1031 1000" cl...</a> • Anchor element found with no link content and no name and/or ID attribute. (#modebar-c74f48 > div:nth-child(3) > a:nth-child(1)) <a rel="tooltip" class="modebar-btn" data-title="Zoom in" data-attr="zoom" data-val="in" data-toggle="false" data-gravity="n"><svg viewBox="0 0 875 1000" cla...</a> • Anchor element found with no link content and no name and/or ID attribute. (#modebar-c74f48 > div:nth-child(3) > a:nth-child(2)) <a rel="tooltip" class="modebar-btn" data-title="Zoom out" data-attr="zoom" data-val="out" data-toggle="false" data-gravity="n"><svg viewBox="0 0 875 1000" cla...</a> • Anchor element found with no link content and no name and/or ID attribute. (#modebar-c74f48 > div:nth-child(3) > a:nth-child(3)) <a rel="tooltip" class="modebar-btn" data-title="Autoscale" data-attr="zoom" data-val="auto" data-toggle="false" data-gravity="n"><svg viewBox="0 0 1000 1000" cl...</a> • Anchor element found with no link content and no name and/or ID attribute. (#modebar-c74f48 > div:nth-child(3) > a:nth-child(4)) <a rel="tooltip" class="modebar-btn" data-title="Reset axes" data-attr="zoom" data-val="reset" data-toggle="false" data-gravity="n"><svg viewBox="0 0 928.6 1000" c...</a> • Anchor element found with no link content and no name and/or ID attribute. (#modebar-c74f48 > div:nth-child(4) > a:nth-child(1)) <a rel="tooltip" class="modebar-btn" data-title="Toggle Spike Lines" data-attr="_cartesianSpikesEnabled" data-val="on" data-toggle="false" data-gravity="n"><svg viewBox="0 0 1000 1000" cl...</a> • Anchor element found with no link content and no name and/or ID attribute. (#modebar-c74f48 > div:nth-child(4) > a:nth-child(2)) <a rel="tooltip" class="modebar-btn" data-title="Show closest data on hover" data-attr="hovermode" data-val="closest" data-toggle="false" data-gravity="ne"><svg viewBox="0 0 1500 1000" cl...</a> • Anchor element found with no link content and no name and/or ID attribute. (#modebar-c74f48 > div:nth-child(4) > a:nth-child(3)) <a rel="tooltip" class="modebar-btn active" data-title="Compare data on hover" data-attr="hovermode" data-val="x" data-toggle="false" data-gravity="ne"><svg viewBox="0 0 1125 1000" cl...</a> ✘ 0/1 URLs passed ``` </details> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/alexcjohnson"><img src="https://avatars.githubusercontent.com/u/2678795?v=4" />alexcjohnson</a> commented <strong> 1 year ago</strong> </div> <div class="markdown-body"> <p>Thanks @warrickball - do you have a reference for how exactly <code>id</code> and / or <code>name</code> attributes improve accessibility? These anchor elements are the actual modebar buttons, <code>rel="tooltip"</code> looks to me like an obsolete artifact we should remove (the buttons have tooltips added by CSS based on the <code>data-title</code> attribute).</p> <p>The buttons are created here:</p> <p><a href="https://github.com/plotly/plotly.js/blob/ab5e16afc34fa4f22efffef129f811381580e0fe/src/components/modebar/modebar.js#L140">https://github.com/plotly/plotly.js/blob/ab5e16afc34fa4f22efffef129f811381580e0fe/src/components/modebar/modebar.js#L140</a></p> <p>based on config items in <a href="https://github.com/plotly/plotly.js/blob/master/src/components/modebar/buttons.js">buttons.js</a></p> <p>Obvious <code>name</code> would be <code>config.name</code>, or <code>id</code> maybe <code>config.name + '-' + this.graphInfo._fullLayout._uid</code></p> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/warrickball"><img src="https://avatars.githubusercontent.com/u/20858744?v=4" />warrickball</a> commented <strong> 1 year ago</strong> </div> <div class="markdown-body"> <p>I want to first disclaim that I am absolutely not an expert on any of this. My understanding is that this error is associated with <a href="https://www.w3.org/TR/2008/REC-WCAG20-20081211/#ensure-compat-rsv">principle 4.1.2</a> of the <a href="https://www.w3.org/WAI/standards-guidelines/wcag/">Web Content Accessibility Guidelines (WCAG) standards (v2.0)</a>. Some searching online led me to <a href="https://www.w3.org/TR/WCAG20-TECHS/H91">this advice</a>, which I think suggests using something like the current <code>data-title</code> for the <code>title</code> field. </p> <p>I'll see if I can make that change myself and run <code>pa11y-ci</code> on it. If that passes, I'm happy to open a PR. I don't know much JavaScript but I'm guessing I could use the same logical block (in the code you linked) that sets <code>data-title</code> to also set <code>title</code>.</p> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/warrickball"><img src="https://avatars.githubusercontent.com/u/20858744?v=4" />warrickball</a> commented <strong> 1 year ago</strong> </div> <div class="markdown-body"> <p>Hmmm... Just setting the <code>title</code> attribute didn't work and I wasn't sure about generating a whole set of short names. An alternative recommended <a href="https://css-tricks.com/accessible-svgs/#aa-2-inline-svg">here</a> suggests adding a <code><title></code> tag as the first child of the inline SVG element. I copied the short description from <code>data-title</code> to such an element and <code>pa11y-ci</code> reports no error. I'll see if I can figure out how to code this.</p> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/warrickball"><img src="https://avatars.githubusercontent.com/u/20858744?v=4" />warrickball</a> commented <strong> 1 year ago</strong> </div> <div class="markdown-body"> <p>I think I can see where the new code would go but I think it's beyond me to implement. This looks like the code that creates the icon:</p> <p><a href="https://github.com/plotly/plotly.js/blob/ab5e16afc34fa4f22efffef129f811381580e0fe/src/components/modebar/modebar.js#L196-L230">https://github.com/plotly/plotly.js/blob/ab5e16afc34fa4f22efffef129f811381580e0fe/src/components/modebar/modebar.js#L196-L230</a></p> <p>My take would be to try to add an extra child <code><title>...</title></code> at L218, where <code>...</code> is the same information that goes into <code>data-title</code>. But I don't see that being passed down into the icon creator and am not sure how one would do that. </p> <p>The creator that writes bundled SVG code only seems to apply to <code>newplotlylogo</code> (see <code>src/fonts/ploticon.js</code>), so I guess the solution there (and for any hardcoded SVG icons added later) is to add a <code>title</code> tag to the hardcoded <code>svg</code> tag.</p> </div> </div> <div class="page-bar-simple"> </div> <div class="footer"> <ul class="body"> <li>© <script> document.write(new Date().getFullYear()) </script> Githubissues.</li> <li>Githubissues is a development platform for aggregating issues.</li> </ul> </div> <script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.min.js"></script> <script src="/githubissues/assets/js.js"></script> <script src="/githubissues/assets/markdown.js"></script> <script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.4.0/build/highlight.min.js"></script> <script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.4.0/build/languages/go.min.js"></script> <script> hljs.highlightAll(); </script> </body> </html>