168 lines
4.4 KiB
JavaScript
168 lines
4.4 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
/* eslint-env browser */
|
|
"use strict";
|
|
|
|
// A list of menu items.
|
|
//
|
|
// This component provides keyboard navigation amongst any focusable
|
|
// children.
|
|
|
|
const {
|
|
Children,
|
|
PureComponent,
|
|
} = require("resource://devtools/client/shared/vendor/react.mjs");
|
|
const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs");
|
|
const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
|
|
const { div } = dom;
|
|
|
|
const lazy = {};
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
focusableSelector: "resource://devtools/client/shared/focus.mjs",
|
|
});
|
|
|
|
class MenuList extends PureComponent {
|
|
static get propTypes() {
|
|
return {
|
|
// ID to assign to the list container.
|
|
id: PropTypes.string,
|
|
|
|
// Children of the list.
|
|
children: PropTypes.any,
|
|
|
|
// Called whenever there is a change to the hovered or selected child.
|
|
// The callback is passed the ID of the highlighted child or null if no
|
|
// child is highlighted.
|
|
onHighlightedChildChange: PropTypes.func,
|
|
};
|
|
}
|
|
|
|
constructor(props) {
|
|
super(props);
|
|
|
|
this.onKeyDown = this.onKeyDown.bind(this);
|
|
this.onMouseOverOrFocus = this.onMouseOverOrFocus.bind(this);
|
|
this.onMouseOutOrBlur = this.onMouseOutOrBlur.bind(this);
|
|
this.notifyHighlightedChildChange =
|
|
this.notifyHighlightedChildChange.bind(this);
|
|
|
|
this.setWrapperRef = element => {
|
|
this.wrapperRef = element;
|
|
};
|
|
}
|
|
|
|
onMouseOverOrFocus(e) {
|
|
this.notifyHighlightedChildChange(e.target.id);
|
|
}
|
|
|
|
onMouseOutOrBlur() {
|
|
const hoveredElem = this.wrapperRef.querySelector(":hover");
|
|
if (!hoveredElem) {
|
|
this.notifyHighlightedChildChange(null);
|
|
}
|
|
}
|
|
|
|
notifyHighlightedChildChange(id) {
|
|
if (this.props.onHighlightedChildChange) {
|
|
this.props.onHighlightedChildChange(id);
|
|
}
|
|
}
|
|
|
|
onKeyDown(e) {
|
|
// Check if the focus is in the list.
|
|
if (
|
|
!this.wrapperRef ||
|
|
!this.wrapperRef.contains(e.target.ownerDocument.activeElement)
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const getTabList = () =>
|
|
Array.from(this.wrapperRef.querySelectorAll(lazy.focusableSelector));
|
|
|
|
switch (e.key) {
|
|
case "Tab":
|
|
case "ArrowUp":
|
|
case "ArrowDown":
|
|
{
|
|
const tabList = getTabList();
|
|
const currentElement = e.target.ownerDocument.activeElement;
|
|
const currentIndex = tabList.indexOf(currentElement);
|
|
if (currentIndex !== -1) {
|
|
let nextIndex;
|
|
if (e.key === "ArrowDown" || (e.key === "Tab" && !e.shiftKey)) {
|
|
nextIndex =
|
|
currentIndex === tabList.length - 1 ? 0 : currentIndex + 1;
|
|
} else {
|
|
nextIndex =
|
|
currentIndex === 0 ? tabList.length - 1 : currentIndex - 1;
|
|
}
|
|
tabList[nextIndex].focus();
|
|
e.preventDefault();
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "Home":
|
|
{
|
|
const firstItem = this.wrapperRef.querySelector(
|
|
lazy.focusableSelector
|
|
);
|
|
if (firstItem) {
|
|
firstItem.focus();
|
|
e.preventDefault();
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "End":
|
|
{
|
|
const tabList = getTabList();
|
|
if (tabList.length) {
|
|
tabList[tabList.length - 1].focus();
|
|
e.preventDefault();
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
render() {
|
|
const attr = {
|
|
role: "menu",
|
|
ref: this.setWrapperRef,
|
|
onKeyDown: this.onKeyDown,
|
|
onMouseOver: this.onMouseOverOrFocus,
|
|
onMouseOut: this.onMouseOutOrBlur,
|
|
onFocus: this.onMouseOverOrFocus,
|
|
onBlur: this.onMouseOutOrBlur,
|
|
className: "menu-standard-padding",
|
|
};
|
|
|
|
if (this.props.id) {
|
|
attr.id = this.props.id;
|
|
}
|
|
|
|
// Add padding for checkbox image if necessary.
|
|
let hasCheckbox = false;
|
|
Children.forEach(this.props.children, (child, i) => {
|
|
if (child == null || typeof child == "undefined") {
|
|
console.warn("MenuList children at index", i, "is", child);
|
|
return;
|
|
}
|
|
|
|
if (typeof child?.props?.checked !== "undefined") {
|
|
hasCheckbox = true;
|
|
}
|
|
});
|
|
if (hasCheckbox) {
|
|
attr.className = "checkbox-container menu-standard-padding";
|
|
}
|
|
|
|
return div(attr, this.props.children);
|
|
}
|
|
}
|
|
|
|
module.exports = MenuList;
|