Files
indiekit-blog/js/time-difference.js
Ricardo e236b4bf65 a11y: comprehensive WCAG 2.1 Level AA accessibility audit
- Add skip-to-main-content link and main content ID target
- Add prefers-reduced-motion media queries for all animations
- Enhance visible focus indicators (2px offset, high-contrast ring)
- Replace ~160 text-surface-500 instances with text-surface-600/dark:text-surface-400
  for 4.5:1+ contrast ratio compliance
- Add aria-hidden="true" to ~30+ decorative SVG icons across sidebars/widgets
- Convert facepile containers from div to semantic ul/li with role="list"
- Add aria-label to icon-only buttons (share, sort controls)
- Add sr-only labels to form inputs (webmention, search)
- Add aria-live="polite" to dynamically loaded webmentions
- Add aria-label with relative+absolute date to time-difference component
- Add keyboard handlers (Enter/Space) to custom interactive elements
- Add aria-label to nav landmarks (table of contents)
- Fix modal focus trap and dialog accessibility
- Fix lightbox keyboard navigation and screen reader announcements

Confab-Link: http://localhost:8080/sessions/edb1b7b0-da66-4486-bd9c-d1cfa7553b88
2026-03-07 18:58:08 +01:00

95 lines
2.6 KiB
JavaScript

/**
* <time-difference> Web Component
* Progressively enhances <time> elements with relative date display.
* Falls back to static date text when JS is unavailable.
*
* Usage: <time-difference><time datetime="2026-02-15T...">February 15, 2026</time></time-difference>
*
* Inspired by zachleat.com's time-difference component.
*/
class TimeDifference extends HTMLElement {
static register(tagName = "time-difference") {
if ("customElements" in window) {
customElements.define(tagName, TimeDifference);
}
}
connectedCallback() {
this.update();
// Auto-update every 60 seconds
this._interval = setInterval(() => this.update(), 60000);
}
disconnectedCallback() {
clearInterval(this._interval);
}
update() {
const time = this.querySelector("time[datetime]");
if (!time) return;
const datetime = time.getAttribute("datetime");
if (!datetime) return;
const date = new Date(datetime);
if (isNaN(date.getTime())) return;
const now = new Date();
const diffMs = now - date;
// Don't show relative time for future dates
if (diffMs < 0) return;
const diffSec = Math.floor(diffMs / 1000);
const diffMin = Math.floor(diffSec / 60);
const diffHour = Math.floor(diffMin / 60);
const diffDay = Math.floor(diffHour / 24);
const diffWeek = Math.floor(diffDay / 7);
const diffMonth = Math.floor(diffDay / 30.44);
const diffYear = Math.floor(diffDay / 365.25);
let value, unit;
if (diffSec < 60) {
value = -diffSec;
unit = "second";
} else if (diffMin < 60) {
value = -diffMin;
unit = "minute";
} else if (diffHour < 24) {
value = -diffHour;
unit = "hour";
} else if (diffDay < 7) {
value = -diffDay;
unit = "day";
} else if (diffWeek < 4) {
value = -diffWeek;
unit = "week";
} else if (diffMonth < 12) {
value = -diffMonth;
unit = "month";
} else {
value = -diffYear;
unit = "year";
}
try {
const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
const relative = rtf.format(value, unit);
// Store original text as title for hover tooltip
const originalText = time.textContent.trim();
if (!time.hasAttribute("title")) {
time.setAttribute("title", originalText);
}
// aria-label provides the full context: "2 days ago (March 5, 2026)"
time.setAttribute("aria-label", relative + " (" + originalText + ")");
time.textContent = relative;
} catch {
// Intl.RelativeTimeFormat not supported, keep static text
}
}
}
TimeDifference.register();