diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /browser/components/newtab/content-src/components/ContextMenu | |
parent | Initial commit. (diff) | |
download | thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'browser/components/newtab/content-src/components/ContextMenu')
3 files changed, 305 insertions, 0 deletions
diff --git a/browser/components/newtab/content-src/components/ContextMenu/ContextMenu.jsx b/browser/components/newtab/content-src/components/ContextMenu/ContextMenu.jsx new file mode 100644 index 0000000000..5ea6a57f71 --- /dev/null +++ b/browser/components/newtab/content-src/components/ContextMenu/ContextMenu.jsx @@ -0,0 +1,176 @@ +/* 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/. */ + +import React from "react"; +import { connect } from "react-redux"; + +export class ContextMenu extends React.PureComponent { + constructor(props) { + super(props); + this.hideContext = this.hideContext.bind(this); + this.onShow = this.onShow.bind(this); + this.onClick = this.onClick.bind(this); + } + + hideContext() { + this.props.onUpdate(false); + } + + onShow() { + if (this.props.onShow) { + this.props.onShow(); + } + } + + componentDidMount() { + this.onShow(); + setTimeout(() => { + global.addEventListener("click", this.hideContext); + }, 0); + } + + componentWillUnmount() { + global.removeEventListener("click", this.hideContext); + } + + onClick(event) { + // Eat all clicks on the context menu so they don't bubble up to window. + // This prevents the context menu from closing when clicking disabled items + // or the separators. + event.stopPropagation(); + } + + render() { + // Disabling focus on the menu span allows the first tab to focus on the first menu item instead of the wrapper. + return ( + // eslint-disable-next-line jsx-a11y/interactive-supports-focus + <span className="context-menu"> + <ul + role="menu" + onClick={this.onClick} + onKeyDown={this.onClick} + className="context-menu-list" + > + {this.props.options.map((option, i) => + option.type === "separator" ? ( + <li key={i} className="separator" role="separator" /> + ) : ( + option.type !== "empty" && ( + <ContextMenuItem + key={i} + option={option} + hideContext={this.hideContext} + keyboardAccess={this.props.keyboardAccess} + /> + ) + ) + )} + </ul> + </span> + ); + } +} + +export class _ContextMenuItem extends React.PureComponent { + constructor(props) { + super(props); + this.onClick = this.onClick.bind(this); + this.onKeyDown = this.onKeyDown.bind(this); + this.onKeyUp = this.onKeyUp.bind(this); + this.focusFirst = this.focusFirst.bind(this); + } + + onClick(event) { + this.props.hideContext(); + this.props.option.onClick(event); + } + + // Focus the first menu item if the menu was accessed via the keyboard. + focusFirst(button) { + if (this.props.keyboardAccess && button) { + button.focus(); + } + } + + // This selects the correct node based on the key pressed + focusSibling(target, key) { + const parent = target.parentNode; + const closestSiblingSelector = + key === "ArrowUp" ? "previousSibling" : "nextSibling"; + if (!parent[closestSiblingSelector]) { + return; + } + if (parent[closestSiblingSelector].firstElementChild) { + parent[closestSiblingSelector].firstElementChild.focus(); + } else { + parent[closestSiblingSelector][ + closestSiblingSelector + ].firstElementChild.focus(); + } + } + + onKeyDown(event) { + const { option } = this.props; + switch (event.key) { + case "Tab": + // tab goes down in context menu, shift + tab goes up in context menu + // if we're on the last item, one more tab will close the context menu + // similarly, if we're on the first item, one more shift + tab will close it + if ( + (event.shiftKey && option.first) || + (!event.shiftKey && option.last) + ) { + this.props.hideContext(); + } + break; + case "ArrowUp": + case "ArrowDown": + event.preventDefault(); + this.focusSibling(event.target, event.key); + break; + case "Enter": + case " ": + event.preventDefault(); + this.props.hideContext(); + option.onClick(); + break; + case "Escape": + this.props.hideContext(); + break; + } + } + + // Prevents the default behavior of spacebar + // scrolling the page & auto-triggering buttons. + onKeyUp(event) { + if (event.key === " ") { + event.preventDefault(); + } + } + + render() { + const { option } = this.props; + return ( + <li role="presentation" className="context-menu-item"> + <button + className={option.disabled ? "disabled" : ""} + role="menuitem" + onClick={this.onClick} + onKeyDown={this.onKeyDown} + onKeyUp={this.onKeyUp} + ref={option.first ? this.focusFirst : null} + aria-haspopup={ + option.id === "newtab-menu-edit-topsites" ? "dialog" : null + } + > + <span data-l10n-id={option.string_id || option.id} /> + </button> + </li> + ); + } +} + +export const ContextMenuItem = connect(state => ({ + Prefs: state.Prefs, +}))(_ContextMenuItem); diff --git a/browser/components/newtab/content-src/components/ContextMenu/ContextMenuButton.jsx b/browser/components/newtab/content-src/components/ContextMenu/ContextMenuButton.jsx new file mode 100644 index 0000000000..0364f5386a --- /dev/null +++ b/browser/components/newtab/content-src/components/ContextMenu/ContextMenuButton.jsx @@ -0,0 +1,72 @@ +/* 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/. */ + +import React from "react"; + +export class ContextMenuButton extends React.PureComponent { + constructor(props) { + super(props); + this.state = { + showContextMenu: false, + contextMenuKeyboard: false, + }; + this.onClick = this.onClick.bind(this); + this.onKeyDown = this.onKeyDown.bind(this); + this.onUpdate = this.onUpdate.bind(this); + } + + openContextMenu(isKeyBoard, event) { + if (this.props.onUpdate) { + this.props.onUpdate(true); + } + this.setState({ + showContextMenu: true, + contextMenuKeyboard: isKeyBoard, + }); + } + + onClick(event) { + event.preventDefault(); + this.openContextMenu(false, event); + } + + onKeyDown(event) { + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + this.openContextMenu(true, event); + } + } + + onUpdate(showContextMenu) { + if (this.props.onUpdate) { + this.props.onUpdate(showContextMenu); + } + this.setState({ showContextMenu }); + } + + render() { + const { tooltipArgs, tooltip, children, refFunction } = this.props; + const { showContextMenu, contextMenuKeyboard } = this.state; + + return ( + <React.Fragment> + <button + aria-haspopup="true" + data-l10n-id={tooltip} + data-l10n-args={tooltipArgs ? JSON.stringify(tooltipArgs) : null} + className="context-menu-button icon" + onKeyDown={this.onKeyDown} + onClick={this.onClick} + ref={refFunction} + /> + {showContextMenu + ? React.cloneElement(children, { + keyboardAccess: contextMenuKeyboard, + onUpdate: this.onUpdate, + }) + : null} + </React.Fragment> + ); + } +} diff --git a/browser/components/newtab/content-src/components/ContextMenu/_ContextMenu.scss b/browser/components/newtab/content-src/components/ContextMenu/_ContextMenu.scss new file mode 100644 index 0000000000..c0074128e6 --- /dev/null +++ b/browser/components/newtab/content-src/components/ContextMenu/_ContextMenu.scss @@ -0,0 +1,57 @@ +@use 'sass:math'; + +.context-menu { + background: var(--newtab-background-color-secondary); + border-radius: $context-menu-border-radius; + box-shadow: $context-menu-shadow; + display: block; + font-size: $context-menu-font-size; + margin-inline-start: 5px; + inset-inline-start: 100%; + position: absolute; + top: math.div($context-menu-button-size, 4); + z-index: 8; + + > ul { + list-style: none; + margin: 0; + padding: $context-menu-outer-padding 0; + + > li { + margin: 0; + width: 100%; + + &.separator { + border-bottom: $border-secondary; + margin: $context-menu-outer-padding 0; + } + + > a, + > button { + align-items: center; + color: inherit; + cursor: pointer; + display: flex; + width: 100%; + line-height: 16px; + outline: none; + border: 0; + padding: $context-menu-item-padding; + white-space: nowrap; + + &:is(:focus, :hover) { + background: var(--newtab-element-secondary-hover-color); + } + + &:active { + background: var(--newtab-element-secondary-active-color); + } + + &.disabled { + opacity: 0.4; + pointer-events: none; + } + } + } + } +} |