diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /devtools/client/shared/components/menu/MenuList.js | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/shared/components/menu/MenuList.js')
-rw-r--r-- | devtools/client/shared/components/menu/MenuList.js | 164 |
1 files changed, 164 insertions, 0 deletions
diff --git a/devtools/client/shared/components/menu/MenuList.js b/devtools/client/shared/components/menu/MenuList.js new file mode 100644 index 0000000000..4c355cca10 --- /dev/null +++ b/devtools/client/shared/components/menu/MenuList.js @@ -0,0 +1,164 @@ +/* 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.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const { div } = dom; +const { + focusableSelector, +} = require("resource://devtools/client/shared/focus.js"); + +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(e) { + 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(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(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; |