mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-10 21:54:47 +08:00
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 <jalyn@microsoft.com> Reviewed-by: Robert Paveza <Rob.Paveza@microsoft.com>
This commit is contained in:
@@ -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<!Item>} */
|
||||
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() {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -321,6 +321,9 @@
|
||||
<message name="IDS_DEVTOOLS_9db6195620a7743228ce74addf1414d8" desc="Text in Debugger Plugin of the Sources panel">
|
||||
Associated files should be added to the file tree. You can debug these resolved source files as regular JavaScript files.
|
||||
</message>
|
||||
<message name="IDS_DEVTOOLS_9e72db64445e9083fe77c30ce28ac067" desc="Text exposed to screen reader when navigating through a blackboxed call frame in the sources panel.">
|
||||
blackboxed
|
||||
</message>
|
||||
<message name="IDS_DEVTOOLS_9f7753821d867a81273a50de2b1e0cac" desc="Title of an action under the Debugger category that can be invoked through the Command Menu">
|
||||
Pause script execution
|
||||
</message>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user