jdsteinbach / eleventy-plugin-toc

11ty plugin to generate a TOC from page content
62 stars 19 forks source link

Add ARIA patterns to the wrapper element #17

Closed thedigitalman closed 3 years ago

thedigitalman commented 4 years ago

Thanks for writing this plugin! I'm currently working on a design system using 11ty and this has been very useful.

I have two accessibility related suggestions.

  1. Add aria-label and aria-labelledby to help identify the navigation landmark.
  2. Add role="navigation" for any wrapper other than "nav".

4.3.6 Navigation | WAI-ARIA Authoring Practices 1.1

aria-label

The aria-label attribute could be included by default with a value of "Table of contents". A new option of wrapperLabel could be added to replace the default value.

/* Config */

{
    wrapperLabel: 'Article contents'
}

/* Result */

<nav aria-label="Article contents">
    <ol>
        …
    </ol>
</nav>

aria-labelledby

If the author wants visible text to describe the table of contents, they could set a wrapperLabelHeading option to "true".

That would add aria-labelledby="toc-label" to the <nav> element, and a <h2> with an id of "toc-label" with the wrapperLabel option providing the text.

/* Config */

{
    wrapperLabel: 'Article contents',
    wrapperLabelHeading: true
}

/* Result */

<nav aria-labelledby="toc-label">
    <h2 id="toc-label">Table of contents</h2>

    <ol>
        …
    </ol>
</nav>

role="navigation"

If the wrapper is anything other then <nav>, add role="navigation".

/* Config */

{
    wrapper: 'div'
}

/* Result */

<div role="navigation" aria-label="Table of contents">
    <ol>
        …
    </ol>
</div>

Navigation Landmark: ARIA Landmark Example

thedigitalman commented 4 years ago

I wasn't able to publish a branch for this feature. But, here's the working code I'm using.

const cheerio = require('cheerio')

const ParseOptions = require('./ParseOptions')
const NestHeadings = require('./NestHeadings')
const BuildList = require('./BuildList')

const defaults = {
  tags: ['h2', 'h3', 'h4'],
  wrapper: 'nav',
  wrapperClass: 'toc',
  wrapperLabel: 'Table of contents',
  wrapperHeading: 'h2',
  wrapperHeadingClass: 'toc-heading',
  ul: false,
  flat: false,
}

const BuildTOC = (text, opts) => {
  const {tags, wrapper, wrapperClass, wrapperLabel, wrapperHeading, wrapperHeadingClass, ul, flat} = ParseOptions(opts, defaults)
  const $ = cheerio.load(text)
  const headings = NestHeadings(tags, $)

  if (wrapper !== 'nav') {
    return headings.length > 0
    ? `<${wrapper} class="${wrapperClass}" role="navigation" aria-labelledby="toc-label">
        <${wrapperHeading} class="${wrapperHeadingClass}" id="toc-label">${wrapperLabel}</${wrapperHeading}>
        ${BuildList(headings, ul, flat)}</${wrapper}>`
    : undefined
  } else {
    return headings.length > 0
    ? `<${wrapper} class="${wrapperClass}" aria-label="${wrapperLabel}">
        ${BuildList(headings, ul, flat)}</${wrapper}>`
    : undefined
  }
}

module.exports = BuildTOC