From 7cd600f9d2ed1b5f3dbce9ea71bf2a98d5f150b2 Mon Sep 17 00:00:00 2001 From: Jack Lynch Date: Mon, 13 Jan 2020 14:48:57 -0800 Subject: [PATCH] Reland: Make call stack keyboard navigable This is a reland of https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/1880515, which was reverted at https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/1961190 due to test failures that do not repro or appear to be related. This PR makes the call stack sidebar pane in the Sources panel more accessible by: - enabling keyboard navigation of the call frame list by managing call frame selection outside of ListControl as suggested by Joel in [2] - adding the aria-disabled attribute to unselectable call frames - adding an aria description to blackboxed call frames This will break several web tests, so [3] must be merged first to disable them. Originally reviewed in [1], where the web test fixes for this CL now live. Video showing navigation: https://i.imgur.com/55tJy2k.mp4 [1] https://chromium-review.googlesource.com/c/chromium/src/+/1715121 [2] https://chromium-review.googlesource.com/c/chromium/src/+/1690089/11#message-3e9e158cce4628b01398d9cf84e37550b55b14c6 [3] https://chromium-review.googlesource.com/c/chromium/src/+/1893117 Bug: 963183 Change-Id: I826e1c5c2f78c235823a71c956fde6ef73bcccac Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/1965855 Commit-Queue: Jack Lynch Reviewed-by: Robert Paveza --- front_end/sources/CallStackSidebarPane.js | 95 +++++++++++++++++----- front_end/sources/callStackSidebarPane.css | 11 ++- front_end/sources/sources_strings.grdp | 3 + front_end/ui/ARIAUtils.js | 8 ++ 4 files changed, 97 insertions(+), 20 deletions(-) diff --git a/front_end/sources/CallStackSidebarPane.js b/front_end/sources/CallStackSidebarPane.js index 1d3ee45201..d991ee8ad7 100644 --- a/front_end/sources/CallStackSidebarPane.js +++ b/front_end/sources/CallStackSidebarPane.js @@ -38,6 +38,7 @@ export default class CallStackSidebarPane extends UI.SimpleView { this._notPausedMessageElement = this.contentElement.createChild('div', 'gray-info-message'); this._notPausedMessageElement.textContent = Common.UIString('Not paused'); + this._notPausedMessageElement.tabIndex = -1; /** @type {!UI.ListModel} */ this._items = new UI.ListModel(); @@ -45,7 +46,13 @@ export default class CallStackSidebarPane extends UI.SimpleView { this._list = new UI.ListControl(this._items, this, UI.ListMode.NonViewport); this.contentElement.appendChild(this._list.element); this._list.element.addEventListener('contextmenu', this._onContextMenu.bind(this), false); - this._list.element.addEventListener('click', this._onClick.bind(this), false); + self.onInvokeElement(this._list.element, event => { + const item = this._list.itemForNode(/** @type {?Node} */ (event.target)); + if (item) { + this._activateItem(item); + event.consume(true); + } + }); this._showMoreMessageElement = this._createShowMoreMessageElement(); this._showMoreMessageElement.classList.add('hidden'); @@ -84,6 +91,7 @@ export default class CallStackSidebarPane extends UI.SimpleView { const details = UI.context.flavor(SDK.DebuggerPausedDetails); if (!details) { + this.setDefaultFocusedElement(this._notPausedMessageElement); this._notPausedMessageElement.classList.remove('hidden'); this._blackboxedMessageElement.classList.add('hidden'); this._showMoreMessageElement.classList.add('hidden'); @@ -141,6 +149,10 @@ export default class CallStackSidebarPane extends UI.SimpleView { this._items.replaceAll(items); if (this._maxAsyncStackChainDepth === _defaultMaxAsyncStackChainDepth) { this._list.selectNextItem(true /* canWrap */, false /* center */); + const selectedItem = this._list.selectedItem(); + if (selectedItem) { + this._activateItem(selectedItem); + } } this._updatedForTest(); } @@ -203,9 +215,20 @@ export default class CallStackSidebarPane extends UI.SimpleView { linkElement.textContent = item.linkText.trimMiddle(30); linkElement.title = item.linkText; element.classList.toggle('blackboxed-call-frame', item.isBlackboxed); + if (item.isBlackboxed) { + UI.ARIAUtils.setDescription(element, ls`blackboxed`); + } + if (!item[Sources.CallStackSidebarPane._debuggerCallFrameSymbol]) { + UI.ARIAUtils.setDisabled(element, true); + } } + const isSelected = + item[Sources.CallStackSidebarPane._debuggerCallFrameSymbol] === UI.context.flavor(SDK.DebuggerModel.CallFrame); + element.classList.toggle('selected', isSelected); + UI.ARIAUtils.setSelected(element, isSelected); element.classList.toggle('hidden', !this._showBlackboxed && item.isBlackboxed); element.appendChild(UI.Icon.create('smallicon-thick-right-arrow', 'selected-call-frame-icon')); + element.tabIndex = item === this._list.selectedItem() ? 0 : -1; return element; } @@ -225,7 +248,7 @@ export default class CallStackSidebarPane extends UI.SimpleView { * @return {boolean} */ isItemSelectable(item) { - return !!item[_debuggerCallFrameSymbol]; + return true; } /** @@ -237,13 +260,14 @@ export default class CallStackSidebarPane extends UI.SimpleView { */ selectedItemChanged(from, to, fromElement, toElement) { if (fromElement) { - fromElement.classList.remove('selected'); + fromElement.tabIndex = -1; } if (toElement) { - toElement.classList.add('selected'); - } - if (to) { - this._activateItem(to); + this.setDefaultFocusedElement(toElement); + toElement.tabIndex = 0; + if (this.hasFocus()) { + toElement.focus(); + } } } @@ -254,7 +278,7 @@ export default class CallStackSidebarPane extends UI.SimpleView { * @return {boolean} */ updateSelectedItemARIA(fromElement, toElement) { - return false; + return true; } /** @@ -265,13 +289,17 @@ export default class CallStackSidebarPane extends UI.SimpleView { element.createChild('span'); const showAllLink = element.createChild('span', 'link'); showAllLink.textContent = Common.UIString('Show blackboxed frames'); - showAllLink.addEventListener('click', () => { + UI.ARIAUtils.markAsLink(showAllLink); + showAllLink.tabIndex = 0; + const showAll = () => { this._showBlackboxed = true; for (const item of this._items) { this._refreshItem(item); } this._blackboxedMessageElement.classList.toggle('hidden', true); - }); + }; + showAllLink.addEventListener('click', showAll); + showAllLink.addEventListener('keydown', event => isEnterKey(event) && showAll()); return element; } @@ -328,15 +356,34 @@ export default class CallStackSidebarPane extends UI.SimpleView { if (this._muteActivateItem || !uiLocation) { return; } + this._list.selectItem(item); const debuggerCallFrame = item[_debuggerCallFrameSymbol]; - if (debuggerCallFrame && UI.context.flavor(SDK.DebuggerModel.CallFrame) !== debuggerCallFrame) { + const oldItem = this.activeCallFrameItem(); + if (debuggerCallFrame && oldItem !== item) { debuggerCallFrame.debuggerModel.setSelectedCallFrame(debuggerCallFrame); UI.context.setFlavor(SDK.DebuggerModel.CallFrame, debuggerCallFrame); + if (oldItem) { + this._refreshItem(oldItem); + } + this._refreshItem(item); } else { Common.Revealer.reveal(uiLocation); } } + /** + * @return {?Sources.CallStackSidebarPane.Item} + */ + activeCallFrameItem() { + const callFrame = UI.context.flavor(SDK.DebuggerModel.CallFrame); + if (callFrame) { + return this._items.find( + callFrameItem => callFrameItem[Sources.CallStackSidebarPane._debuggerCallFrameSymbol] === callFrame) || + null; + } + return null; + } + /** * @param {!UI.ContextMenu} contextMenu * @param {!Workspace.UISourceCode} uiSourceCode @@ -374,18 +421,28 @@ export default class CallStackSidebarPane extends UI.SimpleView { } } - /** - * @return {boolean} - */ _selectNextCallFrameOnStack() { - return this._list.selectNextItem(false /* canWrap */, false /* center */); + const oldItem = this.activeCallFrameItem(); + const startIndex = oldItem ? this._items.indexOf(oldItem) + 1 : 0; + for (let i = startIndex; i < this._items.length; i++) { + const newItem = this._items.at(i); + if (newItem[Sources.CallStackSidebarPane._debuggerCallFrameSymbol]) { + this._activateItem(newItem); + break; + } + } } - /** - * @return {boolean} - */ _selectPreviousCallFrameOnStack() { - return this._list.selectPreviousItem(false /* canWrap */, false /* center */); + const oldItem = this.activeCallFrameItem(); + const startIndex = oldItem ? this._items.indexOf(oldItem) - 1 : this._items.length - 1; + for (let i = startIndex; i >= 0; i--) { + const newItem = this._items.at(i); + if (newItem[Sources.CallStackSidebarPane._debuggerCallFrameSymbol]) { + this._activateItem(newItem); + break; + } + } } _copyStackTrace() { diff --git a/front_end/sources/callStackSidebarPane.css b/front_end/sources/callStackSidebarPane.css index 66d967fc0a..1f0b6c0aaf 100644 --- a/front_end/sources/callStackSidebarPane.css +++ b/front_end/sources/callStackSidebarPane.css @@ -16,6 +16,10 @@ margin-left: 5px; } +.blackboxed-message > .link:focus { + outline-width: unset; +} + .show-more-message { text-align: center; font-style: italic; @@ -49,6 +53,11 @@ background-color: #eee; } +.call-frame-item[data-keyboard-focus="true"]:focus, +.call-frame-item.async-header[data-keyboard-focus="true"]:focus .call-frame-item-title { + background-color: var(--focus-bg-color); +} + .async-header + .call-frame-item { border-top: 0; } @@ -71,7 +80,6 @@ border-top: 1px solid #d8d8d8; margin-top: 8px; position: absolute; - z-index: -1; left: 0; } @@ -81,6 +89,7 @@ background-color: white; margin-left: -5px; padding: 0 5px; + z-index: 1; } .blackboxed-call-frame { diff --git a/front_end/sources/sources_strings.grdp b/front_end/sources/sources_strings.grdp index f8056236ea..cafd8fe3a6 100644 --- a/front_end/sources/sources_strings.grdp +++ b/front_end/sources/sources_strings.grdp @@ -321,6 +321,9 @@ Associated files should be added to the file tree. You can debug these resolved source files as regular JavaScript files. + + blackboxed + Pause script execution diff --git a/front_end/ui/ARIAUtils.js b/front_end/ui/ARIAUtils.js index f12f171bf3..db315a9602 100644 --- a/front_end/ui/ARIAUtils.js +++ b/front_end/ui/ARIAUtils.js @@ -340,6 +340,14 @@ export function setCheckboxAsIndeterminate(element) { element.setAttribute('aria-checked', 'mixed'); } +/** + * @param {!Element} element + * @param {boolean} value + */ +export function setDisabled(element, value) { + element.setAttribute('aria-disabled', !!value); +} + /** * @param {!Element} element * @param {boolean} value