diff --git a/front_end/browser_debugger/DOMBreakpointsSidebarPane.js b/front_end/browser_debugger/DOMBreakpointsSidebarPane.js
index e888440781..0a0d43f7bd 100644
--- a/front_end/browser_debugger/DOMBreakpointsSidebarPane.js
+++ b/front_end/browser_debugger/DOMBreakpointsSidebarPane.js
@@ -30,18 +30,25 @@
/**
* @implements {UI.ContextFlavorListener}
+ * @implements {UI.ListDelegate}
*/
export class DOMBreakpointsSidebarPane extends UI.VBox {
constructor() {
super(true);
this.registerRequiredCSS('browser_debugger/domBreakpointsSidebarPane.css');
- this._listElement = this.contentElement.createChild('div', 'breakpoint-list hidden');
this._emptyElement = this.contentElement.createChild('div', 'gray-info-message');
this._emptyElement.textContent = Common.UIString('No breakpoints');
+ /** @type {!UI.ListModel.} */
+ this._breakpoints = new UI.ListModel();
+ /** @type {!UI.ListControl.} */
+ this._list = new UI.ListControl(this._breakpoints, this, UI.ListMode.NonViewport);
+ this.contentElement.appendChild(this._list.element);
+ this._list.element.classList.add('breakpoint-list', 'hidden');
+ UI.ARIAUtils.markAsList(this._list.element);
+ UI.ARIAUtils.setAccessibleName(this._list.element, ls`DOM Breakpoints list`);
+ this._emptyElement.tabIndex = -1;
- /** @type {!Map} */
- this._items = new Map();
SDK.targetManager.addModelListener(
SDK.DOMDebuggerModel, SDK.DOMDebuggerModel.Events.DOMBreakpointAdded, this._breakpointAdded, this);
SDK.targetManager.addModelListener(
@@ -56,10 +63,114 @@ export class DOMBreakpointsSidebarPane extends UI.VBox {
}
}
- this._highlightedElement = null;
+ this._highlightedBreakpoint = null;
this._update();
}
+ /**
+ * @override
+ * @param {!SDK.DOMDebuggerModel.DOMBreakpoint} item
+ * @return {!Element}
+ */
+ createElementForItem(item) {
+ const element = createElementWithClass('div', 'breakpoint-entry');
+ element.addEventListener('contextmenu', this._contextMenu.bind(this, item), true);
+ UI.ARIAUtils.markAsListitem(element);
+ element.tabIndex = this._list.selectedItem() === item ? 0 : -1;
+
+ const checkboxLabel = UI.CheckboxLabel.create(/* title */ '', item.enabled);
+ const checkboxElement = checkboxLabel.checkboxElement;
+ checkboxElement.addEventListener('click', this._checkboxClicked.bind(this, item), false);
+ checkboxElement.tabIndex = -1;
+ UI.ARIAUtils.markAsHidden(checkboxLabel);
+ element.appendChild(checkboxLabel);
+
+ const labelElement = createElementWithClass('div', 'dom-breakpoint');
+ element.appendChild(labelElement);
+ element.addEventListener('keydown', event => {
+ if (event.key === ' ') {
+ checkboxElement.click();
+ event.consume(true);
+ }
+ });
+
+ const description = createElement('div');
+ const breakpointTypeLabel = BrowserDebugger.DOMBreakpointsSidebarPane.BreakpointTypeLabels.get(item.type);
+ description.textContent = breakpointTypeLabel;
+ const linkifiedNode = createElementWithClass('monospace');
+ linkifiedNode.style.display = 'block';
+ labelElement.appendChild(linkifiedNode);
+ Common.Linkifier.linkify(item.node, {preventKeyboardFocus: true}).then(linkified => {
+ linkifiedNode.appendChild(linkified);
+ UI.ARIAUtils.setAccessibleName(checkboxElement, ls`${breakpointTypeLabel}: ${linkified.deepTextContent()}`);
+ });
+
+ labelElement.appendChild(description);
+
+ const checkedStateText = item.enabled ? ls`checked` : ls`unchecked`;
+ if (item === this._highlightedBreakpoint) {
+ element.classList.add('breakpoint-hit');
+ UI.ARIAUtils.setDescription(element, ls`${checkedStateText} breakpoint hit`);
+ } else {
+ UI.ARIAUtils.setDescription(element, checkedStateText);
+ }
+
+
+ this._emptyElement.classList.add('hidden');
+ this._list.element.classList.remove('hidden');
+
+ return element;
+ }
+
+ /**
+ * @override
+ * @param {!SDK.DOMDebuggerModel.DOMBreakpoint} item
+ * @return {number}
+ */
+ heightForItem(item) {
+ return 0;
+ }
+
+ /**
+ * @override
+ * @param {!SDK.DOMDebuggerModel.DOMBreakpoint} item
+ * @return {boolean}
+ */
+ isItemSelectable(item) {
+ return true;
+ }
+
+ /**
+ * @override
+ * @param {?Element} fromElement
+ * @param {?Element} toElement
+ * @return {boolean}
+ */
+ updateSelectedItemARIA(fromElement, toElement) {
+ return true;
+ }
+
+ /**
+ * @override
+ * @param {?SDK.DOMDebuggerModel.DOMBreakpoint} from
+ * @param {?SDK.DOMDebuggerModel.DOMBreakpoint} to
+ * @param {?Element} fromElement
+ * @param {?Element} toElement
+ */
+ selectedItemChanged(from, to, fromElement, toElement) {
+ if (fromElement) {
+ fromElement.tabIndex = -1;
+ }
+
+ if (toElement) {
+ this.setDefaultFocusedElement(toElement);
+ toElement.tabIndex = 0;
+ if (this.hasFocus()) {
+ toElement.focus();
+ }
+ }
+ }
+
/**
* @param {!Common.Event} event
*/
@@ -71,10 +182,11 @@ export class DOMBreakpointsSidebarPane extends UI.VBox {
* @param {!Common.Event} event
*/
_breakpointToggled(event) {
+ const hadFocus = this.hasFocus();
const breakpoint = /** @type {!SDK.DOMDebuggerModel.DOMBreakpoint} */ (event.data);
- const item = this._items.get(breakpoint);
- if (item) {
- item.checkbox.checked = breakpoint.enabled;
+ this._list.refreshItem(breakpoint);
+ if (hadFocus) {
+ this.focus();
}
}
@@ -82,17 +194,28 @@ export class DOMBreakpointsSidebarPane extends UI.VBox {
* @param {!Common.Event} event
*/
_breakpointsRemoved(event) {
+ const hadFocus = this.hasFocus();
const breakpoints = /** @type {!Array} */ (event.data);
+ let lastIndex = -1;
for (const breakpoint of breakpoints) {
- const item = this._items.get(breakpoint);
- if (item) {
- this._items.delete(breakpoint);
- this._listElement.removeChild(item.element);
+ const index = this._breakpoints.indexOf(breakpoint);
+ if (index >= 0) {
+ this._breakpoints.remove(index);
+ lastIndex = index;
}
}
- if (!this._listElement.firstChild) {
+ if (this._breakpoints.length === 0) {
this._emptyElement.classList.remove('hidden');
- this._listElement.classList.add('hidden');
+ this.setDefaultFocusedElement(this._emptyElement);
+ this._list.element.classList.add('hidden');
+ } else if (lastIndex >= 0) {
+ const breakpointToSelect = this._breakpoints.at(lastIndex);
+ if (breakpointToSelect) {
+ this._list.selectItem(breakpointToSelect);
+ }
+ }
+ if (hadFocus) {
+ this.focus();
}
}
@@ -100,43 +223,18 @@ export class DOMBreakpointsSidebarPane extends UI.VBox {
* @param {!SDK.DOMDebuggerModel.DOMBreakpoint} breakpoint
*/
_addBreakpoint(breakpoint) {
- const element = createElementWithClass('div', 'breakpoint-entry');
- element.addEventListener('contextmenu', this._contextMenu.bind(this, breakpoint), true);
-
- const checkboxLabel = UI.CheckboxLabel.create('', breakpoint.enabled);
- const checkboxElement = checkboxLabel.checkboxElement;
- checkboxElement.addEventListener('click', this._checkboxClicked.bind(this, breakpoint), false);
- element.appendChild(checkboxLabel);
-
- const labelElement = createElementWithClass('div', 'dom-breakpoint');
- element.appendChild(labelElement);
-
- const description = createElement('div');
- const breakpointTypeLabel = BreakpointTypeLabels.get(breakpoint.type);
- description.textContent = breakpointTypeLabel;
- const linkifiedNode = createElementWithClass('monospace');
- linkifiedNode.style.display = 'block';
- labelElement.appendChild(linkifiedNode);
- Common.Linkifier.linkify(breakpoint.node).then(linkified => {
- linkifiedNode.appendChild(linkified);
- UI.ARIAUtils.setAccessibleName(checkboxElement, ls`${breakpointTypeLabel}: ${linkified.deepTextContent()}`);
- });
- labelElement.appendChild(description);
-
- const item = {breakpoint: breakpoint, element: element, checkbox: checkboxElement};
- element._item = item;
- this._items.set(breakpoint, item);
-
- let currentElement = this._listElement.firstChild;
- while (currentElement) {
- if (currentElement._item && currentElement._item.breakpoint.type < breakpoint.type) {
- break;
+ this._breakpoints.insertWithComparator(breakpoint, (breakpointA, breakpointB) => {
+ if (breakpointA.type > breakpointB.type) {
+ return -1;
}
- currentElement = currentElement.nextSibling;
+ if (breakpointA.type < breakpointB.type) {
+ return 1;
+ }
+ return 0;
+ });
+ if (!this.hasFocus()) {
+ this._list.selectItem(this._breakpoints.at(0));
}
- this._listElement.insertBefore(element, currentElement);
- this._emptyElement.classList.add('hidden');
- this._listElement.classList.remove('hidden');
}
/**
@@ -145,6 +243,8 @@ export class DOMBreakpointsSidebarPane extends UI.VBox {
*/
_contextMenu(breakpoint, event) {
const contextMenu = new UI.ContextMenu(event);
+ contextMenu.defaultSection().appendItem(
+ ls`Reveal DOM node in Elements panel`, Common.Revealer.reveal.bind(null, breakpoint.node));
contextMenu.defaultSection().appendItem(Common.UIString('Remove breakpoint'), () => {
breakpoint.domDebuggerModel.removeDOMBreakpoint(breakpoint.node, breakpoint.type);
});
@@ -156,13 +256,10 @@ export class DOMBreakpointsSidebarPane extends UI.VBox {
/**
* @param {!SDK.DOMDebuggerModel.DOMBreakpoint} breakpoint
+ * @param {!Event} event
*/
- _checkboxClicked(breakpoint) {
- const item = this._items.get(breakpoint);
- if (!item) {
- return;
- }
- breakpoint.domDebuggerModel.toggleDOMBreakpoint(breakpoint, item.checkbox.checked);
+ _checkboxClicked(breakpoint, event) {
+ breakpoint.domDebuggerModel.toggleDOMBreakpoint(breakpoint, event.target.checked);
}
/**
@@ -175,13 +272,15 @@ export class DOMBreakpointsSidebarPane extends UI.VBox {
_update() {
const details = UI.context.flavor(SDK.DebuggerPausedDetails);
+ if (this._highlightedBreakpoint) {
+ const oldHighlightedBreakpoint = this._highlightedBreakpoint;
+ delete this._highlightedBreakpoint;
+ this._list.refreshItem(oldHighlightedBreakpoint);
+ }
if (!details || !details.auxData || details.reason !== SDK.DebuggerModel.BreakReason.DOM) {
- if (this._highlightedElement) {
- this._highlightedElement.classList.remove('breakpoint-hit');
- delete this._highlightedElement;
- }
return;
}
+
const domDebuggerModel = details.debuggerModel.target().model(SDK.DOMDebuggerModel);
if (!domDebuggerModel) {
return;
@@ -191,18 +290,15 @@ export class DOMBreakpointsSidebarPane extends UI.VBox {
return;
}
- let element = null;
- for (const item of this._items.values()) {
- if (item.breakpoint.node === data.node && item.breakpoint.type === data.type) {
- element = item.element;
+ for (const breakpoint of this._breakpoints) {
+ if (breakpoint.node === data.node && breakpoint.type === data.type) {
+ this._highlightedBreakpoint = breakpoint;
}
}
- if (!element) {
- return;
+ if (this._highlightedBreakpoint) {
+ this._list.refreshItem(this._highlightedBreakpoint);
}
UI.viewManager.showView('sources.domBreakpoints');
- element.classList.add('breakpoint-hit');
- this._highlightedElement = element;
}
}
diff --git a/front_end/browser_debugger/browser_debugger_strings.grdp b/front_end/browser_debugger/browser_debugger_strings.grdp
index 7cd1d3edd8..c4cf304f0a 100644
--- a/front_end/browser_debugger/browser_debugger_strings.grdp
+++ b/front_end/browser_debugger/browser_debugger_strings.grdp
@@ -15,6 +15,9 @@
breakpoint hit
+
+ Reveal DOM node in Elements panel
+
Break on
@@ -27,6 +30,9 @@
Refresh global listeners
+
+ $1schecked breakpoint hit
+
Any XHR or fetch
@@ -39,6 +45,9 @@
Global Listeners
+
+ DOM Breakpoints list
+
Event Listener Breakpoints
diff --git a/front_end/browser_debugger/domBreakpointsSidebarPane.css b/front_end/browser_debugger/domBreakpointsSidebarPane.css
index d5622c2f12..f9a34b21e8 100644
--- a/front_end/browser_debugger/domBreakpointsSidebarPane.css
+++ b/front_end/browser_debugger/domBreakpointsSidebarPane.css
@@ -21,6 +21,10 @@
padding: 2px 0;
}
+.breakpoint-entry[data-keyboard-focus="true"] {
+ background-color: var(--focus-bg-color);
+}
+
.breakpoint-list .breakpoint-entry:hover {
background-color: #eee;
}
diff --git a/front_end/langpacks/shared_strings.grdp b/front_end/langpacks/shared_strings.grdp
index 1c90a1d469..3b2e65912d 100644
--- a/front_end/langpacks/shared_strings.grdp
+++ b/front_end/langpacks/shared_strings.grdp
@@ -124,6 +124,9 @@
JS Heap
+
+ checked
+
N/A
@@ -427,6 +430,9 @@
Collapse children
+
+ unchecked
+
Closure
diff --git a/front_end/ui/ListControl.js b/front_end/ui/ListControl.js
index b54ff8d4ec..19b3603e48 100644
--- a/front_end/ui/ListControl.js
+++ b/front_end/ui/ListControl.js
@@ -489,7 +489,6 @@ export default class ListControl {
const newItem = this._selectedItem;
const newElement = this._selectedIndex !== -1 ? this._elementAtIndex(index) : null;
- UI.ARIAUtils.setActiveDescendant(this.element, newElement);
this._delegate.selectedItemChanged(oldItem, newItem, /** @type {?Element} */ (oldElement), newElement);
if (!this._delegate.updateSelectedItemARIA(/** @type {?Element} */ (oldElement), newElement)) {
if (oldElement) {
diff --git a/front_end/ui/ui_strings.grdp b/front_end/ui/ui_strings.grdp
index 8c0421e48e..d5937847f8 100644
--- a/front_end/ui/ui_strings.grdp
+++ b/front_end/ui/ui_strings.grdp
@@ -63,9 +63,6 @@
Async Call
-
- checked
-
Open in new tab
@@ -240,9 +237,6 @@
Once page is reloaded, DevTools will automatically reconnect.
-
- unchecked
-
more
diff --git a/test/webtests/http/tests/devtools/a11y-axe-core/sources/dom-breakpoints-pane-a11y-test-expected.txt b/test/webtests/http/tests/devtools/a11y-axe-core/sources/dom-breakpoints-pane-a11y-test-expected.txt
index 65648a94fc..cc84d71b0f 100644
--- a/test/webtests/http/tests/devtools/a11y-axe-core/sources/dom-breakpoints-pane-a11y-test-expected.txt
+++ b/test/webtests/http/tests/devtools/a11y-axe-core/sources/dom-breakpoints-pane-a11y-test-expected.txt
@@ -1,39 +1,8 @@
Testing accessibility in the DOM breakpoints pane.
Setting DOM breakpoints.
-DOM breakpoints container text content: DOM BreakpointsSubtree modifiedNode removedNo breakpoints
-DOM breakpoints pane text content: Subtree modifiedNode removedNo breakpoints
+DOM breakpoints container text content: DOM BreakpointsNo breakpointsSubtree modifiedcheckedNode removedunchecked
+DOM breakpoints pane text content: No breakpointsSubtree modifiedcheckedNode removedunchecked
Running the axe-core linter on the DOM breakpoints pane.
-aXe violations: [
- {
- "ruleDescription": "Ensures every form element has a label",
- "helpUrl": "https://dequeuniversity.com/rules/axe/3.3/label?application=axeAPI",
- "ruleId": "label",
- "impact": "critical",
- "failedNodes": [
- {
- "target": [
- [
- ".flex-none.flex-auto.vbox:nth-child(7) > .flex-auto.vbox",
- ".breakpoint-entry:nth-child(1) > span[is=\"dt-checkbox\"]",
- "#ui-checkbox-label9"
- ]
- ],
- "html": "",
- "failureSummary": "Fix any of the following:\n aria-label attribute does not exist or is empty\n aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty\n Form element does not have an implicit (wrapped)