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