/* 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/. */
// Return a copy of |obj| minus |fields|. const omit = (obj, fields) => { const objCopy = { ...obj }; for (const field of fields) { delete objCopy[field];
} return objCopy;
};
class MenuButton extends PureComponent { static get propTypes() { return { // The toolbox document that will be used for rendering the menu popup.
toolboxDoc: PropTypes.object.isRequired,
// A text content for the button.
label: PropTypes.string,
// Optional, either: // - false or missing if no icon should be displayed // - true if an icon should be displayed and is set via CSS // - a string set to the URL of the icon to associate with the MenuButton // e.g. chrome://devtools/skin/image/foo.svg
icon: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]),
// An optional ID to assign to the menu's container tooltip object.
menuId: PropTypes.string,
// The preferred side of the anchor element to display the menu. // Defaults to "bottom".
menuPosition: PropTypes.string.isRequired,
// The offset of the menu from the anchor element. // Defaults to -5.
menuOffset: PropTypes.number.isRequired,
// The menu content.
children: PropTypes.any,
// Callback function to be invoked when the button is clicked.
onClick: PropTypes.func,
// Callback function to be invoked when the child panel is closed.
onCloseButton: PropTypes.func,
};
}
this.state = {
expanded: false, // In tests, initialize the menu immediately.
isMenuInitialized: flags.testing || false,
win: props.toolboxDoc.defaultView.top,
}; this.ignoreNextClick = false;
this.initializeTooltip();
}
componentDidMount() { if (!this.state.isMenuInitialized) { // Initialize the menu when the button is focused or moused over. for (const event of ["focus", "mousemove"]) { this.buttonRef.current.addEventListener(
event,
() => { if (!this.state.isMenuInitialized) { this.setState({ isMenuInitialized: true });
}
},
{ once: true }
);
}
}
}
// FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
UNSAFE_componentWillReceiveProps(nextProps) { // If the window changes, we need to regenerate the HTMLTooltip or else the // XUL wrapper element will appear above (in terms of z-index) the old // window, and not the new. const win = nextProps.toolboxDoc.defaultView.top; if (
nextProps.toolboxDoc !== this.props.toolboxDoc || this.state.win !== win ||
nextProps.menuId !== this.props.menuId
) { this.setState({ win }); this.resetTooltip(); this.initializeTooltip();
}
}
componentDidUpdate() { // The MenuButton creates the child panel when initializing the MenuButton. // If the children function is called during the rendering process, // this child list size might change. So we need to adjust content size here. if (typeofthis.props.children === "function") { this.resizeContent();
}
}
if (this.props.menuId) {
tooltipProps.id = this.props.menuId;
}
this.tooltip = new HTMLTooltip(this.props.toolboxDoc, tooltipProps); this.tooltip.on("hidden", this.onHidden);
}
async resetTooltip() { if (!this.tooltip) { return;
}
// Mark the menu as closed since the onHidden callback may not be called in // this case. this.setState({ expanded: false }); this.tooltip.off("hidden", this.onHidden); this.tooltip.destroy(); this.tooltip = null;
}
// Used by the call site to indicate that the menu content has changed so // its container should be updated.
resizeContent() { if (!this.state.expanded || !this.tooltip || !this.buttonRef.current) { return;
}
// When we are closing the menu we will get a 'hidden' event before we get // a 'click' event. We want to re-enable the pointer-events: auto setting we // use on the button while the menu is visible, but we don't want to do it // until after the subsequent click event since otherwise we will end up // re-opening the menu. // // For mouse events, we achieve this by using setTimeout(..., 0) to schedule // a separate task to run after the click event, but in the case of touch // events the event order differs and the setTimeout callback will run before // the click event. // // In order to prevent that we detect touch events and set a flag to ignore // the next click event. However, we need to differentiate between touch drag // events and long press events (which don't generate a 'click') and "taps" // (which do). We do that by looking for a 'touchmove' event and clearing the // flag if we get one.
onTouchStart(evt) { const touchend = () => { const anchorRect = this.buttonRef.current.getClientRects()[0]; const { clientX, clientY } = evt.changedTouches[0]; // We need to check that the click is inside the bounds since when the // menu is being closed the button will currently have // pointer-events: none (and if we don't check the bounds we will end up // ignoring unrelated clicks). if (
anchorRect.x <= clientX &&
clientX <= anchorRect.x + anchorRect.width &&
anchorRect.y <= clientY &&
clientY <= anchorRect.y + anchorRect.height
) { this.ignoreNextClick = true;
}
};
onHidden() { this.setState({ expanded: false }); // While the menu is open, if we click _anywhere_ outside the menu, it will // automatically close. This is performed by the XUL wrapper before we get // any chance to see any event. To avoid immediately re-opening the menu // when we process the subsequent click event on this button, we set // 'pointer-events: none' on the button while the menu is open. // // After the menu is closed we need to remove the pointer-events style (so // the button works again) but we don't want to do it immediately since the // "popuphidden" event which triggers this callback might be dispatched // before the "click" event that we want to ignore. As a result, we queue // up a task using setTimeout() to run after the "click" event. this.state.win.setTimeout(() => { if (this.buttonRef.current) { this.buttonRef.current.style.pointerEvents = "auto";
} this.state.win.removeEventListener("touchstart", this.onTouchStart, true);
}, 0);
if (e.target === this.buttonRef.current) { // On Mac, even after clicking the button it doesn't get focus. // Force focus to the button so that our keydown handlers get called. this.buttonRef.current.focus();
if (this.props.onClick) { this.props.onClick(e);
}
if (!e.defaultPrevented) { const wasKeyboardEvent = e.screenX === 0 && e.screenY === 0; // If the popup menu will be shown, disable this button in order to // prevent reopening the popup menu. See extended comment in onHidden(). // above. // // Also, we should _not_ set 'pointer-events: none' if // ui.popup.disable_autohide pref is in effect since, in that case, // there's no redundant hiding behavior and we actually want clicking // the button to close the menu. if (
!this.state.expanded &&
!Services.prefs.getBoolPref("ui.popup.disable_autohide", false)
) { this.buttonRef.current.style.pointerEvents = "none";
}
await this.toggleMenu(e.target); // If the menu was activated by keyboard, focus the first item. if (wasKeyboardEvent && this.tooltip) { this.tooltip.focus();
}
// MenuButton creates the children dynamically when clicking the button, // so execute the goggle menu after updating the children panel. if (typeofthis.props.children === "function") { this.forceUpdate();
}
} // If we clicked one of the menu items, then, by default, we should // auto-collapse the menu. // // We check for the defaultPrevented state, however, so that menu items can // turn this behavior off (e.g. a menu item with an embedded button).
} elseif ( this.state.expanded &&
!e.defaultPrevented &&
e.target.matches(focusableSelector)
) { this.hideMenu();
}
}
onKeyDown(e) { if (!this.state.expanded) { return;
}
case"Tab": case"ArrowDown": if (isButtonFocussed && this.tooltip) { if (this.tooltip.focus()) {
e.preventDefault();
}
} break;
case"ArrowUp": if (isButtonFocussed && this.tooltip) { if (this.tooltip.focusEnd()) {
e.preventDefault();
}
} break; case"t": if ((isMacOS && e.metaKey) || (!isMacOS && e.ctrlKey)) { // Close the menu if the user opens a new tab while it is still open. // // Bug 1499271: Once toolbox has been converted to XUL we should watch // for the 'visibilitychange' event instead of explicitly looking for // Ctrl+T. this.hideMenu();
} break;
}
}
render() { const buttonProps = { // Pass through any props set on the button, except the ones we handle // here.
...omit(this.props, Object.keys(MenuButton.propTypes)),
onClick: this.onClick, "aria-expanded": this.state.expanded, "aria-haspopup": "menu",
ref: this.buttonRef,
};
if (this.state.expanded) {
buttonProps.onKeyDown = this.onKeyDown;
}
if (this.props.menuId) {
buttonProps["aria-controls"] = this.props.menuId;
}
if (this.props.icon) { const iconClass = "menu-button--iconic";
buttonProps.className = buttonProps.className
? `${buttonProps.className} ${iconClass}`
: iconClass; // `icon` may be a boolean and the icon URL will be set in CSS. if (typeofthis.props.icon == "string") {
buttonProps.style = { "--menuitem-icon-image": "url(" + this.props.icon + ")",
};
}
}
if (this.state.isMenuInitialized) { const menu = createPortal( typeofthis.props.children === "function"
? this.props.children()
: this.props.children, this.tooltip.panel
);
¤ Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.0.23Bemerkung:
(vorverarbeitet)
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung ist noch experimentell.