Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,12 @@ export default class DomPathUtils {
let slotParts = parts[2].match(/(slot\[\d+\])\/([^[]*)\[(\d+)\]/)!;
let slot = this.docDomPathToElement(doc, parts[1]+slotParts[1]);
let count = parseInt(slotParts[3]);
for (let slotIdx=0; slotIdx < (slot as any).assignedNodes().length; ++slotIdx) {
let slotNode = (slot as any).assignedNodes()[slotIdx];
// Use assignedElements() if available, otherwise filter assignedNodes() for element nodes
let assignedElems = (slot as any).assignedElements
? (slot as any).assignedElements()
: (slot as any).assignedNodes().filter((n: Node) => n.nodeType === 1);
for (let slotIdx=0; slotIdx < assignedElems.length; ++slotIdx) {
let slotNode = assignedElems[slotIdx];
if (slotNode.nodeName.toLowerCase() === slotParts[2].toLowerCase()) {
--count;
if (count === 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
MenuItemSelectable
} from "@carbon/react";
import { UtilIssue } from '../../util/UtilIssue';
import { PathMatcher } from '../../util/PathMatcher';

import {
Information
Expand Down Expand Up @@ -190,11 +191,11 @@ export class ReportSection extends React.Component<ReportSectionProps, ReportSec
reportIssues = reportIssues.filter((issue: UIIssue) => {
issue.ignored = this.state.ignoredIssues.some(ignoredIssue => issueBaselineMatch(ignoredIssue, issue));
const checked = this.devtoolsAppController.getLevelFilters();
let retVal = ( ((checked["Hidden"] && issue.ignored) || checked[UtilIssue.valueToStringSingular(issue.value) as eFilterLevel])
let retVal = ( ((checked["Hidden"] && issue.ignored) || checked[UtilIssue.valueToStringSingular(issue.value) as eFilterLevel])
&& (!this.state.focusMode
|| !this.state.selectedPath
|| issue.path.dom.startsWith(this.state.selectedPath)
)
|| PathMatcher.matchesPath(issue.path.dom, this.state.selectedPath)
)
);
if (!checked["Hidden"] && issue.ignored) {
return false; // JCH is this an override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { ePanel, getDevtoolsController, ViewState } from '../devtoolsController'
import { UtilIssue } from '../../util/UtilIssue';
import { UtilIssueReact } from '../../util/UtilIssueReact';
import { getBGController, issueBaselineMatch } from '../../background/backgroundController';
import { PathMatcher } from '../../util/PathMatcher';

export interface IRowGroup {
id: string;
Expand Down Expand Up @@ -783,7 +784,7 @@ export class ReportTreeGrid<RowType extends IRowGroup> extends React.Component<R
let selectedNode: boolean = !!this.props.selectedPath
&& this.props.selectedPath === thisIssue.path.dom;
let selectedDescendant: boolean = !!this.props.selectedPath
&& thisIssue.path.dom.startsWith(this.props.selectedPath);
&& PathMatcher.matchesPath(thisIssue.path.dom, this.props.selectedPath);
let focused: boolean = this.state.tabRowId === rowId
bodyContent.push(
<Grid
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import ReactDOM from 'react-dom';
import { IIssue, IReport, UIIssue, eFilterLevel } from '../../interfaces/interfaces';
import { UtilIssue } from '../../util/UtilIssue';
import { UtilIssueReact } from '../../util/UtilIssueReact';
import { PathMatcher } from '../../util/PathMatcher';
import { getDevtoolsController, ScanningState, ViewState } from '../devtoolsController';
import { getBGController, issueBaselineMatch, TabChangeType } from '../../background/backgroundController';
import {
Expand Down Expand Up @@ -176,6 +177,7 @@ export class ScanSection extends React.Component<{}, ScanSectionState> {
})
this.devtoolsController.addSelectedElementPathListener(async (newPath) => {
this.setState( { selectedElemPath: newPath });
this.setPath(newPath);
})
this.devtoolsController.addFocusModeListener(async (newValue) => {
this.setState({ focusMode: newValue })
Expand All @@ -196,10 +198,12 @@ export class ScanSection extends React.Component<{}, ScanSectionState> {
}
})
this.reportListener((await this.devtoolsController.getReport())!);
this.setState({
viewState: (await this.devtoolsController.getViewState())!,
const currentPath = (await this.devtoolsController.getSelectedElementPath())!;
this.setState({
viewState: (await this.devtoolsController.getViewState())!,
storeReports: (await this.devtoolsController.getStoreReports()),
selectedElemPath: (await this.devtoolsController.getSelectedElementPath())! || "/html",
selectedElemPath: currentPath || "/html",
selectedPath: currentPath || null,
focusMode: (await this.devtoolsController.getFocusMode()),
storedReportsCount: (await this.devtoolsController.getStoredReportsMeta()).length,
canScan: (await this.bgController.getTabInfo(tabId)).canScan
Expand Down Expand Up @@ -259,7 +263,7 @@ export class ScanSection extends React.Component<{}, ScanSectionState> {
for (const issue of issues) {
let sing = UtilIssue.valueToStringSingular(issue.value);
++counts[sing as eLevel].total;
if (!this.state.selectedPath || issue.path.dom.startsWith(this.state.selectedPath)) {
if (!this.state.selectedPath || PathMatcher.matchesPath(issue.path.dom, this.state.selectedPath)) {
++counts[sing as eLevel].focused;
}
}
Expand All @@ -268,12 +272,12 @@ export class ScanSection extends React.Component<{}, ScanSectionState> {
for (const ignoredIssue of this.state.ignoredIssues) {
if (!issues.some(issue => issueBaselineMatch(issue, ignoredIssue))) continue;
++counts["Hidden" as eLevel].total;
if (!this.state.selectedPath || ignoredIssue.path.dom.startsWith(this.state.selectedPath)) {
if (!this.state.selectedPath || PathMatcher.matchesPath(ignoredIssue.path.dom, this.state.selectedPath)) {
++counts["Hidden" as eLevel].focused;
}
let sing = UtilIssue.valueToStringSingular(ignoredIssue.value);
--counts[sing as eLevel].total;
if (!this.state.selectedPath || ignoredIssue.path.dom.startsWith(this.state.selectedPath)) {
if (!this.state.selectedPath || PathMatcher.matchesPath(ignoredIssue.path.dom, this.state.selectedPath)) {
--counts[sing as eLevel].focused;
}
}
Expand All @@ -300,7 +304,7 @@ export class ScanSection extends React.Component<{}, ScanSectionState> {
let retVal = (this.state.checked[UtilIssue.valueToStringSingular(issue.value) as eLevel]
&& (!this.state.focusMode
|| !this.state.selectedPath
|| issue.path.dom.startsWith(this.state.selectedPath)
|| PathMatcher.matchesPath(issue.path.dom, this.state.selectedPath)
)
);
return retVal;
Expand Down
210 changes: 167 additions & 43 deletions accessibility-checker-extension/src/ts/devtools/devtoolsAppController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,58 +151,182 @@ export class DevtoolsAppController {
}

public hookSelectionChange() {
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;
const injectPathGenerator = () => {
chrome.devtools.inspectedWindow.eval(`
Object.defineProperty(window, '__getACEPath', {
value: function(node) {
function getIndex(n) {
if (n.assignedSlot) {
const assigned = n.assignedSlot.assignedElements
? n.assignedSlot.assignedElements()
: Array.from(n.assignedSlot.assignedNodes()).filter(x => x.nodeType === 1);
let count = 0;
for (const el of assigned) {
if (el === n) break;
if (el.localName === n.localName) count++;
}
return "/" + n.localName + "[" + (count + 1) + "]";
}
const parent = n.parentNode;
if (!parent) return "/" + n.localName + "[1]";
let count = 0;
const children = parent.children;
for (let i = 0; i < children.length; i++) {
if (children[i] === n) break;
if (children[i].localName === n.localName) count++;
}
return "/" + n.localName + "[" + (count + 1) + "]";
}

function getSlotIndex(slot) {
let count = 1;
let sib = slot.previousElementSibling;
while (sib) {
if (sib.localName === 'slot') count++;
sib = sib.previousElementSibling;
}
return count;
}

// Walk from an <iframe> element up to the top-level document, returning
// the path prefix for everything above the iframe boundary.
// Same-origin only: cross-origin contentDocument access throws and is caught.
function getIframePrefixPath(iframeElem) {
let prefix = "";
let current = iframeElem;
while (current && current.nodeType === 1) {
prefix = getIndex(current) + prefix;
const parent = current.parentNode;
if (!parent) break;
if (parent.nodeType === 9) {
// Reached the top-level document — stop.
break;
} else if (parent.nodeType === 11) {
prefix = "/#document-fragment[1]" + prefix;
current = parent.host;
} else if (parent.nodeType === 1) {
current = parent;
} else {
break;
}
}
return prefix;
}

try {
if (!node || node.nodeType !== 1) return "";

let segments = "";
let current = node;

while (current && current.nodeType === 1) {
const assignedSlot = current.assignedSlot;

if (assignedSlot) {
segments = getIndex(current) + segments;
segments = "/slot[" + getSlotIndex(assignedSlot) + "]" + segments;
const slotParent = assignedSlot.parentNode;
if (!slotParent) break;
if (slotParent.nodeType === 11) {
segments = "/#document-fragment[1]" + segments;
current = slotParent.host;
} else if (slotParent.nodeType === 9) {
// Slot's parent is a document — check if it belongs to a same-origin
// <iframe> and prepend that iframe's path prefix, exactly as the
// non-slot branch does.
try {
const parentWin = slotParent.defaultView;
if (parentWin && parentWin.parent && parentWin.parent !== parentWin) {
const parentDoc = parentWin.parent.document;
const iframes = parentDoc.querySelectorAll("iframe");
let iframeElem = null;
for (const iframe of iframes) {
try {
if (iframe.contentDocument === slotParent) {
iframeElem = iframe;
break;
}
} catch (e) {}
}
if (iframeElem) {
segments = getIframePrefixPath(iframeElem) + segments;
}
}
node = node.previousElementSibling;
} catch (e) {
// Cross-origin parent window access denied — stop here.
}
return "/"+findName.toLowerCase()+"["+count+"]";
break;
} else if (slotParent.nodeType === 1) {
current = slotParent;
} else {
break;
}
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) {}
} else {
segments = getIndex(current) + segments;
const parent = current.parentNode;
if (!parent) break;

if (parent.nodeType === 11) {
segments = "/#document-fragment[1]" + segments;
current = parent.host;
} else if (parent.nodeType === 9) {
// Reached a document boundary. Check if this document belongs
// to a same-origin <iframe> so we can prepend its path prefix.
try {
const parentWin = parent.defaultView;
if (parentWin && parentWin.parent && parentWin.parent !== parentWin) {
// We are inside a nested document. Find the <iframe> in
// the parent window whose contentDocument is this document.
const parentDoc = parentWin.parent.document;
const iframes = parentDoc.querySelectorAll("iframe");
let iframeElem = null;
for (const iframe of iframes) {
try {
if (iframe.contentDocument === parent) {
iframeElem = iframe;
break;
}
} catch (e) {
// Cross-origin iframe — skip silently.
}
} catch (e) {}
node = parentElement;
}
if (iframeElem) {
segments = getIframePrefixPath(iframeElem) + segments;
}
}
} catch (e) {
// Cross-origin parent window access denied — stop here.
}
return retVal;
} catch (err) {
console.error(err);
break;
} else if (parent.nodeType === 1) {
current = parent;
} else {
break;
}
}
}

return segments;
} catch(err) {
return "";
}
};
`);
};

injectPathGenerator();
chrome.devtools.network.onNavigated.addListener(() => {
injectPathGenerator();
});

chrome.devtools.panels.elements.onSelectionChanged.addListener(() => {
chrome.devtools.inspectedWindow.eval(
`window.__getACEPath && window.__getACEPath($0)`,
async (result: string) => {
await this.devToolsController.setSelectedElementPath(result, true);
}
})($0)`, async (result: string) => {
await this.devToolsController.setSelectedElementPath(result, true);
});
);
});
chrome.devtools.inspectedWindow.eval(`inspect(document.documentElement);`);
}

///////////////////////////////////////////////////////////////////////////
Expand Down
Loading
Loading