diff --git a/codespace-telemetry-debug.log b/codespace-telemetry-debug.log
new file mode 100644
index 0000000..8fbdcb4
--- /dev/null
+++ b/codespace-telemetry-debug.log
@@ -0,0 +1,10 @@
+[2026-03-06T12:52:04.705Z] === CodespaceTelemetryService Constructor Started ===
+[2026-03-06T12:52:04.709Z] Session ID: 1772801524705-3xagwzqnckb
+[2026-03-06T12:52:04.716Z] CODESPACE_NAME: undefined
+[2026-03-06T12:52:04.719Z] CODESPACES: undefined
+[2026-03-06T12:52:04.722Z] GITHUB_USER: undefined
+[2026-03-06T12:52:04.725Z] GITHUB_CODESPACE_TOKEN: NOT SET
+[2026-03-06T12:52:04.727Z] NOT IN GENUINE GITHUB CODESPACE - Service disabled
+[2026-03-06T12:52:04.730Z] CODESPACE_NAME: missing
+[2026-03-06T12:52:04.732Z] CODESPACES: missing
+[2026-03-06T12:52:04.734Z] GITHUB_CODESPACE_TOKEN: missing
diff --git a/src/ui/chevron.svg b/src/ui/chevron.svg
new file mode 100644
index 0000000..3a82e36
--- /dev/null
+++ b/src/ui/chevron.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/src/ui/copy.svg b/src/ui/copy.svg
new file mode 100644
index 0000000..c53fcc5
--- /dev/null
+++ b/src/ui/copy.svg
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/src/ui/highlighter.css b/src/ui/highlighter.css
index fcf2780..841a92c 100644
--- a/src/ui/highlighter.css
+++ b/src/ui/highlighter.css
@@ -1,8 +1,6 @@
.abledom-highlight {
- background-color: yellow;
box-sizing: border-box;
display: none;
- opacity: 0.6;
position: fixed;
z-index: 100499;
}
@@ -28,3 +26,20 @@
height: calc(100% + 20px);
margin: -10px 0;
}
+
+.abledom-dimming-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100vw;
+ height: 100vh;
+ background: rgba(0, 0, 0, 0.5);
+ z-index: 100498;
+ pointer-events: none;
+ opacity: 0;
+ transition: opacity 0.2s ease;
+}
+
+.abledom-dimming-overlay.visible {
+ opacity: 1;
+}
diff --git a/src/ui/sparkles.svg b/src/ui/sparkles.svg
new file mode 100644
index 0000000..83fb953
--- /dev/null
+++ b/src/ui/sparkles.svg
@@ -0,0 +1,7 @@
+
diff --git a/src/ui/ui.css b/src/ui/ui.css
index 03000f0..ae373fc 100644
--- a/src/ui/ui.css
+++ b/src/ui/ui.css
@@ -1,174 +1,355 @@
#abledom-report {
- bottom: 20px;
- display: flex;
- flex-direction: column;
- left: 10px;
- max-height: 80%;
- max-width: 60%;
- padding: 4px 8px;
+ bottom: 24px;
+ right: 24px;
position: fixed;
z-index: 100500;
+ font-family:
+ -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu,
+ Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
+ display: flex;
+ flex-direction: column; /* Items stack top-to-bottom: List then Button */
+ justify-content: flex-end; /* Push content to the bottom of the container */
+ align-items: flex-end; /* Align items to the right */
+ pointer-events: none; /* Let clicks pass through empty space */
+ width: auto;
+ max-width: 100%;
+ max-height: 90vh; /* Allow list to take up height but not overflow screen */
+ height: auto;
}
-#abledom-report :focus-visible {
- outline: 3px solid red;
- mix-blend-mode: difference;
-}
-
-#abledom-report.abledom-align-left {
- left: 10px;
- right: auto;
+#abledom-report * {
+ pointer-events: auto; /* Re-enable pointer events on children */
+ box-sizing: border-box;
}
-#abledom-report.abledom-align-right {
- left: auto;
- right: 10px;
+.abledom-floating-button {
+ height: 40px;
+ border-radius: 20px;
+ background-color: #1a1a1a;
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 6px;
+ padding: 0 16px;
+ transition:
+ transform 0.2s cubic-bezier(0.4, 0, 0.2, 1),
+ background-color 0.2s,
+ box-shadow 0.2s;
+ color: white;
+ user-select: none;
+ /* Fixed positioning to prevent movement */
+ position: fixed;
+ bottom: 24px;
+ right: 24px;
+ z-index: 100501;
+ font-size: 13px;
+ font-weight: 600;
+ letter-spacing: 0.3px;
}
-#abledom-report.abledom-align-bottom {
- bottom: 20px;
- top: auto;
+.abledom-floating-button:hover {
+ background-color: #2d2d2d;
+ box-shadow: 0 6px 20px rgba(0, 0, 0, 0.5);
+ transform: translateY(-1px);
}
-#abledom-report.abledom-align-top {
- /* flex-direction: column-reverse; */
- bottom: auto;
- top: 10px;
+.abledom-floating-button.active {
+ background-color: #111111;
}
-.abledom-menu-container {
- backdrop-filter: blur(3px);
- border-radius: 8px;
- box-shadow: 0px 0px 4px rgba(127, 127, 127, 0.5);
- display: inline-block;
- margin: 2px auto 2px 0;
+.abledom-floating-label {
+ white-space: nowrap;
}
-#abledom-report.abledom-align-right .abledom-menu-container {
- margin: 2px 0 2px auto;
+.abledom-floating-counter {
+ background: rgba(255, 255, 255, 0.25);
+ padding: 1px 8px;
+ border-radius: 10px;
+ font-size: 12px;
+ font-weight: bold;
+ min-width: 20px;
+ text-align: center;
}
-.abledom-menu {
- background-color: rgba(140, 10, 121, 0.7);
- border-radius: 8px;
- color: white;
- display: inline flex;
- font-family: Arial, Helvetica, sans-serif;
- font-size: 16px;
- line-height: 26px;
+/* Corner alignment controls - shown on button hover */
+.abledom-alignment-bar {
+ position: absolute;
+ z-index: 100502;
+ display: flex;
+ gap: 2px;
+ background: white;
+ border-radius: 4px;
padding: 4px;
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
+ opacity: 0;
+ pointer-events: none;
+ transition:
+ opacity 0.2s,
+ transform 0.2s;
+ /* Default: appears to the left of the button, vertically centered */
+ right: calc(100% + 8px);
+ top: 50%;
+ transform: translateY(-50%) translateX(8px);
}
-.abledom-menu .issues-count {
- margin: 0 8px;
- display: inline-block;
+.abledom-floating-button:hover .abledom-alignment-bar {
+ opacity: 1;
+ pointer-events: auto;
+ transform: translateY(-50%) translateX(0);
}
-.abledom-menu .button {
- all: unset;
- color: #000;
+.abledom-align-btn {
+ width: 28px;
+ height: 28px;
+ border: none;
+ background: none;
cursor: pointer;
- background: linear-gradient(
- 180deg,
- rgba(255, 255, 255, 1) 0%,
- rgba(200, 200, 200, 1) 100%
- );
- border-radius: 6px;
- border: 1px solid rgba(255, 255, 255, 0.4);
- box-sizing: border-box;
- line-height: 0px;
- margin-right: 4px;
- max-height: 26px;
- padding: 2px;
- text-decoration: none;
+ border-radius: 4px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: #757575;
+ padding: 4px;
+ transition:
+ background-color 0.15s,
+ color 0.15s;
}
-.abledom-menu .align-button {
- border-right-color: rgba(0, 0, 0, 0.4);
- border-radius: 0;
- margin: 0;
+.abledom-align-btn:hover {
+ background: #f0f0f0;
+ color: #333;
}
-.abledom-menu .align-button:active,
-#abledom-report .pressed {
- background: linear-gradient(
- 180deg,
- rgba(130, 130, 130, 1) 0%,
- rgba(180, 180, 180, 1) 100%
- );
+.abledom-align-btn.active {
+ background: #e0e0e0;
+ color: #1a1a1a;
}
-.abledom-menu .align-button-first {
- border-top-left-radius: 6px;
- border-bottom-left-radius: 6px;
- margin-left: 8px;
-}
-.abledom-menu .align-button-last {
- border-top-right-radius: 6px;
- border-bottom-right-radius: 6px;
- border-right-color: rgba(255, 255, 255, 0.4);
+.abledom-align-btn svg {
+ width: 16px;
+ height: 16px;
+ stroke: currentColor;
+ fill: none;
}
+/* Ensure no margin on the list interfering with layout */
.abledom-issues-container {
- overflow: auto;
- max-height: calc(100vh - 100px);
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ /* Push list up to avoid overlapping the fixed button */
+ margin-bottom: 76px; /* button + alignment bar + gap */
+ max-height: calc(90vh - 120px);
+ overflow-y: auto;
+ width: 340px;
+ padding-right: 4px; /* Space for scrollbar */
+ /* padding-bottom: 16px; */
}
-#abledom-report.abledom-align-right .abledom-issues-container {
- text-align: right;
+/* Scrollbar styling */
+.abledom-issues-container::-webkit-scrollbar {
+ width: 4px;
+}
+.abledom-issues-container::-webkit-scrollbar-thumb {
+ background: rgba(0, 0, 0, 0.2);
+ border-radius: 4px;
}
.abledom-issue-container {
- backdrop-filter: blur(3px);
- border-radius: 8px;
- box-shadow: 0px 0px 4px rgba(127, 127, 127, 0.5);
- display: inline-flex;
- margin: 2px 0;
+ /* Replaces existing style */
+ display: flex;
+ width: 100%;
}
.abledom-issue {
- background-color: rgba(164, 2, 2, 0.7);
+ background: white;
border-radius: 8px;
- color: white;
- display: inline flex;
- font-family: Arial, Helvetica, sans-serif;
- font-size: 16px;
- line-height: 26px;
- padding: 4px;
+ border: 1px solid #ddd;
+ padding: 12px;
+ width: 100%;
+ font-size: 14px;
+ line-height: 1.4;
+ color: #333;
+ border-left: 4px solid #d32f2f; /* Default warning color */
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ transition:
+ opacity 0.2s,
+ box-shadow 0.2s;
+}
+
+.abledom-issue:hover {
+ box-shadow:
+ 0 4px 16px rgba(0, 0, 0, 0.22),
+ 0 2px 6px rgba(0, 0, 0, 0.1);
}
+
.abledom-issue_warning {
- background-color: rgba(163, 82, 1, 0.7);
+ border-left-color: #f57c00;
}
.abledom-issue_info {
- background-color: rgba(0, 0, 255, 0.7);
+ border-left-color: #1976d2;
}
+/* Button base styles */
.abledom-issue .button {
- all: unset;
- color: #000;
+ background: none;
+ border: none;
cursor: pointer;
- background: linear-gradient(
- 180deg,
- rgba(255, 255, 255, 1) 0%,
- rgba(200, 200, 200, 1) 100%
- );
- border-radius: 6px;
- border: 1px solid rgba(255, 255, 255, 0.4);
- box-sizing: border-box;
- line-height: 0px;
- margin-right: 4px;
- max-height: 26px;
- padding: 2px;
+ padding: 4px;
+ border-radius: 4px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ color: #757575;
+ transition:
+ color 0.15s,
+ background-color 0.15s,
+ transform 0.15s;
text-decoration: none;
+ line-height: 1;
}
.abledom-issue .button:hover {
- opacity: 0.7;
+ background-color: rgba(0, 0, 0, 0.05);
+ transform: translateY(-1px);
}
-.abledom-issue .button.close {
- background: none;
- border-color: transparent;
- color: #fff;
- margin: 0;
+.abledom-issue .button svg {
+ width: 18px; /* Slightly larger */
+ height: 18px;
+ fill: none;
+ stroke: currentColor;
+}
+
+/* Button-specific styling */
+.abledom-issue .abledom-button-log:hover {
+ color: #5c6bc0; /* Indigo */
+ background-color: rgba(92, 107, 192, 0.1);
+}
+
+.abledom-issue .abledom-button-reveal:hover {
+ color: #ef6c00; /* Deep Orange */
+ background-color: rgba(239, 108, 0, 0.1);
+}
+
+.abledom-issue .abledom-button-bug:hover {
+ color: #d32f2f; /* Red */
+ background-color: rgba(211, 47, 47, 0.1);
+}
+
+.abledom-issue .abledom-button-copy:hover {
+ color: #8e24aa; /* Purple */
+ background-color: rgba(142, 36, 170, 0.1);
+}
+
+.abledom-issue .abledom-button-help:hover {
+ color: #0288d1; /* Light Blue */
+ background-color: rgba(2, 136, 209, 0.1);
+}
+
+.abledom-issue .abledom-button-hide:hover {
+ color: #424242; /* Darker Gray */
+ background-color: rgba(0, 0, 0, 0.1);
+}
+
+/* Ensure SVG stroke/fill is handled correctly based on the SVG content */
+.abledom-issue .button svg circle,
+.abledom-issue .button svg line,
+.abledom-issue .button svg path,
+.abledom-issue .button svg polyline,
+.abledom-issue .button svg ellipse {
+ /* Let inherited properties work */
+}
+
+/* Layout for actions inside issue card */
+.abledom-issue-actions {
+ display: flex;
+ justify-content: flex-end;
+ gap: 4px;
+ margin-top: 4px;
+ border-top: 1px solid #eee;
+ padding-top: 8px;
+}
+
+.abledom-issue-message {
+ margin-bottom: 4px;
+}
+
+/* Group Headers */
+.abledom-group-header {
+ border-bottom: 1px solid #ddd;
+ padding: 8px 12px;
+ background: #f9f9f9;
+ cursor: pointer;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ font-weight: 600;
+ user-select: none;
+ font-size: 14px;
+ color: #333;
+ margin-top: 8px;
+ border-radius: 4px;
+ transition: background-color 0.2s;
+}
+
+.abledom-group-header:first-child {
+ margin-top: 0;
+}
+
+.abledom-group-header:hover {
+ background: #ececec;
+}
+
+.abledom-group-title {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.abledom-group-content {
+ display: none;
+ margin-top: 8px;
+ flex-direction: column;
+ gap: 8px; /* Maintain gap between cards */
+ padding-bottom: 4px; /* Slight bottom padding */
+}
+
+.abledom-group-content.expanded {
+ display: flex;
+}
+
+.abledom-group-count {
+ background: #4c4c4c;
+ color: #fbfdff;
+ padding: 2px 8px;
+ border-radius: 10px;
+ font-size: 12px;
+ font-weight: bold;
+ min-width: 20px;
+ text-align: center;
+}
+
+.chevron {
+ width: 16px;
+ height: 16px;
+ transition: transform 0.2s ease;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: #757575;
+}
+
+.chevron svg {
+ width: 100%;
+ height: 100%;
+ fill: currentColor;
+}
+
+.abledom-group-header.expanded .chevron {
+ transform: rotate(90deg);
}
diff --git a/src/ui/ui.ts b/src/ui/ui.ts
index 792586a..c4b12fa 100644
--- a/src/ui/ui.ts
+++ b/src/ui/ui.ts
@@ -27,13 +27,10 @@ import svgLog from "./log.svg?raw";
import svgReveal from "./reveal.svg?raw";
// @ts-expect-error parsed assets
import svgBugReport from "./bug.svg?raw";
-
-// @ts-expect-error parsed assets
-import svgHideAll from "./hideall.svg?raw";
// @ts-expect-error parsed assets
-import svgMuteAll from "./muteall.svg?raw";
+import svgSparkles from "./sparkles.svg?raw";
// @ts-expect-error parsed assets
-import svgShowAll from "./showall.svg?raw";
+import svgChevron from "./chevron.svg?raw";
// @ts-expect-error parsed assets
import svgAlignTopLeft from "./aligntopleft.svg?raw";
// @ts-expect-error parsed assets
@@ -67,8 +64,6 @@ enum UIAlignments {
TopRight = "top-right",
}
-const pressedClass = "pressed";
-
// interface WindowWithAbleDOMDevtools extends Window {
// __ableDOMDevtools?: {
// revealElement?: (element: HTMLElement) => Promise;
@@ -93,6 +88,10 @@ export class IssueUI {
return instance._wrapper;
}
+ get rule(): ValidationRule {
+ return this._rule;
+ }
+
isHidden = false;
constructor(
@@ -145,16 +144,28 @@ export class IssueUI {
: ""
}`,
})
+ .openTag("div", { class: "abledom-issue-message" })
+ .text(issue.message)
+ .closeTag()
+ .openTag("div", { class: "abledom-issue-actions" })
.openTag(
"button",
{
- class: "button",
+ class: "button abledom-button-log",
+ type: "button",
title: "Log to Console",
},
(logButton) => {
- logButton.onclick = () => {
+ logButton.onclick = (e) => {
+ e.stopPropagation();
const { id, message, element, rel, help, ...extra } = issue;
+ console.log(
+ "%cAbleDOM Issue:",
+ "font-weight: bold; color: #d32f2f",
+ issue,
+ );
+
this._core.log(
"AbleDOM: ",
"\nid:",
@@ -175,41 +186,26 @@ export class IssueUI {
.openTag(
"button",
{
- class: "button",
- // title: "Reveal in Elements panel",
+ class: "button abledom-button-reveal",
+ type: "button",
title: "Scroll element into view",
},
(revealButton: HTMLElement) => {
const element = issue.element;
if (element) {
- revealButton.onclick = () => {
+ revealButton.onclick = (e) => {
+ e.stopPropagation();
+ element.scrollIntoView({
+ behavior: "smooth",
+ block: "center",
+ inline: "center",
+ });
this._issuesUI?.highlight(element, true);
};
} else {
revealButton.style.display = "none";
}
- // const body = win.document.body;
- // const hasDevTools =
- // !!(win as WindowWithAbleDOMDevtools).__ableDOMDevtools
- // ?.revealElement && false; // Temtorarily disabling the devtools plugin integration.
-
- // if (hasDevTools && element && body.contains(element)) {
- // revealButton.onclick = () => {
- // const revealElement = (win as WindowWithAbleDOMDevtools)
- // .__ableDOMDevtools?.revealElement;
-
- // if (revealElement && body.contains(element)) {
- // revealElement(element).then((revealed: boolean) => {
- // if (!revealed) {
- // // TODO
- // }
- // });
- // }
- // };
- // } else {
- // revealButton.style.display = "none";
- // }
},
)
.element(svgReveal)
@@ -217,7 +213,8 @@ export class IssueUI {
.openTag(
"button",
{
- class: "button",
+ class: "button abledom-button-bug",
+ type: "button",
title: "Report bug",
},
(bugReportButton) => {
@@ -230,7 +227,8 @@ export class IssueUI {
bugReportButton.title = title;
}
- bugReportButton.onclick = () => {
+ bugReportButton.onclick = (e) => {
+ e.stopPropagation();
bugReport.onClick(issue);
};
} else {
@@ -240,16 +238,61 @@ export class IssueUI {
)
.element(svgBugReport)
.closeTag()
- .text(issue.message)
+ .openTag(
+ "button",
+ {
+ class: "button abledom-button-copy",
+ type: "button",
+ title: "Copy AI Prompt",
+ },
+ (copyButton) => {
+ copyButton.onclick = (e) => {
+ e.stopPropagation();
+ const { id, message, element, help } = issue;
+ const elementHtml = element?.outerHTML || "N/A";
+ const prompt = `Fix the following accessibility issue:
+Rule ID: ${id}
+Message: ${message}
+Help Link: ${help || "N/A"}
+Element HTML:
+\`\`\`html
+${elementHtml}
+\`\`\``;
+
+ navigator.clipboard
+ .writeText(prompt)
+ .then(() => {
+ console.log(
+ "%cPrompt copied to clipboard!",
+ "color: green; font-weight: bold;",
+ );
+ // Optional: temporarily change icon or show tooltip
+ const originalTitle = copyButton.title;
+ copyButton.title = "Copied!";
+ setTimeout(() => {
+ copyButton.title = originalTitle;
+ }, 2000);
+ })
+ .catch((err) => {
+ console.error("Failed to copy prompt: ", err);
+ });
+ };
+ },
+ )
+ .element(svgSparkles)
+ .closeTag()
.openTag(
"a",
{
- class: "button close",
+ class: "button close abledom-button-help",
href: issue.help || "/",
title: "Open help",
target: "_blank",
},
(help) => {
+ help.onclick = (e) => {
+ e.stopPropagation();
+ };
if (!issue.help) {
help.style.display = "none";
}
@@ -260,11 +303,13 @@ export class IssueUI {
.openTag(
"button",
{
- class: "button close",
+ class: "button close abledom-button-hide",
+ type: "button",
title: "Hide",
},
(closeButton) => {
- closeButton.onclick = () => {
+ closeButton.onclick = (e) => {
+ e.stopPropagation();
this.toggle(false);
this._issuesUI?.highlight(null);
};
@@ -273,6 +318,7 @@ export class IssueUI {
.element(svgClose)
.closeTag()
.closeTag()
+ .closeTag()
.closeTag();
}
@@ -301,21 +347,25 @@ export class IssueUI {
}
}
+interface IssueGroup {
+ header: HTMLElementWithAbleDOMUIFlag;
+ content: HTMLElementWithAbleDOMUIFlag;
+ countElement: HTMLElementWithAbleDOMUIFlag;
+ count: number;
+}
+
export class IssuesUI {
private _win: Window | undefined;
private _container: HTMLElement | undefined;
private _issuesContainer: HTMLElement | undefined;
- private _menuElement: HTMLElement | undefined;
- private _issueCountElement: HTMLSpanElement | undefined;
- private _showAllButton: HTMLElement | undefined;
- private _hideAllButton: HTMLElement | undefined;
- private _alignBottomLeftButton: HTMLElement | undefined;
- private _alignTopLeftButton: HTMLElement | undefined;
- private _alignTopRightButton: HTMLElement | undefined;
- private _alignBottomRightButton: HTMLElement | undefined;
-
- private _isMuted = false;
+ private _toggleButton: HTMLElement | undefined;
+ private _toggleButtonCount: HTMLElement | undefined;
+ private _alignmentBar: HTMLElement | undefined;
+
private _issues: Set = new Set();
+ private _areIssuesVisible = false;
+
+ private _groups: Map = new Map();
private _getHighlighter?: () => ElementHighlighter | undefined;
@@ -352,242 +402,289 @@ export class IssuesUI {
doc.createElement("div")) as HTMLElementWithAbleDOMUIFlag;
issuesContainer.__abledomui = true;
issuesContainer.className = "abledom-issues-container";
+ issuesContainer.style.display = "none";
container.appendChild(issuesContainer);
- const menuElement = (this._menuElement =
- doc.createElement("div")) as HTMLElementWithAbleDOMUIFlag;
- menuElement.__abledomui = true;
- menuElement.className = "abledom-menu-container";
- container.appendChild(menuElement);
-
- new DOMBuilder(menuElement)
- .openTag("div", { class: "abledom-menu" })
- .openTag(
- "span",
- {
- class: "issues-count",
- title: "Number of issues",
- },
- (issueCountElement) => {
- this._issueCountElement = issueCountElement;
- },
- )
- .closeTag()
- .openTag(
- "button",
- {
- class: "button",
- title: "Show all issues",
- },
- (showAllButton) => {
- this._showAllButton = showAllButton;
-
- showAllButton.onclick = () => {
- this.showAll();
- };
- },
- )
- .element(svgShowAll)
- .closeTag()
+ new DOMBuilder(container)
.openTag(
- "button",
+ "div",
{
- class: "button",
- title: "Hide all issues",
+ class: "abledom-floating-button",
+ title: "Toggle issues visibility",
},
- (hideAllButton) => {
- this._hideAllButton = hideAllButton;
-
- hideAllButton.onclick = () => {
- this.hideAll();
+ (toggleButton) => {
+ this._toggleButton = toggleButton;
+ toggleButton.onclick = () => {
+ this.toggleIssuesVisibility();
};
},
)
- .element(svgHideAll)
+ .openTag("span", { class: "abledom-floating-label" })
+ .text("Issues")
.closeTag()
- .openTag(
- "button",
- {
- class: "button",
- title: "Mute newly appearing issues",
- },
- (muteButton) => {
- muteButton.onclick = () => {
- const isMuted = (this._isMuted =
- muteButton.classList.toggle(pressedClass));
-
- if (isMuted) {
- muteButton.setAttribute("title", "Unmute newly appearing issues");
- } else {
- muteButton.setAttribute("title", "Mute newly appearing issues");
- }
- };
- },
- )
- .element(svgMuteAll)
+ .openTag("span", { class: "abledom-floating-counter" }, (span) => {
+ this._toggleButtonCount = span;
+ span.textContent = "0";
+ })
.closeTag()
- .openTag(
- "button",
- {
- class: "button align-button align-button-first pressed",
- title: "Attach issues to bottom left",
- },
- (alignBottomLeftButton) => {
- this._alignBottomLeftButton = alignBottomLeftButton;
+ .closeTag();
- alignBottomLeftButton.onclick = () => {
- this.setUIAlignment(UIAlignments.BottomLeft);
- };
- },
- )
- .element(svgAlignBottomLeft)
- .closeTag()
- .openTag(
- "button",
- {
- class: "button align-button",
- title: "Attach issues to top left",
- },
- (alignTopLeftButton) => {
- this._alignTopLeftButton = alignTopLeftButton;
+ // Corner alignment controls (child of toggle button, shown on hover)
+ const alignmentBar = doc.createElement(
+ "div",
+ ) as HTMLElementWithAbleDOMUIFlag;
+ alignmentBar.__abledomui = true;
+ alignmentBar.className = "abledom-alignment-bar";
+ // Stop clicks on alignment bar from toggling issues
+ alignmentBar.onclick = (e) => e.stopPropagation();
+
+ const corners: Array<{
+ svg: typeof svgAlignBottomRight;
+ pos: UIAlignments;
+ title: string;
+ }> = [
+ {
+ svg: svgAlignTopLeft,
+ pos: UIAlignments.TopLeft,
+ title: "Move to top-left",
+ },
+ {
+ svg: svgAlignTopRight,
+ pos: UIAlignments.TopRight,
+ title: "Move to top-right",
+ },
+ {
+ svg: svgAlignBottomLeft,
+ pos: UIAlignments.BottomLeft,
+ title: "Move to bottom-left",
+ },
+ {
+ svg: svgAlignBottomRight,
+ pos: UIAlignments.BottomRight,
+ title: "Move to bottom-right",
+ },
+ ];
+
+ const alignBtns: HTMLElement[] = [];
+
+ for (const corner of corners) {
+ const btn = doc.createElement("button") as HTMLElementWithAbleDOMUIFlag;
+ btn.__abledomui = true;
+ btn.className = "abledom-align-btn";
+ btn.title = corner.title;
+ corner.svg(btn);
+
+ if (corner.pos === UIAlignments.BottomRight) {
+ btn.classList.add("active");
+ }
- alignTopLeftButton.onclick = () => {
- this.setUIAlignment(UIAlignments.TopLeft);
- };
- },
- )
- .element(svgAlignTopLeft)
- .closeTag()
- .openTag(
- "button",
- {
- class: "button align-button",
- title: "Attach issues to top right",
- },
- (alignTopRightButton) => {
- this._alignTopRightButton = alignTopRightButton;
+ btn.onclick = () => {
+ alignBtns.forEach((b) => b.classList.remove("active"));
+ btn.classList.add("active");
+ this._setAlignment(corner.pos);
+ };
- alignTopRightButton.onclick = () => {
- this.setUIAlignment(UIAlignments.TopRight);
- };
- },
- )
- .element(svgAlignTopRight)
- .closeTag()
- .openTag(
- "button",
- {
- class: "button align-button align-button-last",
- title: "Attach issues to bottom right",
- },
- (alignBottomRightButton) => {
- this._alignBottomRightButton = alignBottomRightButton;
+ alignBtns.push(btn);
+ alignmentBar.appendChild(btn);
+ }
- alignBottomRightButton.onclick = () => {
- this.setUIAlignment(UIAlignments.BottomRight);
- };
- },
- )
- .element(svgAlignBottomRight)
- .closeTag()
- .closeTag();
+ this._alignmentBar = alignmentBar;
+ // Append alignment bar inside the toggle button
+ if (this._toggleButton) {
+ this._toggleButton.appendChild(alignmentBar);
+ }
doc.body.appendChild(container);
}
- private setUIAlignment(alignment: UIAlignments) {
- if (!this._container || !this._issuesContainer || !this._menuElement) {
+ private _setAlignment(pos: UIAlignments) {
+ const btn = this._toggleButton;
+ const bar = this._alignmentBar;
+ const container = this._container;
+ const listContainer = this._issuesContainer;
+
+ if (!btn || !container) {
return;
}
- this._alignBottomLeftButton?.classList.remove(pressedClass);
- this._alignBottomRightButton?.classList.remove(pressedClass);
- this._alignTopLeftButton?.classList.remove(pressedClass);
- this._alignTopRightButton?.classList.remove(pressedClass);
-
- this._container.classList.remove(
- "abledom-align-left",
- "abledom-align-right",
- "abledom-align-top",
- "abledom-align-bottom",
- );
- let containerClasses: string[] = [];
- let issuesFirst = false;
+ // Reset positions
+ btn.style.top = "auto";
+ btn.style.bottom = "auto";
+ btn.style.left = "auto";
+ btn.style.right = "auto";
+ if (container) {
+ container.style.top = "auto";
+ container.style.bottom = "auto";
+ container.style.left = "auto";
+ container.style.right = "auto";
+ container.style.flexDirection = "column";
+ container.style.justifyContent = "flex-end";
+ container.style.alignItems = "flex-end";
+ }
+ if (listContainer) {
+ listContainer.style.marginBottom = "";
+ listContainer.style.marginTop = "";
+ }
+ // Flip alignment bar to the opposite side of the screen edge
+ if (bar) {
+ bar.style.left = "";
+ bar.style.right = "";
+ }
- switch (alignment) {
- case UIAlignments.BottomLeft:
- containerClasses = ["abledom-align-left", "abledom-align-bottom"];
- issuesFirst = true;
- this._alignBottomLeftButton?.classList.add(pressedClass);
- break;
+ switch (pos) {
case UIAlignments.BottomRight:
- containerClasses = ["abledom-align-right", "abledom-align-bottom"];
- issuesFirst = true;
- this._alignBottomRightButton?.classList.add(pressedClass);
+ btn.style.bottom = "24px";
+ btn.style.right = "24px";
+ if (bar) {
+ bar.style.right = "calc(100% + 8px)";
+ bar.style.left = "auto";
+ }
+ if (container) {
+ container.style.bottom = "24px";
+ container.style.right = "24px";
+ container.style.justifyContent = "flex-end";
+ container.style.alignItems = "flex-end";
+ }
+ if (listContainer) {
+ listContainer.style.marginBottom = "76px";
+ }
break;
- case UIAlignments.TopLeft:
- containerClasses = ["abledom-align-left", "abledom-align-top"];
- this._alignTopLeftButton?.classList.add(pressedClass);
+ case UIAlignments.BottomLeft:
+ btn.style.bottom = "24px";
+ btn.style.left = "24px";
+ if (bar) {
+ bar.style.left = "calc(100% + 8px)";
+ bar.style.right = "auto";
+ }
+ if (container) {
+ container.style.bottom = "24px";
+ container.style.left = "24px";
+ container.style.alignItems = "flex-start";
+ }
+ if (listContainer) {
+ listContainer.style.marginBottom = "76px";
+ }
break;
case UIAlignments.TopRight:
- containerClasses = ["abledom-align-right", "abledom-align-top"];
- this._alignTopRightButton?.classList.add(pressedClass);
+ btn.style.top = "24px";
+ btn.style.right = "24px";
+ if (bar) {
+ bar.style.right = "calc(100% + 8px)";
+ bar.style.left = "auto";
+ }
+ if (container) {
+ container.style.top = "24px";
+ container.style.right = "24px";
+ container.style.justifyContent = "flex-start";
+ container.style.alignItems = "flex-end";
+ }
+ if (listContainer) {
+ listContainer.style.marginTop = "76px";
+ listContainer.style.marginBottom = "0";
+ }
+ break;
+ case UIAlignments.TopLeft:
+ btn.style.top = "24px";
+ btn.style.left = "24px";
+ if (bar) {
+ bar.style.left = "calc(100% + 8px)";
+ bar.style.right = "auto";
+ }
+ if (container) {
+ container.style.top = "24px";
+ container.style.left = "24px";
+ container.style.justifyContent = "flex-start";
+ container.style.alignItems = "flex-start";
+ }
+ if (listContainer) {
+ listContainer.style.marginTop = "76px";
+ listContainer.style.marginBottom = "0";
+ }
break;
}
+ }
- this._container.classList.add(...containerClasses);
- this._container.insertBefore(
- this._issuesContainer,
- issuesFirst ? this._menuElement : null,
- );
+ toggleIssuesVisibility() {
+ this._areIssuesVisible = !this._areIssuesVisible;
+ if (this._issuesContainer) {
+ this._issuesContainer.style.display = this._areIssuesVisible
+ ? "flex" // Using flex for column layout
+ : "none";
+ }
+ if (this._toggleButton) {
+ if (this._areIssuesVisible) {
+ this._toggleButton.classList.add("active");
+ } else {
+ this._toggleButton.classList.remove("active");
+ }
+ }
}
private _setIssuesCount(count: number) {
- if (!this._menuElement) {
- return;
+ if (this._toggleButtonCount) {
+ this._toggleButtonCount.textContent = `${count}`;
}
- const countElement = this._issueCountElement;
-
- if (countElement && count > 0) {
- countElement.textContent = "";
- new DOMBuilder(countElement)
- .openTag("strong")
- .text(`${count}`)
- .closeTag()
- .text(` issue${count > 1 ? "s" : ""}`);
-
- this._menuElement.style.display = "block";
- } else {
- this._menuElement.style.display = "none";
+ if (this._container) {
+ this._container.style.display = count > 0 ? "block" : "none";
}
}
- private _setShowHideButtonsVisibility() {
- const showAllButton = this._showAllButton;
- const hideAllButton = this._hideAllButton;
+ private _getOrCreateGroup(ruleName: string): IssueGroup {
+ if (this._groups.has(ruleName)) {
+ return this._groups.get(ruleName)!;
+ }
- if (!showAllButton || !hideAllButton) {
- return;
+ const doc = this._win?.document;
+ if (!doc) {
+ throw new Error("Document not found");
}
- let allHidden = true;
- let allVisible = true;
+ const header = doc.createElement("div") as HTMLElementWithAbleDOMUIFlag;
+ header.__abledomui = true;
+ header.className = "abledom-group-header";
- for (let issue of this._issues) {
- if (issue.isHidden) {
- allVisible = false;
- } else {
- allHidden = false;
- }
+ const content = doc.createElement("div") as HTMLElementWithAbleDOMUIFlag;
+ content.__abledomui = true;
+ content.className = "abledom-group-content";
- if (!allHidden && !allVisible) {
- break;
- }
+ const countElement = doc.createElement("span");
+ countElement.className = "abledom-group-count";
+ countElement.textContent = "0";
+
+ const chevronWrapper = doc.createElement("div");
+ chevronWrapper.className = "chevron";
+ svgChevron(chevronWrapper);
+
+ const title = doc.createElement("div");
+ title.className = "abledom-group-title";
+ title.appendChild(chevronWrapper);
+ const titleText = doc.createElement("span");
+ titleText.textContent = ruleName;
+ title.appendChild(titleText);
+
+ header.appendChild(title);
+ header.appendChild(countElement);
+
+ header.onclick = () => {
+ const isExpanded = header.classList.toggle("expanded");
+ content.classList.toggle("expanded", isExpanded);
+ };
+
+ if (this._issuesContainer) {
+ this._issuesContainer.appendChild(header);
+ this._issuesContainer.appendChild(content);
}
- hideAllButton.style.display = allHidden ? "none" : "block";
- showAllButton.style.display = allVisible ? "none" : "block";
+ const group: IssueGroup = {
+ header,
+ content,
+ countElement: countElement as HTMLElementWithAbleDOMUIFlag,
+ count: 0,
+ };
+ this._groups.set(ruleName, group);
+
+ return group;
}
addIssue(issue: IssueUI) {
@@ -605,19 +702,17 @@ export class IssuesUI {
throw new Error("IssuesUI is not initialized");
}
- if (this._isMuted) {
- issue.toggle(false, true);
- }
+ // Always show the issue in the list (if not muted logic requires otherwise, but simpler is better)
+ issue.toggle(true, true);
- const issueUIWraper = IssueUI.getElement(issue);
- issueUIWraper && this._issuesContainer.appendChild(issueUIWraper);
+ const group = this._getOrCreateGroup(issue.rule.name);
+ group.count++;
+ group.countElement.textContent = `${group.count}`;
- IssueUI.setOnToggle(issue, () => {
- this._setShowHideButtonsVisibility();
- });
+ const issueUIWraper = IssueUI.getElement(issue);
+ issueUIWraper && group.content.appendChild(issueUIWraper);
this._setIssuesCount(this._issues.size);
- this._setShowHideButtonsVisibility();
}
removeIssue(issue: IssueUI) {
@@ -627,25 +722,25 @@ export class IssuesUI {
this._issues.delete(issue);
+ if (!this.headless) {
+ const ruleName = issue.rule.name;
+ if (this._groups.has(ruleName)) {
+ const group = this._groups.get(ruleName)!;
+ group.count--;
+ group.countElement.textContent = `${group.count}`;
+
+ if (group.count <= 0) {
+ group.header.remove();
+ group.content.remove();
+ this._groups.delete(ruleName);
+ }
+ }
+ }
+
this._setIssuesCount(this._issues.size);
- this._setShowHideButtonsVisibility();
this.highlight(null);
}
- hideAll() {
- this._issues.forEach((issue) => {
- issue.toggle(false);
- });
- this._setShowHideButtonsVisibility();
- }
-
- showAll() {
- this._issues.forEach((issue) => {
- issue.toggle(true);
- });
- this._setShowHideButtonsVisibility();
- }
-
highlight(
element: HTMLElement | null,
scrollIntoView?: boolean,
@@ -660,21 +755,19 @@ export class IssuesUI {
delete this._getHighlighter;
delete this._container;
delete this._issuesContainer;
- delete this._menuElement;
- delete this._issueCountElement;
- delete this._showAllButton;
- delete this._hideAllButton;
- delete this._alignBottomLeftButton;
- delete this._alignTopLeftButton;
- delete this._alignTopRightButton;
- delete this._alignBottomRightButton;
+ delete this._toggleButton;
+ delete this._toggleButtonCount;
+ delete this._alignmentBar;
}
}
export class ElementHighlighter {
private _window: Window | undefined;
private _container: HTMLElementWithAbleDOMUIFlag | undefined;
+ private _overlay: HTMLElementWithAbleDOMUIFlag | undefined;
private _element: HTMLElement | undefined;
+ private _previousZIndex: string | undefined;
+ private _previousPosition: string | undefined;
private _cancelScrollTimer: (() => void) | undefined;
private _intersectionObserver: IntersectionObserver | undefined;
private _cancelAutoHideTimer: (() => void) | undefined;
@@ -699,6 +792,12 @@ export class ElementHighlighter {
.openTag("div", { class: "abledom-highlight-border2" })
.closeTag();
+ const overlay: HTMLElementWithAbleDOMUIFlag =
+ win.document.createElement("div");
+ overlay.__abledomui = true;
+ overlay.className = "abledom-dimming-overlay";
+ this._overlay = overlay;
+
win.addEventListener("scroll", this._onScroll, true);
}
@@ -712,6 +811,7 @@ export class ElementHighlighter {
}
if (!element) {
+ this._restoreElementStyle();
delete this._element;
this._unobserve();
this._hide();
@@ -725,8 +825,16 @@ export class ElementHighlighter {
return;
}
+ this._restoreElementStyle();
this._element = element;
+ this._previousZIndex = element.style.zIndex;
+ this._previousPosition = element.style.position;
+ element.style.zIndex = "100499";
+ if (!element.style.position || element.style.position === "static") {
+ element.style.position = "relative";
+ }
+
if (scrollIntoView) {
element.scrollIntoView({ block: "center" });
}
@@ -764,6 +872,13 @@ export class ElementHighlighter {
style.left = `${rect.left}px`;
container.style.display = "block";
+
+ if (this._overlay) {
+ if (!this._overlay.parentElement) {
+ win.document.body.appendChild(this._overlay);
+ }
+ this._overlay.classList.add("visible");
+ }
}
});
@@ -771,18 +886,32 @@ export class ElementHighlighter {
}
dispose() {
+ this._restoreElementStyle();
this._unobserve();
this._cancelScrollTimer?.();
this._cancelAutoHideTimer?.();
this._window?.removeEventListener("scroll", this._onScroll, true);
this._container?.remove();
+ this._overlay?.remove();
delete this._element;
delete this._container;
+ delete this._overlay;
delete this._window;
}
private _hide() {
this._container && (this._container.style.display = "none");
+ this._overlay?.classList.remove("visible");
+ }
+
+ private _restoreElementStyle() {
+ const el = this._element;
+ if (el) {
+ el.style.zIndex = this._previousZIndex ?? "";
+ el.style.position = this._previousPosition ?? "";
+ delete this._previousZIndex;
+ delete this._previousPosition;
+ }
}
private _unobserve() {
diff --git a/tests/tabindex/tabindex.html b/tests/tabindex/tabindex.html
index 3ef0f4e..819dccd 100644
--- a/tests/tabindex/tabindex.html
+++ b/tests/tabindex/tabindex.html
@@ -10,6 +10,7 @@
Tab Index Rule
+
Flag