forked from GoogleChrome/lighthouse
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
report: split topbar features (GoogleChrome#12926)
- Loading branch information
1 parent
18daaab
commit 5ee3dc7
Showing
10 changed files
with
771 additions
and
674 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
Oops, something went wrong.