Skip to content

Commit

Permalink
report: split topbar features (GoogleChrome#12926)
Browse files Browse the repository at this point in the history
  • Loading branch information
connorjclark authored and satya-nutella committed Sep 7, 2021
1 parent 18daaab commit 5ee3dc7
Show file tree
Hide file tree
Showing 10 changed files with 771 additions and 674 deletions.
3 changes: 2 additions & 1 deletion report/clients/psi.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {DetailsRenderer} from '../renderer/details-renderer.js';
import {DOM} from '../renderer/dom.js';
import {ElementScreenshotRenderer} from '../renderer/element-screenshot-renderer.js';
import {I18n} from '../renderer/i18n.js';
import {openTreemap} from '../renderer/open-tab.js';
import {PerformanceCategoryRenderer} from '../renderer/performance-category-renderer.js';
import {ReportUIFeatures} from '../renderer/report-ui-features.js';
import {Util} from '../renderer/util.js';
Expand Down Expand Up @@ -125,7 +126,7 @@ export function prepareLabData(LHResult, document) {
container: buttonContainer,
text: Util.i18n.strings.viewTreemapLabel,
icon: 'treemap',
onClick: () => ReportUIFeatures.openTreemap(lhResult),
onClick: () => openTreemap(lhResult),
});
}
};
Expand Down
13 changes: 13 additions & 0 deletions report/renderer/dom.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
*/
'use strict';

/* eslint-env browser */

/** @typedef {HTMLElementTagNameMap & {[id: string]: HTMLElement}} HTMLElementByTagName */
/** @template {string} T @typedef {import('typed-query-selector/parser').ParseSelector<T, Element>} ParseSelector */

Expand Down Expand Up @@ -254,4 +256,15 @@ export class DOM {
const elements = Array.from(context.querySelectorAll(query));
return elements;
}

/**
* Fires a custom DOM event on target.
* @param {string} name Name of the event.
* @param {Node=} target DOM node to fire the event on.
* @param {*=} detail Custom data to include.
*/
fireEventOn(name, target = this._document, detail) {
const event = new CustomEvent(name, detail ? {detail} : undefined);
target.dispatchEvent(event);
}
}
215 changes: 215 additions & 0 deletions report/renderer/drop-down-menu.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
/**
* @license Copyright 2021 The Lighthouse Authors. All Rights Reserved.
* 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.
*/
'use strict';

/* eslint-env browser */

/** @typedef {import('./dom.js').DOM} DOM */

export class DropDownMenu {
/**
* @param {DOM} dom
*/
constructor(dom) {
/** @type {DOM} */
this._dom = dom;
/** @type {HTMLElement} */
this._toggleEl; // eslint-disable-line no-unused-expressions
/** @type {HTMLElement} */
this._menuEl; // eslint-disable-line no-unused-expressions

this.onDocumentKeyDown = this.onDocumentKeyDown.bind(this);
this.onToggleClick = this.onToggleClick.bind(this);
this.onToggleKeydown = this.onToggleKeydown.bind(this);
this.onMenuFocusOut = this.onMenuFocusOut.bind(this);
this.onMenuKeydown = this.onMenuKeydown.bind(this);

this._getNextMenuItem = this._getNextMenuItem.bind(this);
this._getNextSelectableNode = this._getNextSelectableNode.bind(this);
this._getPreviousMenuItem = this._getPreviousMenuItem.bind(this);
}

/**
* @param {function(MouseEvent): any} menuClickHandler
*/
setup(menuClickHandler) {
this._toggleEl = this._dom.find('button.lh-tools__button', this._dom.document());
this._toggleEl.addEventListener('click', this.onToggleClick);
this._toggleEl.addEventListener('keydown', this.onToggleKeydown);

this._menuEl = this._dom.find('div.lh-tools__dropdown', this._dom.document());
this._menuEl.addEventListener('keydown', this.onMenuKeydown);
this._menuEl.addEventListener('click', menuClickHandler);
}

close() {
this._toggleEl.classList.remove('lh-active');
this._toggleEl.setAttribute('aria-expanded', 'false');
if (this._menuEl.contains(this._dom.document().activeElement)) {
// Refocus on the tools button if the drop down last had focus
this._toggleEl.focus();
}
this._menuEl.removeEventListener('focusout', this.onMenuFocusOut);
this._dom.document().removeEventListener('keydown', this.onDocumentKeyDown);
}

/**
* @param {HTMLElement} firstFocusElement
*/
open(firstFocusElement) {
if (this._toggleEl.classList.contains('lh-active')) {
// If the drop down is already open focus on the element
firstFocusElement.focus();
} else {
// Wait for drop down transition to complete so options are focusable.
this._menuEl.addEventListener('transitionend', () => {
firstFocusElement.focus();
}, {once: true});
}

this._toggleEl.classList.add('lh-active');
this._toggleEl.setAttribute('aria-expanded', 'true');
this._menuEl.addEventListener('focusout', this.onMenuFocusOut);
this._dom.document().addEventListener('keydown', this.onDocumentKeyDown);
}

/**
* Click handler for tools button.
* @param {Event} e
*/
onToggleClick(e) {
e.preventDefault();
e.stopImmediatePropagation();

if (this._toggleEl.classList.contains('lh-active')) {
this.close();
} else {
this.open(this._getNextMenuItem());
}
}

/**
* Handler for tool button.
* @param {KeyboardEvent} e
*/
onToggleKeydown(e) {
switch (e.code) {
case 'ArrowUp':
e.preventDefault();
this.open(this._getPreviousMenuItem());
break;
case 'ArrowDown':
case 'Enter':
case ' ':
e.preventDefault();
this.open(this._getNextMenuItem());
break;
default:
// no op
}
}

/**
* Handler for tool DropDown.
* @param {KeyboardEvent} e
*/
onMenuKeydown(e) {
const el = /** @type {?HTMLElement} */ (e.target);

switch (e.code) {
case 'ArrowUp':
e.preventDefault();
this._getPreviousMenuItem(el).focus();
break;
case 'ArrowDown':
e.preventDefault();
this._getNextMenuItem(el).focus();
break;
case 'Home':
e.preventDefault();
this._getNextMenuItem().focus();
break;
case 'End':
e.preventDefault();
this._getPreviousMenuItem().focus();
break;
default:
// no op
}
}

/**
* Keydown handler for the document.
* @param {KeyboardEvent} e
*/
onDocumentKeyDown(e) {
if (e.keyCode === 27) { // ESC
this.close();
}
}

/**
* Focus out handler for the drop down menu.
* @param {FocusEvent} e
*/
onMenuFocusOut(e) {
const focusedEl = /** @type {?HTMLElement} */ (e.relatedTarget);

if (!this._menuEl.contains(focusedEl)) {
this.close();
}
}

/**
* @param {Array<Node>} allNodes
* @param {?HTMLElement=} startNode
* @return {HTMLElement}
*/
_getNextSelectableNode(allNodes, startNode) {
const nodes = allNodes.filter(/** @return {node is HTMLElement} */ (node) => {
if (!(node instanceof HTMLElement)) {
return false;
}

// 'Save as Gist' option may be disabled.
if (node.hasAttribute('disabled')) {
return false;
}

// 'Save as Gist' option may have display none.
if (window.getComputedStyle(node).display === 'none') {
return false;
}

return true;
});

let nextIndex = startNode ? (nodes.indexOf(startNode) + 1) : 0;
if (nextIndex >= nodes.length) {
nextIndex = 0;
}

return nodes[nextIndex];
}

/**
* @param {?HTMLElement=} startEl
* @return {HTMLElement}
*/
_getNextMenuItem(startEl) {
const nodes = Array.from(this._menuEl.childNodes);
return this._getNextSelectableNode(nodes, startEl);
}

/**
* @param {?HTMLElement=} startEl
* @return {HTMLElement}
*/
_getPreviousMenuItem(startEl) {
const nodes = Array.from(this._menuEl.childNodes).reverse();
return this._getNextSelectableNode(nodes, startEl);
}
}
26 changes: 26 additions & 0 deletions report/renderer/features-util.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* @license Copyright 2021 The Lighthouse Authors. All Rights Reserved.
* 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.
*/
'use strict';

/* eslint-env browser */

/** @typedef {import('./dom.js').DOM} DOM */

/**
* @param {DOM} dom
* @param {boolean} [force]
*/
export function toggleDarkTheme(dom, force) {
const el = dom.find('.lh-vars', dom.document());
// This seems unnecessary, but in DevTools, passing "undefined" as the second
// parameter acts like passing "false".
// https://github.com/ChromeDevTools/devtools-frontend/blob/dd6a6d4153647c2a4203c327c595692c5e0a4256/front_end/dom_extension/DOMExtension.js#L809-L819
if (typeof force === 'undefined') {
el.classList.toggle('lh-dark');
} else {
el.classList.toggle('lh-dark', force);
}
}
Loading

0 comments on commit 5ee3dc7

Please sign in to comment.