From 0956c1119e6559bcdc58df3e6f212fa3d3ca1e4e Mon Sep 17 00:00:00 2001 From: Jason Morris Date: Sun, 4 Jan 2026 21:17:06 -0500 Subject: [PATCH 1/2] Add follow focus bookmarklet --- bookmarklets/follow-focus-console.js | 25 +++++++++++++++++++++++++ data/auditing.html | 1 + data/bookmarks.html | 1 + 3 files changed, 27 insertions(+) create mode 100644 bookmarklets/follow-focus-console.js diff --git a/bookmarklets/follow-focus-console.js b/bookmarklets/follow-focus-console.js new file mode 100644 index 0000000..bd77e00 --- /dev/null +++ b/bookmarklets/follow-focus-console.js @@ -0,0 +1,25 @@ +/** + * @bookmarklet Follow focus (in console) + * @description Displays the currently focused DOM node in the console + * @author Jason Morris + * @authorUrl https://jasonmorris.com + * @tags accessibility, wcag:2.4.3 + * @auditing true + * @pageTest self + */ +(function () { + if (window._focusLogEnabled) { + document.removeEventListener("focusin", window._focusLogHandler); + window._focusLogEnabled = false; + console.log("Focus logging disabled."); + alert("Focus logging disabled."); + } else { + window._focusLogHandler = function (e) { + console.log("Focused:", e.target); + }; + document.addEventListener("focusin", window._focusLogHandler); + window._focusLogEnabled = true; + console.log("Focus logging enabled."); + alert("Focus logging enabled. Open DevTools console (F12) to see results."); + } +})(); diff --git a/data/auditing.html b/data/auditing.html index 0d259ab..f57cdd0 100644 --- a/data/auditing.html +++ b/data/auditing.html @@ -12,6 +12,7 @@

Bookmarks Menu

Character key shortcuts
Find Duplicate ARIA Roles
Focus everything +
Follow focus (in console)
Force focus indicator
Document outline in console
Headings diff --git a/data/bookmarks.html b/data/bookmarks.html index bda1813..21b89f1 100644 --- a/data/bookmarks.html +++ b/data/bookmarks.html @@ -22,6 +22,7 @@

Bookmarks Menu

Duplicate IDs
Find Duplicate ARIA Roles
Focus everything +
Follow focus (in console)
Force focus indicator
Grayscale
Grouped fields From 22b884d509ff5f7a21de9e6331a025e0b4efa01d Mon Sep 17 00:00:00 2001 From: Jason Morris Date: Sun, 4 Jan 2026 21:39:42 -0500 Subject: [PATCH 2/2] Add target spacing bookmarklet --- bookmarklets/target-spacing.js | 311 +++++++++++++++++++++++++++++++++ data/auditing.html | 1 + data/bookmarks.html | 1 + 3 files changed, 313 insertions(+) create mode 100644 bookmarklets/target-spacing.js diff --git a/bookmarklets/target-spacing.js b/bookmarklets/target-spacing.js new file mode 100644 index 0000000..00d31fe --- /dev/null +++ b/bookmarklets/target-spacing.js @@ -0,0 +1,311 @@ +/** + * @bookmarklet Target spacing check + * @description Identify controls with spacing or size issues + * @author Jason Morris + * @authorUrl https://jasonmorris.com + * @tags accessibility, wcag:2.5.8 + * @auditing true + * @pageTest self + */ +(function () { + "use strict"; + + const existing = document.getElementById("wcag-target-size-overlay"); + if (existing) { + existing.remove(); + return; + } + + const MIN_SIZE = 24; + const CIRCLE_RADIUS = 12; + + const interactiveSelectors = [ + "a[href]", + "button", + 'input:not([type="hidden"])', + "select", + "textarea", + "summary", + '[role="button"]', + '[role="link"]', + '[role="menuitem"]', + '[role="menuitemcheckbox"]', + '[role="menuitemradio"]', + '[role="tab"]', + "[onclick]", + '[tabindex]:not([tabindex="-1"])', + ]; + + const elements = Array.from( + document.querySelectorAll(interactiveSelectors.join(",")) + ); + + const visibleElements = elements.filter((el) => { + const style = window.getComputedStyle(el); + const rect = el.getBoundingClientRect(); + return ( + style.display !== "none" && + style.visibility !== "hidden" && + rect.width > 0 && + rect.height > 0 + ); + }); + + function isInlineTarget(el) { + const style = window.getComputedStyle(el); + const display = style.display; + if (!display.includes("inline")) return false; + const parent = el.parentElement; + if (!parent) return false; + const parentText = parent.textContent.trim(); + const elText = el.textContent.trim(); + return parentText.length > elText.length + 10; + } + + function meetsMinimumSize(rect) { + return rect.width >= MIN_SIZE && rect.height >= MIN_SIZE; + } + + function getCenter(rect) { + return { + x: rect.left + rect.width / 2, + y: rect.top + rect.height / 2, + }; + } + + function circlesIntersect(center1, center2) { + const dx = center1.x - center2.x; + const dy = center1.y - center2.y; + const distance = Math.sqrt(dx * dx + dy * dy); + return distance < MIN_SIZE; + } + + function circleIntersectsRect(center, rect) { + const closestX = Math.max(rect.left, Math.min(center.x, rect.right)); + const closestY = Math.max(rect.top, Math.min(center.y, rect.bottom)); + const dx = center.x - closestX; + const dy = center.y - closestY; + const distance = Math.sqrt(dx * dx + dy * dy); + return distance < CIRCLE_RADIUS; + } + + const results = visibleElements.map((el, index) => { + const rect = el.getBoundingClientRect(); + const isInline = isInlineTarget(el); + const meetsSize = meetsMinimumSize(rect); + return { + element: el, + rect: rect, + index: index, + isInline: isInline, + meetsSize: meetsSize, + center: getCenter(rect), + violation: null, + reason: null, + }; + }); + + results.forEach((target) => { + if (target.meetsSize || target.isInline) { + return; + } + const hasViolation = results.some((other) => { + if (other === target) return false; + if (circleIntersectsRect(target.center, other.rect)) { + target.violation = "spacing"; + target.reason = "Undersized target with insufficient spacing"; + return true; + } + if (!other.meetsSize && !other.isInline) { + if (circlesIntersect(target.center, other.center)) { + target.violation = "spacing"; + target.reason = "Undersized target circles intersect"; + return true; + } + } + return false; + }); + if (!hasViolation) { + target.violation = null; + } + }); + + results.forEach((target) => { + if (!target.meetsSize && !target.isInline && !target.violation) { + return; + } + if ( + !target.meetsSize && + !target.isInline && + target.violation === "spacing" + ) { + return; + } + if (!target.meetsSize && !target.isInline && !target.violation) { + target.violation = null; + } + }); + + const violations = results.filter((r) => r.violation); + const warnings = results.filter( + (r) => !r.meetsSize && !r.isInline && !r.violation + ); + const passes = results.filter((r) => r.meetsSize || r.isInline); + + const overlay = document.createElement("div"); + overlay.id = "wcag-target-size-overlay"; + overlay.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + background: white; + border: 2px solid #333; + border-radius: 8px; + padding: 0; + max-width: 400px; + z-index: 999999; + font-family: system-ui, -apple-system, sans-serif; + font-size: 14px; + line-height: 1.5; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); + `; + + const headerHTML = ` +
+

Target spacing

+
+ + +
+
+ `; + + const contentHTML = ` +
+
+
+ + ${violations.length} Violations +
+
+ + ${warnings.length} Undersized (but sufficient spacing) +
+
+ + ${passes.length} Pass +
+
+
+ Targets highlighted on page. Violations do not meet WCAG 2.5.8 Level AA, programmatically. Manual review is advised. +
+
+ `; + + overlay.innerHTML = headerHTML + contentHTML; + + document.body.appendChild(overlay); + + const header = document.getElementById("wcag-header"); + const content = document.getElementById("wcag-content"); + const minimizeBtn = document.getElementById("wcag-minimize"); + let isMinimized = false; + + minimizeBtn.addEventListener("click", () => { + isMinimized = !isMinimized; + content.style.display = isMinimized ? "none" : "block"; + minimizeBtn.innerHTML = isMinimized ? "+" : "–"; + minimizeBtn.setAttribute("aria-label", isMinimized ? "Expand" : "Minimize"); + }); + + document.getElementById("wcag-close").addEventListener("click", () => { + overlay.remove(); + document.querySelectorAll(".wcag-highlight").forEach((el) => el.remove()); + }); + + let isDragging = false; + let dragOffsetX = 0; + let dragOffsetY = 0; + + header.addEventListener("mousedown", (e) => { + if (e.target.tagName === "BUTTON") return; + isDragging = true; + const rect = overlay.getBoundingClientRect(); + dragOffsetX = e.clientX - rect.left; + dragOffsetY = e.clientY - rect.top; + overlay.style.right = "auto"; + }); + + document.addEventListener("mousemove", (e) => { + if (!isDragging) return; + overlay.style.left = e.clientX - dragOffsetX + "px"; + overlay.style.top = e.clientY - dragOffsetY + "px"; + }); + + document.addEventListener("mouseup", () => { + isDragging = false; + }); + + results.forEach((target) => { + const highlight = document.createElement("div"); + highlight.className = "wcag-highlight"; + highlight.style.cssText = ` + position: absolute; + left: ${target.rect.left + window.scrollX}px; + top: ${target.rect.top + window.scrollY}px; + width: ${target.rect.width}px; + height: ${target.rect.height}px; + pointer-events: none; + z-index: 999998; + box-sizing: border-box; + `; + + if (target.violation) { + highlight.style.outline = "2px solid #dc3545"; + highlight.style.backgroundColor = "rgba(220, 53, 69, 0.2)"; + if (!target.meetsSize) { + const circle = document.createElement("div"); + circle.style.cssText = ` + position: absolute; + left: ${target.rect.width / 2 - CIRCLE_RADIUS}px; + top: ${target.rect.height / 2 - CIRCLE_RADIUS}px; + width: ${MIN_SIZE}px; + height: ${MIN_SIZE}px; + outline: 4px solid #dc3545; + outline-offset: -4px; + border-radius: 50%; + pointer-events: none; + `; + highlight.appendChild(circle); + } + } else if (!target.meetsSize && !target.isInline) { + highlight.style.outline = "2px solid #ffc107"; + highlight.style.backgroundColor = "rgba(255, 193, 7, 0.2)"; + const circle = document.createElement("div"); + circle.style.cssText = ` + position: absolute; + left: ${target.rect.width / 2 - CIRCLE_RADIUS}px; + top: ${target.rect.height / 2 - CIRCLE_RADIUS}px; + width: ${MIN_SIZE}px; + height: ${MIN_SIZE}px; + outline: 4px solid #ffc107; + outline-offset: -4px; + border-radius: 50%; + pointer-events: none; + `; + highlight.appendChild(circle); + } else { + highlight.style.outline = "1px solid #28a745"; + highlight.style.backgroundColor = "rgba(40, 167, 69, 0.1)"; + } + + document.body.appendChild(highlight); + }); + + console.log("Target spacing analysis:", { + total: results.length, + violations: violations.length, + warnings: warnings.length, + passes: passes.length, + }); +})(); diff --git a/data/auditing.html b/data/auditing.html index f57cdd0..d64c734 100644 --- a/data/auditing.html +++ b/data/auditing.html @@ -25,6 +25,7 @@

Bookmarks Menu

Parsing only
Placeholder contrast checker
Validate +
Target spacing check
Text spacing
Titles
ANDI diff --git a/data/bookmarks.html b/data/bookmarks.html index 21b89f1..29dedfd 100644 --- a/data/bookmarks.html +++ b/data/bookmarks.html @@ -50,6 +50,7 @@

Bookmarks Menu

WAVE report
Show focus styles
Remove onpaste +
Target spacing check
Test CSP
Test local JS
Text spacing