diff --git a/packages/code-analyzer-core/output-templates/html-template-0.0.12.txt b/packages/code-analyzer-core/output-templates/html-template-0.0.13.txt similarity index 86% rename from packages/code-analyzer-core/output-templates/html-template-0.0.12.txt rename to packages/code-analyzer-core/output-templates/html-template-0.0.13.txt index c5e3e564..0f11ea30 100644 --- a/packages/code-analyzer-core/output-templates/html-template-0.0.12.txt +++ b/packages/code-analyzer-core/output-templates/html-template-0.0.13.txt @@ -47,11 +47,11 @@ fetch(link.href, fetchOpts); } })(); - + // ==== START OF VIOLATIONS ==== const data = {{###VIOLATIONS###}}; // ==== END OF VIOLATIONS ==== - + class Model { constructor(data2) { (this.violations = this.processViolations(data2)), @@ -182,6 +182,7 @@ (this.defaultSort = { column: "file", direction: "asc" }), (this.currentViolations = []), (this.currentViolationIndex = -1), + (this.triggeringElement = null), (this.summary = document.getElementById("summary")), (this.violationsTable = document.getElementById("violationsTable")), (this.tableBody = this.violationsTable.querySelector("tbody")), @@ -265,11 +266,48 @@ } updateBoxes(data2) { const counts = data2.reduce( - (acc, v) => ((acc[v.severity] = (acc[v.severity] || 0) + 1), acc), - {}, - ); - for (let i = 1; i <= 5; i++) - document.getElementById(`sev${i}`).textContent = counts[i] || 0; + (acc, v) => ((acc[v.severity] = (acc[v.severity] || 0) + 1), acc), + {}, + ), + severityNames = { + 1: "Critical", + 2: "High", + 3: "Moderate", + 4: "Low", + 5: "Info", + }; + for (let i = 1; i <= 5; i++) { + const count = counts[i] || 0; + document.getElementById(`sev${i}`).textContent = count; + const box = document.querySelector(`[data-severity="${i}"]`); + box && + box.setAttribute( + "aria-label", + `Severity ${i}, ${severityNames[i]}: ${count} violations`, + ); + } + } + updateBoxesAriaLabels(filteredData) { + const filteredCounts = filteredData.reduce( + (acc, v) => ((acc[v.severity] = (acc[v.severity] || 0) + 1), acc), + {}, + ), + severityNames = { + 1: "Critical", + 2: "High", + 3: "Moderate", + 4: "Low", + 5: "Info", + }; + for (let i = 1; i <= 5; i++) { + const count = filteredCounts[i] || 0, + box = document.querySelector(`[data-severity="${i}"]`); + box && + box.setAttribute( + "aria-label", + `Severity ${i}, ${severityNames[i]}: ${count} violations`, + ); + } } toggleBoxes(severity) { document.querySelectorAll(".box").forEach((box) => { @@ -308,6 +346,7 @@ populateRow(violation) { const row = this.tableBody.insertRow(); (row.dataset.index = this.model.violations.indexOf(violation)), + (row.tabIndex = 0), (row.innerHTML = ` ${this.model.getFile(violation.file)} @@ -376,6 +415,7 @@ (this.currentViolations = sortedViolations), this.populateTable(sortedViolations, groupBy), this.updateSummary(sortedViolations), + this.updateBoxesAriaLabels(sortedViolations), this.toggleBoxes(severityFilter), this.updateReset(); } @@ -390,14 +430,23 @@ updateArrow() { document.querySelectorAll("#violationsTable th").forEach((th) => { th.classList.remove("asc", "desc"), - th.dataset.sort === this.defaultSort.column && - th.classList.add(this.defaultSort.direction); + th.dataset.sort === this.defaultSort.column + ? (th.classList.add(this.defaultSort.direction), + th.setAttribute( + "aria-sort", + "asc" === this.defaultSort.direction + ? "ascending" + : "descending", + )) + : th.setAttribute("aria-sort", "none"); }); } updatePagination() { (this.panelPrev.disabled = this.currentViolationIndex <= 0), (this.panelNext.disabled = this.currentViolationIndex >= this.currentViolations.length - 1); + const contextText = `Violation ${this.currentViolationIndex + 1} of ${this.currentViolations.length}`; + document.getElementById("panel-title").textContent = contextText; } populatePanel(violation) { const panelFields = [ @@ -433,15 +482,35 @@ (this.panelContent.innerHTML = ""), this.panelContent.appendChild(dl); } - showPanel(violation) { + getFocusableElements() { + return this.panel.querySelectorAll( + 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])', + ); + } + trapFocus(event) { + const focusableElements = this.getFocusableElements(), + firstElement = focusableElements[0], + lastElement = focusableElements[focusableElements.length - 1]; + "Tab" === event.key && + (event.shiftKey + ? document.activeElement === firstElement && + (event.preventDefault(), lastElement.focus()) + : document.activeElement === lastElement && + (event.preventDefault(), firstElement.focus())); + } + showPanel(violation, triggeringElement = null) { (this.currentViolationIndex = this.currentViolations.findIndex( (v) => v === violation, )), + (this.triggeringElement = triggeringElement), this.populatePanel(violation), this.overlay.classList.add("visible"), this.panel.classList.add("visible"), this.updatePagination(), - (document.body.style.overflow = "hidden"); + (document.body.style.overflow = "hidden"), + setTimeout(() => { + this.panel.focus(); + }, 100); } paginatePanel(direction) { const newIndex = this.currentViolationIndex + direction; @@ -456,7 +525,10 @@ this.overlay.classList.remove("visible"), this.panel.classList.remove("visible"), setTimeout(() => { - document.body.style.overflow = ""; + (document.body.style.overflow = ""), + this.triggeringElement && + this.triggeringElement.focus && + this.triggeringElement.focus(); }, 300); } populateVersions(versions) { @@ -512,7 +584,7 @@ if (row) { const violationIndex = row.dataset.index, violation = this.model.violations[violationIndex]; - this.view.showPanel(violation); + this.view.showPanel(violation, row); } } handleGroupBy(value) { @@ -520,7 +592,20 @@ this.view.updateTable(); } handleShortcuts(event) { + if (this.view.overlay.classList.contains("visible")) + return ( + this.view.trapFocus(event), + "Escape" === event.key + ? (event.preventDefault(), void this.handleEscapeKey()) + : "ArrowLeft" === event.key || "ArrowUp" === event.key + ? (event.preventDefault(), void this.handleArrowKey(-1)) + : "ArrowRight" === event.key || "ArrowDown" === event.key + ? (event.preventDefault(), void this.handleArrowKey(1)) + : void 0 + ); if ("INPUT" === document.activeElement.tagName) return; + if (event.metaKey || event.ctrlKey || event.altKey || event.shiftKey) + return; const action = { n: () => this.handleGroupBy("file"), e: () => this.handleGroupBy("engine"), @@ -680,7 +765,7 @@ font-size: 16px; font-weight: normal; margin: 0; - padding: 20px 20px 20px 36px; + padding: 20px 0 20px 36px; text-align: center; } @@ -692,14 +777,15 @@ font-size: 16px; font-weight: normal; margin: 0; - padding: 20px 20px 20px 36px; + padding: 20px 0 20px 36px; text-align: center; } - .header p.help { + .header a.help { font-size: 16px; margin: 0; - padding: 20px 0 20px 20px; + padding: 20px 0 20px 0; + display: inline-block; } .boxes { @@ -798,13 +884,13 @@ margin: 20px 20px; } - .toolbar h3 { + .toolbar h4 { color: var(--color-text); font-size: 14px; } @media screen and (max-width: 1200px) { - .toolbar h3 { + .toolbar h4 { display: none; } } @@ -987,7 +1073,7 @@ opacity: 1; } - table th.asc::after { + table th.desc::after { transform: rotate(180deg); } @@ -1073,6 +1159,12 @@ font-size: 12px; } + .footer code { + font-family: var(--font-mono); + font-size: 11px; + font-weight: normal; + } + .versions { display: flex; flex-direction: row; @@ -1518,68 +1610,115 @@
- -
+

Salesforce

Code Analyzer Report – - + {{###TIMESTAMP###}} - +

-

- Developer Docs -

+ Developer Docs
-
-
-

Severity 1 Critical

-

0

+ -
-

Found 0 violations

+
+

Found 0 violations

-

Filter

+ -
+ - - - - - - - + + + + +
- +
- - - - - - - - + + + + + + + + diff --git a/packages/code-analyzer-core/src/output-formats/results/html-run-results-format.ts b/packages/code-analyzer-core/src/output-formats/results/html-run-results-format.ts index 1a230ab9..8e419747 100644 --- a/packages/code-analyzer-core/src/output-formats/results/html-run-results-format.ts +++ b/packages/code-analyzer-core/src/output-formats/results/html-run-results-format.ts @@ -8,7 +8,7 @@ import { toJsonResultsOutput } from "./json-run-results-format"; -const HTML_TEMPLATE_VERSION: string = '0.0.12'; +const HTML_TEMPLATE_VERSION: string = '0.0.13'; const HTML_TEMPLATE_FILE: string = path.resolve(__dirname, '..', '..', '..', 'output-templates', `html-template-${HTML_TEMPLATE_VERSION}.txt`); /** diff --git a/packages/code-analyzer-core/test/test-data/expectedOutputFiles/multipleViolations.goldfile.html b/packages/code-analyzer-core/test/test-data/expectedOutputFiles/multipleViolations.goldfile.html index d86dae2d..667200ec 100644 --- a/packages/code-analyzer-core/test/test-data/expectedOutputFiles/multipleViolations.goldfile.html +++ b/packages/code-analyzer-core/test/test-data/expectedOutputFiles/multipleViolations.goldfile.html @@ -47,11 +47,11 @@ fetch(link.href, fetchOpts); } })(); - + // ==== START OF VIOLATIONS ==== const data = {"runDir":"{{ESCAPEDRUNDIR}}","violationCounts":{"total":6,"sev1":0,"sev2":1,"sev3":3,"sev4":2,"sev5":0},"versions":{"code-analyzer":"{{CORE_VERSION}}","stubEngine1":"0.0.1","stubEngine2":"0.1.0","stubEngine3":"1.0.0"},"violations":[{"rule":"stub1RuleA","engine":"stubEngine1","severity":4,"tags":["Recommended","CodeStyle"],"primaryLocationIndex":0,"locations":[{"file":"test{{PATHSEP}}config.test.ts","startLine":3,"startColumn":6,"endLine":11,"endColumn":8}],"message":"SomeViolationMessage1","resources":["https://example.com/stub1RuleA"]},{"rule":"stub1RuleA","engine":"stubEngine1","severity":4,"tags":["Recommended","CodeStyle"],"primaryLocationIndex":0,"locations":[{"file":"test{{PATHSEP}}test-data{{PATHSEP}}sample-input-files{{PATHSEP}}subfolder with spaces{{PATHSEP}}some-target-file.ts","startLine":10,"startColumn":4,"endLine":11,"endColumn":2}],"message":"SomeViolationMessage1","resources":["https://example.com/stub1RuleA"]},{"rule":"stub1RuleC","engine":"stubEngine1","severity":3,"tags":["Recommended","Performance","Custom"],"primaryLocationIndex":0,"locations":[{"file":"test{{PATHSEP}}code-analyzer.test.ts","startLine":21,"startColumn":7,"endLine":25,"endColumn":4}],"message":"SomeViolationMessage2","resources":["https://example.com/stub1RuleC","https://example.com/aViolationSpecificUrl1","https://example.com/violationSpecificUrl2"]},{"rule":"stub1RuleE","engine":"stubEngine1","severity":3,"tags":["Performance"],"primaryLocationIndex":0,"locations":[{"file":"test{{PATHSEP}}code-analyzer.test.ts","startLine":56,"startColumn":4}],"message":"Some Violation that contains\na new line in `it` and "various" 'quotes'. Also it has <brackets> that may need to be {escaped}.","resources":["https://example.com/stub1RuleE","https://example.com/stub1RuleE_2"]},{"rule":"stub2RuleC","engine":"stubEngine2","severity":2,"tags":["Recommended","BestPractice"],"primaryLocationIndex":2,"locations":[{"file":"test{{PATHSEP}}stubs.ts","startLine":4,"startColumn":13},{"file":"test{{PATHSEP}}test-helpers.ts","startLine":9,"startColumn":1},{"file":"test{{PATHSEP}}stubs.ts","startLine":76,"startColumn":8}],"message":"SomeViolationMessage3","resources":[]},{"rule":"stub3RuleA","engine":"stubEngine3","severity":3,"tags":["Recommended","ErrorProne"],"primaryLocationIndex":2,"locations":[{"file":"test{{PATHSEP}}stubs.ts","startLine":20,"startColumn":10,"endLine":22,"endColumn":25,"comment":"Comment at location 1"},{"file":"test{{PATHSEP}}test-helpers.ts","startLine":5,"startColumn":10,"comment":"Comment at location 2"},{"file":"test{{PATHSEP}}stubs.ts","startLine":90,"startColumn":1,"endLine":95,"endColumn":10}],"message":"SomeViolationMessage4","resources":[]}]}; // ==== END OF VIOLATIONS ==== - + class Model { constructor(data2) { (this.violations = this.processViolations(data2)), @@ -182,6 +182,7 @@ (this.defaultSort = { column: "file", direction: "asc" }), (this.currentViolations = []), (this.currentViolationIndex = -1), + (this.triggeringElement = null), (this.summary = document.getElementById("summary")), (this.violationsTable = document.getElementById("violationsTable")), (this.tableBody = this.violationsTable.querySelector("tbody")), @@ -265,11 +266,48 @@ } updateBoxes(data2) { const counts = data2.reduce( - (acc, v) => ((acc[v.severity] = (acc[v.severity] || 0) + 1), acc), - {}, - ); - for (let i = 1; i <= 5; i++) - document.getElementById(`sev${i}`).textContent = counts[i] || 0; + (acc, v) => ((acc[v.severity] = (acc[v.severity] || 0) + 1), acc), + {}, + ), + severityNames = { + 1: "Critical", + 2: "High", + 3: "Moderate", + 4: "Low", + 5: "Info", + }; + for (let i = 1; i <= 5; i++) { + const count = counts[i] || 0; + document.getElementById(`sev${i}`).textContent = count; + const box = document.querySelector(`[data-severity="${i}"]`); + box && + box.setAttribute( + "aria-label", + `Severity ${i}, ${severityNames[i]}: ${count} violations`, + ); + } + } + updateBoxesAriaLabels(filteredData) { + const filteredCounts = filteredData.reduce( + (acc, v) => ((acc[v.severity] = (acc[v.severity] || 0) + 1), acc), + {}, + ), + severityNames = { + 1: "Critical", + 2: "High", + 3: "Moderate", + 4: "Low", + 5: "Info", + }; + for (let i = 1; i <= 5; i++) { + const count = filteredCounts[i] || 0, + box = document.querySelector(`[data-severity="${i}"]`); + box && + box.setAttribute( + "aria-label", + `Severity ${i}, ${severityNames[i]}: ${count} violations`, + ); + } } toggleBoxes(severity) { document.querySelectorAll(".box").forEach((box) => { @@ -308,6 +346,7 @@ populateRow(violation) { const row = this.tableBody.insertRow(); (row.dataset.index = this.model.violations.indexOf(violation)), + (row.tabIndex = 0), (row.innerHTML = `
FilenameSeverityRuleEngineMessageTagsLineColumnFilename + Severity + RuleEngine + Message + Tags + Line + + Column +
${this.model.getFile(violation.file)} @@ -376,6 +415,7 @@ (this.currentViolations = sortedViolations), this.populateTable(sortedViolations, groupBy), this.updateSummary(sortedViolations), + this.updateBoxesAriaLabels(sortedViolations), this.toggleBoxes(severityFilter), this.updateReset(); } @@ -390,14 +430,23 @@ updateArrow() { document.querySelectorAll("#violationsTable th").forEach((th) => { th.classList.remove("asc", "desc"), - th.dataset.sort === this.defaultSort.column && - th.classList.add(this.defaultSort.direction); + th.dataset.sort === this.defaultSort.column + ? (th.classList.add(this.defaultSort.direction), + th.setAttribute( + "aria-sort", + "asc" === this.defaultSort.direction + ? "ascending" + : "descending", + )) + : th.setAttribute("aria-sort", "none"); }); } updatePagination() { (this.panelPrev.disabled = this.currentViolationIndex <= 0), (this.panelNext.disabled = this.currentViolationIndex >= this.currentViolations.length - 1); + const contextText = `Violation ${this.currentViolationIndex + 1} of ${this.currentViolations.length}`; + document.getElementById("panel-title").textContent = contextText; } populatePanel(violation) { const panelFields = [ @@ -433,15 +482,35 @@ (this.panelContent.innerHTML = ""), this.panelContent.appendChild(dl); } - showPanel(violation) { + getFocusableElements() { + return this.panel.querySelectorAll( + 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])', + ); + } + trapFocus(event) { + const focusableElements = this.getFocusableElements(), + firstElement = focusableElements[0], + lastElement = focusableElements[focusableElements.length - 1]; + "Tab" === event.key && + (event.shiftKey + ? document.activeElement === firstElement && + (event.preventDefault(), lastElement.focus()) + : document.activeElement === lastElement && + (event.preventDefault(), firstElement.focus())); + } + showPanel(violation, triggeringElement = null) { (this.currentViolationIndex = this.currentViolations.findIndex( (v) => v === violation, )), + (this.triggeringElement = triggeringElement), this.populatePanel(violation), this.overlay.classList.add("visible"), this.panel.classList.add("visible"), this.updatePagination(), - (document.body.style.overflow = "hidden"); + (document.body.style.overflow = "hidden"), + setTimeout(() => { + this.panel.focus(); + }, 100); } paginatePanel(direction) { const newIndex = this.currentViolationIndex + direction; @@ -456,7 +525,10 @@ this.overlay.classList.remove("visible"), this.panel.classList.remove("visible"), setTimeout(() => { - document.body.style.overflow = ""; + (document.body.style.overflow = ""), + this.triggeringElement && + this.triggeringElement.focus && + this.triggeringElement.focus(); }, 300); } populateVersions(versions) { @@ -512,7 +584,7 @@ if (row) { const violationIndex = row.dataset.index, violation = this.model.violations[violationIndex]; - this.view.showPanel(violation); + this.view.showPanel(violation, row); } } handleGroupBy(value) { @@ -520,7 +592,20 @@ this.view.updateTable(); } handleShortcuts(event) { + if (this.view.overlay.classList.contains("visible")) + return ( + this.view.trapFocus(event), + "Escape" === event.key + ? (event.preventDefault(), void this.handleEscapeKey()) + : "ArrowLeft" === event.key || "ArrowUp" === event.key + ? (event.preventDefault(), void this.handleArrowKey(-1)) + : "ArrowRight" === event.key || "ArrowDown" === event.key + ? (event.preventDefault(), void this.handleArrowKey(1)) + : void 0 + ); if ("INPUT" === document.activeElement.tagName) return; + if (event.metaKey || event.ctrlKey || event.altKey || event.shiftKey) + return; const action = { n: () => this.handleGroupBy("file"), e: () => this.handleGroupBy("engine"), @@ -680,7 +765,7 @@ font-size: 16px; font-weight: normal; margin: 0; - padding: 20px 20px 20px 36px; + padding: 20px 0 20px 36px; text-align: center; } @@ -692,14 +777,15 @@ font-size: 16px; font-weight: normal; margin: 0; - padding: 20px 20px 20px 36px; + padding: 20px 0 20px 36px; text-align: center; } - .header p.help { + .header a.help { font-size: 16px; margin: 0; - padding: 20px 0 20px 20px; + padding: 20px 0 20px 0; + display: inline-block; } .boxes { @@ -798,13 +884,13 @@ margin: 20px 20px; } - .toolbar h3 { + .toolbar h4 { color: var(--color-text); font-size: 14px; } @media screen and (max-width: 1200px) { - .toolbar h3 { + .toolbar h4 { display: none; } } @@ -987,7 +1073,7 @@ opacity: 1; } - table th.asc::after { + table th.desc::after { transform: rotate(180deg); } @@ -1073,6 +1159,12 @@ font-size: 12px; } + .footer code { + font-family: var(--font-mono); + font-size: 11px; + font-weight: normal; + } + .versions { display: flex; flex-direction: row; @@ -1518,68 +1610,115 @@
- -
+

Salesforce

Code Analyzer Report – - + {{###TIMESTAMP###}} - +

-

- Developer Docs -

+ Developer Docs
-
-
-

Severity 1 Critical

-

0

+ -
-

Found 0 violations

+
+

Found 0 violations

-

Filter

+ -
+ - - - - - - - + + + + +
- +
- - - - - - - - + + + + + + + + diff --git a/packages/code-analyzer-core/test/test-data/expectedOutputFiles/unexpectedEngineErrorViolation.goldfile.html b/packages/code-analyzer-core/test/test-data/expectedOutputFiles/unexpectedEngineErrorViolation.goldfile.html index 2f828841..86217e5e 100644 --- a/packages/code-analyzer-core/test/test-data/expectedOutputFiles/unexpectedEngineErrorViolation.goldfile.html +++ b/packages/code-analyzer-core/test/test-data/expectedOutputFiles/unexpectedEngineErrorViolation.goldfile.html @@ -47,11 +47,11 @@ fetch(link.href, fetchOpts); } })(); - + // ==== START OF VIOLATIONS ==== const data = {"runDir":"{{ESCAPEDRUNDIR}}","violationCounts":{"total":1,"sev1":1,"sev2":0,"sev3":0,"sev4":0,"sev5":0},"versions":{"code-analyzer":"{{CORE_VERSION}}","throwingEngine":"3.0.0"},"violations":[{"rule":"UnexpectedEngineError","engine":"throwingEngine","severity":1,"tags":[],"primaryLocationIndex":0,"locations":[{"comment":"Undefined Code Location"}],"message":"The engine with name 'throwingEngine' threw an unexpected error: SomeErrorMessageFromThrowingEngine","resources":[]}]}; // ==== END OF VIOLATIONS ==== - + class Model { constructor(data2) { (this.violations = this.processViolations(data2)), @@ -182,6 +182,7 @@ (this.defaultSort = { column: "file", direction: "asc" }), (this.currentViolations = []), (this.currentViolationIndex = -1), + (this.triggeringElement = null), (this.summary = document.getElementById("summary")), (this.violationsTable = document.getElementById("violationsTable")), (this.tableBody = this.violationsTable.querySelector("tbody")), @@ -265,11 +266,48 @@ } updateBoxes(data2) { const counts = data2.reduce( - (acc, v) => ((acc[v.severity] = (acc[v.severity] || 0) + 1), acc), - {}, - ); - for (let i = 1; i <= 5; i++) - document.getElementById(`sev${i}`).textContent = counts[i] || 0; + (acc, v) => ((acc[v.severity] = (acc[v.severity] || 0) + 1), acc), + {}, + ), + severityNames = { + 1: "Critical", + 2: "High", + 3: "Moderate", + 4: "Low", + 5: "Info", + }; + for (let i = 1; i <= 5; i++) { + const count = counts[i] || 0; + document.getElementById(`sev${i}`).textContent = count; + const box = document.querySelector(`[data-severity="${i}"]`); + box && + box.setAttribute( + "aria-label", + `Severity ${i}, ${severityNames[i]}: ${count} violations`, + ); + } + } + updateBoxesAriaLabels(filteredData) { + const filteredCounts = filteredData.reduce( + (acc, v) => ((acc[v.severity] = (acc[v.severity] || 0) + 1), acc), + {}, + ), + severityNames = { + 1: "Critical", + 2: "High", + 3: "Moderate", + 4: "Low", + 5: "Info", + }; + for (let i = 1; i <= 5; i++) { + const count = filteredCounts[i] || 0, + box = document.querySelector(`[data-severity="${i}"]`); + box && + box.setAttribute( + "aria-label", + `Severity ${i}, ${severityNames[i]}: ${count} violations`, + ); + } } toggleBoxes(severity) { document.querySelectorAll(".box").forEach((box) => { @@ -308,6 +346,7 @@ populateRow(violation) { const row = this.tableBody.insertRow(); (row.dataset.index = this.model.violations.indexOf(violation)), + (row.tabIndex = 0), (row.innerHTML = `
FilenameSeverityRuleEngineMessageTagsLineColumnFilename + Severity + RuleEngine + Message + Tags + Line + + Column +
${this.model.getFile(violation.file)} @@ -376,6 +415,7 @@ (this.currentViolations = sortedViolations), this.populateTable(sortedViolations, groupBy), this.updateSummary(sortedViolations), + this.updateBoxesAriaLabels(sortedViolations), this.toggleBoxes(severityFilter), this.updateReset(); } @@ -390,14 +430,23 @@ updateArrow() { document.querySelectorAll("#violationsTable th").forEach((th) => { th.classList.remove("asc", "desc"), - th.dataset.sort === this.defaultSort.column && - th.classList.add(this.defaultSort.direction); + th.dataset.sort === this.defaultSort.column + ? (th.classList.add(this.defaultSort.direction), + th.setAttribute( + "aria-sort", + "asc" === this.defaultSort.direction + ? "ascending" + : "descending", + )) + : th.setAttribute("aria-sort", "none"); }); } updatePagination() { (this.panelPrev.disabled = this.currentViolationIndex <= 0), (this.panelNext.disabled = this.currentViolationIndex >= this.currentViolations.length - 1); + const contextText = `Violation ${this.currentViolationIndex + 1} of ${this.currentViolations.length}`; + document.getElementById("panel-title").textContent = contextText; } populatePanel(violation) { const panelFields = [ @@ -433,15 +482,35 @@ (this.panelContent.innerHTML = ""), this.panelContent.appendChild(dl); } - showPanel(violation) { + getFocusableElements() { + return this.panel.querySelectorAll( + 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])', + ); + } + trapFocus(event) { + const focusableElements = this.getFocusableElements(), + firstElement = focusableElements[0], + lastElement = focusableElements[focusableElements.length - 1]; + "Tab" === event.key && + (event.shiftKey + ? document.activeElement === firstElement && + (event.preventDefault(), lastElement.focus()) + : document.activeElement === lastElement && + (event.preventDefault(), firstElement.focus())); + } + showPanel(violation, triggeringElement = null) { (this.currentViolationIndex = this.currentViolations.findIndex( (v) => v === violation, )), + (this.triggeringElement = triggeringElement), this.populatePanel(violation), this.overlay.classList.add("visible"), this.panel.classList.add("visible"), this.updatePagination(), - (document.body.style.overflow = "hidden"); + (document.body.style.overflow = "hidden"), + setTimeout(() => { + this.panel.focus(); + }, 100); } paginatePanel(direction) { const newIndex = this.currentViolationIndex + direction; @@ -456,7 +525,10 @@ this.overlay.classList.remove("visible"), this.panel.classList.remove("visible"), setTimeout(() => { - document.body.style.overflow = ""; + (document.body.style.overflow = ""), + this.triggeringElement && + this.triggeringElement.focus && + this.triggeringElement.focus(); }, 300); } populateVersions(versions) { @@ -512,7 +584,7 @@ if (row) { const violationIndex = row.dataset.index, violation = this.model.violations[violationIndex]; - this.view.showPanel(violation); + this.view.showPanel(violation, row); } } handleGroupBy(value) { @@ -520,7 +592,20 @@ this.view.updateTable(); } handleShortcuts(event) { + if (this.view.overlay.classList.contains("visible")) + return ( + this.view.trapFocus(event), + "Escape" === event.key + ? (event.preventDefault(), void this.handleEscapeKey()) + : "ArrowLeft" === event.key || "ArrowUp" === event.key + ? (event.preventDefault(), void this.handleArrowKey(-1)) + : "ArrowRight" === event.key || "ArrowDown" === event.key + ? (event.preventDefault(), void this.handleArrowKey(1)) + : void 0 + ); if ("INPUT" === document.activeElement.tagName) return; + if (event.metaKey || event.ctrlKey || event.altKey || event.shiftKey) + return; const action = { n: () => this.handleGroupBy("file"), e: () => this.handleGroupBy("engine"), @@ -680,7 +765,7 @@ font-size: 16px; font-weight: normal; margin: 0; - padding: 20px 20px 20px 36px; + padding: 20px 0 20px 36px; text-align: center; } @@ -692,14 +777,15 @@ font-size: 16px; font-weight: normal; margin: 0; - padding: 20px 20px 20px 36px; + padding: 20px 0 20px 36px; text-align: center; } - .header p.help { + .header a.help { font-size: 16px; margin: 0; - padding: 20px 0 20px 20px; + padding: 20px 0 20px 0; + display: inline-block; } .boxes { @@ -798,13 +884,13 @@ margin: 20px 20px; } - .toolbar h3 { + .toolbar h4 { color: var(--color-text); font-size: 14px; } @media screen and (max-width: 1200px) { - .toolbar h3 { + .toolbar h4 { display: none; } } @@ -987,7 +1073,7 @@ opacity: 1; } - table th.asc::after { + table th.desc::after { transform: rotate(180deg); } @@ -1073,6 +1159,12 @@ font-size: 12px; } + .footer code { + font-family: var(--font-mono); + font-size: 11px; + font-weight: normal; + } + .versions { display: flex; flex-direction: row; @@ -1518,68 +1610,115 @@
- -
+

Salesforce

Code Analyzer Report – - + {{###TIMESTAMP###}} - +

-

- Developer Docs -

+ Developer Docs
-
-
-

Severity 1 Critical

-

0

+ -
-

Found 0 violations

+
+

Found 0 violations

-

Filter

+ -
+ - - - - - - - + + + + +
- +
- - - - - - - - + + + + + + + + diff --git a/packages/code-analyzer-core/test/test-data/expectedOutputFiles/zeroViolations.goldfile.html b/packages/code-analyzer-core/test/test-data/expectedOutputFiles/zeroViolations.goldfile.html index ecd188bd..a0154f3a 100644 --- a/packages/code-analyzer-core/test/test-data/expectedOutputFiles/zeroViolations.goldfile.html +++ b/packages/code-analyzer-core/test/test-data/expectedOutputFiles/zeroViolations.goldfile.html @@ -47,11 +47,11 @@ fetch(link.href, fetchOpts); } })(); - + // ==== START OF VIOLATIONS ==== const data = {"runDir":"{{ESCAPEDRUNDIR}}","violationCounts":{"total":0,"sev1":0,"sev2":0,"sev3":0,"sev4":0,"sev5":0},"versions":{"code-analyzer":"{{CORE_VERSION}}"},"violations":[]}; // ==== END OF VIOLATIONS ==== - + class Model { constructor(data2) { (this.violations = this.processViolations(data2)), @@ -182,6 +182,7 @@ (this.defaultSort = { column: "file", direction: "asc" }), (this.currentViolations = []), (this.currentViolationIndex = -1), + (this.triggeringElement = null), (this.summary = document.getElementById("summary")), (this.violationsTable = document.getElementById("violationsTable")), (this.tableBody = this.violationsTable.querySelector("tbody")), @@ -265,11 +266,48 @@ } updateBoxes(data2) { const counts = data2.reduce( - (acc, v) => ((acc[v.severity] = (acc[v.severity] || 0) + 1), acc), - {}, - ); - for (let i = 1; i <= 5; i++) - document.getElementById(`sev${i}`).textContent = counts[i] || 0; + (acc, v) => ((acc[v.severity] = (acc[v.severity] || 0) + 1), acc), + {}, + ), + severityNames = { + 1: "Critical", + 2: "High", + 3: "Moderate", + 4: "Low", + 5: "Info", + }; + for (let i = 1; i <= 5; i++) { + const count = counts[i] || 0; + document.getElementById(`sev${i}`).textContent = count; + const box = document.querySelector(`[data-severity="${i}"]`); + box && + box.setAttribute( + "aria-label", + `Severity ${i}, ${severityNames[i]}: ${count} violations`, + ); + } + } + updateBoxesAriaLabels(filteredData) { + const filteredCounts = filteredData.reduce( + (acc, v) => ((acc[v.severity] = (acc[v.severity] || 0) + 1), acc), + {}, + ), + severityNames = { + 1: "Critical", + 2: "High", + 3: "Moderate", + 4: "Low", + 5: "Info", + }; + for (let i = 1; i <= 5; i++) { + const count = filteredCounts[i] || 0, + box = document.querySelector(`[data-severity="${i}"]`); + box && + box.setAttribute( + "aria-label", + `Severity ${i}, ${severityNames[i]}: ${count} violations`, + ); + } } toggleBoxes(severity) { document.querySelectorAll(".box").forEach((box) => { @@ -308,6 +346,7 @@ populateRow(violation) { const row = this.tableBody.insertRow(); (row.dataset.index = this.model.violations.indexOf(violation)), + (row.tabIndex = 0), (row.innerHTML = `
FilenameSeverityRuleEngineMessageTagsLineColumnFilename + Severity + RuleEngine + Message + Tags + Line + + Column +
${this.model.getFile(violation.file)} @@ -376,6 +415,7 @@ (this.currentViolations = sortedViolations), this.populateTable(sortedViolations, groupBy), this.updateSummary(sortedViolations), + this.updateBoxesAriaLabels(sortedViolations), this.toggleBoxes(severityFilter), this.updateReset(); } @@ -390,14 +430,23 @@ updateArrow() { document.querySelectorAll("#violationsTable th").forEach((th) => { th.classList.remove("asc", "desc"), - th.dataset.sort === this.defaultSort.column && - th.classList.add(this.defaultSort.direction); + th.dataset.sort === this.defaultSort.column + ? (th.classList.add(this.defaultSort.direction), + th.setAttribute( + "aria-sort", + "asc" === this.defaultSort.direction + ? "ascending" + : "descending", + )) + : th.setAttribute("aria-sort", "none"); }); } updatePagination() { (this.panelPrev.disabled = this.currentViolationIndex <= 0), (this.panelNext.disabled = this.currentViolationIndex >= this.currentViolations.length - 1); + const contextText = `Violation ${this.currentViolationIndex + 1} of ${this.currentViolations.length}`; + document.getElementById("panel-title").textContent = contextText; } populatePanel(violation) { const panelFields = [ @@ -433,15 +482,35 @@ (this.panelContent.innerHTML = ""), this.panelContent.appendChild(dl); } - showPanel(violation) { + getFocusableElements() { + return this.panel.querySelectorAll( + 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])', + ); + } + trapFocus(event) { + const focusableElements = this.getFocusableElements(), + firstElement = focusableElements[0], + lastElement = focusableElements[focusableElements.length - 1]; + "Tab" === event.key && + (event.shiftKey + ? document.activeElement === firstElement && + (event.preventDefault(), lastElement.focus()) + : document.activeElement === lastElement && + (event.preventDefault(), firstElement.focus())); + } + showPanel(violation, triggeringElement = null) { (this.currentViolationIndex = this.currentViolations.findIndex( (v) => v === violation, )), + (this.triggeringElement = triggeringElement), this.populatePanel(violation), this.overlay.classList.add("visible"), this.panel.classList.add("visible"), this.updatePagination(), - (document.body.style.overflow = "hidden"); + (document.body.style.overflow = "hidden"), + setTimeout(() => { + this.panel.focus(); + }, 100); } paginatePanel(direction) { const newIndex = this.currentViolationIndex + direction; @@ -456,7 +525,10 @@ this.overlay.classList.remove("visible"), this.panel.classList.remove("visible"), setTimeout(() => { - document.body.style.overflow = ""; + (document.body.style.overflow = ""), + this.triggeringElement && + this.triggeringElement.focus && + this.triggeringElement.focus(); }, 300); } populateVersions(versions) { @@ -512,7 +584,7 @@ if (row) { const violationIndex = row.dataset.index, violation = this.model.violations[violationIndex]; - this.view.showPanel(violation); + this.view.showPanel(violation, row); } } handleGroupBy(value) { @@ -520,7 +592,20 @@ this.view.updateTable(); } handleShortcuts(event) { + if (this.view.overlay.classList.contains("visible")) + return ( + this.view.trapFocus(event), + "Escape" === event.key + ? (event.preventDefault(), void this.handleEscapeKey()) + : "ArrowLeft" === event.key || "ArrowUp" === event.key + ? (event.preventDefault(), void this.handleArrowKey(-1)) + : "ArrowRight" === event.key || "ArrowDown" === event.key + ? (event.preventDefault(), void this.handleArrowKey(1)) + : void 0 + ); if ("INPUT" === document.activeElement.tagName) return; + if (event.metaKey || event.ctrlKey || event.altKey || event.shiftKey) + return; const action = { n: () => this.handleGroupBy("file"), e: () => this.handleGroupBy("engine"), @@ -680,7 +765,7 @@ font-size: 16px; font-weight: normal; margin: 0; - padding: 20px 20px 20px 36px; + padding: 20px 0 20px 36px; text-align: center; } @@ -692,14 +777,15 @@ font-size: 16px; font-weight: normal; margin: 0; - padding: 20px 20px 20px 36px; + padding: 20px 0 20px 36px; text-align: center; } - .header p.help { + .header a.help { font-size: 16px; margin: 0; - padding: 20px 0 20px 20px; + padding: 20px 0 20px 0; + display: inline-block; } .boxes { @@ -798,13 +884,13 @@ margin: 20px 20px; } - .toolbar h3 { + .toolbar h4 { color: var(--color-text); font-size: 14px; } @media screen and (max-width: 1200px) { - .toolbar h3 { + .toolbar h4 { display: none; } } @@ -987,7 +1073,7 @@ opacity: 1; } - table th.asc::after { + table th.desc::after { transform: rotate(180deg); } @@ -1073,6 +1159,12 @@ font-size: 12px; } + .footer code { + font-family: var(--font-mono); + font-size: 11px; + font-weight: normal; + } + .versions { display: flex; flex-direction: row; @@ -1518,68 +1610,115 @@
- -
+

Salesforce

Code Analyzer Report – - + {{###TIMESTAMP###}} - +

-

- Developer Docs -

+ Developer Docs
-
-
-

Severity 1 Critical

-

0

+ -
-

Found 0 violations

+
+

Found 0 violations

-

Filter

+ -
+ - - - - - - - + + + + +
- +
- - - - - - - - + + + + + + + +
FilenameSeverityRuleEngineMessageTagsLineColumnFilename + Severity + RuleEngine + Message + Tags + Line + + Column +