Upgrade accessibility of heading-anchors component to prefer sibling links (if anchor position available)

This commit is contained in:
Zach Leatherman 2024-08-26 14:53:52 -05:00
parent c87bda3621
commit cdd2fd7a42
2 changed files with 78 additions and 32 deletions

View File

@ -256,3 +256,7 @@ header {
margin-right: 1em; margin-right: 1em;
} }
/* Anchor links color */
a[href]:is(:link, :visited).heading-a {
color: #aaa;
}

View File

@ -1,5 +1,5 @@
// Thank you to https://github.com/daviddarnes/heading-anchors // Thank you to https://github.com/daviddarnes/heading-anchors
// Thank you to https://amberwilson.co.uk/blog/are-your-anchor-links-accessible/#what-are-anchor-links-exactly%3F // Thank you to https://amberwilson.co.uk/blog/are-your-anchor-links-accessible/
class HeadingAnchors extends HTMLElement { class HeadingAnchors extends HTMLElement {
static register(tagName) { static register(tagName) {
@ -8,61 +8,97 @@ class HeadingAnchors extends HTMLElement {
} }
} }
static attributes = {
exclude: "data-heading-anchors-exclude"
};
static classes = {
anchor: "heading-a",
heading: "heading-a-h", // only used for nested method
}
static defaultSelector = "h2,h3,h4"; static defaultSelector = "h2,h3,h4";
static featureTest() { static featureTest() {
return "replaceSync" in CSSStyleSheet.prototype;
}
static css = `
.heading-anchor {
text-decoration: none;
}
/* For headings that already have links */
:is(h1, h2, h3, h4, h5, h6):has(a[href]:not(.heading-anchor)):is(:hover, :focus-within) .heading-anchor:after {
opacity: 1;
}
.heading-anchor:focus:after,
.heading-anchor:hover:after {
opacity: 1;
}
.heading-anchor:after {
content: "#";
content: "#" / "";
margin-left: .25em;
color: #aaa;
opacity: 0;
}`;
constructor() {
if (!HeadingAnchors.featureTest()) {
return ; return ;
} }
static css = `
.${HeadingAnchors.classes.anchor} {
text-decoration: none;
font-weight: 400;
opacity: 0;
transition: opacity .15s;
padding-left: .25em;
padding-right: .25em;
}
/* nested */
:is(h1,h2,h3,h4,h5,h6):is(:focus, :hover) .${HeadingAnchors.classes.anchor},
/* sibling */
:is(h1,h2,h3,h4,h5,h6) + .${HeadingAnchors.classes.anchor}:is(:focus, :hover),
:is(h1,h2,h3,h4,h5,h6):is(:focus,:hover) + .${HeadingAnchors.classes.anchor} {
opacity: 1;
}
@supports not (anchor-name: none) {
.${HeadingAnchors.classes.heading} {
position: relative;
}
.${HeadingAnchors.classes.anchor} {
position: absolute;
left: 0;
transform: translateX(-100%);
}
}
@supports (anchor-name: none) {
.${HeadingAnchors.classes.anchor} {
position: absolute;
right: anchor(left);
top: anchor(top);
}
}`;
get supportsTest() {
return "replaceSync" in CSSStyleSheet.prototype;
}
get supportsAnchorPosition() {
return CSS.supports("anchor-name: none");
}
constructor() {
super(); super();
if(!this.supportsTest) {
return;
}
let sheet = new CSSStyleSheet(); let sheet = new CSSStyleSheet();
sheet.replaceSync(HeadingAnchors.css); sheet.replaceSync(HeadingAnchors.css);
document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet]; document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet];
} }
connectedCallback() { connectedCallback() {
if (!HeadingAnchors.featureTest()) { if (!this.supportsTest) {
return; return;
} }
this.headings.forEach((heading) => { this.headings.forEach((heading, index) => {
if(!heading.hasAttribute("data-heading-anchors-optout")) { if(!heading.hasAttribute(HeadingAnchors.attributes.exclude)) {
let anchor = this.getAnchorElement(heading); let anchor = this.getAnchorElement(heading);
if(heading.querySelector(":scope a[href]")) {
// Fallback if the heading already has a link // Prefers anchor position approach for better accessibility
anchor.setAttribute("aria-label", `Jump to section: ${heading.textContent}`); // https://amberwilson.co.uk/blog/are-your-anchor-links-accessible/
heading.appendChild(anchor); if(this.supportsAnchorPosition) {
let anchorName = `--h-a_${index}`;
heading.style.anchorName = anchorName;
anchor.style.positionAnchor = anchorName;
let fontSize = parseInt(getComputedStyle(heading).getPropertyValue("font-size"), 10);
anchor.style.fontSize = `${(fontSize / 16).toFixed(3)}em`;
heading.after(anchor);
} else { } else {
// entire heading is a link heading.classList.add(HeadingAnchors.classes.heading);
for(let child of heading.childNodes) {
anchor.appendChild(child);
}
heading.appendChild(anchor); heading.appendChild(anchor);
} }
} }
@ -72,7 +108,13 @@ class HeadingAnchors extends HTMLElement {
getAnchorElement(heading) { getAnchorElement(heading) {
let anchor = document.createElement("a"); let anchor = document.createElement("a");
anchor.href = `#${heading.id}`; anchor.href = `#${heading.id}`;
anchor.classList.add("heading-anchor"); anchor.classList.add(HeadingAnchors.classes.anchor);
if(this.supportsAnchorPosition) {
anchor.innerHTML = `<span class="visually-hidden">Jump to section titled: ${heading.textContent}</span><span aria-hidden="true">#</span>`;
} else {
anchor.innerHTML = `<span aria-hidden="true">#</span>`;
}
return anchor; return anchor;
} }