diff --git a/accessibility-checker-engine/src/genHelp.ts b/accessibility-checker-engine/src/genHelp.ts index 93ac07506..b9ade7d64 100644 --- a/accessibility-checker-engine/src/genHelp.ts +++ b/accessibility-checker-engine/src/genHelp.ts @@ -79,6 +79,12 @@ const valueMap = { "Fail": "Recommendation", "Pass": "Pass", "Manual": "Recommendation" + }, + "INFORMATION": { + "POTENTIAL": "Needs review", + "FAIL": "Violation", + "PASS": "Pass", + "MANUAL": "Recommendation" } }; diff --git a/accessibility-checker-engine/src/v4/rules/element_tabbable_visible.ts b/accessibility-checker-engine/src/v4/rules/element_tabbable_visible.ts index 436265973..17d74a87f 100644 --- a/accessibility-checker-engine/src/v4/rules/element_tabbable_visible.ts +++ b/accessibility-checker-engine/src/v4/rules/element_tabbable_visible.ts @@ -67,6 +67,7 @@ export let element_tabbable_visible: Rule = { let top = bounds['top']; let left = bounds['left']; + if (Object.keys(onfocus_styles).length === 0 ) { // no onfocus position change, but could be changed from js return RulePotential("potential_visible", []); @@ -91,7 +92,7 @@ export let element_tabbable_visible: Rule = { } } - if (top > 0 && left > 0) + if (top >= 0 && left >= 0) return RulePass("pass"); else return RulePotential("potential_visible", []); diff --git a/accessibility-checker-extension/manifest_Chrome.json b/accessibility-checker-extension/manifest_Chrome.json index 77012da01..a8b6c6ebc 100644 --- a/accessibility-checker-extension/manifest_Chrome.json +++ b/accessibility-checker-extension/manifest_Chrome.json @@ -43,5 +43,11 @@ "page": "options.html", "open_in_tab": true }, + "content_scripts": [ + { + "matches": [""], + "js": ["draw.js"] + } + ], "incognito": "split" } diff --git a/accessibility-checker-extension/manifest_Firefox.json b/accessibility-checker-extension/manifest_Firefox.json index b7daa8696..ab0806151 100644 --- a/accessibility-checker-extension/manifest_Firefox.json +++ b/accessibility-checker-extension/manifest_Firefox.json @@ -35,6 +35,12 @@ ], "persistent": true }, + "content_scripts": [ + { + "matches": [""], + "js": ["draw.js"] + } + ], "options_ui": { "page": "options.html", "open_in_tab": true diff --git a/accessibility-checker-extension/package-lock.json b/accessibility-checker-extension/package-lock.json index e0a57f3c0..c38e229c8 100644 --- a/accessibility-checker-extension/package-lock.json +++ b/accessibility-checker-extension/package-lock.json @@ -20,6 +20,7 @@ "react-tooltip": "^4.2.21", "redux": "^4.1.2", "string-hash": "^1.1.3", + "tabbable": "^5.2.0", "webext-redux": "^2.1.9" }, "devDependencies": { @@ -16377,6 +16378,11 @@ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true }, + "node_modules/tabbable": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-5.2.1.tgz", + "integrity": "sha512-40pEZ2mhjaZzK0BnI+QGNjJO8UYx9pP5v7BGe17SORTO0OEuuaAwQTkAp8whcZvqon44wKFOikD+Al11K3JICQ==" + }, "node_modules/tapable": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", @@ -30950,6 +30956,11 @@ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true }, + "tabbable": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-5.2.1.tgz", + "integrity": "sha512-40pEZ2mhjaZzK0BnI+QGNjJO8UYx9pP5v7BGe17SORTO0OEuuaAwQTkAp8whcZvqon44wKFOikD+Al11K3JICQ==" + }, "tapable": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", diff --git a/accessibility-checker-extension/package.json b/accessibility-checker-extension/package.json index 44a54a6ed..ac8015eef 100644 --- a/accessibility-checker-extension/package.json +++ b/accessibility-checker-extension/package.json @@ -29,6 +29,7 @@ "react-tooltip": "^4.2.21", "redux": "^4.1.2", "string-hash": "^1.1.3", + "tabbable": "^5.2.0", "webext-redux": "^2.1.9" }, "devDependencies": { diff --git a/accessibility-checker-extension/src/assets/element.svg b/accessibility-checker-extension/src/assets/element.svg new file mode 100644 index 000000000..c99cba2cb --- /dev/null +++ b/accessibility-checker-extension/src/assets/element.svg @@ -0,0 +1,14 @@ + + + ABA868F0-239E-4409-AF9F-DF4F34529E69 + + + + + + ? + + + + + \ No newline at end of file diff --git a/accessibility-checker-extension/src/assets/enter.svg b/accessibility-checker-extension/src/assets/enter.svg new file mode 100644 index 000000000..c1932166b --- /dev/null +++ b/accessibility-checker-extension/src/assets/enter.svg @@ -0,0 +1,14 @@ + + + BEF87785-B132-4847-A765-81711DC71A86 + + + + + + enter + + + + + \ No newline at end of file diff --git a/accessibility-checker-extension/src/assets/esc.svg b/accessibility-checker-extension/src/assets/esc.svg new file mode 100644 index 000000000..93acb7e9d --- /dev/null +++ b/accessibility-checker-extension/src/assets/esc.svg @@ -0,0 +1,14 @@ + + + 91EACC98-4DCC-482B-972C-9773EE418282 + + + + + + esc + + + + + \ No newline at end of file diff --git a/accessibility-checker-extension/src/assets/img/2_A11yIssues.png b/accessibility-checker-extension/src/assets/img/2_A11yIssues.png new file mode 100644 index 000000000..c4da7e421 Binary files /dev/null and b/accessibility-checker-extension/src/assets/img/2_A11yIssues.png differ diff --git a/accessibility-checker-extension/src/assets/img/3.1Checker1.png b/accessibility-checker-extension/src/assets/img/3.1Checker1.png new file mode 100644 index 000000000..421d2ca04 Binary files /dev/null and b/accessibility-checker-extension/src/assets/img/3.1Checker1.png differ diff --git a/accessibility-checker-extension/src/assets/img/3.1Checker2.png b/accessibility-checker-extension/src/assets/img/3.1Checker2.png new file mode 100644 index 000000000..b91fcfd73 Binary files /dev/null and b/accessibility-checker-extension/src/assets/img/3.1Checker2.png differ diff --git a/accessibility-checker-extension/src/assets/img/3.1Checker3.png b/accessibility-checker-extension/src/assets/img/3.1Checker3.png new file mode 100644 index 000000000..cb14016fd Binary files /dev/null and b/accessibility-checker-extension/src/assets/img/3.1Checker3.png differ diff --git a/accessibility-checker-extension/src/assets/img/3.1Checker4.png b/accessibility-checker-extension/src/assets/img/3.1Checker4.png new file mode 100644 index 000000000..458f2b5af Binary files /dev/null and b/accessibility-checker-extension/src/assets/img/3.1Checker4.png differ diff --git a/accessibility-checker-extension/src/assets/img/3.2Report.png b/accessibility-checker-extension/src/assets/img/3.2Report.png new file mode 100644 index 000000000..6e2f819cd Binary files /dev/null and b/accessibility-checker-extension/src/assets/img/3.2Report.png differ diff --git a/accessibility-checker-extension/src/assets/img/3.3Multi1.png b/accessibility-checker-extension/src/assets/img/3.3Multi1.png new file mode 100644 index 000000000..9f0fc2e71 Binary files /dev/null and b/accessibility-checker-extension/src/assets/img/3.3Multi1.png differ diff --git a/accessibility-checker-extension/src/assets/img/3.3Multi2.png b/accessibility-checker-extension/src/assets/img/3.3Multi2.png new file mode 100644 index 000000000..2a71b24fb Binary files /dev/null and b/accessibility-checker-extension/src/assets/img/3.3Multi2.png differ diff --git a/accessibility-checker-extension/src/assets/img/3.3Multi3.png b/accessibility-checker-extension/src/assets/img/3.3Multi3.png new file mode 100644 index 000000000..cae7091a1 Binary files /dev/null and b/accessibility-checker-extension/src/assets/img/3.3Multi3.png differ diff --git a/accessibility-checker-extension/src/assets/img/3.3Multi4.png b/accessibility-checker-extension/src/assets/img/3.3Multi4.png new file mode 100644 index 000000000..b39eff114 Binary files /dev/null and b/accessibility-checker-extension/src/assets/img/3.3Multi4.png differ diff --git a/accessibility-checker-extension/src/assets/img/3.4Focus.png b/accessibility-checker-extension/src/assets/img/3.4Focus.png new file mode 100644 index 000000000..99f685b07 Binary files /dev/null and b/accessibility-checker-extension/src/assets/img/3.4Focus.png differ diff --git a/accessibility-checker-extension/src/assets/img/3.5Keyboard1.png b/accessibility-checker-extension/src/assets/img/3.5Keyboard1.png new file mode 100644 index 000000000..bb9759a2e Binary files /dev/null and b/accessibility-checker-extension/src/assets/img/3.5Keyboard1.png differ diff --git a/accessibility-checker-extension/src/assets/img/3.5Keyboard2.png b/accessibility-checker-extension/src/assets/img/3.5Keyboard2.png new file mode 100644 index 000000000..bee0ba809 Binary files /dev/null and b/accessibility-checker-extension/src/assets/img/3.5Keyboard2.png differ diff --git a/accessibility-checker-extension/src/assets/img/3.5Keyboard3.png b/accessibility-checker-extension/src/assets/img/3.5Keyboard3.png new file mode 100644 index 000000000..425295e00 Binary files /dev/null and b/accessibility-checker-extension/src/assets/img/3.5Keyboard3.png differ diff --git a/accessibility-checker-extension/src/assets/img/3.5Keyboard4.png b/accessibility-checker-extension/src/assets/img/3.5Keyboard4.png new file mode 100644 index 000000000..072b47fa7 Binary files /dev/null and b/accessibility-checker-extension/src/assets/img/3.5Keyboard4.png differ diff --git a/accessibility-checker-extension/src/assets/img/3.5Keyboard5.png b/accessibility-checker-extension/src/assets/img/3.5Keyboard5.png new file mode 100644 index 000000000..53dbc26ed Binary files /dev/null and b/accessibility-checker-extension/src/assets/img/3.5Keyboard5.png differ diff --git a/accessibility-checker-extension/src/assets/img/4_A11yAssess.png b/accessibility-checker-extension/src/assets/img/4_A11yAssess.png new file mode 100644 index 000000000..3e22b514c Binary files /dev/null and b/accessibility-checker-extension/src/assets/img/4_A11yAssess.png differ diff --git a/accessibility-checker-extension/src/assets/img/4_A11yAssess2.png b/accessibility-checker-extension/src/assets/img/4_A11yAssess2.png new file mode 100644 index 000000000..1d6e77f16 Binary files /dev/null and b/accessibility-checker-extension/src/assets/img/4_A11yAssess2.png differ diff --git a/accessibility-checker-extension/src/assets/img/4_A11yAssess3.png b/accessibility-checker-extension/src/assets/img/4_A11yAssess3.png new file mode 100644 index 000000000..53dbc26ed Binary files /dev/null and b/accessibility-checker-extension/src/assets/img/4_A11yAssess3.png differ diff --git a/accessibility-checker-extension/src/assets/img/5_Options.png b/accessibility-checker-extension/src/assets/img/5_Options.png index a0d9c12ef..0b42d4129 100644 Binary files a/accessibility-checker-extension/src/assets/img/5_Options.png and b/accessibility-checker-extension/src/assets/img/5_Options.png differ diff --git a/accessibility-checker-extension/src/assets/img/7.1Report.png b/accessibility-checker-extension/src/assets/img/7.1Report.png new file mode 100644 index 000000000..0178dd2cf Binary files /dev/null and b/accessibility-checker-extension/src/assets/img/7.1Report.png differ diff --git a/accessibility-checker-extension/src/assets/img/keyboard-element.svg b/accessibility-checker-extension/src/assets/img/keyboard-element.svg new file mode 100644 index 000000000..c99cba2cb --- /dev/null +++ b/accessibility-checker-extension/src/assets/img/keyboard-element.svg @@ -0,0 +1,14 @@ + + + ABA868F0-239E-4409-AF9F-DF4F34529E69 + + + + + + ? + + + + + \ No newline at end of file diff --git a/accessibility-checker-extension/src/assets/img/keyboard-issue.svg b/accessibility-checker-extension/src/assets/img/keyboard-issue.svg new file mode 100644 index 000000000..fe8e14acc --- /dev/null +++ b/accessibility-checker-extension/src/assets/img/keyboard-issue.svg @@ -0,0 +1,14 @@ + + + DABF6102-76FE-422A-884C-E437E09FD2C1 + + + + + + 2 + + + + + \ No newline at end of file diff --git a/accessibility-checker-extension/src/assets/keyboard disabled.svg b/accessibility-checker-extension/src/assets/keyboard disabled.svg new file mode 100644 index 000000000..b9380bb78 --- /dev/null +++ b/accessibility-checker-extension/src/assets/keyboard disabled.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/accessibility-checker-extension/src/assets/keyboard.svg b/accessibility-checker-extension/src/assets/keyboard.svg new file mode 100644 index 000000000..a554910ec --- /dev/null +++ b/accessibility-checker-extension/src/assets/keyboard.svg @@ -0,0 +1,20 @@ + + + + + keyboard + + + + + + + + + + + + + + + diff --git a/accessibility-checker-extension/src/assets/keyboard_issue.svg b/accessibility-checker-extension/src/assets/keyboard_issue.svg new file mode 100644 index 000000000..fe8e14acc --- /dev/null +++ b/accessibility-checker-extension/src/assets/keyboard_issue.svg @@ -0,0 +1,14 @@ + + + DABF6102-76FE-422A-884C-E437E09FD2C1 + + + + + + 2 + + + + + \ No newline at end of file diff --git a/accessibility-checker-extension/src/assets/keyboard_white.svg b/accessibility-checker-extension/src/assets/keyboard_white.svg new file mode 100644 index 000000000..28a7bd231 --- /dev/null +++ b/accessibility-checker-extension/src/assets/keyboard_white.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/accessibility-checker-extension/src/assets/left_right.svg b/accessibility-checker-extension/src/assets/left_right.svg new file mode 100644 index 000000000..86e074445 --- /dev/null +++ b/accessibility-checker-extension/src/assets/left_right.svg @@ -0,0 +1,20 @@ + + + 031F1C18-78A7-4C27-8B19-CE16C4EFA091 + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/accessibility-checker-extension/src/assets/shift.svg b/accessibility-checker-extension/src/assets/shift.svg new file mode 100644 index 000000000..5fc7a8fc5 --- /dev/null +++ b/accessibility-checker-extension/src/assets/shift.svg @@ -0,0 +1,14 @@ + + + D3A07593-9AEB-438F-A488-33165D0EDC88 + + + + + + shift + + + + + \ No newline at end of file diff --git a/accessibility-checker-extension/src/assets/space.svg b/accessibility-checker-extension/src/assets/space.svg new file mode 100644 index 000000000..26df7df69 --- /dev/null +++ b/accessibility-checker-extension/src/assets/space.svg @@ -0,0 +1,14 @@ + + + 5B4A58AF-6967-48BE-9D53-43759508E177 + + + + + + space + + + + + \ No newline at end of file diff --git a/accessibility-checker-extension/src/assets/tab.svg b/accessibility-checker-extension/src/assets/tab.svg new file mode 100644 index 000000000..7b05fa6f4 --- /dev/null +++ b/accessibility-checker-extension/src/assets/tab.svg @@ -0,0 +1,14 @@ + + + 42A5480D-BC89-4F97-9EA1-4D87385C4D01 + + + + + + tab + + + + + \ No newline at end of file diff --git a/accessibility-checker-extension/src/assets/tab_stop.svg b/accessibility-checker-extension/src/assets/tab_stop.svg new file mode 100644 index 000000000..ffc8fbfa6 --- /dev/null +++ b/accessibility-checker-extension/src/assets/tab_stop.svg @@ -0,0 +1,14 @@ + + + B69E59E4-7146-4C70-822C-F22C4159D066 + + + + + + 1 + + + + + \ No newline at end of file diff --git a/accessibility-checker-extension/src/assets/up_down.svg b/accessibility-checker-extension/src/assets/up_down.svg new file mode 100644 index 000000000..220d28739 --- /dev/null +++ b/accessibility-checker-extension/src/assets/up_down.svg @@ -0,0 +1,20 @@ + + + D6834F74-CBFE-472D-A7CF-5B654DB202C0 + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/accessibility-checker-extension/src/manifest.json b/accessibility-checker-extension/src/manifest.json index 210b4f06c..96300110a 100644 --- a/accessibility-checker-extension/src/manifest.json +++ b/accessibility-checker-extension/src/manifest.json @@ -38,6 +38,12 @@ "background": { "service_worker": "background.js" }, + "content_scripts": [ + { + "matches": [""], + "js": ["draw.js"] + } + ], "options_ui": { "page": "options.html", "open_in_tab": true diff --git a/accessibility-checker-extension/src/ts/background/index.ts b/accessibility-checker-extension/src/ts/background/index.ts index 1035367d0..ae1ee847d 100644 --- a/accessibility-checker-extension/src/ts/background/index.ts +++ b/accessibility-checker-extension/src/ts/background/index.ts @@ -14,237 +14,276 @@ limitations under the License. *****************************************************************************/ -import BackgroundMessaging from "../util/backgroundMessaging"; -import EngineCache from './helper/engineCache'; -import Config from "./helper/config"; -import { ACMetricsLogger } from "../util/ACMetricsLogger"; -import OptionMessaging from "../util/optionMessaging"; - -let metrics = new ACMetricsLogger("ac-extension"); - -function myExecuteScript( - params: any, - pCB?: (any) | undefined): void -{ - if (chrome && chrome.scripting && chrome.scripting.executeScript) { - chrome.scripting.executeScript(params, pCB); - } else { - if (params.func) { - chrome.tabs.executeScript( - params.target.tabId as number, - { - code: `(${params.func.toString()})()`, - frameId: params.target.frameIds[0], - matchAboutBlank: true - }, - (res) => { - if (!res) { - pCB && pCB(res); - } else { - pCB && pCB(res.map(item => ({ result: item }))); - } - }) + import BackgroundMessaging from "../util/backgroundMessaging"; + import EngineCache from './helper/engineCache'; + import Config from "./helper/config"; + import { ACMetricsLogger } from "../util/ACMetricsLogger"; + import OptionMessaging from "../util/optionMessaging"; + + let metrics = new ACMetricsLogger("ac-extension"); + + function myExecuteScript( + params: any, + pCB?: (any) | undefined): void + { + if (chrome && chrome.scripting && chrome.scripting.executeScript) { + chrome.scripting.executeScript(params, pCB); } else { - chrome.tabs.executeScript( - params.target.tabId as number, - { - file: params.files[0], - frameId: params.target.frameIds[0], - matchAboutBlank: true - }, - (res) => { - if (params.files[0].includes("ace.js")) { - chrome.tabs.executeScript( - params.target.tabId as number, - { - code: `window.ace = ace`, - frameId: params.target.frameIds[0], - matchAboutBlank: true - }, - (res) => { - if (!res) { - pCB && pCB(res); - } else { - pCB && pCB(res.map(item => ({ result: item }))); - } - }) - } else { - pCB && pCB(res.map(item => ({ result: item }))); - } - }) + if (params.func) { + chrome.tabs.executeScript( + params.target.tabId as number, + { + code: `(${params.func.toString()})()`, + frameId: params.target.frameIds[0], + matchAboutBlank: true + }, + (res) => { + if (!res) { + pCB && pCB(res); + } else { + pCB && pCB(res.map(item => ({ result: item }))); + } + }) + } else { + chrome.tabs.executeScript( + params.target.tabId as number, + { + file: params.files[0], + frameId: params.target.frameIds[0], + matchAboutBlank: true + }, + (res) => { + if (params.files[0].includes("ace.js")) { + chrome.tabs.executeScript( + params.target.tabId as number, + { + code: `window.ace = ace`, + frameId: params.target.frameIds[0], + matchAboutBlank: true + }, + (res) => { + if (!res) { + pCB && pCB(res); + } else { + pCB && pCB(res.map(item => ({ result: item }))); + } + }) + } else { + pCB && pCB(res.map(item => ({ result: item }))); + } + }) + } } } -} - -async function initTab(tabId: number, archiveId: string) { - // Determine if we've ever loaded any engine - let isLoaded = await new Promise((resolve, reject) => { - myExecuteScript({ - target: { tabId: tabId, frameIds: [0] }, - func: () => (typeof (window as any).ace) - }, function (res: any) { - if (chrome.runtime.lastError) { - reject(chrome.runtime.lastError.message); - } - resolve(res[0].result !== "undefined"); - }) - }); - - // Switch to the appropriate engine for this archiveId - let engineFile = await EngineCache.getEngine(archiveId); - await new Promise((resolve, reject) => { - myExecuteScript({ - target: { tabId: tabId, frameIds: [0] }, - files: [engineFile] - }, function (res: any) { - if (chrome.runtime.lastError) { - reject(chrome.runtime.lastError.message); - } - resolve(res); + + async function initTab(tabId: number, archiveId: string) { + // Determine if we've ever loaded any engine + let isLoaded = await new Promise((resolve, reject) => { + myExecuteScript({ + target: { tabId: tabId, frameIds: [0] }, + func: () => (typeof (window as any).ace) + }, function (res: any) { + if (chrome.runtime.lastError) { + reject(chrome.runtime.lastError.message); + } + resolve(res[0].result !== "undefined"); + }) }); - }); - - // Initialize the listeners once - if (!isLoaded) { + + + + // Switch to the appropriate engine for this archiveId + let engineFile = await EngineCache.getEngine(archiveId); await new Promise((resolve, reject) => { myExecuteScript({ target: { tabId: tabId, frameIds: [0] }, - files: ["/tabListeners.js"] - }, function (_res: any) { + files: [engineFile] + }, function (res: any) { if (chrome.runtime.lastError) { reject(chrome.runtime.lastError.message); } - resolve(_res); + resolve(res); }); }); + + // Initialize the listeners once + if (!isLoaded) { + await new Promise((resolve, reject) => { + myExecuteScript({ + target: { tabId: tabId, frameIds: [0] }, + files: ["/tabListeners.js"] + }, function (_res: any) { + if (chrome.runtime.lastError) { + reject(chrome.runtime.lastError.message); + } + resolve(_res); + }); + }); + } } -} - -BackgroundMessaging.addListener("DAP_CACHED", async (message: any) => { - await BackgroundMessaging.sendToTab(message.tabId, "DAP_CACHED_TAB", { tabId: message.tabId, tabURL: message.tabURL, origin: message.origin }); - return true; -}); - -BackgroundMessaging.addListener("DAP_SCAN", async (message: any) => { - chrome.storage.local.get("OPTIONS", async function (result: any) { - try { - // Determine which archive we're scanning with - let archiveId = Config.defaultArchiveId + ""; - const archives = await EngineCache.getArchives(); - const validArchive = ((id: string) => id && archives.some(archive => archive.id === id)); - - if (!validArchive(archiveId)) archiveId = "latest"; - if (result.OPTIONS && result.OPTIONS.selected_archive && validArchive(result.OPTIONS.selected_archive.id)) { - archiveId = result.OPTIONS.selected_archive.id; + + BackgroundMessaging.addListener("DAP_CACHED", async (message: any) => { + await BackgroundMessaging.sendToTab(message.tabId, "DAP_CACHED_TAB", { tabId: message.tabId, tabURL: message.tabURL, origin: message.origin }); + + return true; + }); + + BackgroundMessaging.addListener("DAP_SCAN", async (message: any) => { + chrome.storage.local.get("OPTIONS", async function (result: any) { + try { + // Determine which archive we're scanning with + let archiveId = Config.defaultArchiveId + ""; + const archives = await EngineCache.getArchives(); + const validArchive = ((id: string) => id && archives.some(archive => archive.id === id)); + + if (!validArchive(archiveId)) archiveId = "latest"; + if (result.OPTIONS && result.OPTIONS.selected_archive && validArchive(result.OPTIONS.selected_archive.id)) { + archiveId = result.OPTIONS.selected_archive.id; + } + let selectedArchive = archives.filter(archive => archive.id === archiveId)[0]; + + // Determine which policy we're scanning with + let policyId: string = selectedArchive.policies[0].id; + const validPolicy = ((id: string) => id && selectedArchive.policies.some(policy => policy.id === id)); + if (!validPolicy(policyId)) policyId = "IBM_Accessibility"; + if (result.OPTIONS && result.OPTIONS.selected_ruleset && validPolicy(result.OPTIONS.selected_ruleset.id)) { + policyId = result.OPTIONS.selected_ruleset.id; + } + + await BackgroundMessaging.sendToTab(message.tabId, "DAP_SCAN_TAB", { + tabId: message.tabId, + tabURL: message.tabURL, + archiveId: archiveId, + archiveVersion: selectedArchive.version, + policyId: policyId, + origin: message.origin + }); + } catch (err) { + console.error(err); } - let selectedArchive = archives.filter(archive => archive.id === archiveId)[0]; - - // Determine which policy we're scanning with - let policyId: string = selectedArchive.policies[0].id; - const validPolicy = ((id: string) => id && selectedArchive.policies.some(policy => policy.id === id)); - if (!validPolicy(policyId)) policyId = "IBM_Accessibility"; - if (result.OPTIONS && result.OPTIONS.selected_ruleset && validPolicy(result.OPTIONS.selected_ruleset.id)) { - policyId = result.OPTIONS.selected_ruleset.id; + + return true; + }); + }); + + BackgroundMessaging.addListener("DAP_SCAN_TAB_COMPLETE", async (message: any) => { + try { + await BackgroundMessaging.sendToPanel("DAP_SCAN_COMPLETE", message); + if (message.archiveId && message.policyId) { + let browser = (navigator.userAgent.match(/\) ([^)]*)$/) || ["", "Unknown"])[1]; + let totalTime = (message.report != undefined)? message.report.totalTime: message.totalTime; + metrics.profileV2(totalTime, browser, message.policyId); + metrics.sendLogsV2(); } - - await BackgroundMessaging.sendToTab(message.tabId, "DAP_SCAN_TAB", { - tabId: message.tabId, - tabURL: message.tabURL, - archiveId: archiveId, - archiveVersion: selectedArchive.version, - policyId: policyId, - origin: message.origin - }); } catch (err) { console.error(err); } return true; }); -}); - -BackgroundMessaging.addListener("DAP_SCAN_TAB_COMPLETE", async (message: any) => { - try { - BackgroundMessaging.sendToPanel("DAP_SCAN_COMPLETE", message); - if (message.archiveId && message.policyId) { - let browser = (navigator.userAgent.match(/\) ([^)]*)$/) || ["", "Unknown"])[1]; - let totalTime = (message.report != undefined)? message.report.totalTime: message.totalTime; - metrics.profileV2(totalTime, browser, message.policyId); - metrics.sendLogsV2(); - } - } catch (err) { - console.error(err); - } - return true; -}); - -BackgroundMessaging.addListener("TAB_INFO", async (message: any) => { - return await new Promise((resolve, _reject) => { - chrome.tabs.get(message.tabId, async function (tab: any) { - //chrome.tabs.get({ 'active': true, 'lastFocusedWindow': true }, async function (tabs) { - let canScan = await new Promise((resolve, _reject) => { - if (tab.id < 0) return resolve(false); - myExecuteScript({ - target: { tabId: tab.id, frameIds: [0] }, - func: () => (typeof (window as any).ace) - }, function (res: any) { - resolve(!!res); - }) + + BackgroundMessaging.addListener("TAB_INFO", async (message: any) => { + return await new Promise((resolve, _reject) => { + chrome.tabs.get(message.tabId, async function (tab: any) { + //chrome.tabs.get({ 'active': true, 'lastFocusedWindow': true }, async function (tabs) { + let canScan = await new Promise((resolve, _reject) => { + if (tab.id < 0) return resolve(false); + myExecuteScript({ + target: { tabId: tab.id, frameIds: [0] }, + func: () => (typeof (window as any).ace) + }, function (res: any) { + resolve(!!res); + }) + }); + tab.canScan = canScan; + resolve(tab); }); - tab.canScan = canScan; - resolve(tab); }); }); -}); - -BackgroundMessaging.addListener("DAP_SCREENSHOT", async (_message: any) => { - return await new Promise((resolve, reject) => { - //@ts-ignore - chrome.tabs.captureVisibleTab(null, {}, function (image:string) { - resolve(image); - reject(new Error("Capture failed")); + + BackgroundMessaging.addListener("DAP_SCREENSHOT", async (_message: any) => { + return await new Promise((resolve, reject) => { + //@ts-ignore + chrome.tabs.captureVisibleTab(null, {}, function (image:string) { + resolve(image); + reject(new Error("Capture failed")); + }); }); }); -}); - -BackgroundMessaging.addListener("DAP_Rulesets", async (message: any) => { - return await new Promise((resolve, reject) => { - - chrome.storage.local.get("OPTIONS", async function (result: any) { - let archiveId = Config.defaultArchiveId + ""; - - if (result.OPTIONS) { - archiveId = result.OPTIONS.selected_archive.id; - } - await initTab(message.tabId, archiveId); - try { - myExecuteScript({ - target: { tabId: message.tabId, frameIds: [0] }, - func: () => (new (window as any).ace.Checker().rulesets) - }, function (res: any) { - if (chrome.runtime.lastError) { - reject(chrome.runtime.lastError.message); - } else { - resolve(res[0].result); - } - }) - } catch (err) { - reject(err); - } - }) + + BackgroundMessaging.addListener("DAP_Rulesets", async (message: any) => { + return await new Promise((resolve, reject) => { + + chrome.storage.local.get("OPTIONS", async function (result: any) { + let archiveId = Config.defaultArchiveId + ""; + // console.log("result.OPTIONS.selected_archive = ",(typeof(result.OPTIONS.selected_archive))); + if (result.OPTIONS && (typeof(result.OPTIONS.selected_archive)) !== "undefined" && (typeof(result.OPTIONS.selected_archive)) !== null) { + archiveId = result.OPTIONS.selected_archive.id; + } else { + archiveId = "latest"; + } + await initTab(message.tabId, archiveId); + try { + myExecuteScript({ + target: { tabId: message.tabId, frameIds: [0] }, + func: () => (new (window as any).ace.Checker().rulesets) + }, function (res: any) { + if (chrome.runtime.lastError) { + reject(chrome.runtime.lastError.message); + } else { + resolve(res[0].result); + } + }) + } catch (err) { + reject(err); + } + }) + }); }); -}); - -BackgroundMessaging.addListener("OPTIONS", async (message: any) => { - return await OptionMessaging.optionMessageHandling(message); - }); - -// TODO: TAB: I broke this in making sure to not change all panels. Need to revisit -chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { - BackgroundMessaging.sendToPanel("TAB_UPDATED", { - tabId: tabId, - status: changeInfo && changeInfo.status, - tabUrl: tab.url, - tabTitle: tab.title + + BackgroundMessaging.addListener("OPTIONS", async (message: any) => { + return await OptionMessaging.optionMessageHandling(message); + }); + + // TODO: TAB: I broke this in making sure to not change all panels. Need to revisit + chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { + BackgroundMessaging.sendToPanel("TAB_UPDATED", { + tabId: tabId, + status: changeInfo && changeInfo.status, + tabUrl: tab.url, + tabTitle: tab.title + }); }); -}); - + + BackgroundMessaging.addListener("DRAW_TABS_TO_BACKGROUND", async (message: any) => { + await BackgroundMessaging.sendToTab(message.tabId, + "DRAW_TABS_TO_CONTEXT_SCRIPTS", + { tabId: message.tabId, tabURL: message.tabURL, tabStopsResults: message.tabStopsResults, + tabStopsErrors: message.tabStopsErrors, tabStopLines: message.tabStopLines, + tabStopOutlines: message.tabStopOutlines, tabStopAlerts: message.tabStopAlerts, + tabStopFirstTime: message.tabStopFirstTime, + }); + return true; + }); + + BackgroundMessaging.addListener("HIGHLIGHT_TABSTOP_TO_BACKGROUND", async (message: any) => { + await BackgroundMessaging.sendToTab(message.tabId, "HIGHLIGHT_TABSTOP_TO_CONTEXT_SCRIPTS", { tabId: message.tabId, tabURL: message.tabURL, tabStopId: message.tabStopId}); + + return true; + }); + + BackgroundMessaging.addListener("DELETE_DRAW_TABS_TO_CONTEXT_SCRIPTS", async (message: any) => { + await BackgroundMessaging.sendToTab(message.tabId, "DELETE_DRAW_TABS_TO_CONTEXT_SCRIPTS", { tabId: message.tabId, tabURL: message.tabURL }); + + return true; + }); + + BackgroundMessaging.addListener("TABSTOP_XPATH_ONCLICK", async (message: any) => { + // console.log("Message TABSTOP_XPATH_ONCLICK received in background, xpath: "+ message.xpath) + await BackgroundMessaging.sendToPanel("TABSTOP_XPATH_ONCLICK", { + xpath: message.xpath, + circleNumber: message.circleNumber + }); + + return true; + }); + \ No newline at end of file diff --git a/accessibility-checker-extension/src/ts/contentScripts/draw.ts b/accessibility-checker-extension/src/ts/contentScripts/draw.ts new file mode 100644 index 000000000..8d35dd76a --- /dev/null +++ b/accessibility-checker-extension/src/ts/contentScripts/draw.ts @@ -0,0 +1,1292 @@ +import TabMessaging from "../util/tabMessaging"; + +TabMessaging.addListener("DRAW_TABS_TO_CONTEXT_SCRIPTS", async (message: any) => { + injectCSS( + ` + .line { + stroke-width: 2px; + stroke: black; + } + .lineError { + stroke-width: 2px; + stroke: red; + } + .lineEmboss { + stroke-width: 1px; + stroke: white; + } + .lineEmbossError { + stroke-width: 1px; + stroke: white; + } + .lineTop { + stroke-width: 1px; + stroke: black; + } + .lineBottom { + stroke-width: 1px; + stroke: black; + } + .lineLeft { + stroke-width: 1px; + stroke: black; + } + .lineRight { + stroke-width: 1px; + stroke: black; + } + ` + ); + injectCSS( + `#svgCircle{ + position: absolute !important; + top: 0 !important; + left: 0 !important; + overflow: visible !important; + pointer-events: auto !important; + z-index: 2147483646 !important; + visibility: visible !important; + cursor: pointer !important; + } + + .noHighlightSVG{ + fill: #E6D6FF; + stroke-width: 1px; + stroke: black; + } + + .highlightSVG{ + fill: #BB8EFF; + stroke-width: 3px; + stroke: black; + } + + .noHighlightSVGtriangle{ + fill: #FFB077; + stroke-width: 1px; + stroke: black; + } + + .highlightSVGtriangle{ + fill: #FC7B1E; + stroke-width: 3px; + stroke: black; + } + + .textColorWhite{ + fill: black + } + + .textColorBlack{ + fill: black + } + + ` + ); + + injectCSS( + `#svgLine{ + position: absolute !important; + top: 0 !important; + left: 0 !important; + overflow: visible !important; + pointer-events: none !important; + z-index: 2147483646 !important; + visibility: visible !important; + } + .svgIconTest{ + position: absolute !important; + overflow: visible !important; + pointer-events: none !important; + z-index: 2147483646 !important; + } + .circleText{ + pointer-events: none !important; + } + + .circleSmall{ + font-size: 12px !important; + } + ` + ); + + // Create nodes that have keyboard errors + let tabStopsErrors = JSON.parse(JSON.stringify(message.tabStopsErrors)); + + // Create nodes that are tabbable, i.e., in the tab chain + let regularTabstops: any = JSON.parse(JSON.stringify(message.tabStopsResults)); + for (let index = 0; index < message.tabStopsResults.length; index++) { + const tabElem = message.tabStopsResults[index]; + let flagMatchFound = false; + message.tabStopsErrors.forEach((errorElem: any) => { + if (tabElem.path.dom === errorElem.path.dom) { + flagMatchFound = true; + } + }); + if (flagMatchFound) { + regularTabstops[index].nodeHasError = true + } + } + + // console.log("----------------"); + // console.log(regularTabstops); + // console.log(tabStopsErrors); + // console.log("----------------"); + + // JCH - this allows the web to scroll to the top before drawing occurs + goToTop().then(function() { + setTimeout(() => { + let iframes: any = []; + draw(regularTabstops, tabStopsErrors, message.tabStopLines, message.tabStopOutlines,iframes).then(function() { + drawErrors(tabStopsErrors, regularTabstops, message.tabStopOutlines,iframes); + }); + + }, 1000) + + }); + + + + // Here is a possibile approach to window resize events: + // 1. Catch window resize events (they come in bunches) + // 2. When there is a "reasonable" since the last event + // 3. Turn off window resize event listener + // ---- Make sure the following happen sequentially using promises ------- + // 4. Delete drawing + // 5. Scan (make sure scan cannot be started from anywhere else) + // we can update the tabstops and tabstop errors + // 6. When scan finished make sure page is at the top + // 7. draw tabstops regular and with errors + // 8. Turn back on window resize event listener + + + // For softlaunch use notification or just help + window.addEventListener("resize", debounce( resizeContent, 250 )); + + // left mouse click listener for the circles and triangles + window.addEventListener('click', function(event:any) { + // console.log("---------------------------------------"); + // console.log("main doc left mouse click catcher"); + // console.log("event.target = ",event.target); + handleTabHighlight(event,document,"click",""); + + }); + + + // Tab key listener for main window + window.addEventListener('keyup', function(event:any) { + // console.log("main doc key catcher"); + if ((event.target.shadowRoot instanceof ShadowRoot) === false) { + // console.log("CALL FUNCTION handleTabHighlight for main doc"); + handleTabHighlight(event, document, "main", ""); + } + }); + + + // Find all iframes nodes + let frames = document.getElementsByTagName("iframe"); + + for (let i = 0; i < frames.length; i++) { + if (frames[i] != null) { + frames[i].contentWindow?.addEventListener('keyup', function(event:any) { + console.log("iframe key catcher"); + let iframePath = getXPathForElement(frames[i]); // since iframes in main doc + handleTabHighlight(event,frames[i].contentWindow?.document,"iframe",iframePath); + }) + } + } + + // Find all shadow dom host nodes + let shadowDoms:any = []; + let allNodes = document.querySelectorAll("*"); + for (let i = 0; i < allNodes.length; i++) { + if (allNodes[i].shadowRoot) { + shadowDoms.push(allNodes[i]); + } + } + // console.log(shadowDoms.length); + + for (let i = 0; i < shadowDoms.length; i++) { + if (shadowDoms[i] != null) { + // console.log("Got shadow dom: ",shadowDoms[i]); + shadowDoms[i].shadowRoot?.addEventListener('keyup', function(event:any) { + // console.log("shadow dom key catcher"); + let focusElement = shadowDoms[i].shadowRoot?.activeElement; + let focusElementPath = getXPathForElement(focusElement); + // JCH TODO 1 for the doc frag ONLY works for 1 level doc frags + focusElementPath = "/#document-fragment"+"["+"1"+"]"+ focusElementPath; + handleTabHighlight(event,shadowDoms[i],"shadowdom",focusElementPath); + }) + } + } + + return true; +}); + +function handleTabHighlight(event:any,doc:any,docType:string,iframeStr:string) { // doc type is main, iframe, shadowdom, click + // console.log("Function: handleTabHighlight"); + let elementXpath = ""; + + if (!event.shiftKey && event.key === "Tab") { // only catch Tab key + // console.log("Got TAB Key"); + // console.log("TAB doc = ", doc); + if (docType === "main") { + // console.log("Got main doc element"); + let element = doc.activeElement; // get element just tabbed to which has focus + elementXpath = getXPathForElement(element); // in main doc so just get xpath + } + + // if we have iframe + if (docType === "iframe") { + // console.log("Got iframe element"); + let element = doc.activeElement; // get element just tabbed to which has focus + elementXpath = getXPathForElement(element); // in main doc so just get xpath + elementXpath = iframeStr + elementXpath; + } + + // if we have shadow dom no need to do anything special + if (docType === "shadowdom") { + console.log("we have an element in a shadow dom"); + let sdXpath = getXPathForElement(doc); + let element = doc.shadowRoot.activeElement; + elementXpath = getXPathForElement(element); + // need #document-fragment[n] + elementXpath = sdXpath+iframeStr; + } + + // get circle or polygon with matching xpath + let circle = document.querySelector('circle[xpath="'+elementXpath+'"]'); + let polygon = document.querySelector('polygon[xpath="'+elementXpath+'"]'); + + // console.log("circle = ",circle); + // console.log("polygon = ",polygon); + + let prevHighlightedElement; + // find previouse highlighted element which is either a circle or triangle so will be within document + if (prevHighlightedElement = document.getElementsByClassName("highlightSVG")[0]) { + // console.log("Found prevHighlightedElement is circle = ", prevHighlightedElement); + } else if (prevHighlightedElement = document.getElementsByClassName("highlightSVGtriangle")[0]) { + // console.log("Found prevHighlightedElement is polygon = ", prevHighlightedElement ); + } + // for prevHighlightedElement remove highlightSVG and add noHighlightSVG + if (prevHighlightedElement) { + // console.log("prevHighlightedElement.tagName = ", prevHighlightedElement.tagName); + if (prevHighlightedElement.tagName === "circle") { + prevHighlightedElement.classList.remove("highlightSVG"); + prevHighlightedElement.classList.add("noHighlightSVG"); + } + else if (prevHighlightedElement.tagName === "polygon") { + prevHighlightedElement.classList.remove("highlightSVGtriangle"); + prevHighlightedElement.classList.add("noHighlightSVGtriangle"); + } + // console.log("prevHighlightedElement unhighlighted = ",prevHighlightedElement); + } else { + // console.log("No prevHighlightedElement to highlight") + } + // Highlight circle + if (circle) { + circle?.classList.remove("noHighlightSVG"); + circle?.classList.add("highlightSVG"); + // console.log("circle highlighted = ",circle); + } else { + // console.log("No circle to highlight = ",circle); + } + if (polygon) { + polygon?.classList.remove("noHighlightSVGtriangle"); + polygon?.classList.add("highlightSVGtriangle"); + // console.log("polygon highlighted = ",polygon); + } else { + // console.log("No polygon to highlight = ",circle); + } + } else if (event.shiftKey && event.key === "Tab") { // catch SHIFT TAB + // console.log("Got SHIFT TAB Key"); + // console.log("TAB doc = ", doc); + if (docType === "main") { + console.log("Got main doc element"); + let element = doc.activeElement; // get element just tabbed to which has focus + elementXpath = getXPathForElement(element); // in main doc so just get xpath + } + + // if we have iframe + if (docType === "iframe") { + // console.log("Got iframe element"); + let element = doc.activeElement; // get element just tabbed to which has focus + elementXpath = getXPathForElement(element); // in main doc so just get xpath + elementXpath = iframeStr + elementXpath; + } + + // if we have shadow dom no need to do anything special + if (docType === "shadowdom") { + // console.log("Got shadow dom element"); + let sdXpath = getXPathForElement(doc); + let element = doc.shadowRoot.activeElement; + elementXpath = getXPathForElement(element); + // need #document-fragment[n] + elementXpath = sdXpath+iframeStr; + } + + console.log("elementXpath right before matching = ",elementXpath); + // get circle or polygon with matching xpath + let circle = document.querySelector('circle[xpath="'+elementXpath+'"]'); + let polygon = document.querySelector('polygon[xpath="'+elementXpath+'"]'); + + // console.log("circle = ",circle); + // console.log("polygon = ",polygon); + + let prevHighlightedElement; + // find previouse highlighted element which is either a circle or triangle so will be within document + + if (prevHighlightedElement = document.getElementsByClassName("highlightSVG")[0]) { + // console.log("Found prevHighlightedElement is circle = ", prevHighlightedElement); + } else if (prevHighlightedElement = document.getElementsByClassName("highlightSVGtriangle")[0]) { + // console.log("Found prevHighlightedElement is polygon = ", prevHighlightedElement ); + } + // for prevHighlightedElement remove highlightSVG and add noHighlightSVG + + if (prevHighlightedElement) { + // console.log("prevHighlightedElement.tagName = ", prevHighlightedElement.tagName); + if (prevHighlightedElement.tagName === "circle") { + prevHighlightedElement.classList.remove("highlightSVG"); + prevHighlightedElement.classList.add("noHighlightSVG"); + } + else if (prevHighlightedElement.tagName === "polygon") { + prevHighlightedElement.classList.remove("highlightSVGtriangle"); + prevHighlightedElement.classList.add("noHighlightSVGtriangle"); + } + // console.log("prevHighlightedElement unhighlighted = ",prevHighlightedElement); + } else { + // console.log("No prevHighlightedElement to highlight") + } + // Highlight circle + if (circle) { + circle?.classList.remove("noHighlightSVG"); + circle?.classList.add("highlightSVG"); + // console.log("circle highlighted = ",circle); + } else { + // console.log("No circle to highlight = ",circle); + } + if (polygon) { + polygon?.classList.remove("noHighlightSVGtriangle"); + polygon?.classList.add("highlightSVGtriangle"); + // console.log("polygon highlighted = ",polygon); + } else { + // console.log("No polygon to highlight = ",circle); + } + } else if (event.detail !== 0) { + if (event.target.tagName === "circle" || event.target.tagName === "polygon") { + let circle; + if (event.target.tagName === "circle") { + circle = event.target; + } + let polygon; + if (event.target.tagName === "polygon") { + polygon = event.target; + } + + let element = selectPath(event.target.getAttribute("xpath")); // circle's element that we want to have focus + + elementXpath = getXPathForElement(element); // path if not in iframe + + + // if we have iframe + if (docType === "iframe") { + let element = doc.activeElement; // get element just tabbed to which has focus + elementXpath = getXPathForElement(element); // in main doc so just get xpath + elementXpath = iframeStr + elementXpath; + console.log("iframeStr = ",iframeStr) + } + + // console.log("elementXpath = ",elementXpath); + + // get circle or polygon with matching xpath + // let circle = document.querySelector('circle[xpath="'+elementXpath+'"]'); + // let polygon = document.querySelector('polygon[xpath="'+elementXpath+'"]'); + let prevHighlightedElement; + if (prevHighlightedElement = doc.getElementsByClassName("highlightSVG")[0] || document.getElementsByClassName("highlightSVG")[0]) { + // console.log("Found prevHighlightedElement is circle = ", prevHighlightedElement); + } else if (prevHighlightedElement = doc.getElementsByClassName("highlightSVGtriangle")[0] || document.getElementsByClassName("highlightSVGtriangle")[0]) { + // console.log("Found prevHighlightedElement is polygon = ", prevHighlightedElement ); + } + // for prevHighlightedElement remove highlightSVG and add noHighlightSVG + + if (prevHighlightedElement) { + // console.log("prevHighlightedElement.tagName = ", prevHighlightedElement.tagName); + if (prevHighlightedElement.tagName === "circle") { + prevHighlightedElement.classList.remove("highlightSVG"); + prevHighlightedElement.classList.add("noHighlightSVG"); + } + else if (prevHighlightedElement.tagName === "polygon") { + prevHighlightedElement.classList.remove("highlightSVGtriangle"); + prevHighlightedElement.classList.add("noHighlightSVGtriangle"); + } + // console.log("prevHighlightedElement unhighlighted = ",prevHighlightedElement); + } else { + // console.log("No prevHighlightedElement to highlight") + } + // Highlight circle + if (circle) { + circle?.classList.remove("noHighlightSVG"); + circle?.classList.add("highlightSVG"); + // console.log("circle highlighted = ",circle); + } else { + // console.log("No circle to highlight = ",circle); + } + if (polygon) { + polygon?.classList.remove("noHighlightSVGtriangle"); + polygon?.classList.add("highlightSVGtriangle"); + // console.log("polygon highlighted = ",polygon); + } else { + // console.log("No circle to highlight = ",circle); + } + } + } +} + +// Debounce +function debounce(func:any, time:any) { + // turn off resize event + var time = time || 100; // 100 by default if no param + var timer: any; + return function(event:any) { + if (timer) clearTimeout(timer); + timer = setTimeout(func, time, event); + }; +} + +// Function with stuff to execute +function resizeContent() { + // Do loads of stuff once window has resized + let resize = true; + TabMessaging.sendToBackground("TABSTOP_RESIZE", { resize: resize } ); + + // Turn resize listener back on +} + +function getXPathForElement(element: any) { + const idx: any = (sib: any, name: any) => sib ? idx(sib.previousElementSibling, name || sib.localName) + (sib.localName == name) : 1; + const segs: any = (elm: any) => (!elm || elm.nodeType !== 1) ? [''] : [...segs(elm.parentNode), `${elm.localName.toLowerCase()}[${idx(elm)}]`]; + return segs(element).join('/'); +} + +TabMessaging.addListener("HIGHLIGHT_TABSTOP_TO_CONTEXT_SCRIPTS", async (message: any) => { + // Clearing any that are already highlighted + document.querySelectorAll(".highlightSVG").forEach(e => e.classList.remove("highlightSVG")); + // Highlighting any that are "clicked" + document.getElementsByClassName("circleNumber" + message.tabStopId)[0].classList.add("highlightSVG"); + return true; +}); + +//@ts-ignore +TabMessaging.addListener("DELETE_DRAW_TABS_TO_CONTEXT_SCRIPTS", async (message: any) => { + deleteDrawing(".deleteMe"); + return true; +}); + +function injectCSS(styleString: string) { + const style = document.createElement('style'); + style.textContent = styleString; + document.head.append(style); +} + +async function draw(tabstops: any, tabStopsErrors: any, lines:boolean, outlines:boolean,iframes:any) { + // console.log("Inside draw") + await redraw(tabstops, tabStopsErrors, lines, outlines, iframes); +} + +async function drawErrors(tabStopsErrors: any, tabStops: any, outlines: boolean, iframes: any) { + // console.log("Inside drawErrors") + await redrawErrors(tabStopsErrors, tabStops, outlines, iframes); + return true; +} + +function deleteDrawing(classToRemove: string) { + // console.log("Function: deleteDrawing"); + document.querySelectorAll(classToRemove).forEach(e => e.remove()); +} + + +function redrawErrors(tabStopsErrors: any, tabStops: any, outlines: boolean, iframes: any) { + // JCH - FIX drawing ? trangle if there is already a tabbable triangle + // console.log("Function: redrawErrors"); + setTimeout(() => { + let tabbableNodesXpaths = getNodesXpaths(tabStops); + + let nodes = getNodesXpaths(tabStopsErrors); + let nodeXpaths = nodes; + nodes = convertXpathsToHtmlElements(nodeXpaths); + + // console.log("tabStopsErrors = ", tabStopsErrors); + nodes = nodes.filter(function (el: any) { // Removing failure case of null nodes being sent + return el != null; + }); + + // console.log("nodes.length = ",nodes.length); + for (let i = 0; i < nodes.length; i++) { + // console.log("nodes[",i,"] = ",nodes[i]); + // Check if already taken care of in the tabbable elements + let skipErrorNode = false; + for (let j=0; j < tabbableNodesXpaths.length; j++) { + if (nodeXpaths[i] === tabbableNodesXpaths[j]) { + // console.log("Already in Tab Chain"); + // console.log(tabStopsErrors[i].ruleId); + skipErrorNode = true; // JCH - already taken care of in redraw + } else { + // console.log("Not in Tab Chain"); + // console.log(tabStopsErrors[i].ruleId); + // console.log("nodeXpaths[",i,"] = ",nodeXpaths[i]); + } + } + if (skipErrorNode === true) { + // console.log("JCH - skip out"); + continue; // JCH - don't put up non triangle for an element if already done in redraw + } + + if (nodeXpaths[i].includes("body")) { // JCH - non tabbable nodes must be within body + // console.log("Non tabbable nodes[",i,"] = ",nodes[i]); + + if (nodes[i] != null ) { // JCH - tabbable nodes + if (nodes[i] != null ) { // JCH - tabbable nodes + // console.log("Non tabbable nodes[",i,"] element exists"); + if (typeof nodes[i].tagName !== 'undefined' || nodes[i].tagName !== null ) { // JCH - tabbable nodes + // console.log("Non tabbable nodes[",i,"] tagName is ",nodes[i].tagName); + if (typeof nodes[i].getBoundingClientRect !== 'undefined' || nodes[i].getBoundingClientRect != null) { + // console.log("Non tabbable nodes[",i,"] has bounding rect"); + } + else { + // console.log("Non tabbable nodes[",i,"] has NO bounding rect"); + } + } else { + // console.log("Non tabbablenodes[",i,"].tagName is null $$$$$"); + } + } else { + // console.log("Non tabbable nodes[",i,"] is null $$$$$"); + } + } + // console.log("--------------------------------"); + + if (nodes[i] != null ) { // JCH - if node exists + + + // coords for nodes[i] and its bounding box if not in iframe or shadow dom + let x = nodes[i].getBoundingClientRect().x; + let xPlusWidth = nodes[i].getBoundingClientRect().x + nodes[i].getBoundingClientRect().width; + + let y = nodes[i].getBoundingClientRect().y; + let yPlusHeight = nodes[i].getBoundingClientRect().y + nodes[i].getBoundingClientRect().height; + + // adjustment for iframes + // if element inside iframe get iframe coordinates the add coordinates of element to those of iframe + // console.log("xpath = ",nodeXpaths[i]); + + if (nodeXpaths[i].includes("iframe")) { // this is for element i + // find and store iframe + let lastElement = nodeXpaths[i].slice(nodeXpaths[i].lastIndexOf('/')); + + if (lastElement.includes("iframe")) { // this is for the iframe element + // console.log("We Have an iframe, lastElement", lastElement); + if (!iframes.find((e:any) => e.name === nodeXpaths[i])) { // already in iframes + const iframe = {element: nodes[i], name: nodeXpaths[i], x: nodes[i].getBoundingClientRect().x, y: nodes[i].getBoundingClientRect().y}; + iframes.push(iframe); + // console.log(iframes); + } + // no need to adjust coords as the iframe is an element on the main page + // console.log(iframes); + } else { // this is for elements that are within an iframe + // get the iframe string iframe[n] + // console.log("We have and element in an iframe"); + // console.log("iframeString = ",iframeString); + let realIframeString = nodeXpaths[i].slice(0,nodeXpaths[i].indexOf('/html', nodeXpaths[i].indexOf('/html')+1)); + // console.log("realIframeString = ",realIframeString); + // find the iframe in iframes + // console.log(iframes); + const iframesObj = iframes.find((e:any) => e.name === realIframeString); + // console.log("iframesObj = ",iframesObj); + x = iframesObj.x + nodes[i].getBoundingClientRect().x; + y = iframesObj.y + nodes[i].getBoundingClientRect().y; + + } + } + + // If the circle is being drawn slighly off of the screen move it into the screen + // Note: here we assume radius is 13 + if (x <= 15) { + x += 15 - x; + } + if (y <= 15) { + y += 15 - y; + } + + // see below lines as we draw triangle after lines + // console.log("Triangle x,y = ",x,",",y); + + if (outlines) { + + // MAKE BOX AROUND ACTIVE COMPONENT + makeLine(x, y, xPlusWidth, y, ["lineError"]); + makeLine(x, y, x, yPlusHeight, ["lineError"]); + makeLine(xPlusWidth, y, xPlusWidth, yPlusHeight, ["lineError"]); + makeLine(x, yPlusHeight, xPlusWidth, yPlusHeight, ["lineError"]); + + // Make white stroke around active component outline + makeLine(x - 1, y - 1, xPlusWidth + 1, y - 1, ["lineEmbossError"]); + makeLine(x - 1, y - 1, x - 1, yPlusHeight + 1, ["lineEmbossError"]); + makeLine(xPlusWidth + 1, y - 1, xPlusWidth + 1, yPlusHeight + 1, ["lineEmbossError"]); + makeLine(x - 1, yPlusHeight + 1, xPlusWidth + 1, yPlusHeight + 1, ["lineEmbossError"]); + + // Make white stroke inside active component outline + makeLine(x + 1, y + 1, xPlusWidth - 1, y + 1, ["lineEmbossError"]); + makeLine(x + 1, y + 1, x + 1, yPlusHeight - 1, ["lineEmbossError"]); + makeLine(xPlusWidth - 1, y + 1, xPlusWidth - 1, yPlusHeight - 1, ["lineEmbossError"]); + makeLine(x + 1, yPlusHeight - 1, xPlusWidth - 1, yPlusHeight - 1, ["lineEmbossError"]); + } + + // Logic used from: https://math.stackexchange.com/questions/1344690/is-it-possible-to-find-the-vertices-of-an-equilateral-triangle-given-its-center + let triangleLegLength = 27; + let triangleXShifted = x; + let triangleYShifted = y+1; // Shift 1 px to center the ? we draw + // If the triangle is being drawn slighly off of the screen move it into the screen + if (triangleXShifted >= -10 && triangleXShifted <= 6) { + triangleXShifted = 14; + } + if (triangleYShifted >= -10 && triangleYShifted <= 6) { + triangleYShifted = 14; + } + // console.log("Not Tabbable ERROR i = ",i," so makeTriangle"); + makeTriangle( + triangleXShifted, triangleYShifted - (Math.sqrt(3)/3)*triangleLegLength , + triangleXShifted-triangleLegLength/2, triangleYShifted+(Math.sqrt(3)/6)*triangleLegLength, + triangleXShifted+triangleLegLength/2, triangleYShifted+(Math.sqrt(3)/6)*triangleLegLength, + "Error"+i.toString(), nodeXpaths[i]) + + makeTextSmall(x, y, "?", "textColorBlack"); + } else { + continue; + } + } + } + }, 1); +} + + +// @ts-ignore +function redraw(tabstops: any, tabStopsErrors: any, lines: boolean, outlines: boolean, iframes: any) { + // console.log("Function: redraw"); + // JCH - do circles and triangles coord calculations before lines and outlines + // as centers of circles and triangles set the basic coords + + setTimeout(() => { + let offset = 3; + let nodes = getNodesXpaths(tabstops); + let nodeXpaths = nodes; + nodes = convertXpathsToHtmlElements(nodeXpaths); + + // console.log("Tabbable elements: nodes.length = ",nodes.length); + for (let i = 0; i < nodes.length; i++) { + if (nodes[i] != null) { + // console.log("Tabbable nodes[",i,"] element exists"); + if (typeof nodes[i].tagName !== 'undefined' || nodes[i].tagName !== null ) { // JCH - tabbable nodes + // console.log("Tabbable nodes[",i,"] tagName is ",nodes[i].tagName); + if (typeof nodes[i].getBoundingClientRect !== 'undefined' || nodes[i].getBoundingClientRect != null) { + // console.log("Tabbable nodes[",i,"] has bounding rect", nodes[i].getBoundingClientRect().x,",",nodes[i].getBoundingClientRect().y); + } + else { + // console.log("Tabbable nodes[",i,"] has NO bounding rect"); + } + } else { + // console.log("Tabbable nodes[",i,"].tagName is null $$$$$"); + } + } + // console.log("--------------------------------"); + if (nodes[i+1] != null && i+1 < nodes.length) { + // console.log("Tabbable nodes[",i+1,"] element exists"); + if (typeof nodes[i+1].tagName !== 'undefined' || nodes[i+1].tagName !== null ) { // JCH - tabbable nodes + // console.log("Tabbable nodes[",i+1,"] tagName is ",nodes[i+1].tagName); + if (typeof nodes[i+1].getBoundingClientRect !== 'undefined' || nodes[i+1].getBoundingClientRect != null) { + // console.log("Tabbable nodes[",i+1,"] has bounding rect", nodes[i+1].getBoundingClientRect().x,",",nodes[i+1].getBoundingClientRect().y); + } + else { + // console.log("Tabbable nodes[",i+1,"] has NO bounding rect"); + } + } else { + // console.log("Tabbable nodes[",i+1,"].tagName is null $$$$$"); + } + } + // console.log("--------------------------------"); + } + + for (let i = 0; i < nodes.length; i++) { //Make lines between numbers + if (nodes[i] != null ) { // JCH - tabbable nodes + if (tabstops[i].hasOwnProperty("nodeHasError") && tabstops[i].nodeHasError) { // if true should draw triangle instead of circle + + // coords for nodes[i] and its bounding box if not in iframe or shadow dom + let x = nodes[i].getBoundingClientRect().x; + let xPlusWidth = nodes[i].getBoundingClientRect().x + nodes[i].getBoundingClientRect().width; + + let y = nodes[i].getBoundingClientRect().y; + let yPlusHeight = nodes[i].getBoundingClientRect().y + nodes[i].getBoundingClientRect().height; + + let triangleLegLength = 27; + + // adjustment for iframes + // if element inside iframe get iframe coordinates the add coordinates of element to those of iframe + if (nodeXpaths[i].includes("iframe")) { // this is for element i + // find and store iframe + let lastElement = nodeXpaths[i].slice(nodeXpaths[i].lastIndexOf('/')); + if (lastElement.includes("iframe")) { // this is for the iframe element + if (!iframes.find((e:any) => e.name === nodeXpaths[i])) { // already in iframes + const iframe = {element: nodes[i], name: nodeXpaths[i], x: nodes[i].getBoundingClientRect().x, y: nodes[i].getBoundingClientRect().y}; + iframes.push(iframe); + } + // no need to adjust coords as the iframe is an element on the main page + } else { // this is for elements that are within an iframe + let realIframeString = nodeXpaths[i].slice(0,nodeXpaths[i].indexOf('/html', nodeXpaths[i].indexOf('/html')+1)); + const iframesObj = iframes.find((e:any) => e.name === realIframeString); + // adjust coords since in iframe + x = iframesObj.x + nodes[i].getBoundingClientRect().x; + y = iframesObj.y + nodes[i].getBoundingClientRect().y; + } + } + + // If the circle is being drawn slighly off of the screen move it into the screen + // Note: here we assume radius is 13 + if (x <= 15) { + x += 15 - x; + } + if (y <= 15) { + y += 15 - y; + } + + // see below lines as we draw triangle after lines + + // for line to next tabbable element find next tabbable element that exists + let nextTabbableElement; + for (let j = i+1; j < nodes.length; j++) { + if (nodes[j] != null) { + nextTabbableElement = nodes[j]; + break; + } + } + + if (lines) { + if (i < nodes.length - 1) { + if (typeof nodes[i].getBoundingClientRect !== 'undefined' || nodes[i].getBoundingClientRect != null) { + // console.log("nodes[",i,"] has bounding rect"); + } + else { + continue; + // console.log("nodes[",i,"] has NO bounding rect"); + } + if (typeof nextTabbableElement.getBoundingClientRect !== 'undefined' || nextTabbableElement.getBoundingClientRect != null) { + // console.log("nextTabbableElement has bounding rect"); + } + else { + continue; + // console.log("nextTabbableElement has NO bounding rect"); + } + + let slope = (nextTabbableElement.getBoundingClientRect().y - offset - nodes[i].getBoundingClientRect().y - offset) / (nextTabbableElement.getBoundingClientRect().x - offset - nodes[i].getBoundingClientRect().x - offset); + let x1, y1, x2, y2; + x1 = x; + y1 = y; + + // coords for nodes[i+1] or nextTabbableElement if not in iframe or shadow dom + x2 = nextTabbableElement.getBoundingClientRect().x; + y2 = nextTabbableElement.getBoundingClientRect().y; + + // let check if the next tabbable element is an iframe + if (nodeXpaths[i+1].includes("iframe")) { + let lastElement = nodeXpaths[i+1].slice(nodeXpaths[i+1].lastIndexOf('/')); + + if (lastElement.includes("iframe")) { // this is for the iframe element + if (!iframes.find((e:any) => e.name === nodeXpaths[i+1])) { // already in iframes + const iframe = {element: nodes[i+1], name: nodeXpaths[i+1], x: nodes[i+1].getBoundingClientRect().x, y: nodes[i+1].getBoundingClientRect().y}; + iframes.push(iframe); + // adjust coords + x2 = nodes[i+1].getBoundingClientRect().x; + y2 = nodes[i+1].getBoundingClientRect().y; + } + // no need to adjust coords as the iframe is an element on the main page + } else { // this is for elements that are within an iframe + // get the iframe string iframe[n] + let realIframeString = nodeXpaths[i+1].slice(0,nodeXpaths[i+1].indexOf('/html', nodeXpaths[i+1].indexOf('/html')+1)); + const iframesObj = iframes.find((e:any) => e.name === realIframeString); + // adjust coords + x2 = iframesObj.x + nodes[i+1].getBoundingClientRect().x; + y2 = iframesObj.y + nodes[i+1].getBoundingClientRect().y; + } + } + + // If the if the 2nd circle is being drawn slighly off of the screen move it into the screen + // Note: here we assume radius is 13 + if (x2 <= 15) { + x2 += 15 - x2; + } + if (y2 <= 15) { + y2 += 15 - y2; + } + + // console.log("x1 = ",x1," x2 = ",x2," y1 = ",y1," y2 = ",y2); + makeLine(x1, y1, x2, y2, ["line"]); + + // Create white outline + if (Math.abs(slope) < 1) { // Low slope move y + makeLine(x1, y1 - 2, x2, y2 - 2, ["lineEmboss"]); + makeLine(x1, y1 + 2, x2, y2 + 2, ["lineEmboss"]); + + } else { // high slope move x + makeLine(x1 - 2, y1, x2 - 2, y2, ["lineEmboss"]); + makeLine(x1 + 2, y1, x2 + 2, y2, ["lineEmboss"]); + } + } + } + + if (i < nodes.length) { + makeTriangle( + x, y - (Math.sqrt(3)/3)*triangleLegLength , + x-triangleLegLength/2, y+(Math.sqrt(3)/6)*triangleLegLength, + x+triangleLegLength/2, y+(Math.sqrt(3)/6)*triangleLegLength, + i.toString(), nodeXpaths[i]); + + + makeTextSmall(x, y, (i + 1).toString(), "textColorBlack"); + } + + if (outlines) { + + // Make box around active component + makeLine(x, y, xPlusWidth, y, ["line", "lineTop", "lineNumber" + i]); + makeLine(x, y, x, yPlusHeight, ["line", "lineLeft", "lineNumber" + i]); + makeLine(xPlusWidth, y, xPlusWidth, yPlusHeight, ["line", "lineRight", "lineNumber" + i]); + makeLine(x, yPlusHeight, xPlusWidth, yPlusHeight, ["line", "lineBottom", "lineNumber" + i]); + + // Make white stroke around active component outline + makeLine(x - 1, y - 1, xPlusWidth + 1, y - 1, ["lineEmboss"]); + makeLine(x - 1, y - 1, x - 1, yPlusHeight + 1, ["lineEmboss"]); + makeLine(xPlusWidth + 1, y - 1, xPlusWidth + 1, yPlusHeight + 1, ["lineEmboss"]); + makeLine(x - 1, yPlusHeight + 1, xPlusWidth + 1, yPlusHeight + 1, ["lineEmboss"]); + + // Make white stroke inside active component outline + makeLine(x + 1, y + 1, xPlusWidth - 1, y + 1, ["lineEmboss"]); + makeLine(x + 1, y + 1, x + 1, yPlusHeight - 1, ["lineEmboss"]); + makeLine(xPlusWidth - 1, y + 1, xPlusWidth - 1, yPlusHeight - 1, ["lineEmboss"]); + makeLine(x + 1, yPlusHeight - 1, xPlusWidth - 1, yPlusHeight - 1, ["lineEmboss"]); + } + } else { // This is the defalt case were we just draw a circle + // coords for nodes[i] and its bounding box if not in iframe or shadow dom + let x = nodes[i].getBoundingClientRect().x; + let xPlusWidth = nodes[i].getBoundingClientRect().x + nodes[i].getBoundingClientRect().width; + + let y = nodes[i].getBoundingClientRect().y; + let yPlusHeight = nodes[i].getBoundingClientRect().y + nodes[i].getBoundingClientRect().height; + + // adjustment for iframes + // if element inside iframe get iframe coordinates the add coordinates of element to those of iframe + if (nodeXpaths[i].includes("iframe")) { // this is for element i + // find and store iframe + let lastElement = nodeXpaths[i].slice(nodeXpaths[i].lastIndexOf('/')); + + if (lastElement.includes("iframe")) { // this is for the iframe element + // console.log("We Have an iframe, lastElement", lastElement); + if (!iframes.find((e:any) => e.name === nodeXpaths[i])) { // already in iframes + const iframe = {element: nodes[i], name: nodeXpaths[i], x: nodes[i].getBoundingClientRect().x, y: nodes[i].getBoundingClientRect().y}; + iframes.push(iframe); + } + // no need to adjust coords as the iframe is an element on the main page + } else { // this is for elements that are within an iframe + // get the iframe string iframe[n] + let realIframeString = nodeXpaths[i].slice(0,nodeXpaths[i].indexOf('/html', nodeXpaths[i].indexOf('/html')+1)); + // find the iframe in iframes + const iframesObj = iframes.find((e:any) => e.name === realIframeString); + // console.log("iframesObj = ",iframesObj); + x = iframesObj.x + nodes[i].getBoundingClientRect().x; + y = iframesObj.y + nodes[i].getBoundingClientRect().y; + + } + } + + // If the circle is being drawn slighly off of the screen move it into the screen + // Note: here we assume radius is 13 + if (x <= 15) { + x += 15 - x; + } + if (y <= 15) { + y += 15 - y; + } + + // see below lines as we draw circle after lines + + // for line to next tabbable element find next tabbable element that exists + let nextTabbableElement; + for (let j = i+1; j < nodes.length; j++) { + if (nodes[j] != null) { + nextTabbableElement = nodes[j]; + break; + } + } + + if (lines) { + if (i < nodes.length - 1) { + let slope = (nextTabbableElement.getBoundingClientRect().y - offset - nodes[i].getBoundingClientRect().y - offset) / (nextTabbableElement.getBoundingClientRect().x - offset - nodes[i].getBoundingClientRect().x - offset); + let x1, y1, x2, y2; + x1 = x; + y1 = y; + + // coords for nodes[i+1] or nextTabbableElement if not in iframe or shadow dom + x2 = nodes[i+1].getBoundingClientRect().x; + y2 = nodes[i+1].getBoundingClientRect().y; + + // let check if the next tabbable element is an iframe + if (nodeXpaths[i+1].includes("iframe")) { + // find and store iframe + let lastElement = nodeXpaths[i+1].slice(nodeXpaths[i+1].lastIndexOf('/')); + if (lastElement.includes("iframe")) { // this is for the iframe element + // console.log("We Have an iframe, lastElement", lastElement); + if (!iframes.find((e:any) => e.name === nodeXpaths[i+1])) { // already in iframes + const iframe = {element: nodes[i+1], name: nodeXpaths[i+1], x: nodes[i+1].getBoundingClientRect().x, y: nodes[i+1].getBoundingClientRect().y}; + iframes.push(iframe); + x2 = nodes[i+1].getBoundingClientRect().x; + y2 = nodes[i+1].getBoundingClientRect().y; + } + } else { // this is for elements that are within an iframe + // get the iframe string iframe[n] + let realIframeString = nodeXpaths[i+1].slice(0,nodeXpaths[i+1].indexOf('/html', nodeXpaths[i+1].indexOf('/html')+1)); + // find the iframe in iframes + const iframesObj = iframes.find((e:any) => e.name === realIframeString); + // adjust coords + x2 = iframesObj.x + nodes[i+1].getBoundingClientRect().x; + y2 = iframesObj.y + nodes[i+1].getBoundingClientRect().y; + } + } + + // If the if the 2nd circle is being drawn slighly off of the screen move it into the screen + // Note: here we assume radius is 13 + if (x2 <= 15) { + x2 += 15 - x2; + } + if (y2 <= 15) { + y2 += 15 - y2; + } + + // console.log("x1 = ",x1," x2 = ",x2," y1 = ",y1," y2 = ",y2); + makeLine(x1, y1, x2, y2, ["line"]); + + // Create white outline + if (Math.abs(slope) < 1) { // Low slope move y + makeLine(x1, y1 - 2, x2, y2 - 2, ["lineEmboss"]); + makeLine(x1, y1 + 2, x2, y2 + 2, ["lineEmboss"]); + + } else { // high slope move x + makeLine(x1 - 2, y1, x2 - 2, y2, ["lineEmboss"]); + makeLine(x1 + 2, y1, x2 + 2, y2, ["lineEmboss"]); + } + } + } + + // draw circles after lines + + makeCircleSmall(x, y, i.toString(), 13, nodeXpaths[i]); + makeTextSmall(x, y, (i + 1).toString(),"textColorWhite"); + + + if (outlines) { + + // Make box around active component + makeLine(x, y, xPlusWidth, y, ["line", "lineTop", "lineNumber" + i]); + makeLine(x, y, x, yPlusHeight, ["line", "lineLeft", "lineNumber" + i]); + makeLine(xPlusWidth, y, xPlusWidth, yPlusHeight, ["line", "lineRight", "lineNumber" + i]); + makeLine(x, yPlusHeight, xPlusWidth, yPlusHeight, ["line", "lineBottom", "lineNumber" + i]); + + // Make white stroke around active component outline + makeLine(x - 1, y - 1, xPlusWidth + 1, y - 1, ["lineEmboss"]); + makeLine(x - 1, y - 1, x - 1, yPlusHeight + 1, ["lineEmboss"]); + makeLine(xPlusWidth + 1, y - 1, xPlusWidth + 1, yPlusHeight + 1, ["lineEmboss"]); + makeLine(x - 1, yPlusHeight + 1, xPlusWidth + 1, yPlusHeight + 1, ["lineEmboss"]); + + // Make white stroke inside active component outline + makeLine(x + 1, y + 1, xPlusWidth - 1, y + 1, ["lineEmboss"]); + makeLine(x + 1, y + 1, x + 1, yPlusHeight - 1, ["lineEmboss"]); + makeLine(xPlusWidth - 1, y + 1, xPlusWidth - 1, yPlusHeight - 1, ["lineEmboss"]); + makeLine(x + 1, yPlusHeight - 1, xPlusWidth - 1, yPlusHeight - 1, ["lineEmboss"]); + } + } + } else { + continue; + } + } + + }, 1) +} + +function makeCircleSmall(x1: number, y1: number, circleNumber: string, radius: number, xpath: string) { + var circleClone = createSVGCircleTemplate(); + circleClone.removeAttribute("id"); + circleClone.classList.add("deleteMe"); + circleClone.classList.add("circleNumber" + circleNumber); + circleClone.setAttribute('cx', String(x1)); + circleClone.setAttribute('cy', String(y1)); + circleClone.setAttribute('pointer-events', "auto"); + circleClone.setAttribute('r', String(radius)); + circleClone.setAttribute('xpath', xpath); + circleClone.onclick = () => { + TabMessaging.sendToBackground("TABSTOP_XPATH_ONCLICK", { xpath: xpath, circleNumber: circleNumber + 1 }) + }; + if (document.getElementById("svgCircle") == null) { + const elemSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + elemSVG.setAttribute("id", "svgCircle"); + elemSVG.classList.add("dynamic"); + document.body.appendChild(elemSVG); + } + // console.log("Inject circle circleNumber" + circleNumber); + document.getElementById('svgCircle')?.appendChild(circleClone) +} + +function makeTriangle(x1: number, y1: number, x2: number, y2: number,x3: number, y3: number, circleNumber: string, xpath: string) { + // + // + // + + // { + TabMessaging.sendToBackground("TABSTOP_XPATH_ONCLICK", { xpath: xpath, circleNumber: circleNumber + 1 }) + }; + if (document.getElementById("svgCircle") == null) { + const elemSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + elemSVG.setAttribute("id", "svgCircle"); + document.body.appendChild(elemSVG); + } + // console.log("Inject triangle circleNumber" + circleNumber); + document.getElementById('svgCircle')?.appendChild(triangleClone); +} + + + +function makeTextSmall(x1: number, y1: number, n: string, textColorClassName?: string) { + + // TODO: Find possible better way to deal with this (Talk to design) + // If the circle is being drawn slighly off of the screen move it into the screen + if (x1 >= -10 && x1 <= 6) { + x1 = 12; + } + if (y1 >= -10 && y1 <= 6) { + y1 = 12; + } + + // let text = document.getElementsByClassName('circleText')[0] + var textClone = createSVGCircleTextTemplate();//text.cloneNode(true); + textClone.removeAttribute("id"); + textClone.classList.add("deleteMe"); + textClone.classList.add("circleSmall"); + if(textColorClassName){ + textClone.classList.add(textColorClassName); + } + + if (n.length >= 3) { // If number has 3+ digits shift it a few more px to center it + textClone.setAttribute('x', String(x1 - 10)); + textClone.setAttribute('y', String(y1 + 4)); + } else if (n.length == 2) { // number has 2 digits + textClone.setAttribute('x', String(x1 - 6)); + textClone.setAttribute('y', String(y1 + 4)); + } else { // number has 1 digit + textClone.setAttribute('x', String(x1 - 3)); + textClone.setAttribute('y', String(y1 + 3)); + } + textClone.innerHTML = n; + if (document.getElementById("svgCircle") == null) { + const elemSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + elemSVG.setAttribute("id", "svgCircle"); + document.body.appendChild(elemSVG); + } + document.getElementById('svgCircle')?.appendChild(textClone) +} + +function makeLine(x1: number, y1: number, x2: number, y2: number, CSSclass?: string[]) { + // console.log("Inject line"); + // let line = document.getElementsByClassName('tabLine')[0] + var lineClone = createSVGLineTemplate()//line.cloneNode(true); + if (CSSclass) { + for (let i = 0; i < CSSclass.length; i++) { + lineClone.classList.add(CSSclass[i]); + } + } + lineClone.removeAttribute("id"); + lineClone.classList.add("deleteMe"); + lineClone.setAttribute('x1', String(x1)); + lineClone.setAttribute('y1', String(y1)); + lineClone.setAttribute('x2', String(x2)); + lineClone.setAttribute('y2', String(y2)); + if (document.getElementById("svgLine") == null) { + const elemSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + elemSVG.setAttribute("id", "svgLine"); + document.body.appendChild(elemSVG); + } + document.getElementById('svgLine')?.appendChild(lineClone); +} + +function createSVGTriangleTemplate() { + // This is what we are creating: + // + // THIS PART-> + // + // + // var elemCircle = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); + var elemCircle = document.createElementNS('http://www.w3.org/2000/svg', 'polygon'); + // elemCircle.setAttribute("id", "triangle"); + elemCircle.setAttribute("id", "circle"); + elemCircle.setAttribute("class", "tabCircle"); + elemCircle.classList.add("dynamic"); + elemCircle.classList.add("noHighlightSVGtriangle"); + elemCircle.setAttribute("stroke", "black"); + elemCircle.setAttribute("stroke-width", "1"); + elemCircle.setAttribute("stroke-linejoin", "round"); + return elemCircle +} + + +function createSVGCircleTemplate() { + // This is what we are creating: + // + // THIS PART-> + // + // + var elemCircle = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); + elemCircle.setAttribute("id", "circle"); + elemCircle.setAttribute("class", "tabCircle"); + elemCircle.classList.add("dynamic"); + elemCircle.classList.add("noHighlightSVG"); + elemCircle.setAttribute("stroke", "grey"); + elemCircle.setAttribute("stroke-width", "1"); + return elemCircle +} + +function createSVGCircleTextTemplate() { + // This is what we are creating: + // + // + // THIS PART-> + // + var elemText = document.createElementNS('http://www.w3.org/2000/svg', 'text'); + elemText.setAttribute("class", "circleText"); + elemText.setAttribute("font-family", "helvetica"); + elemText.setAttribute("font-size", "10"); + elemText.setAttribute("font-weight", "normal"); + elemText.setAttribute("fill", "white"); + return elemText +} + +function createSVGLineTemplate() { + // This is what we are creating: + // + // + // + var elemLine = document.createElementNS('http://www.w3.org/2000/svg', 'line'); + elemLine.setAttribute("id", "line"); + elemLine.setAttribute("class", "tabLine"); + return elemLine +} + +function convertXpathsToHtmlElements(xpaths: any) { + // console.log("Function: convertXpathsToHtmlElements: ") + let results: any = []; + xpaths.map((xpath: any) => { + // console.log("xpath ",index); + let element; + // console.log("xpath = ",xpath); + element = selectPath(xpath); + results.push(element); + }); + return results; +} + +function getNodesXpaths(nodes: any) { + // console.log("Inside getNodesXpaths"); + let tabXpaths: any = []; + nodes.map((result: any) => { + if (result != null) { + // console.log("result.path.dom = "+result.path.dom); + tabXpaths.push(result.path.dom); + } + }); + return tabXpaths; +} + +async function goToTop() { + window.scrollTo({ + top: 0, + left: 0, + behavior: 'smooth' + }); +} + +function lookup(doc: any, xpath:string) { + if (doc.nodeType === 11) { // document fragment + let selector = ":host" + xpath.replace(/\//g, " > ").replace(/\[(\d+)\]/g, ":nth-of-type($1)"); // fixed from original + let element = doc.querySelector(selector); + return element; + } else { // regular doc type = 9 + let nodes = doc.evaluate(xpath, doc, null, XPathResult.ANY_TYPE, null); + let element = nodes.iterateNext(); + if (element) { + return element; + } else { + return null; + } + } +} + +// @ts-ignore +function selectPath(srcPath: any) { + let doc = document; + let element = null; + while (srcPath && (srcPath.includes("iframe") || srcPath.includes("#document-fragment"))) { + if (srcPath.includes("iframe")) { + let parts = srcPath.match(/(.*?iframe\[\d+\])(.*)/); + let iframe = lookup(doc, parts[1]); + element = iframe || element; + if (iframe && iframe.contentDocument) { + doc = iframe.contentDocument; + srcPath = parts[2]; + } else { + srcPath = null; + } + } else if (srcPath.includes("#document-fragment")) { + let parts = srcPath.match(/(.*?)\/#document-fragment\[\d+\](.*)/); + let fragment = lookup(doc, parts[1]); // get fragment which is in main document + element = fragment || element; + if (fragment && fragment.shadowRoot) { + doc = fragment.shadowRoot; + srcPath = parts[2]; + } else { + srcPath = null; + } + } else { + srcPath = null; + } + } + if (srcPath) { + element = lookup(doc, srcPath) || element; + } + return element; +} \ No newline at end of file diff --git a/accessibility-checker-extension/src/ts/contentScripts/index.ts b/accessibility-checker-extension/src/ts/contentScripts/index.ts new file mode 100644 index 000000000..328a8b3ea --- /dev/null +++ b/accessibility-checker-extension/src/ts/contentScripts/index.ts @@ -0,0 +1 @@ +import "./draw.ts" diff --git a/accessibility-checker-extension/src/ts/devtools/DevToolsPanelApp.tsx b/accessibility-checker-extension/src/ts/devtools/DevToolsPanelApp.tsx index fa1ac09a5..e8ff4c21d 100644 --- a/accessibility-checker-extension/src/ts/devtools/DevToolsPanelApp.tsx +++ b/accessibility-checker-extension/src/ts/devtools/DevToolsPanelApp.tsx @@ -14,980 +14,1273 @@ limitations under the License. *****************************************************************************/ -import React from "react"; -import Header from "./Header"; -import ReportManagerHeader from "./ReportManagerHeader"; -import ReportManagerTable from "./ReportManagerTable" -import Help from "./Help"; -import ReportSummary from "./ReportSummary"; -import ReportSplash from "./ReportSplash"; -import Report, { preprocessReport, IReport, IReportItem, ICheckpoint, IRuleset } from "./Report"; -import PanelMessaging from '../util/panelMessaging'; -import MultiScanReport from "../xlsxReport/multiScanReport/xlsx/multiScanReport"; -import MultiScanData from "./MultiScanData"; -// import ReportSummaryUtil from '../util/reportSummaryUtil'; -import OptionMessaging from "../util/optionMessaging"; -import BrowserDetection from "../util/browserDetection"; -// import html2canvas from "html2canvas" - -import { - Loading -} from '@carbon/react'; - - -// File is generated by report-react build -import { genReport } from './genReport'; -import HelpHeader from './HelpHeader'; -import { IArchiveDefinition } from '../background/helper/engineCache'; - -interface IPanelProps { - layout: "main" | "sub", -} - -interface IPanelState { - badURL: boolean, - listenerRegistered: boolean, - numScanning: number, - report: IReport | null, - filter: string | null, - tabURL: string, - tabCanScan: boolean, - prevTabURL: string | null, - tabId: number, - tabTitle: string, - selectedItem?: IReportItem, - selectedIssue: IReportItem | null, - rulesets: IRuleset[] | null, - selectedCheckpoint?: ICheckpoint, - learnMore: boolean, - learnItem: IReportItem | null, - showIssueTypeFilter: boolean[], - scanning: boolean, // true when scan taking place - firstScan: boolean, // true when first scan of a url - scanStorage: boolean, // true when scan storing on - currentStoredScan: string, - storedScans: { - actualStoredScan: boolean; // denotes actual stored scan vs a current scan that is kept when scans are not being stored - isSelected: boolean; // stored scan is selected in the Datatable - url: string; - pageTitle: string; - dateTime: number | undefined; - scanLabel: string; - userScanLabel: string; - ruleSet: any; - guidelines: any; - reportDate: Date; - violations: any; - needsReviews: any; - recommendations: any; - elementsNoViolations: number; - elementsNoFailures: number; - storedScan: string; - screenShot: string; - storedScanData: string; - }[], - storedScanCount: number, // number of scans stored - storedScanData: number, // total amount of scan data stored in MB - reportManager: boolean, // true show report manager, false do not show - error: string | null, - archives: IArchiveDefinition[] | null, - selectedArchive: string | null, - selectedPolicy: string | null, - focusedViewFilter: boolean, - focusedViewText: string -} - -export default class DevToolsPanelApp extends React.Component { - state: IPanelState = { - badURL: false, - listenerRegistered: false, - numScanning: 0, - report: null, - filter: null, - tabURL: "", - tabCanScan: false, - prevTabURL: "", // to determine when change url - tabId: -1, - tabTitle: "", - selectedItem: undefined, - selectedIssue: null, - rulesets: null, - learnMore: false, - learnItem: null, - showIssueTypeFilter: [true, true, true, true], - scanning: false, - storedScans: [], - storedScanCount: 0, - storedScanData: 0, - reportManager: false, // start with Report Manager not showing - firstScan: true, - scanStorage: false, - currentStoredScan: "", // true if making report for current stored scan (clear on next scan if scanStorage false) - error: null, - archives: null, - selectedArchive: null, - selectedPolicy: null, - focusedViewFilter: false, - focusedViewText: "" + import React from "react"; + import Header from "./Header"; + import TabStopsHeader from "./TabStopsHeader"; + // import TabStops from "./TabStops"; + import ReportTabStops from "./ReportTabStops"; + import ReportManagerHeader from "./ReportManagerHeader"; + import ReportManagerTable from "./ReportManagerTable" + import Help from "./Help"; + import ReportSummary from "./ReportSummary"; + import ReportSplash from "./ReportSplash"; + import Report, { preprocessReport, IReport, IReportItem, ICheckpoint, IRuleset } from "./Report"; + import PanelMessaging from '../util/panelMessaging'; + import MultiScanReport from "../xlsxReport/multiScanReport/xlsx/multiScanReport"; + import MultiScanData from "./MultiScanData"; + // import ReportSummaryUtil from '../util/reportSummaryUtil'; + import OptionMessaging from "../util/optionMessaging"; + import BrowserDetection from "../util/browserDetection"; + // import html2canvas from "html2canvas" + + import { + Loading + } from '@carbon/react'; + + + // File is generated by report-react build + import { genReport } from './genReport'; + import HelpHeader from './HelpHeader'; + import { IArchiveDefinition } from '../background/helper/engineCache'; + + interface IPanelProps { + layout: "main" | "sub", } - - ignoreNext = false; - leftPanelRef: React.RefObject; - subPanelRef: React.RefObject; - leftPanelItemSelected: React.RefObject; - subPanelItemSelected: React.RefObject; - ref: any; - - constructor(props: any) { - super(props); - this.leftPanelRef = React.createRef(); - this.subPanelRef = React.createRef(); - this.leftPanelItemSelected = React.createRef(); - this.subPanelItemSelected = React.createRef(); - if (this.props.layout === "sub") { - this.getCurrentSelectedElement(); // so selected element shows up in switch before first scan + + interface IPanelState { + badURL: boolean, + listenerRegistered: boolean, + numScanning: number, + report: IReport | null, + filter: string | null, + tabURL: string, + tabCanScan: boolean, + prevTabURL: string | null, + tabId: number, + tabTitle: string, + selectedItem?: IReportItem, + currentSelectedItem?: IReportItem, + selectedIssue: IReportItem | null, + rulesets: IRuleset[] | null, + selectedCheckpoint?: ICheckpoint, + learnMore: boolean, + learnItem: IReportItem | null, + showIssueTypeFilter: boolean[], + scanning: boolean, // true when scan taking place + firstScan: boolean, // true when first scan of a url + scanStorage: boolean, // true when scan storing on + currentStoredScan: string, + storedScans: { + actualStoredScan: boolean; // denotes actual stored scan vs a current scan that is kept when scans are not being stored + isSelected: boolean; // stored scan is selected in the Datatable + url: string; + pageTitle: string; + dateTime: number | undefined; + scanLabel: string; + userScanLabel: string; + ruleSet: any; + guidelines: any; + reportDate: Date; + violations: any; + needsReviews: any; + recommendations: any; + elementsNoViolations: number; + elementsNoFailures: number; + storedScan: string; + screenShot: string; + storedScanData: string; + }[], + storedScanCount: number, // number of scans stored + storedScanData: number, // total amount of scan data stored in MB + reportManager: boolean, // true show report manager, false do not show + error: string | null, + archives: IArchiveDefinition[] | null, + selectedArchive: string | null, + selectedPolicy: string | null, + currentArchive: any; + currentRuleset: any; + focusedViewFilter: boolean, + focusedViewText: string, + // Keyboard Mode + tabStops: [], + tabStopsPanel: boolean, // true show Tab Stops Summary, false do not show + tabStopsResults: IReportItem[], + tabStopsErrors: IReportItem[], + showHideTabStops: boolean, + // local storage vars + tabStopLines: boolean, // repeated since both OptionsApp and DevToolsPanelApp + tabStopOutlines: boolean, // gets the states from local storage + tabStopAlerts: boolean, + tabStopFirstTime: boolean, + // end local storage vars + } + + export default class DevToolsPanelApp extends React.Component { + state: IPanelState = { + badURL: false, + listenerRegistered: false, + numScanning: 0, + report: null, + filter: null, + tabURL: "", + tabCanScan: false, + prevTabURL: "", // to determine when change url + tabId: -1, + tabTitle: "", + selectedItem: undefined, + selectedIssue: null, + rulesets: null, + learnMore: false, + learnItem: null, + showIssueTypeFilter: [true, true, true, true], + scanning: false, + storedScans: [], + storedScanCount: 0, + storedScanData: 0, + reportManager: false, // start with Report Manager not showing + firstScan: true, + scanStorage: false, + currentStoredScan: "", // true if making report for current stored scan (clear on next scan if scanStorage false) + error: null, + archives: null, + selectedArchive: null, + selectedPolicy: null, + currentArchive: null, + currentRuleset: null, + focusedViewFilter: false, + focusedViewText: "", + tabStops: [], // array of xpaths of the tab stops + tabStopsPanel: false, + tabStopsResults: [], + tabStopsErrors: [], + showHideTabStops: false, + tabStopLines: false, + tabStopOutlines: false, + tabStopAlerts: false, + tabStopFirstTime: true, } - - // Only listen to element events on the subpanel - if (this.props.layout === "sub") { - chrome.devtools.panels.elements.onSelectionChanged.addListener(() => { - chrome.devtools.inspectedWindow.eval(`((node) => { - let countNode = (node) => { - let count = 0; - let findName = node.nodeName; - while (node) { - if (node.nodeName === findName) { - ++count; + + ignoreNext = false; + leftPanelRef: React.RefObject; + subPanelRef: React.RefObject; + leftPanelItemSelected: React.RefObject; + subPanelItemSelected: React.RefObject; + ref: any; + + constructor(props: any) { + super(props); + this.leftPanelRef = React.createRef(); + this.subPanelRef = React.createRef(); + this.leftPanelItemSelected = React.createRef(); + this.subPanelItemSelected = React.createRef(); + if (this.props.layout === "sub") { + this.getCurrentSelectedElement(); // so selected element shows up in switch before first scan + } + + // Only listen to element events on the subpanel + if (this.props.layout === "sub") { + chrome.devtools.panels.elements.onSelectionChanged.addListener(() => { + chrome.devtools.inspectedWindow.eval(`((node) => { + let countNode = (node) => { + let count = 0; + let findName = node.nodeName; + while (node) { + if (node.nodeName === findName) { + ++count; + } + node = node.previousElementSibling; } - node = node.previousElementSibling; + return "/"+findName.toLowerCase()+"["+count+"]"; } - return "/"+findName.toLowerCase()+"["+count+"]"; - } - try { - let retVal = ""; - while (node && node.nodeType === 1) { - retVal = countNode(node)+retVal; - if (node.parentElement) { - node = node.parentElement; - } else { - let parentElement = null; - try { - // Check if we're in a shadow DOM - if (node.parentNode && node.parentNode.nodeType === 11) { - parentElement = node.parentNode.host; - retVal = "/#document-fragment[1]"+retVal; - } else { - // Check if we're in an iframe - let parentWin = node.ownerDocument.defaultView.parent; - let iframes = parentWin.document.documentElement.querySelectorAll("iframe"); - for (const iframe of iframes) { - try { - if (iframe.contentDocument === node.ownerDocument) { - parentElement = iframe; - break; - } - } catch (e) {} + try { + let retVal = ""; + while (node && node.nodeType === 1) { + retVal = countNode(node)+retVal; + if (node.parentElement) { + node = node.parentElement; + } else { + let parentElement = null; + try { + // Check if we're in a shadow DOM + if (node.parentNode && node.parentNode.nodeType === 11) { + parentElement = node.parentNode.host; + retVal = "/#document-fragment[1]"+retVal; + } else { + // Check if we're in an iframe + let parentWin = node.ownerDocument.defaultView.parent; + let iframes = parentWin.document.documentElement.querySelectorAll("iframe"); + for (const iframe of iframes) { + try { + if (iframe.contentDocument === node.ownerDocument) { + parentElement = iframe; + break; + } + } catch (e) {} + } } - } - } catch (e) {} - node = parentElement; + } catch (e) {} + node = parentElement; + } } + return retVal; + } catch (err) { + console.error(err); } - return retVal; - } catch (err) { - console.error(err); - } - })($0)`, (result: string) => { - // This filter occurred because we selected an element in the elements tab - this.onFilter(result); - if (this.ignoreNext) { - this.ignoreNext = false; - } + })($0)`, (result: string) => { + // This filter occurred because we selected an element in the elements tab + this.onFilter(result); + if (this.ignoreNext) { + this.ignoreNext = false; + } + }); }); - }); + } } - } - - async componentDidMount() { - // console.log("componentDidMount"); - await this.readOptionsData(); - } - - async readOptionsData() { - await new Promise((resolve, _reject) => { - // console.log("readOptionsData"); - var self = this; - chrome.storage.local.get("OPTIONS", async function (result: any) { - //pick default archive id from env - let archiveId = process.env.defaultArchiveId + ""; - const archives = await self.getArchives(); - const validArchive = ((id: string) => id && archives.some((archive:any) => archive.id === id)); - - //if default archive id is not good, pick 'latest' - if (!validArchive(archiveId)) { - archiveId = "latest"; - } - - //use archive id if it is in storage, - if (result.OPTIONS && result.OPTIONS.selected_archive && validArchive(result.OPTIONS.selected_archive.id)) { - archiveId = result.OPTIONS.selected_archive.id; - } - - let selectedArchive = archives.filter((archive:any) => archive.id === archiveId)[0]; - - let policyId: string = selectedArchive.policies[0].id; - let policyName: string = selectedArchive.policies[0].name; - const validPolicy = ((id: string) => id && selectedArchive.policies.some((policy:any) => policy.id === id)); - - if (!validPolicy(policyId)){ - policyId = "IBM_Accessibility"; + + async xpathFromTabstops(message: any) { + // console.log("xpathFromTabstops XPath:", message.xpath, " circleNumber: ", message.circleNumber); + // JCH take xpath and match to item with same item.path.dom + this.state.tabStopsResults.map((result: any) => { + if (message.xpath === result.path.dom) { + // console.log("result xpath = ",result.path.dom); + this.getSelectedItem(result); + this.selectItem(result); } - - //use policy id if it is in storage - if (result.OPTIONS && result.OPTIONS.selected_ruleset && validPolicy(result.OPTIONS.selected_ruleset.id)) { - policyId = result.OPTIONS.selected_ruleset.id; - policyName = result.OPTIONS.selected_ruleset.name; + }); + // JCH the selected item needs to scroll into view + this.state.tabStopsErrors.map((result: any) => { + if (message.xpath === result.path.dom) { + // console.log("result xpath = ",result.path.dom); + this.getSelectedItem(result); + this.selectItem(result); } - - // to fix when undocked get tab id using chrome.devtools.inspectedWindow.tabId - // and get url using chrome.tabs.get via message "TAB_INFO" - let thisTabId = chrome.devtools.inspectedWindow.tabId; - let tab = await PanelMessaging.sendToBackground("TAB_INFO", { tabId: thisTabId }); - // console.log("tab.id = ", tab.id); - // console.log("tab.url = ", tab.url); - // console.log("tab.title = ", tab.title); - // console.log("tab.canScan = ",tab.canScan); - if (tab.id && tab.url && tab.id && tab.title) { - - if (!tab.canScan) { - // console.log("Found BAD url: ",tab.url); - // console.log("badURL = ",self.state.badURL); - if (self.state.badURL === false) { - self.setState({ badURL: true }); - } - self.setState({ tabURL: tab.url, tabId: tab.id, tabTitle: tab.title, tabCanScan: tab.canScan }); - return; + }); + } + + async componentDidMount() { + // console.log("Function: componentDidMount START"); + // console.log("this.state.tabId = ", this.state.tabId); + await this.readOptionsData(); + // console.log("Function: componentDidMount DONE"); + } + + async readOptionsData() { + // console.log("Function: readOptionsData START"); + await new Promise((resolve, _reject) => { + var self = this; + chrome.storage.local.get("OPTIONS", async (result: any) => { + // console.log(result.OPTIONS); + // pick default archive id from env + let archiveId = process.env.defaultArchiveId + ""; + // console.log("archiveId 1 = ",archiveId); + const archives = await self.getArchives(); + const validArchive = ((id: string) => id && archives.some((archive:any) => archive.id === id)); + + // if default archive id is not good, pick 'latest' + if (!validArchive(archiveId)) { + archiveId = "latest"; + // console.log("archiveId 2 = ",archiveId); + } + + // use archive id if it is in storage, + if (result.OPTIONS && result.OPTIONS.selected_archive && validArchive(result.OPTIONS.selected_archive.id)) { + archiveId = result.OPTIONS.selected_archive.id; + // console.log("archiveId 3 = ",archiveId); } else { - // console.log("Found GOOD url: ",tab.url); - // console.log("badURL = ",self.state.badURL); - if (self.state.badURL === true) { - self.setState({ badURL: false }); - } + archiveId = "latest"; } - let rulesets = await PanelMessaging.sendToBackground("DAP_Rulesets", { tabId: tab.id }) - - if (rulesets.error) { - self.setError(rulesets); - return; + + // console.log("archiveId 3a = ",archiveId); + + + let selectedArchive = archives.filter((archive:any) => archive.id === archiveId)[0]; + + let policyId: string = selectedArchive.policies[0].id; + let policyName: string = selectedArchive.policies[0].name; + const validPolicy = ((id: string) => id && selectedArchive.policies.some((policy:any) => policy.id === id)); + + if (!validPolicy(policyId)){ + policyId = "IBM_Accessibility"; } - - if (!self.state.listenerRegistered) { - PanelMessaging.addListener("TAB_UPDATED", async message => { - self.setState({ tabTitle: message.tabTitle }); // added so titles updated - if (message.tabId === self.state.tabId && message.status === "loading") { - if (message.tabUrl && message.tabUrl != self.state.tabURL) { - self.setState({ report: null, tabURL: message.tabUrl }); - } + + //use policy id if it is in storage + if (result.OPTIONS && result.OPTIONS.selected_ruleset && validPolicy(result.OPTIONS.selected_ruleset.id)) { + policyId = result.OPTIONS.selected_ruleset.id; + policyName = result.OPTIONS.selected_ruleset.name; + } + + let tabStopLines:boolean = true; + let tabStopOutlines:boolean = false; + let tabStopAlerts:boolean = true; + let tabStopFirstTime:boolean = true; + + // Get the Keyboard Mode OPTIONS + if (result.OPTIONS) { + // console.log("Get Keyboard Mode OPTIONS",result.OPTIONS); + tabStopLines = result.OPTIONS.tabStopLines; + tabStopOutlines = result.OPTIONS.tabStopOutlines; + tabStopAlerts = result.OPTIONS.tabStopAlerts; + tabStopFirstTime = result.OPTIONS.tabStopFirstTime; + } + + // to fix when undocked get tab id using chrome.devtools.inspectedWindow.tabId + // and get url using chrome.tabs.get via message "TAB_INFO" + let thisTabId = chrome.devtools.inspectedWindow.tabId; + let tab = await PanelMessaging.sendToBackground("TAB_INFO", { tabId: thisTabId }); + // console.log("tab.id = ", tab.id); + // console.log("tab.url = ", tab.url); + // console.log("tab.title = ", tab.title); + // console.log("tab.canScan = ",tab.canScan); + if (tab.id && tab.url && tab.id && tab.title) { + + if (!tab.canScan) { + // console.log("Found BAD url: ",tab.url); + // console.log("badURL = ",self.state.badURL); + if (self.state.badURL === false) { + self.setState({ badURL: true }); + } + self.setState({ tabURL: tab.url, tabId: tab.id, tabTitle: tab.title, tabCanScan: tab.canScan }); + return; + } else { + // console.log("Found GOOD url: ",tab.url); + // console.log("badURL = ",self.state.badURL); + if (self.state.badURL === true) { + self.setState({ badURL: false }); } + } + let rulesets = await PanelMessaging.sendToBackground("DAP_Rulesets", { tabId: tab.id }) + + if (rulesets.error) { + self.setError(rulesets); + return; + } + + if (!self.state.listenerRegistered) { + PanelMessaging.addListener("TAB_UPDATED", async message => { + self.setState({ tabTitle: message.tabTitle }); // added so titles updated + if (message.tabId === self.state.tabId && message.status === "loading") { + if (message.tabUrl && message.tabUrl != self.state.tabURL) { + self.setState({ report: null, tabURL: message.tabUrl }); + } + } + }); + + + PanelMessaging.addListener("DAP_SCAN_COMPLETE", self.onReport.bind(self)); + + PanelMessaging.sendToBackground("DAP_CACHED", { tabId: tab.id, tabURL: tab.url, origin: self.props.layout }) + + PanelMessaging.addListener("TABSTOP_XPATH_ONCLICK", async message => {self.xpathFromTabstops(message)} ); + } + if (self.props.layout === "sub") { + self.selectElementInElements(); + } + + + + // store OPTIONS + // console.log("Store OPTIONS"); + // console.log("archiveId 4 = ",archiveId); + // console.log("policyName = ",policyId); + self.setState({ rulesets: rulesets, listenerRegistered: true, tabURL: tab.url, + tabId: tab.id, tabTitle: tab.title, tabCanScan: tab.canScan, error: null, archives, + selectedArchive: archiveId, selectedPolicy: policyName, tabStopLines: tabStopLines, + tabStopOutlines: tabStopOutlines, tabStopAlerts: tabStopAlerts, tabStopFirstTime: tabStopFirstTime, + }); - - PanelMessaging.addListener("DAP_SCAN_COMPLETE", self.onReport.bind(self)); - - PanelMessaging.sendToBackground("DAP_CACHED", { tabId: tab.id, tabURL: tab.url, origin: self.props.layout }) - } - if (self.props.layout === "sub") { - self.selectElementInElements(); + console.log("Function: readOptionsData DONE"); } - self.setState({ rulesets: rulesets, listenerRegistered: true, tabURL: tab.url, - tabId: tab.id, tabTitle: tab.title, tabCanScan: tab.canScan, error: null, archives, selectedArchive: archiveId, - selectedPolicy: policyName }); - } - resolve(); - }); - }) - } - - setError = (data: any) => { - - if (data.error) { - this.setState({ error: data.error }); - } - }; - - errorHandler = (error: string | null) => { - - if (error && error.indexOf('Cannot access contents of url "file://') != -1) { - - let sub_s = error.substring(error.indexOf("\"") + 1); - let sub_e = sub_s.substring(0, sub_s.indexOf("\"")); - - return ( - -

Can not scan local file: {sub_e}

-
-

Follow the {" "} - User Guide - {" "}to allow scanning of local .html or .htm files in your browser

-
- ) - } - - return; - } - - async startScan() { - // console.log("startScan"); - let tabURL = this.state.tabURL; - let tabId = this.state.tabId; - // console.log("tabURL = ",tabURL); - // console.log("tabId = ",tabId); - - await this.readOptionsData(); - - let thisTabId = chrome.devtools.inspectedWindow.tabId; - let tab = await PanelMessaging.sendToBackground("TAB_INFO", { tabId: thisTabId }); - - if (!tab.canScan) { - // console.log("Found BAD url: ",tab.url); - // console.log("badURL = ",this.state.badURL); - if (this.state.badURL === false) { - this.setState({ badURL: true }); - } - this.setState({ tabURL: tab.url, tabId: tab.id, tabTitle: tab.title, tabCanScan: tab.canScan }); - return; - } else { - // console.log("Found GOOD url: ",tab.url); - // console.log("badURL = ",this.state.badURL); - if (this.state.badURL === true) { - this.setState({ badURL: false }); - } + resolve(); + }); + }) } - - // if (!this.state.tabCanScan) { - // if (this.state.badURL === false) { - // this.setState({ badURL: true }); - // } - // return; - // } else { - // if (this.state.badURL === true) { - // this.setState({ badURL: false }); - // } - // } - - // if (tabURL !== this.state.prevTabURL) { - // this.setState({firstScan: true}); - // } - - - this.state.prevTabURL = tabURL; - - - - if (tabId === -1) { - // componentDidMount is not done initializing yet - setTimeout(this.startScan.bind(this), 100); - } else { - this.setState({ numScanning: this.state.numScanning + 1, scanning: true }); - try { - await PanelMessaging.sendToBackground("DAP_SCAN", { tabId: tabId, tabURL: tabURL, origin: this.props.layout}) - } catch (err) { - console.error(err); + + setError = (data: any) => { + + if (data.error) { + this.setState({ error: data.error }); } + }; + + errorHandler = (error: string | null) => { + + if (error && error.indexOf('Cannot access contents of url "file://') != -1) { + + let sub_s = error.substring(error.indexOf("\"") + 1); + let sub_e = sub_s.substring(0, sub_s.indexOf("\"")); + + return ( + +

Can not scan local file: {sub_e}

+
+

Follow the {" "} + User Guide + {" "}to allow scanning of local .html or .htm files in your browser

+
+ ) + } + + return; } - } - - collapseAll() { - // if (this.state.report) { - // this.state.report.filterstamp = new Date().getTime(); - // this.setState({ filter: null, report: preprocessReport(this.state.report, null, false), selectedItem: undefined, selectedCheckpoint: undefined }); - // } - this.setState({firstScan: true}); - this.startScan(); - } - - async onReport(message: any): Promise { - try { - if( BrowserDetection.isChrome() && !message.tabURL.startsWith("file:")){ - let blob_url = message.blob_url; - let blob = await fetch(blob_url).then(r => r.blob()); - message = JSON.parse(await blob.text()); + + async startScan() { + // console.log("Function: startScan START"); + let tabURL = this.state.tabURL; + let tabId = this.state.tabId; + + await this.readOptionsData(); + + let thisTabId = chrome.devtools.inspectedWindow.tabId; + let tab = await PanelMessaging.sendToBackground("TAB_INFO", { tabId: thisTabId }); + + if (!tab.canScan) { + if (this.state.badURL === false) { + this.setState({ badURL: true }); + } + this.setState({ tabURL: tab.url, tabId: tab.id, tabTitle: tab.title, tabCanScan: tab.canScan }); + return; + } else { + if (this.state.badURL === true) { + this.setState({ badURL: false }); + } } - - let report = message.report; - let archives = await this.getArchives(); - - if (!report) return; - - let check_option = this.getCheckOption(message.archiveId, message.policyId, archives); - - // JCH add itemIdx to report (used to be in message.report) - report.results.map((result: any, index: any) => { - result["itemIdx"] = index; - }) - let tabId = message.tabId; - - - if (this.state.tabId === tabId) { - report.timestamp = new Date().getTime(); - report.filterstamp = new Date().getTime(); - report.option = check_option; - - this.setState({ - filter: null, - numScanning: Math.max(0, this.state.numScanning - 1), - report: preprocessReport(report, null, false), - selectedItem: undefined - }); + + this.state.prevTabURL = tabURL; + + if (tabId === -1) { + setTimeout(this.startScan.bind(this), 100); + } else { + this.setState({ numScanning: this.state.numScanning + 1, scanning: true }); + try { + await PanelMessaging.sendToBackground("DAP_SCAN", { tabId: tabId, tabURL: tabURL, origin: this.props.layout}); + } catch (err) { + console.error(err); + } } - this.setState({ scanning: false }); // scan done - // console.log("SCAN DONE"); + // console.log("Function: startScan DONE"); + } + + // JCH - not used + collapseAll() { + this.setState({firstScan: true}); + this.startScan(); + } + + async onReport(message: any): Promise { + // only accept messages that are for the tab / website that we are currently on + if (this.state.tabId !== message.tabId) return; - // Cases for storage - // Note: if scanStorage false not storing scans, if true storing scans - // console.log("storedScans.length = ", this.state.storedScans.length, " scanStorage = ", this.state.scanStorage); - - if (this.state.storedScans.length == 0 && this.state.scanStorage === true) { // NO stored scans and storing scans - // console.log("choice 1"); - this.storeScan(); // stores first scan - which is both a current and stored scan - } else if (this.state.storedScans.length == 0 && this.state.scanStorage === false) { // NO stored scans and NOT storing scans - // console.log("choice 2"); - this.storeScan(); // stores first scan which is a current scan - } else if (this.state.storedScans.length == 1 && this.state.scanStorage === true) { // one stored scan and storing scans - // console.log("choice 3"); - if (this.state.storedScans[0].actualStoredScan === false) { - this.clearStoredScans(false); // clears the current scan (not an actualStoredScan) + // console.log("Function: onReport START"); + try { + if( BrowserDetection.isChrome() && !message.tabURL.startsWith("file:")){ + let blob_url = message.blob_url; + let blob = await fetch(blob_url).then(r => r.blob()); + message = JSON.parse(await blob.text()); } - this.storeScan(); - } else if (this.state.storedScans.length == 1 && this.state.scanStorage === false) { // ONE stored scan and NOT storing scans - // console.log("choice 4"); - if (this.state.storedScans[this.state.storedScans.length-1].actualStoredScan === false) { - this.state.storedScans.pop(); // clears the current scan (that is not an actualStoredScan) + + let report = message.report; + let archives = await this.getArchives(); + + if (!report) return; + + let check_option = this.getCheckOption(message.archiveId, message.policyId, archives); + // console.log("check_option = ",check_option); + + // JCH add itemIdx to report (used to be in message.report) + report.results.map((result: any, index: any) => { + result["itemIdx"] = index; + }) + let tabId = message.tabId; + + + if (this.state.tabId === tabId) { + report.timestamp = new Date().getTime(); + report.filterstamp = new Date().getTime(); + report.option = check_option; + + this.setState({ + filter: null, + numScanning: Math.max(0, this.state.numScanning - 1), + report: preprocessReport(report, null, false), + selectedItem: undefined + }); } - this.storeScan(); // add current scan - } else if (this.state.storedScans.length > 1 && this.state.scanStorage === true) { // MULTIPLE stored scans and storing scans - // console.log("choice 5"); - this.storeScan(); // add new current and stored scan - } else if (this.state.storedScans.length > 1 && this.state.scanStorage === false) { // MULTIPLE stored scans and NOT storing scans - // console.log("choice 6"); - if (this.state.storedScans[this.state.storedScans.length-1].actualStoredScan === false) { - this.state.storedScans.pop(); // clears the current scan (that is not an actualStoredScan) + + /* JCH before finish scan collect and order tab stops + * Note: the collection is actually all issues that are tab stops + * + * The errors need to be reworked a bit + * Now we are capturing issues based on a partial set of keyboard rules + * So the key question is how do we want the rule issues mapped to the + * tab stops vs general keyboard issues located else where on the page. + */ + // console.log("JCH DO TABBABLE"); + let tabbable: IReportItem[] = []; + let tabbableErrors: IReportItem[] = []; + report.results.map((result: any) => { + if (result.ruleId === "detector_tabbable") { + // there will always be at least one tab + tabbable?.push(result); + } else if (result.value[1] !== "PASS" && + // 14 Keyboard Mode Rules + // 2.1.1 Keyboard + (result.ruleId === "HAAC_Application_Role_Text" || + result.ruleId === "HAAC_Audio_Video_Triggers" || + result.ruleId === "Rpt_Aria_InvalidTabindexForActivedescendant" || + result.ruleId === "Rpt_Aria_MissingFocusableChild" || + result.ruleId === "Rpt_Aria_MissingKeyboardHandler" || + result.ruleId === "RPT_Elem_EventMouseAndKey" || + // 2.4.3 Focus Order + result.ruleId === "IBMA_Focus_MultiTab" || + result.ruleId === "IBMA_Focus_Tabbable" || + result.ruleID === "element_tabbable_role_invalid" || + // 2.4.7 Focus Visible + result.ruleId === "RPT_Style_HinderFocus1" || + result.ruleId === "WCAG20_Script_FocusBlurs" || + result.ruleId === "element_tabbable_visible" || + // 3.2.1 On Focus + result.ruleId === "WCAG20_Select_NoChangeAction" || + // 4.1.2 Name, Role, Value + result.ruleId === "Rpt_Aria_ValidRole")) { + tabbableErrors?.push(result); + } + }); + if (tabbable !== null) { + tabbable.sort((a: any, b: any) => b.apiArgs[0].tabindex - a.apiArgs[0].tabindex); } - this.storeScan(); // add new current and stored scan - } - - if (this.props.layout === "sub") { - if (this.state.firstScan === true && message.origin === this.props.layout) { - this.selectElementInElements(); - this.setState({firstScan: false}); + + this.setState({ tabStopsResults: tabbable }); + this.setState({ tabStopsErrors: tabbableErrors }); + // console.log("tabbable = ", tabbable); + // console.log("tabbableErrors = ", tabbableErrors); + // JCH - clear visualization + // console.log("Function: onReport - &&&& DELETE TABS after collecting TAB data &&&&") + if (this.state.showHideTabStops === false ) { + // console.log("Function: onReport DELETE_DRAW_TABS_TO_CONTEXT_SCRIPTS"); + await PanelMessaging.sendToBackground("DELETE_DRAW_TABS_TO_CONTEXT_SCRIPTS", { tabId: this.state.tabId, tabURL: this.state.tabURL }); } - - chrome.devtools.inspectedWindow.eval(`((node) => { - let countNode = (node) => { - let count = 0; - let findName = node.nodeName; - while (node) { - if (node.nodeName === findName) { - ++count; + this.setState({ showHideTabStops: true }); + + // End of tab stops stored state + + + this.setState({ scanning: false }); // scan done + // console.log("SCAN DONE"); + + // Cases for storage + // Note: if scanStorage false not storing scans, if true storing scans + // console.log("storedScans.length = ", this.state.storedScans.length, " scanStorage = ", this.state.scanStorage); + + if (this.state.storedScans.length == 0 && this.state.scanStorage === true) { // NO stored scans and storing scans + // console.log("choice 1"); + this.storeScan(); // stores first scan - which is both a current and stored scan + } else if (this.state.storedScans.length == 0 && this.state.scanStorage === false) { // NO stored scans and NOT storing scans + // console.log("choice 2"); + this.storeScan(); // stores first scan which is a current scan + } else if (this.state.storedScans.length == 1 && this.state.scanStorage === true) { // one stored scan and storing scans + // console.log("choice 3"); + if (this.state.storedScans[0].actualStoredScan === false) { + this.clearStoredScans(false); // clears the current scan (not an actualStoredScan) + } + this.storeScan(); + } else if (this.state.storedScans.length == 1 && this.state.scanStorage === false) { // ONE stored scan and NOT storing scans + // console.log("choice 4"); + if (this.state.storedScans[this.state.storedScans.length-1].actualStoredScan === false) { + this.state.storedScans.pop(); // clears the current scan (that is not an actualStoredScan) + } + this.storeScan(); // add current scan + } else if (this.state.storedScans.length > 1 && this.state.scanStorage === true) { // MULTIPLE stored scans and storing scans + // console.log("choice 5"); + this.storeScan(); // add new current and stored scan + } else if (this.state.storedScans.length > 1 && this.state.scanStorage === false) { // MULTIPLE stored scans and NOT storing scans + // console.log("choice 6"); + if (this.state.storedScans[this.state.storedScans.length-1].actualStoredScan === false) { + this.state.storedScans.pop(); // clears the current scan (that is not an actualStoredScan) + } + this.storeScan(); // add new current and stored scan + } + + if (this.props.layout === "sub") { + if (this.state.firstScan === true && message.origin === this.props.layout) { + this.selectElementInElements(); + this.setState({firstScan: false}); + } + + chrome.devtools.inspectedWindow.eval(`((node) => { + let countNode = (node) => { + let count = 0; + let findName = node.nodeName; + while (node) { + if (node.nodeName === findName) { + ++count; + } + node = node.previousElementSibling; } - node = node.previousElementSibling; + return "/"+findName.toLowerCase()+"["+count+"]"; } - return "/"+findName.toLowerCase()+"["+count+"]"; - } - let retVal = ""; - while (node && node.nodeType === 1) { - if (node) { - retVal = countNode(node)+retVal; - if (node.parentElement) { - node = node.parentElement; - } else { - let parentElement = null; - try { - // Check if we're in an iframe - let parentWin = node.ownerDocument.defaultView.parent; - let iframes = parentWin.document.documentElement.querySelectorAll("iframe"); - for (const iframe of iframes) { - try { - if (iframe.contentDocument === node.ownerDocument) { - parentElement = iframe; - break; - } - } catch (e) {} - } - } catch (e) {} - node = parentElement; + let retVal = ""; + while (node && node.nodeType === 1) { + if (node) { + retVal = countNode(node)+retVal; + if (node.parentElement) { + node = node.parentElement; + } else { + let parentElement = null; + try { + // Check if we're in an iframe + let parentWin = node.ownerDocument.defaultView.parent; + let iframes = parentWin.document.documentElement.querySelectorAll("iframe"); + for (const iframe of iframes) { + try { + if (iframe.contentDocument === node.ownerDocument) { + parentElement = iframe; + break; + } + } catch (e) {} + } + } catch (e) {} + node = parentElement; + } } } - } - return retVal; - })($0)`, (result: string) => { - // This filter occurred because we selected an element in the elements tab - this.onFilter(result); - if (this.ignoreNext) { - this.ignoreNext = false; - } - }); + return retVal; + })($0)`, (result: string) => { + // This filter occurred because we selected an element in the elements tab + this.onFilter(result); + if (this.ignoreNext) { + this.ignoreNext = false; + } + }); + } + } catch (err) { + console.error(err); } - } catch (err) { - console.error(err); + // console.log("this.state.showHideTabStops = ",this.state.showHideTabStops); + // console.log("Function: onReport DONE"); + return true; } - return true; - } - - // START - New multi-scan report functions - - async storeScan() { - // console.log("storeScan"); - - // Data to store for Report - var xlsx_props = { - report: this.state.report, - rulesets: this.state.rulesets, - tabTitle: this.state.tabTitle, - tabURL: this.state.tabURL + + // START - New multi-scan report functions + + async storeScan() { + // console.log("storeScan"); + + // Data to store for Report + var xlsx_props = { + report: this.state.report, + rulesets: this.state.rulesets, + tabTitle: this.state.tabTitle, + tabURL: this.state.tabURL + } + + var report: any = this.state.report; + + var violation = report?.counts.total["Violation"]; + var needsReview = report?.counts.total["Needs review"]; + var recommendation = report?.counts.total["Recommendation"]; + var all = report?.counts.total["All"]; + + var element_no_failures = parseInt((((all - recommendation) / all) * 100).toFixed(0)); + var element_no_violations = parseInt((((all - violation) / all) * 100).toFixed(0)); + + // Keep track of number of stored scans (be sure to adjust when clear scans) + this.setState(prevState => { + return {storedScanCount: prevState.storedScanCount + 1} + }); + + // scan label of the current stored scan + // the current scan is always stored for the current scan report + this.setState({ currentStoredScan: "scan" + this.state.storedScanCount }); + + // get only data needed for multi-scan report + const scanData = MultiScanData.issues_sheet_rows(xlsx_props); + + // ***** NOT USING LOCAL STORAGE ***** + // console.log("scanData = ",scanData); + // local storage can only store strings so stringify + // let currentScanData = JSON.stringify(scanData); + // console.log("currentScanData = ",currentScanData); + + // console.log("storage space for currentScanData = ", currentScanData.length); + + // Note: here is where the scan is stored so check to see if can be stored + // if not turn off state scanStorage, present message that must clear scans + // also if they try to turn state scanStorage, again provide message that + // must clear scans before can store scans + // try { + // localStorage.setItem("scan" + this.state.storedScanCount +"Data", currentScanData); + // } catch (e) { + // if (e.name === "QUATA_EXCEEDED_ERR" // Chrome + // || e.name === "NS_ERROR_DOM_QUATA_REACHED") { //Firefox/Safari + // alert('Quota exceeded!'); //data wasn't successfully saved due to quota exceed so throw an error + // } + // } + + // this.setState(prevState => { + // return {storedScanData: prevState.storedScanData + currentScanData.length} + // }); + + // capture screenshot + let canvas = await PanelMessaging.sendToBackground("DAP_SCREENSHOT", { }); + + // let promise = new Promise((resolve, reject) => { + // //@ts-ignore + // chrome.tabs.captureVisibleTab(null, {}, function (image:string) { + // resolve(image); + // reject(new Error("Capture failed")); + // }); + // }); + + // let result:any = await promise; + // canvas = result; + + // Data to store for the Scan other than the issues not much data so saved in state memory + let currentScan = { + actualStoredScan: this.state.scanStorage ? true : false, + isSelected: false, + url: this.state.tabURL, + pageTitle: this.state.tabTitle, + dateTime: this.state.report?.timestamp, + scanLabel: "scan" + this.state.storedScanCount, // is this safe since setState above is async + userScanLabel: "scan" + this.state.storedScanCount, // this is the visible scan label which may be edited by user + ruleSet: report.option.deployment.name, + guidelines: report.option.guideline.name, + reportDate: new Date(report.timestamp), + violations: violation, + needsReviews: needsReview, + recommendations: recommendation, + elementsNoViolations: element_no_violations, + elementsNoFailures: element_no_failures, + storedScan: "scan" + this.state.storedScanCount, + screenShot: canvas, + storedScanData: scanData, + }; + + // Array of stored scans these scans are stored in state memory + this.setState(({ + storedScans: [...this.state.storedScans, currentScan] + })); } - - var report: any = this.state.report; - - var violation = report?.counts.total["Violation"]; - var needsReview = report?.counts.total["Needs review"]; - var recommendation = report?.counts.total["Recommendation"]; - var all = report?.counts.total["All"]; - - var element_no_failures = parseInt((((all - recommendation) / all) * 100).toFixed(0)); - var element_no_violations = parseInt((((all - violation) / all) * 100).toFixed(0)); - - // Keep track of number of stored scans (be sure to adjust when clear scans) - this.setState(prevState => { - return {storedScanCount: prevState.storedScanCount + 1} - }); - - // scan label of the current stored scan - // the current scan is always stored for the current scan report - this.setState({ currentStoredScan: "scan" + this.state.storedScanCount }); - - // get only data needed for multi-scan report - const scanData = MultiScanData.issues_sheet_rows(xlsx_props); - - // ***** NOT USING LOCAL STORAGE ***** - // console.log("scanData = ",scanData); - // local storage can only store strings so stringify - // let currentScanData = JSON.stringify(scanData); - // console.log("currentScanData = ",currentScanData); - - // console.log("storage space for currentScanData = ", currentScanData.length); - - // Note: here is where the scan is stored so check to see if can be stored - // if not turn off state scanStorage, present message that must clear scans - // also if they try to turn state scanStorage, again provide message that - // must clear scans before can store scans - // try { - // localStorage.setItem("scan" + this.state.storedScanCount +"Data", currentScanData); - // } catch (e) { - // if (e.name === "QUATA_EXCEEDED_ERR" // Chrome - // || e.name === "NS_ERROR_DOM_QUATA_REACHED") { //Firefox/Safari - // alert('Quota exceeded!'); //data wasn't successfully saved due to quota exceed so throw an error - // } - // } - - // this.setState(prevState => { - // return {storedScanData: prevState.storedScanData + currentScanData.length} - // }); - - // capture screenshot - let canvas = await PanelMessaging.sendToBackground("DAP_SCREENSHOT", { }); - - // let promise = new Promise((resolve, reject) => { - // //@ts-ignore - // chrome.tabs.captureVisibleTab(null, {}, function (image:string) { - // resolve(image); - // reject(new Error("Capture failed")); - // }); - // }); - - // let result:any = await promise; - // canvas = result; + + storeScanLabel(event:any,i:number) { + const value = event.target.value; - // Data to store for the Scan other than the issues not much data so saved in state memory - let currentScan = { - actualStoredScan: this.state.scanStorage ? true : false, - isSelected: false, - url: this.state.tabURL, - pageTitle: this.state.tabTitle, - dateTime: this.state.report?.timestamp, - scanLabel: "scan" + this.state.storedScanCount, // is this safe since setState above is async - userScanLabel: "scan" + this.state.storedScanCount, // this is the visible scan label which may be edited by user - ruleSet: report.option.deployment.name, - guidelines: report.option.guideline.name, - reportDate: new Date(report.timestamp), - violations: violation, - needsReviews: needsReview, - recommendations: recommendation, - elementsNoViolations: element_no_violations, - elementsNoFailures: element_no_failures, - storedScan: "scan" + this.state.storedScanCount, - screenShot: canvas, - storedScanData: scanData, - }; - - // Array of stored scans these scans are stored in state memory - this.setState(({ - storedScans: [...this.state.storedScans, currentScan] - })); - } - - storeScanLabel(event:any,i:number) { - const value = event.target.value; - - // let storedScansCopy = this.state.storedScans; - let storedScansCopy = JSON.parse(JSON.stringify(this.state.storedScans)); - storedScansCopy[i].userScanLabel = value; - this.setState({storedScans: storedScansCopy}); - } - - setStoredScanCount = () => { - this.setState({ storedScanCount: this.clearStoredScans.length }); - } - - clearStoredScans = (fromMenu: boolean) => { - this.setState({ storedScanCount: 0 }); // reset scan counter - this.setState({storedScans: []}); - if (fromMenu === true) { - this.setState({scanStorage: false}); + // let storedScansCopy = this.state.storedScans; + let storedScansCopy = JSON.parse(JSON.stringify(this.state.storedScans)); + storedScansCopy[i].userScanLabel = value; + this.setState({storedScans: storedScansCopy}); } - }; - - clearSelectedStoredScans() { - let storedScansCopy = this.state.storedScans; - for (let i=0; i { + this.setState({ storedScanCount: this.clearStoredScans.length }); } - this.setState({storedScans: storedScansCopy}); - } - - actualStoredScansCount = () => { - let count = 0; - for (let scan in this.state.storedScans) { - if (this.state.storedScans[scan].actualStoredScan) { - count++; + + clearStoredScans = (fromMenu: boolean) => { + this.setState({ storedScanCount: 0 }); // reset scan counter + this.setState({storedScans: []}); + if (fromMenu === true) { + this.setState({scanStorage: false}); + } + }; + + clearSelectedStoredScans() { + let storedScansCopy = this.state.storedScans; + for (let i=0; i { - // flip scanStorage state each time function runs - if (this.state.scanStorage === true) { - this.setState({ scanStorage: false }); - } else { - this.setState({ scanStorage: true }); + actualStoredScansCount = () => { + let count = 0; + for (let scan in this.state.storedScans) { + if (this.state.storedScans[scan].actualStoredScan) { + count++; + } + } + return count; } - } - - // END - New multi-scan report functions - - getArchives = async () => { - return await OptionMessaging.sendToBackground("OPTIONS", { - command: "getArchives", - }); - }; - - getCheckOption = (archiveId: string, policyId: string, archives: any) => { - var option = archives.find( (element: any) => element.id === archiveId); - - var policy = option.policies; - - var guideline = policy.find( (element: any) => element.id === policyId); - - var ret = {deployment: {id: archiveId, name: option.name}, guideline: {id: policyId, name: guideline.name}}; - - return ret; - } - - onFilter(filter: string) { - if (this.state.report) { - this.state.report.filterstamp = new Date().getTime(); - this.setState({ filter: filter, report: preprocessReport(this.state.report, filter, !this.ignoreNext) }); + startStopScanStoring = () => { + // flip scanStorage state each time function runs + if (this.state.scanStorage === true) { + this.setState({ scanStorage: false }); + } else { + this.setState({ scanStorage: true }); + } } - this.getCurrentSelectedElement(); - } - - reportManagerHandler = () => { - this.setState({ reportManager: true}); - } - - reportHandler = async (scanType:string) => { // parameter is scanType with value [current, all, selected] - if (scanType === "current") { - if (this.state.report && this.state.rulesets) { - var reportObj: any = { - tabURL: this.state.tabURL, - rulesets: this.state.rulesets, - report: { - passUniqueElements: this.state.report.passUniqueElements, - timestamp: this.state.report.timestamp, - nls: this.state.report.nls, - counts: { - "total": this.state.report.counts.total, - "filtered": this.state.report.counts.filtered - }, - results: [] - } - } - for (const result of this.state.report.results) { - reportObj.report.results.push({ - ruleId: result.ruleId, - path: result.path, - value: result.value, - message: result.message, - snippet: result.snippet, - help: result.help - }); - } + // END - New multi-scan report functions - var tabTitle: string = this.state.tabTitle; - var tabTitleSubString = tabTitle ? tabTitle.substring(0, 50) : ""; - var filename = "Accessibility_Report-" + tabTitleSubString + ".html"; - //replace illegal characters in file name - filename = filename.replace(/[/\\?%*:|"<>]/g, '-'); - - var fileContent = "data:text/html;charset=utf-8," + encodeURIComponent(genReport(reportObj)); - var a = document.createElement('a'); - a.href = fileContent; - a.download = filename; - var e = document.createEvent('MouseEvents'); - e.initMouseEvent('click', true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); - a.dispatchEvent(e); + getArchives = async () => { + return await OptionMessaging.sendToBackground("OPTIONS", { + command: "getArchives", + }); + }; + + getCheckOption = (archiveId: string, policyId: string, archives: any) => { + var option = archives.find( (element: any) => element.id === archiveId); + var policy = option.policies; + var guideline = policy.find( (element: any) => element.id === policyId); + var ret = {deployment: {id: archiveId, name: option.name}, guideline: {id: policyId, name: guideline.name}}; + + return ret; + } + + onFilter(filter: string) { + if (this.state.report) { + this.state.report.filterstamp = new Date().getTime(); + this.setState({ filter: filter, report: preprocessReport(this.state.report, filter, !this.ignoreNext) }); } - this.xlsxReportHandler(scanType); - } else { - this.xlsxReportHandler(scanType); + this.getCurrentSelectedElement(); } - } - - xlsxReportHandler = (scanType:string) => { - // console.log("xlsxReportHandler"); - //@ts-ignore - MultiScanReport.multiScanXlsxDownload(this.state.storedScans, scanType, this.state.storedScanCount, this.state.archives); - } - - selectItem(item?: IReportItem, checkpoint?: ICheckpoint) { - if (this.state.report) { - if (!item) { - for (const resultItem of this.state.report.results) { - resultItem.selected = false; + + reportManagerHandler = () => { + this.setState({ reportManager: true}); + } + + + reportHandler = async (scanType:string) => { // parameter is scanType with value [current, all, selected] + if (scanType === "current") { + if (this.state.report && this.state.rulesets) { + var reportObj: any = { + tabURL: this.state.tabURL, + rulesets: this.state.rulesets, + report: { + passUniqueElements: this.state.report.passUniqueElements, + timestamp: this.state.report.timestamp, + nls: this.state.report.nls, + counts: { + "total": this.state.report.counts.total, + "filtered": this.state.report.counts.filtered + }, + results: [] + } + } + for (const result of this.state.report.results) { + reportObj.report.results.push({ + ruleId: result.ruleId, + path: result.path, + value: result.value, + message: result.message, + snippet: result.snippet, + help: result.help + }); + } + + var tabTitle: string = this.state.tabTitle; + var tabTitleSubString = tabTitle ? tabTitle.substring(0, 50) : ""; + var filename = "Accessibility_Report-" + tabTitleSubString + ".html"; + //replace illegal characters in file name + filename = filename.replace(/[/\\?%*:|"<>]/g, '-'); + + var fileContent = "data:text/html;charset=utf-8," + encodeURIComponent(genReport(reportObj)); + var a = document.createElement('a'); + a.href = fileContent; + a.download = filename; + var e = document.createEvent('MouseEvents'); + e.initMouseEvent('click', true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); + a.dispatchEvent(e); } - this.setState({ selectedItem: undefined, report: this.state.report }); + this.xlsxReportHandler(scanType); } else { - if (this.props.layout === "main") { - if (this.state.rulesets && !checkpoint) { - for (const rs of this.state.rulesets) { - if (rs.id === "IBM_Accessibility") { - for (const cp of rs.checkpoints) { - for (const rule of cp.rules) { - if (rule.id === item.ruleId) { - checkpoint = cp; + this.xlsxReportHandler(scanType); + } + } + + xlsxReportHandler = (scanType:string) => { + // console.log("xlsxReportHandler"); + //@ts-ignore + MultiScanReport.multiScanXlsxDownload(this.state.storedScans, scanType, this.state.storedScanCount, this.state.archives); + } + + selectItem(item?: IReportItem, checkpoint?: ICheckpoint) { + if (this.state.report) { + if (!item) { + for (const resultItem of this.state.report.results) { + resultItem.selected = false; + } + this.setState({ selectedItem: undefined, report: this.state.report }); + } else { + if (this.props.layout === "main") { + if (this.state.rulesets && !checkpoint) { + for (const rs of this.state.rulesets) { + if (rs.id === "IBM_Accessibility") { + for (const cp of rs.checkpoints) { + for (const rule of cp.rules) { + if (rule.id === item.ruleId) { + checkpoint = cp; + } } } } } } - } - - for (const resultItem of this.state.report.results) { - resultItem.selected = resultItem.itemIdx === item.itemIdx; - } - this.setState({ selectedItem: item, report: this.state.report, selectedCheckpoint: checkpoint }); - } else if (this.props.layout === "sub") { - if (this.state.report) { + for (const resultItem of this.state.report.results) { - resultItem.selected = resultItem.path.dom === item.path.dom; + resultItem.selected = resultItem.itemIdx === item.itemIdx; } - this.setState({ report: this.state.report }); - } - - var script = - `function lookup(doc, xpath) { - if (doc.nodeType === 11) { - let selector = ":scope" + xpath.replace(/\\//g, " > ").replace(/\\[(\\d+)\\]/g, ":nth-child($1)"); - let element = doc.querySelector(selector); - return element; - } else { - let nodes = doc.evaluate(xpath, doc, null, XPathResult.ANY_TYPE, null); - let element = nodes.iterateNext(); - if (element) { - return element; - } else { - return null; + this.setState({ selectedItem: item, report: this.state.report, selectedCheckpoint: checkpoint }); + } else if (this.props.layout === "sub") { + if (this.state.report) { + for (const resultItem of this.state.report.results) { + resultItem.selected = resultItem.path.dom === item.path.dom; } + this.setState({ report: this.state.report }); } - } - function selectPath(srcPath) { - let doc = document; - let element = null; - while (srcPath && (srcPath.includes("iframe") || srcPath.includes("#document-fragment"))) { - if (srcPath.includes("iframe")) { - let parts = srcPath.match(/(.*?iframe\\[\\d+\\])(.*)/); - let iframe = lookup(doc, parts[1]); - element = iframe || element; - if (iframe && iframe.contentDocument) { - doc = iframe.contentDocument; - srcPath = parts[2]; - } else { - srcPath = null; - } - } else if (srcPath.includes("#document-fragment")) { - let parts = srcPath.match(/(.*?)\\/#document-fragment\\[\\d+\\](.*)/); - let fragment = lookup(doc, parts[1]); - element = fragment || element; - if (fragment && fragment.shadowRoot) { - doc = fragment.shadowRoot; - srcPath = parts[2]; + + var script = + `function lookup(doc, xpath) { + if (doc.nodeType === 11) { + let selector = ":host" + xpath.replace(/\\//g, " > ").replace(/\\[(\\d+)\\]/g, ":nth-of-type($1)"); + let element = doc.querySelector(selector); + return element; + } else { + let nodes = doc.evaluate(xpath, doc, null, XPathResult.ANY_TYPE, null); + let element = nodes.iterateNext(); + if (element) { + return element; } else { - srcPath = null; + return null; } } } - if (srcPath) { - element = lookup(doc, srcPath) || element; - } - if (element) { - inspect(element); - var elementRect = element.getBoundingClientRect(); - var absoluteElementTop = elementRect.top + window.pageYOffset; - var middle = absoluteElementTop - 100; - element.ownerDocument.defaultView.scrollTo({ - top: middle, - behavior: 'smooth' - }); - return true; - } - return; - } - selectPath("${item.path.dom}"); - ` - this.ignoreNext = true; - chrome.devtools.inspectedWindow.eval(script, function (result, isException) { - if (isException) { - console.error(isException); - } - if (!result) { - console.log('Could not select element, it may have moved'); + function selectPath(srcPath) { + let doc = document; + let element = null; + while (srcPath && (srcPath.includes("iframe") || srcPath.includes("#document-fragment"))) { + if (srcPath.includes("iframe")) { + let parts = srcPath.match(/(.*?iframe\\[\\d+\\])(.*)/); + let iframe = lookup(doc, parts[1]); + element = iframe || element; + if (iframe && iframe.contentDocument) { + doc = iframe.contentDocument; + srcPath = parts[2]; + } else { + srcPath = null; + } + } else if (srcPath.includes("#document-fragment")) { + let parts = srcPath.match(/(.*?)\\/#document-fragment\\[\\d+\\](.*)/); + let fragment = lookup(doc, parts[1]); + element = fragment || element; + if (fragment && fragment.shadowRoot) { + doc = fragment.shadowRoot; + srcPath = parts[2]; + } else { + srcPath = null; + } + } + } + if (srcPath) { + element = lookup(doc, srcPath) || element; + } + if (element) { + inspect(element); + var elementRect = element.getBoundingClientRect(); + var absoluteElementTop = elementRect.top + window.pageYOffset; + var middle = absoluteElementTop - 100; + element.ownerDocument.defaultView.scrollTo({ + top: middle, + behavior: 'smooth' + }); + return true; + } + return; } - // do focus after inspected Window script - setTimeout(() => { - var button = document.getElementById('backToListView'); - if (button) { - button.focus(); + selectPath("${item.path.dom}"); + ` + this.ignoreNext = true; + chrome.devtools.inspectedWindow.eval(script, function (result, isException) { + if (isException) { + console.error(isException); } - }, 0); - }); - // This filter occurred because we selected an issue in the Accessibility Checker tab - this.onFilter(item.path.dom) + if (!result) { + console.log('Could not select element, it may have moved'); + } + // do focus after inspected Window script + setTimeout(() => { + var button = document.getElementById('backToListView'); + if (button) { + button.focus(); + } + }, 0); + }); + // This filter occurred because we selected an issue in the Accessibility Checker tab + this.onFilter(item.path.dom) + } } } } - } - - getXPathForElement(element: any) { - const idx: any = (sib: any, name: any) => sib ? idx(sib.previousElementSibling, name || sib.localName) + (sib.localName == name) : 1; - const segs: any = (elm: any) => (!elm || elm.nodeType !== 1) ? [''] : [...segs(elm.parentNode), `${elm.localName.toLowerCase()}[${idx(elm)}]`]; - return segs(element).join('/'); - } - - getCurrentSelectedElement() { - // console.log("getCurrentSelectedElement"); - let mythis = this; - - // Provide text name for focused view element for switch - chrome.devtools.inspectedWindow.eval("$0.tagName", - (result:string, isException) => { - if (isException) { - console.error(isException); + + getXPathForElement(element: any) { + const idx: any = (sib: any, name: any) => sib ? idx(sib.previousElementSibling, name || sib.localName) + (sib.localName == name) : 1; + const segs: any = (elm: any) => (!elm || elm.nodeType !== 1) ? [''] : [...segs(elm.parentNode), `${elm.localName.toLowerCase()}[${idx(elm)}]`]; + return segs(element).join('/'); + } + + getCurrentSelectedElement() { + // console.log("getCurrentSelectedElement"); + let mythis = this; + + // Provide text name for focused view element for switch + chrome.devtools.inspectedWindow.eval("$0.tagName", + (result:string, isException) => { + if (isException) { + console.error(isException); + } + if (!result) { + console.log('Could not get selected element'); + } + // get current element after inspected Window script + setTimeout(() => { + mythis.setState({ focusedViewText: "<"+result.toLowerCase()+">"}); + }, 0); } - if (!result) { - console.log('Could not get selected element'); + ); + } + + selectElementInElements () { + chrome.devtools.inspectedWindow.eval("inspect(document.firstElementChild)", + (result:string, isException) => { + if (isException) { + console.error(isException); + } + if (!result) { + console.log('Could not select element'); + } + // select element after inspected Window script + setTimeout(() => { + // console.log("selected element"); + }, 0); } - // get current element after inspected Window script - setTimeout(() => { - // console.log("result = ",result); - mythis.setState({ focusedViewText: "<"+result.toLowerCase()+">"}); - // console.log("this.state.focusedViewText", this.state.focusedViewText); - }, 0); + ); + } + + getItem(item: IReportItem) { + this.setState({ learnMore: true, learnItem: item }); + } + + getSelectedItem(item: IReportItem) { + this.setState({ selectedIssue: item }); + } + + learnHelp() { + this.setState({ learnMore: false }); + } + + reportManagerHelp() { + // clear all selections ? + this.setState({ reportManager: false }); + } + + setTabStopsShowHide() { + if (this.state.showHideTabStops) { + this.setState({ showHideTabStops: false }); + } else { + this.setState({ showHideTabStops: true }); } - ); - } - - selectElementInElements () { - chrome.devtools.inspectedWindow.eval("inspect(document.firstElementChild)", - (result:string, isException) => { - if (isException) { - console.error(isException); - } - if (!result) { - console.log('Could not select element'); - } - // select element after inspected Window script - setTimeout(() => { - // console.log("selected element"); - }, 0); + setTimeout(function () { + // console.log("tabStopsPanel2 = ", mythis.state.tabStopsPanel); + }, 10); + } + + tabStopsSetFirstTime() { + console.log("Function: tabStopsSetFirstTime"); + + if (this.state.tabStopFirstTime) { + // console.log("setState tabStopFirstTime to false"); + this.setState({ tabStopFirstTime: false }); + } + + this.setState({ + currentArchive: this.state.selectedArchive, + currentRuleset: this.state.rulesets, + tabStopLines: this.state.tabStopLines, + tabStopOutlines: this.state.tabStopOutlines, + tabStopAlerts: this.state.tabStopAlerts, + tabStopFirstTime: false, + }, () => { + this.save_options_to_storage(this.state); }); - } - - getItem(item: IReportItem) { - this.setState({ learnMore: true, learnItem: item }); - } - - getSelectedItem(item: IReportItem) { - // console.log("Function: getSelectedItem item = ", item); - this.setState({ selectedIssue: item }); - } - - learnHelp() { - this.setState({ learnMore: false }); - } - - reportManagerHelp() { - // clear all selections ? - this.setState({ reportManager: false }); - } - - showIssueTypeCheckBoxCallback (checked:boolean[]) { - if (checked[1] == true && checked[2] == true && checked[3] == true) { - // console.log("All true"); - this.setState({ showIssueTypeFilter: [true, checked[1], checked[2], checked[3]] }); - } else if (checked[1] == false && checked[2] == false && checked[3] == false) { - // console.log("All false"); - this.setState({ showIssueTypeFilter: [false, checked[1], checked[2], checked[3]] }); - } else { - // console.log("Mixed"); - this.setState({ showIssueTypeFilter: [false, checked[1], checked[2], checked[3]] }); } - // console.log("In showIssueTypeCheckBoxCallback",this.state.showIssueTypeFilter); - } - - focusedViewCallback (focus:boolean) { - this.setState({ focusedViewFilter: focus}); - } - - render() { - let error = this.state.error; - - if (error) { - return this.errorHandler(error); + save_options_to_storage = async (state: any) => { + // console.log("Function: save_options_to_storage"); + var options = { OPTIONS: state }; + await chrome.storage.local.set(options, function () { + // console.log("options is set to ", options); + }); + }; + + tabStopsHandler() { + // console.log("tabStopsHandler"); + // let mythis = this; + + PanelMessaging.sendToBackground("DELETE_DRAW_TABS_TO_CONTEXT_SCRIPTS", { tabId: this.state.tabId, tabURL: this.state.tabURL }); + this.setState({ tabStopsPanel: false }); + setTimeout(function () { + // console.log("tabStopsPanel1 = ", mythis.state.tabStopsPanel); + }, 1); + this.selectElementInElements(); } - - else if (this.props.layout === "main") { - return -
-
- {!this.state.report && } - {this.state.report && !this.state.selectedItem && } - {this.state.report && this.state.selectedItem && } + + tabStopsHighlight(index: number, result: any) { + // console.log("Highlight tab stop with index = ", index); + PanelMessaging.sendToBackground("HIGHLIGHT_TABSTOP_TO_BACKGROUND", { tabId: this.state.tabId, tabURL: this.state.tabURL, tabStopId: index }); + this.selectItem(result, undefined); + } + + showIssueTypeCheckBoxCallback (checked:boolean[]) { + if (checked[1] == true && checked[2] == true && checked[3] == true) { + // console.log("All true"); + this.setState({ showIssueTypeFilter: [true, checked[1], checked[2], checked[3]] }); + } else if (checked[1] == false && checked[2] == false && checked[3] == false) { + // console.log("All false"); + this.setState({ showIssueTypeFilter: [false, checked[1], checked[2], checked[3]] }); + } else { + // console.log("Mixed"); + this.setState({ showIssueTypeFilter: [false, checked[1], checked[2], checked[3]] }); + } + // console.log("In showIssueTypeCheckBoxCallback",this.state.showIssueTypeFilter); + } + + focusedViewCallback (focus:boolean) { + this.setState({ focusedViewFilter: focus}); + } + + + render() { + // console.log("render --------------"); + // console.log("this.state.this.state.selectedArchive = ",this.state.selectedArchive); + // console.log("this.state.this.state.selectedPolicy = ",this.state.selectedPolicy); + // console.log("this.state.tabStopLines = ",this.state.tabStopLines); + // console.log("this.state.tabStopOutlines = ",this.state.tabStopOutlines); + // console.log("this.state.tabStopAlerts = ",this.state.tabStopAlerts); + // console.log("this.state.tabStopFirstTime = ",this.state.tabStopFirstTime); + + let error = this.state.error; + + if (error) { + return this.errorHandler(error); + } + + else if (this.props.layout === "main") { + return +
+
+ {!this.state.report && } + {this.state.report && !this.state.selectedItem && } + {this.state.report && this.state.selectedItem && } +
+ {this.leftPanelRef.current?.scrollTo(0, 0)} +
+
+
+
+ {this.state.numScanning > 0 ? : <>} + {this.state.report && } +
+
+
- {this.leftPanelRef.current?.scrollTo(0, 0)} -
+ + } else if (this.props.layout === "sub") { + + return + {/* ok now need three way display for Report Manager so need reportManager state */} +
+ + + {/* Report List and Details */} + + +
+
+ +
+
+
+
+ {this.state.learnMore && this.state.report && this.state.learnItem && } +
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+ {/* Note the -72px is there to make sure that the help content starts under the header */} + {this.subPanelRef.current?.scrollTo(0, -72)} +
+
+
-
+
{this.state.numScanning > 0 ? : <>} {this.state.report && }
-
-
-
- } else if (this.props.layout === "sub") { - - return - {/* ok now need three way display for Report Manager so need reportManager state */} -
- - - {/* Report List and Details */} - - -
-
- -
-
-
-
- {this.state.learnMore && this.state.report && this.state.learnItem && } -
-
-
-
- {/* Note the -72px is there to make sure that the help content starts under the header */} - {this.subPanelRef.current?.scrollTo(0, -72)} -
-
-
-
-
- {this.state.numScanning > 0 ? : <>} - {this.state.report && } -
-
-
- } else { - return ERROR + + } else { + return ERROR + } } - } -} + } \ No newline at end of file diff --git a/accessibility-checker-extension/src/ts/devtools/Header.tsx b/accessibility-checker-extension/src/ts/devtools/Header.tsx index 6eae6c0b4..cef1fb332 100644 --- a/accessibility-checker-extension/src/ts/devtools/Header.tsx +++ b/accessibility-checker-extension/src/ts/devtools/Header.tsx @@ -16,22 +16,28 @@ import React from "react"; import ReactTooltip from "react-tooltip"; +import { IReportItem } from "./Report"; import { - Column, Grid, Button, Checkbox, ContentSwitcher, Switch, OverflowMenu, OverflowMenuItem, Modal + Column, Grid, Button, Checkbox, ContentSwitcher, Switch, OverflowMenu, OverflowMenuItem, Modal, ToastNotification, } from '@carbon/react'; -import { Information, ReportData, Renew, ChevronDown } from '@carbon/react/icons/lib/index'; +import { Information, ReportData, Renew, ChevronDown, View, ViewOff, Help, Settings } from '@carbon/react/icons/lib/index'; import { IArchiveDefinition } from '../background/helper/engineCache'; import OptionUtil from '../util/optionUtil'; +import PanelMessaging from '../util/panelMessaging'; const BeeLogo = "/assets/BE_for_Accessibility_darker.svg"; import Violation16 from "../../assets/Violation16.svg"; import NeedsReview16 from "../../assets/NeedsReview16.svg"; import Recommendation16 from "../../assets/Recommendation16.svg"; +// import KCM_On from "../../assets/KCM_button_on.svg"; +// import KCM_Off from "../../assets/KCM_button_on.svg"; +// import KCM_disabled from "../../assets/KCM_button_disabled.svg"; interface IHeaderState { deleteModal: boolean, modalRulsetInfo: boolean, + openKeyboardMode: boolean, } interface IHeaderProps { @@ -80,21 +86,38 @@ interface IHeaderProps { focusedViewFilter: boolean, focusedViewText: string, getCurrentSelectedElement: () => void, - readOptionsData: () => void + readOptionsData: () => void, + tabURL: string, + tabId: number, + setTabStopsShowHide: () => void, + tabStopsResults: IReportItem[], + tabStopsErrors: IReportItem[], + showHideTabStops: boolean, + // Keyboard Mode + tabStopLines:boolean, + tabStopOutlines: boolean, + tabStopAlerts: boolean, + tabStopFirstTime: boolean, + tabStopsSetFirstTime: () => void, } export default class Header extends React.Component { infoButton1Ref: React.RefObject; infoButton2Ref: React.RefObject; + helpButtonRef: React.RefObject; + settingsButtonRef: React.RefObject; constructor(props:any) { super(props); this.infoButton1Ref = React.createRef(); this.infoButton2Ref = React.createRef(); + this.helpButtonRef = React.createRef(); + this.settingsButtonRef = React.createRef(); } state: IHeaderState = { deleteModal: false, modalRulsetInfo: false, + openKeyboardMode: false, }; focusInfoButton1() { @@ -175,7 +198,11 @@ export default class Header extends React.Component }); } - + keyboardModalHandler() { + this.setState({ + openKeyboardMode: true, + }); + } render() { let counts = this.props.counts; @@ -209,23 +236,50 @@ export default class Header extends React.Component let focusText = this.props.focusedViewText; let headerContent = (
- {this.props.layout === "sub" ? - - + {this.props.layout === "sub" ? + // checker view + +

IBM Equal Access Accessibility Checker

+ + + +
- : + : + // accessment view +

IBM Equal Access Accessibility Checker

IBM Accessibility - {/*
- Status: - {this.props.scanStorage === true ? "storing, " : ""} - {this.props.actualStoredScansCount().toString() === "0" ? "no scans stored" : (this.props.actualStoredScansCount().toString() === "1" ? this.props.actualStoredScansCount().toString() + " scan stored" : this.props.actualStoredScansCount().toString() + " scans stored")} -
*/}
} @@ -234,7 +288,7 @@ export default class Header extends React.Component - + This action is irreversible.

- - { - this.setState({ modalRulsetInfo: false }); - this.focusInfoButton1(); - }).bind(this)} - > -

- Get started with the   - - User guide - - . -

-

-

- Currently active rule set: {'"'+OptionUtil.getRuleSetDate(this.props.selectedArchive, this.props.archives)+'"'} - {
}
- Most recent rule set: {'"'+OptionUtil.getRuleSetDate('latest', this.props.archives)+'"'} -

- Currently active guidelines: {'"'+this.props.selectedPolicy+'"'} -

-

-

- - Change rule set - -

-
- + {/* */} - + onKeyDown={this.onKeyDown.bind(this)} /> - Focus view - + + {this.state.openKeyboardMode && this.props.tabStopFirstTime ? +
+ { + this.setState({ openKeyboardMode: false }); + console.log("tabStopFirstTime = ",this.props.tabStopFirstTime); + this.props.tabStopsSetFirstTime(); + }).bind(this)} + onKeyDown={((evt: any) => { + if (evt.key === "Escape") { + this.setState({ openKeyboardMode: false }); + evt.preventDefault(); + evt.stopPropagation(); + return false; + } + return true; + }).bind(this)} + > +
+

+

+ Shows current tab stops. Click any marker or tab through the page for element information. +



+ You can customize this feature in options and read more in the user guide. +

+

+ + + Options + + + User guide + + +

+
+
+
+ : ""}
- +
Status: {this.props.scanStorage === true ? "storing, " : ""} {this.props.actualStoredScansCount().toString() === "0" ? "no scans stored" : (this.props.actualStoredScansCount().toString() === "1" ? this.props.actualStoredScansCount().toString() + " scan stored" : this.props.actualStoredScansCount().toString() + " scans stored")}
+ +
// Content for the Assessment Tab @@ -448,9 +524,11 @@ export default class Header extends React.Component



- You are using a rule set from {OptionUtil.getRuleSetDate(this.props.selectedArchive, this.props.archives)}. + Currently active rule set: {'"'+OptionUtil.getRuleSetDate(this.props.selectedArchive, this.props.archives)+'"'} {
}
- The latest rule set is {OptionUtil.getRuleSetDate('latest', this.props.archives)} + Most recent rule set: {'"'+OptionUtil.getRuleSetDate('latest', this.props.archives)+'"'} +

+ Currently active guidelines: {'"'+this.props.selectedPolicy+'"'}



@@ -562,7 +640,7 @@ export default class Header extends React.Component {this.props.badURL ? -
IBM Equal Access Accessibility Checker cannot run on this URL. +
IBM Equal Access Accesibility Check cannot run on this URL. Please go to a different page.
: "" diff --git a/accessibility-checker-extension/src/ts/devtools/MultiScanData.tsx b/accessibility-checker-extension/src/ts/devtools/MultiScanData.tsx index ec42ae293..0bb3eb1d7 100644 --- a/accessibility-checker-extension/src/ts/devtools/MultiScanData.tsx +++ b/accessibility-checker-extension/src/ts/devtools/MultiScanData.tsx @@ -53,6 +53,12 @@ export default class MultiScanData { "FAIL": "Recommendation", "PASS": "Pass", "MANUAL": "Recommendation" + }, + "INFORMATION": { + "POTENTIAL": "Needs review", + "FAIL": "Violation", + "PASS": "Pass", + "MANUAL": "Recommendation" } }; diff --git a/accessibility-checker-extension/src/ts/devtools/Report.tsx b/accessibility-checker-extension/src/ts/devtools/Report.tsx index 1517634ac..fb5c04272 100644 --- a/accessibility-checker-extension/src/ts/devtools/Report.tsx +++ b/accessibility-checker-extension/src/ts/devtools/Report.tsx @@ -73,6 +73,7 @@ export interface IRuleset { } interface IReportState { + renderFromTabCircleClick: boolean } interface IReportProps { @@ -102,6 +103,12 @@ export const valueMap: { [key: string]: { [key2: string]: string } } = { "FAIL": "Recommendation", "PASS": "Pass", "MANUAL": "Recommendation" + }, + "INFORMATION": { + "POTENTIAL": "Needs review", + "FAIL": "Violation", + "PASS": "Pass", + "MANUAL": "Recommendation" } }; @@ -155,7 +162,9 @@ export function preprocessReport(report: IReport, filter: string | null, scroll: } export default class Report extends React.Component { - state: IReportState = {}; + state: IReportState = { + renderFromTabCircleClick: false + }; render() { const tabLabels : { [key: string] : string }= { diff --git a/accessibility-checker-extension/src/ts/devtools/ReportRow.tsx b/accessibility-checker-extension/src/ts/devtools/ReportRow.tsx index 4a4f34cde..143bbddd2 100644 --- a/accessibility-checker-extension/src/ts/devtools/ReportRow.tsx +++ b/accessibility-checker-extension/src/ts/devtools/ReportRow.tsx @@ -123,14 +123,17 @@ export default class ReportRow extends React.Component {!this.props.focusedViewFilter || (focusedView && (item.selected || item.selectedChild)) ? (this.props.dataFromParent[0] || this.props.dataFromParent[1] && val === "Violation" || this.props.dataFromParent[2] && val === "Needs review" || this.props.dataFromParent[3] && val === "Recommendation") ? - ( + (
this.itemSelectedClickHandler(event, item)} onKeyDown={this.onKeyDown.bind(this)}>
@@ -265,7 +268,8 @@ export default class ReportRow extends React.Component - ) +
) + : "" : "" } diff --git a/accessibility-checker-extension/src/ts/devtools/ReportTabStops.tsx b/accessibility-checker-extension/src/ts/devtools/ReportTabStops.tsx new file mode 100644 index 000000000..edc4eec5d --- /dev/null +++ b/accessibility-checker-extension/src/ts/devtools/ReportTabStops.tsx @@ -0,0 +1,192 @@ +/****************************************************************************** + Copyright:: 2020- IBM, Inc + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + *****************************************************************************/ + +import React from "react"; + +import { Grid } from "@carbon/react"; +import { IReport, + IReportItem, + valueMap +} from "./Report"; + +import Violation16 from "../../assets/Violation16.svg"; +import NeedsReview16 from "../../assets/NeedsReview16.svg"; +import Recommendation16 from "../../assets/Recommendation16.svg"; + +import "../styles/subpanel.scss"; +import "../styles/reportTabStops.scss"; + +interface IReportTabStopsState { +} + +interface IReportTabStopsProps { + report: IReport, + tabStopsHighlight: (index: number, result: any) => void, + tabStopsResults: IReportItem[] // Note: the collection is actually all issues that are tab stops +} + +interface IGroup { + title: string, // aria path for the element role row + counts: { [key: string]: number }, // number of Violations, Needs Review, Recommendations + // associated with the element role + fvCounts: { [key: string]: number }, + items: IReportItem[] // issue rows associated with the element role +}; + +export default class ReportTabStops extends React.Component { + state: IReportTabStopsState = {}; + + componentDidMount() { + // console.log("JCH - ReportTabStops"); + } + + printTabStops() { + // console.log("printTabStops"); + + // console.log("JCH make groups for tabs"); + let itemIdx = 0; + let groups : IGroup[] = [] + let groupMap : { + [key: string]: IGroup + } | null = {}; + if (this.props.report !== null) { + this.props.tabStopsResults.map((result:any) => { // for each tab stop + + for (const item of this.props.report.results) { // for each issue + if (item.value[1] === "PASS") { + continue; + } + // console.log("tabStop ",index, " aria path: ",result.path.aria, item.path.aria); + item.itemIdx = itemIdx++; + // group by element role === aria path + // if (item.path.aria === result.path.aria) { + // console.log("Found match"); + // } else { + // console.log("Match not found"); + // } + + if (item.path.aria === result.path.aria) { + let thisGroup = (groupMap![item.path.aria]); + if (!thisGroup) { + thisGroup = { + title: result.path.aria, + counts: {}, + fvCounts: {}, + items: [] + } + groupMap![item.path.aria] = thisGroup; + groups.push(thisGroup); + } + thisGroup.items.push(item); + let val = valueMap[item.value[0]][item.value[1]] || item.value[0] + "_" + item.value[1]; + thisGroup.counts[val] = (thisGroup.counts[val] || 0) + 1; + if (item.selected || item.selectedChild) { + thisGroup.fvCounts[val] = (thisGroup.fvCounts[val] || 0) + 1; + } + } + } + // console.log("groups before sort = ",groups); + + // JCH - now for tab stops we need to sort issues + // according to type in order Violations, Needs Review, Recommendations + // within each group (i.e., a Tab Stop element) need to sort the items according to their value + const valPriority = ["Violation", "Needs review", "Recommendation"]; + let groupVals = []; + groups.map(group => { + groupVals.length = 0; + group.items.sort( function(a,b) { + let aVal = valueMap[a.value[0]][a.value[1]] || a.value[0] + "_" + a.value[1]; + let bVal = valueMap[b.value[0]][b.value[1]] || b.value[0] + "_" + b.value[1]; + let aIndex = valPriority.indexOf(aVal); + let bIndex = valPriority.indexOf(bVal); + return aIndex - bIndex; + }) + }) + + }) + // console.log("groups = ",groups); + + } + + + let temp:any = []; + let vCount = 1; + let nrCount = 1; + let rCount = 1; + // console.log("Render Tab Stops"); + if (this.props.tabStopsResults !== undefined) { + this.props.tabStopsResults?.map((result: any, index:number) => { + temp.push( + +
+
+ {index+1} +
+
+ { { vCount > 0 && <>Violation{vCount}  } } + { { nrCount > 0 && <>Needs review{nrCount}  } } + { { rCount > 0 && <>Recommendation{rCount} } } +
+ +
+ {result.apiArgs[0].name} +
+
+
+ ); + }); + // console.log("temp = ", temp); + return temp; + } + + } + + render() { + // console.log("start TabStops render"); + + return
+ +
+ Tab stops summary +
+
+ +
+
+ Index +
+
+ Issues +
+
+ Role +
+
+ Name +
+
+
+ {/* {console.log("call printTabStops")} */} + {this.printTabStops()} +
+ } +} diff --git a/accessibility-checker-extension/src/ts/devtools/TabStopsHeader.tsx b/accessibility-checker-extension/src/ts/devtools/TabStopsHeader.tsx new file mode 100644 index 000000000..2c8a03762 --- /dev/null +++ b/accessibility-checker-extension/src/ts/devtools/TabStopsHeader.tsx @@ -0,0 +1,68 @@ +/****************************************************************************** + Copyright:: 2020- IBM, Inc + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + *****************************************************************************/ + + import React from "react"; + + import { + Button, Column, Grid + } from '@carbon/react'; + + + interface ITabStopsHeaderState { + } + + interface ITabStopsHeaderProps { + layout: "main" | "sub", + tabStopsHandler: () => void + } + + export default class TabStopsHeader extends React.Component { + state: ITabStopsHeaderState = {}; + + componentDidMount(){ + var button = document.getElementById('backToListView'); + if (button) { + button.focus(); + } + } + + render() { + // console.log("start TabStopsHeader render"); + let headerContent = (
+ + + + + +
+ +
+
+
+
); + + if (this.props.layout === "main") { + return
+ {headerContent} +
+ } else { + return
+ {headerContent} +
+ } + } + } \ No newline at end of file diff --git a/accessibility-checker-extension/src/ts/options/OptionsApp.tsx b/accessibility-checker-extension/src/ts/options/OptionsApp.tsx index a48c4950a..d099a934a 100644 --- a/accessibility-checker-extension/src/ts/options/OptionsApp.tsx +++ b/accessibility-checker-extension/src/ts/options/OptionsApp.tsx @@ -19,7 +19,7 @@ limitations under the License. import React from "react"; import { Column, Grid, Dropdown, - Button, + Button, Checkbox, Toggle, Modal, Theme } from "@carbon/react"; @@ -40,7 +40,11 @@ interface OptionsAppState { show_reset_notif: boolean; modalRuleSet: boolean; modalGuidelines: boolean; - + // Keyboard Checker Mode options + tabStopLines: boolean; + tabStopOutlines: boolean; + tabStopAlerts: boolean; + tabStopFirstTime: boolean; } class OptionsApp extends React.Component<{}, OptionsAppState> { @@ -55,12 +59,18 @@ class OptionsApp extends React.Component<{}, OptionsAppState> { show_reset_notif: false, modalRuleSet: false, modalGuidelines: false, + // Keyboard Checker Mode options + tabStopLines: true, + tabStopOutlines: false, + tabStopAlerts: true, + tabStopFirstTime: true, }; async componentDidMount() { + console.log("Options App ComponentDidMount"); var self = this; - //get the selected_archive from storage + // get OPTIONS from storage chrome.storage.local.get("OPTIONS", async function (result: any) { //always sync archives with server var archives = await self.getArchives(); @@ -69,19 +79,39 @@ class OptionsApp extends React.Component<{}, OptionsAppState> { var rulesets: any = null; var selected_ruleset: any = null; var currentRuleset: any = null; - - //OPTIONS are not in storage - if (result != null && result.OPTIONS == undefined) { + // Keyboard Mode options + var tabStopLines: any = true; + var tabStopOutlines: any = false; + var tabStopAlerts: any = true; + var tabStopFirstTime: any = true; + + console.log("_____________"); + console.log(result.OPTIONS); + console.log("_____________"); + + // OPTIONS are not in storage + // JCH make this test stronger + if (result == null || result.OPTIONS == undefined || + result.OPTIONS.selected_archive == undefined || + result.OPTIONS.selected_ruleset == undefined) { + // OPTIONS are NOT in storage + console.log("OPTIONS are NOT in storage"); //find the latest archive selected_archive = self.getLatestArchive(archives); rulesets = await self.getRulesets(selected_archive); selected_ruleset = rulesets[0]; + // leave all Keyboard mode options to true, i.e., show all } else { //OPTIONS are in storage + console.log("OPTIONS ARE in storage"); selected_archive = result.OPTIONS.selected_archive; rulesets = result.OPTIONS.rulesets; selected_ruleset = result.OPTIONS.selected_ruleset; + tabStopLines = result.OPTIONS.tabStopLines; + tabStopOutlines = result.OPTIONS.tabStopOutlines; + tabStopAlerts = result.OPTIONS.tabStopAlerts; + tabStopFirstTime = result.OPTIONS.tabStopFirstTime; if (selected_archive) { if (archives.some((archive: any) => (archive.id === selected_archive.id && archive.name === selected_archive.name))) { @@ -104,12 +134,12 @@ class OptionsApp extends React.Component<{}, OptionsAppState> { currentArchive = selected_archive.name; currentRuleset = selected_ruleset.name; - - // self.setState({ archives, selected_archive, rulesets, selected_ruleset }); + self.setState({ archives: archives, selected_archive: selected_archive, rulesets: rulesets, selected_ruleset: selected_ruleset, currentArchive: currentArchive, - currentRuleset: currentRuleset + currentRuleset: currentRuleset, tabStopLines: tabStopLines, tabStopOutlines: tabStopOutlines, + tabStopAlerts: tabStopAlerts, tabStopFirstTime: tabStopFirstTime, }); self.save_options_to_storage(self.state); }); @@ -132,13 +162,7 @@ class OptionsApp extends React.Component<{}, OptionsAppState> { return selected_archive.policies; }; - //save options into browser's storage - save_options_to_storage = async (state: any) => { - var options = { OPTIONS: state }; - await chrome.storage.local.set(options, function () { - console.log("options is set to ", options); - }); - }; + handleArchiveSelect = async (item: any) => { var rulesets = await this.getRulesets(item.selectedItem); @@ -160,12 +184,36 @@ class OptionsApp extends React.Component<{}, OptionsAppState> { // modal choices // delete scans and update ruleset // keep stored scans and don't update rulset (ali says don't give this choice) - this.setState({ currentArchive: this.state.selected_archive.name, currentRuleset: this.state.selected_ruleset.name }) + console.log("handleSave"); + console.log("this.state.selected_archive.name = ",this.state.selected_archive.name); + console.log("this.state.selected_ruleset.name = ",this.state.selected_ruleset.name,); + console.log("this.state.tabStopLines = ",this.state.tabStopLines); + console.log("this.state.tabStopOutlines = ",this.state.tabStopOutlines); + console.log("this.state.tabStopAlerts = ",this.state.tabStopAlerts); + console.log("this.state.tabStopFirstTime = ",this.state.tabStopFirstTime); + + this.setState({ + currentArchive: this.state.selected_archive.name, + currentRuleset: this.state.selected_ruleset.name, + tabStopLines: this.state.tabStopLines, + tabStopOutlines: this.state.tabStopOutlines, + tabStopAlerts: this.state.tabStopAlerts, + tabStopFirstTime: false, + }) this.save_options_to_storage(this.state); this.setState({ show_notif: true, show_reset_notif: false, }); }; - handlReset = () => { + //save options into browser's storage + save_options_to_storage = async (state: any) => { + var options = { OPTIONS: state }; + await chrome.storage.local.set(options, function () { + // console.log("options is set to ", options); + }); + + }; + + handleReset = () => { var selected_archive: any = this.getLatestArchive(this.state.archives); var selected_ruleset: any = this.state.rulesets[0]; @@ -174,6 +222,10 @@ class OptionsApp extends React.Component<{}, OptionsAppState> { selected_ruleset, show_reset_notif: true, show_notif: false, + tabStopLines: true, + tabStopOutlines: true, + tabStopAlerts: true, + tabStopFirstTime: false }); }; @@ -187,6 +239,11 @@ class OptionsApp extends React.Component<{}, OptionsAppState> { ...this.state, }; + // only show keyboard first time notification on first time + // user uses the keyboard visualization - note it is can be + // reset with "Reset to defaults" + + const manifest = chrome.runtime.getManifest(); function displayVersion() { let extVersion = manifest.version; @@ -204,22 +261,31 @@ class OptionsApp extends React.Component<{}, OptionsAppState> {
purple bee icon -

- IBM Accessibility -
Equal Access Toolkit: -
Accessibility Checker -

+
+ IBM Accessibility +
Equal Access Toolkit: +
Accessibility Checker +
@@ -228,10 +294,12 @@ class OptionsApp extends React.Component<{}, OptionsAppState> {
-

IBM Accessibility Checker options

+
IBM Accessibility Checker options
+ +
Rule sets
-
+
Select a rule set deployment date
-
+
Select accessibility guidelines
+
diff --git a/accessibility-checker-extension/src/ts/quickGuideAC/QuickGuideACApp.tsx b/accessibility-checker-extension/src/ts/quickGuideAC/QuickGuideACApp.tsx index 2c49bca79..bff1f2816 100644 --- a/accessibility-checker-extension/src/ts/quickGuideAC/QuickGuideACApp.tsx +++ b/accessibility-checker-extension/src/ts/quickGuideAC/QuickGuideACApp.tsx @@ -22,6 +22,9 @@ import beeLogoUrl from "../../assets/BE_for_Accessibility_darker.svg"; import violation from "../../assets/Violation16.svg"; import needsReview from "../../assets/NeedsReview16.svg"; import recommendation from "../../assets/Recommendation16.svg"; +import tabStop from "../../assets/tab_stop.svg"; +import kbIssues from "../../assets/keyboard_issue.svg"; +import element from "../../assets/element.svg"; interface quickGuideACAppState { } @@ -84,6 +87,11 @@ class quickGuideACApp extends React.Component<{}, quickGuideACAppState> { 5. Keyboard checker mode +
  • + + 6. Troubleshooting + +
  • @@ -219,7 +227,77 @@ class quickGuideACApp extends React.Component<{}, quickGuideACAppState> { src="assets/img/5_Keyboard.png" alt="IBM checker tool highlighting 'keyboard checker mode' icon button" /> +
    +

    Select 'Keyboard checker mode' icon to turn on/off keyboard visualization.

    +
    +

    + +

    + IBM Checker tool highlighting 'keyboard checker mode' icon button +
    +

    + Select 'Keyboard checker mode' icon to turn on/off keyboard visualization. +

    +
    +

    Important note: the keyboard checker mode does not track page changes. Turn the mode off and on again to update the visualization. +

    +
    + webpage with keyboard visualization overlay +

    Select these icons or tab through the page to see code and keyboard access issues:

    +
    +
      +
    • +

      + tab stop icon{" "} + tab stops numbered by tab order of the page +

      +
    • +
    • +

      + keyboard issues icon{" "} + keyboard access issue with tab stop number +

      +
    • +
    • +

      + element issues icon{" "} + element with keyboard access issue (not a tab stop) +

      +
    • +
    +
    +
    + +

    9. Troubleshooting

    +

    + If the Accessibility Checker appears unresponsive: +
    +

      +
    • Close the browser DevTools
    • +
    • Clear browser cookies
    • +
    • Refresh the page
    • +
    • Reopen the browser DevTools
    • +
    • Click the 'Scan' button
    • +
    +

    diff --git a/accessibility-checker-extension/src/ts/styles/option.scss b/accessibility-checker-extension/src/ts/styles/option.scss index 238ad6789..d3d1e55f0 100644 --- a/accessibility-checker-extension/src/ts/styles/option.scss +++ b/accessibility-checker-extension/src/ts/styles/option.scss @@ -41,7 +41,8 @@ body { padding: 2rem 1rem; .icon { - height: 2rem; + height: 38px; + width: 48px; } h2 { @@ -49,11 +50,44 @@ body { margin-top: 2rem; } + .ibm { + height: 20px; + width: 130px; + color: #171717; + font-family: "IBM Plex Sans"; + font-size: 16px; + letter-spacing: 0; + line-height: 20px; + } + + .accessibility { + height: 20px; + width: 130px; + color: #171717; + font-family: "IBM Plex Sans"; + font-size: 16px; + font-weight: bold; + letter-spacing: 0; + line-height: 20px; + } + + .equal-access-toolkit { + height: 56px; + width: 206px; + color: #171717; + font-family: "IBM Plex Sans"; + font-size: 20px; + letter-spacing: 0; + line-height: 28px; + } + .op_version { @include type-style("label-01"); color: $gray-70; } + + p { @include breakpoint('lg') { max-width: 100%; @@ -69,13 +103,46 @@ body { } .cds--dropdown__wrapper { - margin-top: 1.5rem; + margin-top: 1rem; @include breakpoint('md') { max-width: 75%; } } + .checker-options { + height: 28px; + width: 305px; + color: #161616; + font-family: "IBM Plex Sans"; + font-size: 20px; + letter-spacing: 0; + line-height: 28px; + margin-bottom: 32px; + } + + .rule-sets { + height: 16px; + width: 265px; + color: #161616; + font-family: "IBM Plex Sans"; + font-size: 14px; + font-weight: 600; + letter-spacing: 0.16px; + line-height: 18px; + margin-bottom: 16px; + } + + .select-a-rule-set { + height: 22px; + width: 608px; + color: #161616; + font-family: "IBM Plex Sans"; + font-size: 16px; + letter-spacing: 0; + line-height: 22px; + } + .op_helper-text { @include type-style("body-compact-02"); margin-top: 0.5rem; @@ -99,7 +166,7 @@ body { } .cds--dropdown__wrapper { - margin-top: 1.5rem; + margin-top: 1rem; @include breakpoint('md') { max-width: 100%; @@ -134,13 +201,6 @@ body { } } - .rulesetDate { - font-size: 1rem; - font-weight: 600; - letter-spacing: 0; - line-height: 1.375rem; - } - .buttonRow { margin-top: 1.5rem; display: flex; @@ -171,5 +231,15 @@ body { margin-top: 0rem; } } + + .label-text { + height: 16px; + width: 119px; + color: #525252; + font-family: "IBM Plex Sans"; + font-size: 12px; + letter-spacing: 0.32px; + line-height: 16px; + } } } \ No newline at end of file diff --git a/accessibility-checker-extension/src/ts/styles/panel.scss b/accessibility-checker-extension/src/ts/styles/panel.scss index 3f9a907ea..63754b46d 100644 --- a/accessibility-checker-extension/src/ts/styles/panel.scss +++ b/accessibility-checker-extension/src/ts/styles/panel.scss @@ -288,6 +288,8 @@ width: 140px } + + .mainPanel { .helpHeader { diff --git a/accessibility-checker-extension/src/ts/styles/reportTabStops.scss b/accessibility-checker-extension/src/ts/styles/reportTabStops.scss new file mode 100644 index 000000000..f8e5a4e97 --- /dev/null +++ b/accessibility-checker-extension/src/ts/styles/reportTabStops.scss @@ -0,0 +1,12 @@ +/* Emulate Gap Support with Flexbox and Margins */ +.emulated-flex-gap { + --gap: 12px; + display: inline-flex; + flex-wrap: wrap; + margin: calc(-1 * var(--gap)) 0 0 calc(-1 * var(--gap)); + width: calc(100% + var(--gap)); + } + + .emulated-flex-gap > * { + margin: var(--gap) 0 0 var(--gap); + } \ No newline at end of file diff --git a/accessibility-checker-extension/src/ts/styles/subpanel.scss b/accessibility-checker-extension/src/ts/styles/subpanel.scss index 3330e9e03..e4b7a5e86 100644 --- a/accessibility-checker-extension/src/ts/styles/subpanel.scss +++ b/accessibility-checker-extension/src/ts/styles/subpanel.scss @@ -154,7 +154,6 @@ top: 0; left: 0; width: 100%; - min-width: 825px; } .row-counts { diff --git a/accessibility-checker-extension/src/ts/styles/usingAC.scss b/accessibility-checker-extension/src/ts/styles/usingAC.scss index 23d4305a0..a17cbafa8 100644 --- a/accessibility-checker-extension/src/ts/styles/usingAC.scss +++ b/accessibility-checker-extension/src/ts/styles/usingAC.scss @@ -1,3 +1,4 @@ +@use '@carbon/react'; @use '@carbon/styles/scss/type' as *; @use '@carbon/styles/scss/breakpoint' as *; @@ -9,6 +10,18 @@ body { background: #f2f4f8; } +img { + vertical-align: middle; +} + +p { + color: #161616; + font-family: "IBM Plex Sans"; + font-size: 16px; + letter-spacing: 0; +} + + #usingAC-root { @include breakpoint('lg') { height: 100%; @@ -25,47 +38,58 @@ body { } p { - margin-top: 1.5rem; @include breakpoint('md') { max-width: 75%; } } - .toc { - list-style: none; - margin-inline-start: 1rem; - } - .leftPanel { - padding: 2rem 1rem; - + .icon { - height: 2rem; - } - - h1 { - color: #252525; - @include type-style("productive-heading-04"); - margin-top: 2rem; - } - - h3 { - color: #252525; - @include type-style("productive-heading-03"); + height: 2.5rem; + margin-bottom: 2rem; margin-top: 2rem; } .op_version { color: #525252; - @include type-style("body-compact-02"); + @include type-style("body-compact-01"); + margin-top: .5rem; } - p { @include breakpoint('lg') { max-width: 100%; } + margin-top: 2rem; + } + + h1 { + color: #161616; + @include type-style("productive-heading-03"); + } + + h2 { + @include type-style("productive-heading-02"); + margin-top: 1.25rem; + margin-bottom: 0rem; + } + + h3 { + color: #252525; + @include type-style("productive-heading-03"); + margin-top: .5rem; } + ul { + margin-left: 0em; + } + li { + margin-top: 1rem; + } + .sub-menu { + list-style: none; + margin-inline-start: 2rem; + } } .buffer { @@ -76,7 +100,37 @@ body { background-color: white; padding: 3rem 1rem 1rem 1rem; @include breakpoint('lg') { - padding-top: 6rem; + padding-top: 4rem; + } + .rectangle-image { + height: 240px; + width: 400px; + background-color: rgba(0,0,0,0.5); + margin-top: 2rem; + } + .fullWidth-image { + height: 327px; + width: 606.67px; + background-color: rgba(0,0,0,0.5); + margin-top: 2rem; + } + .fullWidth-image2 { + height: 224px; + width: 607px; + background-color: rgba(0,0,0,0.5); + margin-top: 2rem; + } + + p { + @include breakpoint('lg') { + max-width: 100%; + } + } + p { + color: #161616; + font-family: "IBM Plex Sans"; + font-size: 16px; + letter-spacing: 0; } .cds--dropdown__wrapper { @@ -89,22 +143,7 @@ body { .op_helper-text { color: #525252; @include type-style("body-compact-02"); - margin-top: 0.5rem; - } - - p { - font-size: 0.875rem; - font-weight: 400; - line-height: 1.25rem; - letter-spacing: 0.16px; - } - - .pa { - font-size: 0.875rem; - font-weight: 400; - line-height: 1.25rem; - letter-spacing: 0.16px; - margin-top: 0.75rem; + margin-top: 2rem; } .versionDec { @@ -113,22 +152,67 @@ body { font-size: 11px; letter-spacing: 0.13px; line-height: 18px; - margin-top: 3px; + margin-top: 2em; } h1 { - color: #252525; + color: #161616; @include type-style("productive-heading-04"); + margin-bottom: 1rem; + margin-top: 2.25em; } - + h2 { @include type-style("productive-heading-03"); - margin-top: 1.25rem; + margin-top: 2rem; + margin-bottom: 1rem; } - + h3 { - @include type-style("productive-heading-02"); - margin-top: 1.25rem; + color: #252525; + @include type-style("productive-heading-02"); + font-weight: bold; + margin-top: 2rem; + margin-bottom: 0rem; + } + .pa { + margin-top: 1.25rem; + } + ul { + list-style-type: disc; + margin-left: 1em; + margin-top: 0em; + } + li.unorder { + margin-top: .5rem; + } + ol.number { + list-style-type: decimal; + margin-left: 1em; + margin-top: 0em; + } + li.numbers { + margin-top: .5em; + } + li.dashed { + list-style-type: none; } + ul.alpha { + list-style-type: lower-alpha; + margin-left: 1em; + margin-top: 0em; + } + ol.alpha { + list-style-type: lower-alpha; + margin-left: 1em; + margin-top: 0em; + } + li.abc { + margin-top: .5rem; + } + li.indent { + margin-left: 1rem; + margin-top: .5em; + } } } diff --git a/accessibility-checker-extension/src/ts/tab/tabListeners.ts b/accessibility-checker-extension/src/ts/tab/tabListeners.ts index 26d9d6c22..ca79da212 100644 --- a/accessibility-checker-extension/src/ts/tab/tabListeners.ts +++ b/accessibility-checker-extension/src/ts/tab/tabListeners.ts @@ -33,9 +33,10 @@ TabMessaging.addListener("DAP_SCAN_TAB", async (message: any) => { try { let checker = new (window).ace.Checker(); - console.info(`Accessibility Checker - Scanning with archive ${message.archiveId} and policy ${message.policyId}`); + // console.info(`Accessibility Checker - Scanning with archive ${message.archiveId} and policy ${message.policyId}`); - let report = await checker.check(window.document, [message.policyId]); + let report = await checker.check(window.document, [message.policyId, "EXTENSIONS"]); + // console.log(report); (window as any).aceReportCache = { archiveId: message.archiveId, policyId: message.policyId, @@ -43,13 +44,13 @@ TabMessaging.addListener("DAP_SCAN_TAB", async (message: any) => { }; if (report) { let passResults = report.results.filter((result: any) => { - return result.value[1] === "PASS"; + return result.value[1] === "PASS" && result.value[0] !== "INFORMATION"; }) let passXpaths : string[] = passResults.map((result: any) => result.path.dom); report.passUniqueElements = Array.from(new Set(passXpaths)); - report.results = report.results.filter((issue: any) => issue.value[1] !== "PASS"); + report.results = report.results.filter((issue: any) => issue.value[1] !== "PASS" || issue.value[0] === "INFORMATION"); for (let result of report.results) { let engineHelp = checker.engine.getHelp(result.ruleId, result.reasonId, message.archiveId); let version = message.archiveVersion || "latest"; diff --git a/accessibility-checker-extension/src/ts/usingAC/UsingACApp.tsx b/accessibility-checker-extension/src/ts/usingAC/UsingACApp.tsx index 18b14f2f6..fc461dd25 100644 --- a/accessibility-checker-extension/src/ts/usingAC/UsingACApp.tsx +++ b/accessibility-checker-extension/src/ts/usingAC/UsingACApp.tsx @@ -22,6 +22,16 @@ import beeLogoUrl from "../../assets/BE_for_Accessibility_darker.svg"; import violation from "../../assets/Violation16.svg"; import needsReview from "../../assets/NeedsReview16.svg"; import recommendation from "../../assets/Recommendation16.svg"; +import tabStop from "../../assets/tab_stop.svg"; +import kbIssues from "../../assets/keyboard_issue.svg"; +import element from "../../assets/element.svg"; +import enter from "../../assets/enter.svg"; +import esc from "../../assets/esc.svg"; +import leftRight from "../../assets/left_right.svg"; +import shift from "../../assets/shift.svg"; +import space from "../../assets/space.svg"; +import tab from "../../assets/tab.svg"; +import upDown from "../../assets/up_down.svg"; interface UsingACAppState { } @@ -42,81 +52,38 @@ class UsingACApp extends React.Component<{}, UsingACAppState> { return (
    - -
    + +
    purple bee icon -

    - IBM Accessibility -
    - Equal Access Toolkit: -
    - Accessibility Checker -

    -
    -
    -

    User guide

    -
    + +
    +

    Equal Access Toolkit: +
    + Accessibility Checker

    +
    Version {displayVersion()}
    +
    - -

    IBM Accessibility Checker user guide

    -
    Version {displayVersion()}
    -

    - The IBM Equal Access Accessibility Checker is a browser extension - that allows users to evaluate a web-based component or solution - for accessibility issues against W3C Web Content Accessibilty Guidelines - (WCAG) and IBM guidelines with explanations and suitable fixes within the tool. -

    -

    - The extension showcases two views, the{" "} - Accessibility Assessment panel is a comprehensive - accessibility assessment tool to help you identify accessibility - issues and understand how to fix them, while the{" "} - Accessibility Checker tab in the Elements panel - in Chrome or the Inspector panel in Firefox is a code scanner for - developers looking to find and fix issues in code and on the page - quickly. This checker is part of an open suite of accessibility - automation tools. For teams seeking integrated accessibility - testing, IBM offers{" "} - - plug-ins and modules for NodeJS and Karma - {" "} - that perform cross-platform accessibility testing in the build and - development process. These tools use the same test engine as the - Accessibility Checker. -

    -

    1. Prerequisites

    - Supported browsers: -
      +

      + The Accessibility Checker is a browser extension that tests web pages for accessibility issues with W3C Web Content Accessibility Guidelines (WCAG) and IBM requirements. There is a Checker view to find and fix issues in the code and an Assessment view for an executive overview of the page. For teams seeking integrated accessibility testing, IBM offers plug-ins and modules for NodeJS and Karma that perform cross-platform testing in the build and development process. These tools use the same test engine as the Accessibility Checker. +

      +
    +
    +

    + Note: On rare occasions the Accessibility Checker extension does not appear in the developer tools for some sites due to a bug in the developer tools. The workaround is to go to a site where you know the checker will launch, and launch the checker in the developer tools. Then, in the same browser tab, load the site that did not launch. +

    +
    +

    1. How to install

    +

    + Supported browsers: +

    • Google Chrome version 81.x or later
    • Mozilla Firefox version 68.x or later
    -
    - -

    2. Installation

    +

    - Follow the steps below to install the browser extension for Google - Chrome: -
      +

      + For Chrome: +

      1. Open the Chrome browser.
      2. Go to the{" "} @@ -242,17 +190,16 @@ class UsingACApp extends React.Component<{}, UsingACAppState> { in the Chrome Web Store.
      3. - Click the 'Add To Chrome' button. + Click 'Add To Chrome' button.
      +

    - Follow the steps below to install the browser extension for - Mozilla Firefox: -
      -
    1. Open the Firefox browser.
    2. +

      + For Firefox: +

        +
      1. Open the Firefox browser
      2. Go to the{" "} { > IBM Equal Access Accessibility Checker {" "} - in Firefox Browser Add-on. + in Firefox Browser Add-on
      3. - Click the 'Add To Firefox' button. + Click 'Add To Firefox' button
      +

      +
    +

    2. Accessibility issues

    +
    +

    As with any automated test tool for accessibility, these tests don’t catch all issues. Complete your accessibility testing with a {" "} + + quick unit test for accessibility + {" "} or follow the + full accessibility test process + {" "}. +

    - -

    3. Categories of accessibility issues

    -

    The tool reports three kinds of accessibility issues:

    -
      -
    • -

      + IBM checker tool highlighting issues filter and tab list for element roles, requirements, and rules +

      +

      The issues are divided into three types:

      +
      +
      +
        +
      • violation icon {" "} - Violation - accessibility failures that need - to be corrected. -

        + Violation - failures that need + to be corrected
      • -
      • -

        +

      • needs review icon {" "} - Needs review - issues that may not be a - violation. These need a manual review to identify whether - there is an accessibility problem. -

        + Needs review - need manual review to identify if it's a violation
      • -
      • -

        +

      • recommendation icon{" "} - Recommendation - opportunities to apply best - practices to further improve accessibility. -

        + Recommendation - opportunities to apply best practices
      +

      - As with any automated test tool for accessibility, these tests - don’t catch all issues. Complete your accessibility assessment - with a quick unit test for accessibility or follow the full - accessibility test process. -

      - -

      4. Ways to view the issues

      -

      - There are three ways to view and explore the issues identified by - the tool. All views show the same set of issues: + There are three ways to view the same set of issues:

      -
        -
      • +
      +
      +

      + Element roles – issues are organized by the WAI-ARIA roles of the DOM elements. This view shows both implicit and explicit roles, and not the element names. Use this view to explore issues within a specific element and its children. +

      +
      +

      - Requirements - issues are organized by the - IBM requirements, which corresponds to the WCAG 2.1 standards. - Each issue is mapped to the most relevant requirement. This - view makes it easy to see how to classify and - report issues found by the tool. + Requirements – issues are mapped to the most relevant IBM requirement, which corresponds to the WCAG 2.1 standards. Use this view to classify and report issues.

      -
    • -
    • +
    +

    - Element roles - issues are organized in the - hierarchical structure defined by the WAI-ARIA roles of the - DOM elements. This view shows both implicit and explicit - roles. It does not show element names. This view is ideal for - exploring the issues within a specific element and its - children. + Rules - issues organized by rules in the rule set. Use this view to see the different types of issues at once.

    +
    + +

    3. The Checker view

    +
    +

    The Accessibility Checker tab in the Elements panel in Chrome or the Inspector panel in Firefox is a code scanner for developers looking to find and fix issues in code and on the page quickly.

    +
    +
    +

    To use the Checker view, do one of the following:

    +
    +
    +
      +
    • For Chrome: +
        +
      1. From the browser ‘View’ menu, select ‘Developer’
      2. +
      3. Select ‘Developer tools’
      4. +
    • -
    • +
      +
    • For Firefox: +
        +
      1. From the browser ‘Tools‘ menu, select ‘Web Developer’
      2. +
      3. Select ‘Toggle Tools’
      4. +
      +
    • +
      + +
    • Command+Option+I on MacOS® or Control+Shift+I on Microsoft Windows®

    • +
    • Right-click web page, select ‘Inspect’ (Chrome) or ‘Inspect Element’ (Firefox)

    • +
    +

    3.1 Accessibility Checker

    +

    + The Accessibility Checker view is a code scanner for developers looking to find and fix errors quickly as they are building a component. +

    + Developer tools with 'elements tab', 'accessibility checker tab', and scan button highlighted and numbered 1 to 3 +
    +
      +
    1. Open ‘Elements’ panel (Chrome) or ‘Inspector’ panel (Firefox)
    2. +
    3. Select 'Accessibility Checker' from the tabs in the right-hand panel
    4. +
    5. Click the ‘Scan’ button to scan web page
    6. +
    +
    + IBM checker tool highlighting issues filter and tab list for element roles requirements and rules +

    - Rules - issues are organized by the rules in - the rule set, with violations, items that need review, and - recommendations. This view is the best way to see all the - different kinds of issues at once. + The scan results show the total number of issues divided by types. The same set of issues can also be viewed by element roles, requirements, and rules.

    - +
    + IBM Checker tool highlighting list for element roles, requirements, and rules, an expand icon, and a 'learn more' link text +
    +

    View issues by element roles, requirements, or rules and select the expand icon next to an element role/requirement/rule to see the related issues. Click on the ‘learn more’ link to view detailed information and how to fix it.

    +
    + browser highlighting search bar on page, an element in the DOM,and related issues in the checker tool +
    +

    Select an element in the Checker, in the DOM, or use the ‘Inspect element’ command on the web page for the Checker to:

    +
    +
      +
    • Show number of issues of each type within selected element and its children

    • +
    • Open and highlight all issues in element, if any (dark purple highlight)

    • +
    • Open and highlight all issues in element’s children, if any (light purple highlight)

    • +
    • Show the same set of highlighted issues in the different tabs

    +
    +

    Hidden content scanning

    +

    + By default, the Checker skips hidden content (Web pages that use the visibility:hidden or display:none elements). If this content is revealed to users at any point, you must include the content in your test plan. Ensure the tests trigger the display of hidden content for the Checker to test. +

    +
    +
    +

    + Scan local files + The Checker is able to scan local .html or .htm files launched in the Firefox browser by default. Follow the steps below to allow scanning of local .html or .htm files in the Chrome browser: +

    +
    +
      +
    1. Open Chrome browser

    2. +
    3. Open the 'Window' menu

    4. +
    5. Select 'Extensions' menu option to see all installed extensions

    6. +
    7. Click 'Details' button of the IBM Accessibility Checker extension

    8. +
    9. Scroll down and turn on 'Allow access to file URLs'

    10. +
    -

    5. Options

    -

    - Use the options page to change the default rule set for a - supported standard or a date of rule set deployment. By default, - the IBM Equal Access Accessibility Checker uses the latest - deployment with a set of rules that correspond to the most recent - WCAG standards, plus some additional IBM supplemental requirements. Rule sets - with rules that map to specific WCAG versions are also available - to choose from as needed. These rule sets are updated regularly - and each update has a date of deployment. If you need to replicate - an earlier test, choose the deployment date of the original test. -

    -

    - After changing options, close and reopen the developer tools for the change to take effect. -

    -

    - Follow the steps below to open the Accessibility Checker options - page: -

    -
      +

      3.2 Create a scan report

      +

      + To generate a report for a single scan in the Checker view: +

      + an open dropdown menu with focus on 'download current scan'. +
      +

      Open the ‘Reports’ dropdown menu and select ‘Download current scan.’ See Accessibility Checker reports for more details.

      +
      +

      3.3 Create a multi-scan report

      +

      + To combine up to 50 multiple scans into a single report: +

      + an open dropdown menu with focus on 'start storing scans' +
      +

      1. Open the ‘Reports’ dropdown menu and select ‘Start storing scans.'

      +
      + status bar that says 'status:storing, 3 scans stored' +
      +

      2. Scan desired pages, and the number of stored scans will show below scan button.

      +
      + an open dropdown menu with focus on 'download stored scans +
      +

      3. Open the ‘Reports’ dropdown menu and select ‘Download stored scans.’

      +
      + a table showing a list of stored scans +
      +

      You can open the ‘Reports’ dropdown menu and select ‘View stored scans’ to:

      +
      +
        +
      • Select or unselect scans you want in the report with the checkboxes
      • +
      • Select all scans with the checkbox in the first column of the header row
      • +
      • Edit scan labels to differentiate the scans
      • +
      • Select ‘Download’ in the header row to download the multi-scan report
      • +
      • Select ‘Back to list view’ to return to main Checker view
      • +
      + +

      3.4. Focus view

      +

      + The focus view allows you to switch between viewing all issues on the page, or only the issues for a selected element or component in the DOM. +

      + content switcher with two items: html and alt +
      +
        +
      1. Select an element in the DOM, or use the ‘Inspect element’ command on page.

      2. +
      3. Select element name in focus view (e.g. <html>) to view related issues

      4. +
      5. Select ‘All’ in the focus view to see all issues again

      6. +
      +
      +

      3.5. Keyboard checker mode

      +

      + This mode shows a visualization of the keyboard tab order detected on the page, and elements with detectable keyboard access issues. Use this for manual keyboard accessibility testing. +

      + IBM Checker tool highlighting 'keyboard checker mode' icon button +
      +

      + Select 'Keyboard checker mode' icon to turn on/off keyboard visualization. +

      +
      +
      +

      Important note: the keyboard checker mode does not track page changes. Turn the mode off and on again to update the visualization. +

      +
      + webpage with keyboard visualization overlay +

      Select these icons or tab through the page to see code and keyboard access issues:

      +
      +
      • - In the browser tool bar, select the IBM Equal Access - Accessibility Checker icon, shown as a purple bee{" "} Accessibility checker application icon - . This will usually be located in the upper right of the - browser window. An overlay will appear. + src={tabStop} + alt="tab stop icon" + />{" "} + tab stops numbered by tab order of the page

      • - Select 'Options' in the overlay. The options - will open in a new browser tab. - Note: In the Firefox browser when the Enhanced - Tracking Protection option is set to Strict, this causes some - sites or content to break and may prevent the Options page - from opening. Change the browser privacy settings to Standard, to avoid the situation. + keyboard issues icon{" "} + keyboard access issue with tab stop number

      • -
    -

    - Options page screenshot - a page where you can select a Rule set deployment and a Rule set for your checker to use. -

    - -

    5.1 Rule set deployment date

    -

    - {" "} - From the 'Select a rule set deployment date' dropdown choose - one of the following: -

    -
    • - Latest Deployment - use the latest version of the - selected rule set. This is the default option. + element issues icon{" "} + element with keyboard access issue (not a tab stop)

    • -
    • +
    +
    +
    +

    Manual keyboard testing

    +

    Automated tools can’t find all keyboard access issues. Using the visualization, test for basic keyboard navigation:

    +
    +
    +
      +
    1. Make sure every interactive element is a tab stop*

      +
        +
      1. No tab stops on non-interactive or non-visible elements**

      2. +
      3. Tab order matches the visual flow of the page

      4. +
      5. Inspect elements with found issues (select the triangle icons)

      6. +
      +
    2. +
      +
    3. Test interactive elements +
        +
      1. Start from browser URL bar, or click just above the part of page of interest

      2. +
      3. Press tab key to move keyboard focus through interactive elements

      4. +
      5. You shouldn’t get stuck in a keyboard trap (try the Esc key)

      6. +
      7. Operate the controls using standard keys

      8. +
      9. Focus should not jump to an unexpected place when operating controls

      10. +
      11. When dismissing or deleting items, focus should move to sensible location

      12. +
      +
    4. +
    +
    +
    +

    * It may be acceptable to skip interactive elements if the UI provides another keyboard accessible way to perform the same function

    +
    +
    +

    ** It may be acceptable if the first tab stop of the page is “skip to main content” link that is not visible until it has keyboard focus

    +
    +
    +

    Keys to use for testing

    +
    +
      +
    • - <date> Deployment - use a rule set - from a specific date for consistent testing throughout a project, - or to replicate an earlier test. + tab key{" "} + moves to next interactive element

    • -
    • +
    • +

      + shift key{" "} + + + tab key{" "} + moves to previous interactive element +

      +
    • +
    • - Preview Rules - try an experimental preview of - a possible future rule set. + enter key{" "} + activates a link, button, or menu

    • -
    -

    - Select the 'Save' button to keep the changes or - the 'Reset' button to discard changes. - Close and reopen the developer tools for the change to take effect. -

    - -

    5.2 Select accessibility guidelines

    -

    - From the 'Select accessibility guidelines' dropdown choose one of - the following: -

    -
      -
    • +
    • - IBM Accessibility - Rules for Web Content - Accessibility Guidelines WCAG 2.1, levels A and AA, plus - additional IBM requirements. This is the default option. + space key{" "} + selects or unselects a form element, or activates button/dropdown menu

    • - -
    • +
    • - WCAG 2.1 (A,AA) - This is the current W3C recommendation. - Content that conforms to WCAG 2.1 also conforms to WCAG 2.0. - These rules align with the European EN 301 549 standards. + up and down arrow keys{" "} + moves between radio buttons or menu items, and scrolls page vertically

    • - -
    • +
    • - WCAG 2.0 (A,AA) - These - rules align with the Revised US Sec 508 standards, but are not the latest - W3C recommendation. + left and right arrow keys{" "} + may move between tab or menu items, scroll horizontally, and adjust sliders

    • - -
    • +
    • - IBM Accessibility BETA - Extends IBM Accessibility - with experimental rules. + esc keys{" "} + closes modal dialog or dropdown menu

    • -
    +

    - After making a change, select the 'Save' button, then - close and reopen the developer tools for the change to take effect. -

    + Additional keystrokes are defined for particular widgets within the W3C WAI-ARIA Authoring Practices Design Patterns. +

    -

    6. Usage

    -

    - The IBM Equal Access Accessibility Checker offers two views, the - Accessibility Checker view is a code scanner for developers - looking to find and fix errors quickly as they are building a - component, while the Accessibility Assessment view provides - explanation and suggested solutions for each issue reported. - {" "} -

    -

    - Note: On rare occasions the Accessibility Checker - extension does not appear in the developer tools for some sites - due to a bug in the developer tools. The workaround is to go to a - site where you know the checker will launch, and launch the - checker in the developer tools. Then, in the same browser tab, - load the site that did not launch. -

    - -

    6.1 Accessibility Checker

    +

    4. The Assessment view

    -
      -
    1. -

      Open the Developer Tools:

      -
        -
      • -

        - In Chrome: From the browser ‘View’ menu, select - ‘Developer’ and then select ‘Developer tools’, or{" "} -

        -
      • -
      • -

        - In Firefox: From the browser ‘Tools‘ menu, select ‘Web - Developer’ and then select ‘Toggle Tools’, or -

        -
      • -
      • -

        - Press Command+Option+I on MacOS® or{" "} - Control+Shift+I on Microsoft Windows®, - or -

        -
      • -
      • -

        - Right-click on a page element and select ‘Inspect’ - (Chrome) or ‘Inspect Element’ (Firefox). -

        -
      • -
      -
    2. -
    3. -

      - Open the ‘Elements’ panel (Chrome) or ‘Inspector’ panel - (Firefox). -

      -
    4. -
    5. -

      - Select 'Accessibility Checker' from the - tabs in the right-hand pane:{" "} -

      -

      - Accessibility Checker screenshot - a code scanner for developers -

      -
    6. -
    7. -

      - Click the 'Scan' button to scan the web - page. -

      -
    8. -
    9. -

      - The scan result displays the total number of issues found - with individual counts for violations, items that need - review, and recommendations in the issue count region. By - default, the issue list is shown in the 'Element roles' view - (see the previous definition), while 'Requirements' and 'Rules' - tabs are also available. All views show the same set of - issues. -

      -

      - Accessibility Checker results -

      -
    10. -
    11. -

      - To filter issues and focus only on - violations, items that need review or recommendations, - deselect the checkbox by the issue type in the issue count region below the{" "} - 'Scan' button to exclude the issue type from the results. Select - the checkbox by the issue type to include the issue type in the results. - In this screenshot, the 'Needs review' items have been filtered out. -

      -

      - Accessibility Checker results with 'Needs review' issues filtered out -

      -
    12. -
    13. -

      - Select the expand icon (^) next to an element role, requirement, - or rule in the table to display the corresponding issues - found. -

      -
    14. -
    15. -

      - Select the 'learn more' link for an issue to view more detailed help - information that describes the issue and how to fix it. The - help includes links to more detailed explanation, and - summarizes why this issue is important, and who is affected - by it. -

      -
    16. -
    17. - Element roles tab: -
        -
      • -

        - Select an instance of an issue, or an element in the - document object model (DOM), or use the ‘Inspect - element’ command on the web page to: -

        -
          -
        • -

          - Highlight the selected element, or the element - containing the selected issue, in the DOM under the - browser's Elements panel and highlight its location - on the web page. -

          -
        • -
        • -

          - See summary counts showing the number of issues of - each type within the selected element and its - children. -

          -
        • -
        • -

          - Open and highlight all issues in the element, if any - (purple highlight) -

          -
        • -
        • -

          - Open and highlight all issues in the element's - children, if any (light purple highlight). -

          -
        • -
        -
      • -
      -
    18. -
    19. - Requirements tab: -
        -
      • -

        - Select the 'Requirements' tab to view the - scan results by the{" "} - - IBM accessibility requirements.{" "} -

        -
      • -
      • -

        - Select an element or an instance of an issue to - highlight the same set of issues and child issues as in - the 'Element roles' tab. In this view, the issues will - be shown within the relevant requirements. -

        -
      • -
      -
    20. -
    21. - Rules tab: -
        -
      • -

        - {" "} - Select the 'Rules' tab to view the scan - results by the Accessibility Checker rules. -

        -
      • -
      • -

        - Select an element or an instance of an issue to - highlight the same set of issues and child issues as in - the 'Element roles' tab. In this view, the issues will - be shown within the relevant rules. -

        -
      • -
      -
    22. +

      + The Assessment view provides a simplified overall summary, with explanations for each issue. It has less functionality than the Checker view. +

      +
    +
    +

    + To use the Assessment view, do one of the following: +

    +
    +
    +
      +
    • For Chrome:

      +
        +
      1. From the browser ‘View’ menu, select ‘Developer’

      2. +
      3. Select ‘Developer tools’

      4. +
      +
    • +
      +
    • For Firefox:

      +
        +
      1. From the browser ‘Tools‘ menu, select ‘Web Developer’

      2. +
      3. Select ‘Toggle Tools’

      4. +
      +
    • +
      +
    • Command+Option+I on MacOS® or Control+Shift+I on Microsoft Windows®

    • +
    • Right-click web page, select ‘Inspect’ (Chrome) or ‘Inspect Element’ (Firefox)

    • +
    +
    +
      +
    1. Select the 'Accessibility Assessment' panel

    2. +
    3. Click the ‘Scan’ button to scan web page

    4. +
    +
    + IBM Checker tool's accessibility assessment view +
    +

    The left panel shows scan results with total number of issues divided by category, and the right panel shows the report summary.

    +
    + IBM Checker tool's accessibility assessment view highlighting an issue and showing issue details +
    +

    View the issues by element roles, requirements, or rules and select the expand icon next to a requirement/element role/rule to see the related issues, and select an issue to see the detailed description in the right panel.

    +
    + IBM Checker tool's accessibility assessment view highlighting the 'reports' icon button +
    +

    Select the ‘Reports’ icon to download a generated accessibility report. See Accessibility Checker reports for more details.

    +
    -
  • -

    - Switch between tabs to see the same set of highlighted - issues in different views. -

    -
  • -
  • -

    - Use the 'Reports' menu button to download reports - and to store and manage scans to combine into reports. - For details, see Sections{" "} - 6.4 Creating a scan report,{" "} - 6.5 Creating a multi-scan report{" "} - and 7 Accessibility Checker reports. -

    -
  • -
  • -

    - Optionally, you can update the code in the browser's - Elements panel and run 'Scan' again to - confirm your code changes fix the issue. -

    -
  • - +

    5. Options

    +
    +

    + By default, the IBM Accessibility Checker uses the latest deployment with a set of rules that correspond to the most recent WCAG standards, plus additional IBM requirements. Use the options page to change the default rule set for a supported standard or a date of rule set deployment. +

    -

    6.2 Focus View

    +

    - The 'Focus view' switch allows you to switch between viewing all issues on the page, or only the issues for a selected element or component in the DOM. To focus on any individual element or component: + To open the options page:

    +
    -
      -
    1. -

      - Select the element or the component in the DOM, or -

      -
    2. -
    3. -

      - Right-click on a page element and select 'Inspect' (Chrome) or ‘Inspect Element’ (Firefox). -

      -
    4. -
    5. -

      - Select the element name in the ‘Focus View’ switch to view only the issues for that element and its children. -

      -
    6. -
    7. -

      - Select the 'All' option in the ‘Focus View’ switch to see all issues for the page again. -

      -
    8. - -

      By default, after the first scan of a page, all issues are shown, and the <html> element is selected, as shown in this screenshot: -

      - - +
        +
      1. + In the browser tool bar, select the IBM Equal Access + Accessibility Checker icon as shown {" "} Accessibility Checker screenshot - Focus view with all issues + . This will usually be located in the upper right of the + browser window.

        -

        In this screen shot, the search <input> element in the DOM has been selected, and the{" "} - 'Focus View' switch has been set to show all the issues on the whole page: -
        +

      2. +
        +
      3. +

        + In the overlay that appears, select 'Options’ and the options will open in a new browser tab. + Note: In Firefox, the Options page may fail to open if the Enhanced Tracking Protection option is set to Strict. To avoid this, change the browser privacy settings to Standard.

        +
      4. +
      +
    +
    +

    + IBM checker's options page with a 'rule sets' heading and 2 dropdown menus: 'select a rule set deployment date' and 'select accessibility guidelines' +

    + +

    Rule sets

    +

    + Rule sets with rules that map to specific WCAG versions are available. These rule sets are updated regularly and each update has a date of deployment. For consistent testing throughout a project, choose a specific date deployment. To replicate an earlier test, choose the deployment date of the original test. +

    +
    +

    + Options from 'Select a rule set deployment date' dropdown: +

    +
    +
    +
      +
    • - Accessibility Checker screenshot. Focus view switch options are 'input' and 'All' (selected) and all issues on the page are shown + Latest Deployment - the latest version of the + selected rule set(default option)

      -

      In this screen shot, the search <input> element in the DOM has been selected, and the{" "} - ‘Focus View’ switch has been set to show only the issues for that search <input> element: +

    • +
    • +

      + <date> Deployment - the rule set from a specific date

      +
    • +
    • - Accessibility Checker screenshot. Focus view switch options are 'input' (selected) and 'All' only the two issues within the search input element are shown + Preview Rules - try an experimental preview of a possible future rule set

      - - +
    • +
    - -

    6.3 Accessibility Assessment

    -
      -
    1. - Open the Developer Tools: -
        -
      • -

        - In Chrome: From the browser ‘View’ menu, select - ‘Developer’ and then select ‘Developer tools’, or{" "} -

        -
      • -
      • -

        - In Firefox: From the browser ‘Tools menu, select ‘Web - Developer’ and then select ‘Toggle Tools’, or -

        -
      • -
      • -

        - Press Command+Option+I on MacOS® or{" "} - Control+Shift+I on Microsoft Windows®, - or -

        -
      • -
      • -

        - Right-click on a page element and select ‘Inspect’ - (Chrome) or ‘Inspect Element’ (Firefox). -

        -
      • -
      -
    2. -
    3. -

      - Select the 'Accessibility Assessment'{" "} - panel:{" "} -

      -

      - Accessibility Assessment screenshot - A comprehensive accessibility assessment tool -

      -
    4. -
    5. -

      - Click the 'Scan' button to scan the web - page. -

      -
    6. -
    7. -

      - By default, the results display by the{" "} - 'Requirements' with a breakdown of - the total number of issues found by category. -

      -
    8. -
    9. -

      - The right panel displays an Accessibility Checker Report - summary, while the left panel shows the scan result with the - total number of issues found, with individual counts for - violations, items that need review, and recommendations:{" "} -

      -

      - Accessibility Assessment report screenshot - a sample report of Accessibility Assessment -

      -
    10. -
    11. -

      - By default, issues are shown in the 'Requirements' view, while - 'Element roles' and 'Rules' tabs are also available. All - views show the same set of issues. -

      -
    12. -
    13. - Requirements tab: -
        -
      • -

        - Select the expand icon next to a requirement in the table - to display a list of issues found within that - requirement. -

        -
      • -
      • -

        - Select an instance of an issue and the report summary is - replaced with a detailed description that includes the - error level, why the content is failing, what the - requirement is, what resources to use, what to do to fix - the issue, who it affects, and why it is important:{" "} -

        -

        - Accessibility Assessment help panel screenshot - a sample help panel of Accessibility Assessment -

        -
      • -
      -
    14. -
    15. - Element roles tab: -
        -
      • -

        - Select the 'Element roles' tab to view - the scan results organized by element roles on the web - page. -

        -
      • -
      • -

        - Expand an element role to view the issues for that - element role. -

        -
      • -
      • - Select an issue to: -
          -
        • -

          - Highlight the issue. -

          -
        • -
        • -

          - View the detailed description for that issue in the - summary pane (on the left). -

          -
        • -
        -
      • -
      -
    16. -
    17. - Rules tab: -
        -
      • -

        - Select the 'Rules' tab to view the scan - results by the Accessibility Checker rule. -

        -
      • - {" "} -

        - Expand a rule to view the issues for that rule. -

        -
      • -
      • - Select an issue to: -
          -
        • -

          - Highlight the issue. -

          -
        • -
        • -

          - View the detailed description for that issue in - the summary pane (on the left). -

          -
        • -
        -
      • - -
      -
    18. -
    19. -

      - Optionally, you can select the browser's Element panel to - view the Accessibility Checker results alongside the code - and test fixes. -

      -
    20. -
    +

    + Options from 'Select accessibility guidelines' dropdown: +

    - -

    6.4 Creating a scan report

    - To generate a report for a single scan in the Accessibility Checker view: -
      -
    1. -

      - Follow the instructions in - 6.1 Accessibility Checker to scan the web page. -

      -
    2. -
    3. -

      - Open the 'Reports' drop-down menu that follows the scan button. -

      -

      - Screen shot of the 'Reports' menu -

      +
        +
      • +

        + IBM Accessibility - WCAG 2.1 (A, AA) and IBM requirements (default option) +

        +
      • +
      • +

        + WCAG 2.1 (A,AA) - WCAG 2.0. and EN 301 549 standards (W3C’s choice) +

        +
      • +
      • +

        + WCAG 2.0 (A,AA) - referenced by US Section 508 +

        +
      • - -
      • -

        - Select 'Download current scan'. -

        -
      • -
      • -

        - The report for the most recent scan will be downloaded in both HTML and MS Excel spreadsheet formats. -

        -
      • -
    -

    To generate a report for a single scan in the Accessibility Assessment view: -

    -
      -
    1. -

      - Follow the instructions in 6.3 Accessibility Assessment to scan the web page. -

      -
    2. -
    3. -

      - Select the 'Reports' button. -

      -
    4. -
    5. -

      - The report for the most recent scan will be downloaded in both HTML and MS Excel spreadsheet formats. -

      -
    6. -
    -
    +
  • +

    + IBM Accessibility BETA - WCAG 2.1, IBM requirements, and experimental rules +

    +
  • -

    6.5 Creating a multi-scan report

    + +
    +

    - Follow these steps to combine several scans into a single report. - Up to 50 scans may be combined. Reports - with more than 50 scans may not open correctly in MS Excel due to - limitations of the libraries used to write the reports. + Click the 'Save' button to keep the changes or the 'Reset' button to discard changes. Close and reopen the developer tools for the change to take effect.

    -
    -
      -
    1. -

      - Open the Accessibility Checker view (as described in - 6.1 Accessibility Checker). -

      -
    2. -
    3. -

      - Open the 'Reports' drop-down menu. -

      -
    4. -
    5. -

      - Select the 'Start storing scans' menu item. -

      -
    6. -
    7. -

      - The status indicator below the scan button will show that you are now - storing scans to build a report, and that no scans are stored. -

      -

      - Status: storing, no scans stored -

      -
    8. -
    9. -

      - Scan the pages you want to include in the report. This may include scans - of the same page in different states. - The status indicator will update to show how many scans are stored. -

      -

      - Status: storing, 1 scan stored -

      -
    10. -
    11. -

      - When storing scans, you may stop storing scans by selecting the 'Stop storing scans'{" "} - option in the 'Reports' drop-down menu, and your stored scans will not be lost. - You can start storing scans again at any time. -

      -
    12. -
    13. -

      - To remove all stored scans, open the 'Reports' drop-down menu and - select 'Clear stored scans'. -

      -
    14. -
    15. -

      - When you have stored the scans for the report, open the 'Reports' drop-down menu and select{" "} - 'View stored scans'. This opens the Scan manager view, showing a table listing all the stored scans. -

      -

      - Accessibility Checker Stored Scans panel screenshot - a table listing the stored scans -

      -
    16. -
    -

    Creating a report in the scan manager view: -

      -
    1. -

      - Review the scans listed and select the ones you want in the report. Select or unselect all - the stored scans with the checkbox in the first column of the header row of the table. -

      -
    2. -
    3. -

      - Use the 'View' link in the 'Details' column at the right of the table to see a screenshot of each scan. -

      -

      - Popup dialog with screenshot of scan, and details about the scan -

      -
    4. -
    5. -

      - Unselect any scan you do not want in the report by unchecking its checkbox in the table. -

      -

      - Accessibility Checker Stored Scans panel screenshot - one scan is unchecked -

      -
    6. -
    7. -

      - [Optional] To help differentiate the scans in the final report, enter meaningful scan labels - in the 'Scan label’ column of the table. These labels will appear in the final report. -

      -

      - Accessibility Checker Stored Scans panel screenshot - three scans are labelled 'Original scan', 'with form' and 'with link' -

      -
    8. -
    9. -

      - Download the multi-scan report spreadsheet by selecting the 'Download' button at the top of the table. - The spreadsheet will automatically download. -

      -
    10. -
    11. -

      - Remove the selected stored scans using the 'Delete' button. - This preserves memory for new scans to be stored. -

      -
    12. -
    13. -

      - Return to the main checker view by selecting 'Back to list view'. -

      -
    14. -
    -

    6.6 Hidden content scanning

    +

    Keyboard checker mode

    - By default, the tool skips hidden content (Web pages that use the{" "} - visibility:hidden or display:none elements), - if this content is displayed to the user at any point, you must - test the web content by fully exercising the user interface - according to the usage scenarios in your test plan. Ensure the - tests trigger the display of hidden content so that the - Accessibility Checker can validate the content that is displayed. + By default, the keyboard visualization options has the 'Lines connecting tab stops' checkbox selected. Select the 'Element outlines' checkbox to see bounding boxes for each interactive element in the tab order.

    - -

    6.7 Scan local files

    +

    - The Accessibility Checker is able to scan local .html or .htm - files launched in the Firefox browser by default. Follow the steps - below to allow scanning of local .html or .htm files in the Chrome - browser: + The 'Alert notifications' toggles on and off the pop-up notification that appears every time you turn on the keyboard checker mode.

    + +

    6. Accessibility Checker reports

    -
      -
    1. -

      - Open the Chrome browser. -

      -
    2. -
    3. -

      - Open the 'Window' menu. -

      -
    4. -
    5. -

      - Select the 'Extensions' menu option to see - all installed extensions. -

      -
    6. -
    7. -

      - Select the 'Details' button of the IBM Equal - Access Accessibility Checker Extension. -

      -
    8. -
    9. -

      - Scroll down to 'Allow access to file URLs'{" "} - and turn this option on. -

      -
    10. -
    +

    + Single scan reports are provided in both HTML and MS Excel spreadsheet formats. Multi-scan reports are available only in MS Excel spreadsheet format. For how to generate reports, see 3.2 Create a scan report and 3.3 Create a multi-scan report. +

    - -

    6.8 Accessibility Considerations

    -

    - Highlighted below are several accessibility features for adaptability and to ensure ease of access to the Checker functionality, including with keyboard or with a screen reader: -

    -
      -
    1. -

      - The Accessibility Checker tool is responsive to the user's preferred font size and colors. -

      -
    2. -
    3. -

      - Both the Accessibility Assessment view and the Accessibility Checker view are fully keyboard accessible, navigate as follows: -

      -
    4. -
        -
      • -

        - Use the 'tab' key to navigate to any focusable element in the checker, starting - with the 'Scan' button once the checker launches. -

        -
      • -
      • -

        - After running the scan, press the 'tab' key again, to navigate to the 'Reports' drop-down menu button. -

        -
      • -
      • -

        - In the Accessibility Checker view, press the 'tab' key again to navigate to the 'Focus view’ toggle button. - Use the arrow keys to select whether the issue list includes all issues (default) or just the issues for the currently focused element and its children. - This function is not available in the Accessibility Assessment view. -

        -
      • -
      • -

        - Press the 'tab' key to navigate to the checkbox by each issue type and press the 'enter' key to filter the list of issue by Violations, Needs review and/or by Recommendations. -

        -
      • -
      • -

        - Press the 'tab' key to navigate to the Issue List tabs and use the 'right arrow' or the 'left arrow' keys to navigate between the 'Element Roles' view, the 'Requirements' view and the 'Rules' view. -

        -
      • -
      • -

        - Press the 'tab' key to navigate through the issue groupings associated with each requirement, element role or rule. Use the 'enter' key to open or close an issue grouping. Within an open grouping, press the 'tab' key to navigate to each issue, and press the 'enter' key to select the current issue. -

        -
      • -
      • -

        - Press the 'tab' key to move to the 'Learn more' link or to move to the next issue. -

        -
      • -
      -
    5. -

      - Use the headings hierarchy or the implemented landmarks to quickly navigate from one section to another. The list of implemented landmarks are as follows: -

      -
    6. -
        -
      • -

        - The Accessibility Assessment or the Accessibility Checker main landmark contains the main functionality of the tool in each view and includes, -

        -
          -
        • -

          - The Issue Count region: contains the issue count by issue type as well as the total number of issues found. -

          -
        • -
        • -

          - The Issue List region: contains the list of issues grouped by Element Roles, by Requirements or by Rules. -

          -
        • -
        • -

          - In the Accessibility Checker view, the main landmark also containes issue help and the overview of stored scans, when those are - requested by the user. -

          -
        • -
        -
      • -
      • -

        - The Accessibility Assessment view Scan Summary aside or the complementary landmark contains the scan summary, after the scan completes or shows the issue help when any issue is selected. -

        -
      • -
      • -

        - The Accessibility Assessment view Issue Help aside or the complementary landmark contains the issue help when any issue is selected. -

        -
      • -
      -
    +

    + HTML reports + +

    +

    + This interactive report is an HTML file that includes: +

    - -

    7. Accessibility Checker reports

    -

    - The Accessibility Checker can create reports for a single scan, or for multiple scans combined (multi-scan reports). - Single scan reports are provided in both HTML and MS Excel spreadsheet formats. - Multi-scan reports are available only in MS Excel spreadsheet format. - Sections - 6.4 Creating a scan report - {" "} - and - 6.5 Creating a multi-scan report - {" "} - describe how to generate reports. -

    -

    7.1 HTML report

    -

    - This is an interactive report saved as an HTML file for future use. It includes the report - scan date and time, URL, and a summary of test results followed by - the issue details organized by requirements, by element - roles, and by rules. Each instance of an issue also includes a{" "} - 'Learn more' link that opens an overlay - containing a more detailed description of the issue. -

    -

    - The current accessibility status of the Web content displays as a - percentage of elements with no detected violations or items to - review.{" "} -

    -

    - Important Note: This percentage is based on - automated tests only. Be sure to perform additional reviews and - manual tests to complete the accessibility assessments. Use the{" "} - IBM Equal Access Toolkit as - a guide. -

    -

    Screen shot of an Accessibility Checker Report -

    -

    7.2 MS Excel Spreadsheet report

    +
    +
      +
    1. The scan date and time
    2. +
    3. The scanned URL
    4. +
    5. Percentage of elements with no detected violations or items to review
    6. +
    7. A summary of test results
    8. +
    9. Issue details organized by requirements, element roles, and rules
    10. +
    11. ‘Learn more’ link with detailed description for each issue
    12. +
    +
    +
    +

    + Important Note: + + This percentage is based on automated tests only. Be sure to perform additional reviews and manual tests to complete the accessibility assessments. Use the IBM Equal Access Toolkit as a guide. +

    +
    +

    MS Excel Spreadsheet report

    - This is a 5 sheet spreadsheet report. It can describe a single scan, or multiple scans. + Both single scans or multiple scans can generate a five sheet spreadsheet report.

    -
      + an excel spreadsheet of an accessibility scan report +
      +
      1. -

        +

        Overview includes the name of the tool with its version, the scan date, ruleset, guidelines and platform used for the scan, and a summary of the overall results across all included scans.

      2. +
      3. -

        +

        Scan summary provides an overview of the set of scans within the report.

      4. +
      5. -

        +

        Issue Summary provides an overview of the issues found across all the scans. Issues are summarized in a prioritized order, starting with Level 1 items, as defined in the IBM Equal Access Toolkit, followed by Level 2 and Levels 3 @@ -1501,53 +872,161 @@ class UsingACApp extends React.Component<{}, UsingACAppState> { and Recommendations. Counts are provided for each type of issue.

      6. +
      7. -

        +

        Issues has the details of the individual issues. This includes the scan label assigned to the scan, an ID for each issue, relevant accessibility requirements, and toolkit levels.

      8. +
      9. -

        +

        Definition of fields defines the columns in the other sheets.

      -

      - In a multi-scan report where the same page is scanned several times, - issues may be repeated across scans of the same page. These duplicate - issues can be identified by having the same Issue ID. Where a site template - or reused component has issues, these will also be repeated in the report, - but may have different Issue IDs. +

      +
      +

      + Important note: If the same page is scanned multiple times in a multi-scan report, there may be duplicate issues, which can be identified by having the same Issue ID. If a template or reused component has issues, these will also be repeated in the report, but may have different Issue IDs.

      +
      + +

      7. Accessibility considerations

      +
      +

      + Highlighted below are several accessibility features for adaptability and to ensure ease of access to the Checker functionality, including with keyboard or with a screen reader: +

      +
      + +
      +
        +
      1. The Accessibility Checker is responsive to users’ preferred font size and colors.
      2. +
      3. + The tool is fully keyboard accessible, tab order is as follows: +
        +
          +
        1. ‘Scan’ button
        2. +
        3. ‘Reports’ dropdown menu
        4. +
        5. +

          ‘Focus view’ switcher, + left right arrow keys{" "} toggles +

          +
        6. +
        7. +

          Issue filter checkboxes, + enter key{" "} selects checkbox +

        8. +
        9. +

          Issue view tab list, left right arrow keys{" "} navigates between tabs +

          +
        10. +
        11. Issue groupings associated with each requirement (element role or rule)
        12. +
            +
          • +

            + enter key{" "} + opens/closes issue grouping +

            +
          • +
          • +

            + within open grouping, + tab key{" "} + to navigate to each issue +

            +
          • +
          • +

            + enter key{" "} + selects issue +

            +
          • +
          +
        13. ‘Learn more’ link or the next issue
        14. +
        +
        +
      4. +
      5. Use the heading hierarchy or implemented landmarks to navigate:
      6. +
        +
        +
          +
        • The Assessment or Checker view’s main landmark includes: +
          +
          +
            +
          • - Issue count region (by issue type and the total number of issues found)

          • +
          • - Issue list region (issues grouped by element roles/requirements/rules)

          • +
          • - In the Checker view, the main landmark also contains issue help and the overview of stored scans, when those are requested by the user

          • +
          +
          +
        • +
          +
        • The Assessment view Scan Summary aside or the complementary landmark contains the scan summary, after the scan completes or shows the issue help when any issue is selected.

        • +
          +
        • The Assessment view Issue Help aside or the complementary landmark contains the issue help when any issue is selected.

        • +
        +
        +
      +
      +

      8. Feedback

      Visit the{" "} Equal Access git repository to: -
        -
      1. -

        - Report a problem with the checker tool. +

        +

          +
        • +

          + report a problem with the tool

        • -
        • -

          - Report a problem with the checker rules or accuracy of the errors reported by the checker. +

        • +

          + report a problem with the checker rules or accuracy of the errors reported by the tool

        • -
        • -

          - Find information on any existing issues. +

        • +

          + find information on any existing issues

        • -
      +
      +

      9. Troubleshooting

      +

      + If the Accessibility Checker appears unresponsive: +

      +

        +
      • Close the browser DevTools
      • +
      • Clear browser cookies
      • +
      • Refresh the page
      • +
      • Reopen the browser DevTools
      • +
      • Click the 'Scan' button
      • +
      +

      -
    ); diff --git a/accessibility-checker-extension/src/ts/util/contextScriptMessaging.ts b/accessibility-checker-extension/src/ts/util/contextScriptMessaging.ts new file mode 100644 index 000000000..0fae076f7 --- /dev/null +++ b/accessibility-checker-extension/src/ts/util/contextScriptMessaging.ts @@ -0,0 +1,55 @@ +/****************************************************************************** + Copyright:: 2020- IBM, Inc + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + *****************************************************************************/ + +import CommonMessaging from "./commonMessaging"; +// import BrowserDetection from "./browserDetection"; +//import { report } from 'process'; + +export default class ContextScriptMessaging { + + public static addListener(type: string, listener: (message: any) => Promise) { + CommonMessaging.addListener(type, listener); + } + + public static sendToBackground(type: string, message: any): Promise { + // let myMessage = JSON.parse(JSON.stringify(message)); + let myMessage = message + myMessage.type = type; + + + return new Promise((resolve, reject) => { + setTimeout(() => { + chrome.runtime.sendMessage(myMessage, async function (res) { + if (chrome.runtime.lastError) { + reject(chrome.runtime.lastError.message); + } else { + if (res) { + if (typeof res === "string") { + try { + res = JSON.parse(res); + } catch (e) { } + } + resolve(res); + } else { + resolve(); + } + } + }); + }, 0); + }) + } + +} diff --git a/accessibility-checker-extension/src/ts/util/xpath.js b/accessibility-checker-extension/src/ts/util/xpath.js new file mode 100644 index 000000000..56cd45b25 --- /dev/null +++ b/accessibility-checker-extension/src/ts/util/xpath.js @@ -0,0 +1,56 @@ +// Taken from https://stackoverflow.com/a/47088726/3429133. +function getAbsoluteXPath(element) { + var comp, comps = []; + var parent = null; + var xpath = ''; + var getPos = function(element) { + var position = 1, + curNode; + if (element.nodeType == Node.ATTRIBUTE_NODE) { + return null; + } + for (curNode = element.previousSibling; curNode; curNode = curNode.previousSibling) { + if (curNode.nodeName == element.nodeName) { + ++position; + } + } + return position; + }; + + if (element instanceof Document) { + return '/'; + } + + for (; element && !(element instanceof Document); element = element.nodeType == Node.ATTRIBUTE_NODE ? element.ownerElement : element.parentNode) { + comp = comps[comps.length] = {}; + switch (element.nodeType) { + case Node.TEXT_NODE: + comp.name = 'text()'; + break; + case Node.ATTRIBUTE_NODE: + comp.name = '@' + element.nodeName; + break; + case Node.PROCESSING_INSTRUCTION_NODE: + comp.name = 'processing-instruction()'; + break; + case Node.COMMENT_NODE: + comp.name = 'comment()'; + break; + case Node.ELEMENT_NODE: + comp.name = element.nodeName; + break; + } + comp.position = getPos(element); + } + + for (var i = comps.length - 1; i >= 0; i--) { + comp = comps[i]; + xpath += '/' + comp.name.toLowerCase(); + if (comp.position !== null) { + xpath += '[' + comp.position + ']'; + } + } + return xpath; +} + +export default {getAbsoluteXPath} \ No newline at end of file diff --git a/accessibility-checker-extension/webpack.config.js b/accessibility-checker-extension/webpack.config.js index 59f992f8e..1fb0d577a 100644 --- a/accessibility-checker-extension/webpack.config.js +++ b/accessibility-checker-extension/webpack.config.js @@ -37,6 +37,7 @@ module.exports = { devtools: path.join(sourceRootPath, 'ts', 'devtools', 'index.tsx'), devtoolsPanel: path.join(sourceRootPath, 'ts', 'devtoolsPanel', 'index.tsx'), devtoolsSubpanel: path.join(sourceRootPath, 'ts', 'devtoolsSubpanel', 'index.tsx'), + draw: path.join(sourceRootPath, 'ts', 'contentScripts', 'index.ts'), tabListeners: path.join(sourceRootPath, 'ts', 'tab', 'tabListeners.ts'), usingAC: path.join(sourceRootPath, 'ts', 'usingAC', 'index.tsx'), ...contentScripts, diff --git a/report-react/src/IReport.tsx b/report-react/src/IReport.tsx index eac8db955..a5d83329a 100644 --- a/report-react/src/IReport.tsx +++ b/report-react/src/IReport.tsx @@ -72,5 +72,11 @@ export const valueMap: { [key: string]: { [key2: string]: string } } = { "FAIL": "Recommendation", "PASS": "Pass", "MANUAL": "Recommendation" + }, + "INFORMATION": { + "POTENTIAL": "Needs review", + "FAIL": "Violation", + "PASS": "Pass", + "MANUAL": "Recommendation" } }; \ No newline at end of file diff --git a/reportTabStops.scss b/reportTabStops.scss new file mode 100644 index 000000000..f8e5a4e97 --- /dev/null +++ b/reportTabStops.scss @@ -0,0 +1,12 @@ +/* Emulate Gap Support with Flexbox and Margins */ +.emulated-flex-gap { + --gap: 12px; + display: inline-flex; + flex-wrap: wrap; + margin: calc(-1 * var(--gap)) 0 0 calc(-1 * var(--gap)); + width: calc(100% + var(--gap)); + } + + .emulated-flex-gap > * { + margin: var(--gap) 0 0 var(--gap); + } \ No newline at end of file