diff options
Diffstat (limited to 'toolkit/content/widgets')
42 files changed, 993 insertions, 181 deletions
diff --git a/toolkit/content/widgets/arrowscrollbox.js b/toolkit/content/widgets/arrowscrollbox.js index 28de96c8d7..7109891faf 100644 --- a/toolkit/content/widgets/arrowscrollbox.js +++ b/toolkit/content/widgets/arrowscrollbox.js @@ -21,7 +21,7 @@ return ` <html:link rel="stylesheet" href="chrome://global/skin/toolbarbutton.css"/> <html:link rel="stylesheet" href="chrome://global/skin/arrowscrollbox.css"/> - <toolbarbutton id="scrollbutton-up" part="scrollbutton-up" keyNav="false"/> + <toolbarbutton id="scrollbutton-up" part="scrollbutton-up" keyNav="false" data-l10n-id="overflow-scroll-button-up"/> <spacer part="overflow-start-indicator"/> <box class="scrollbox-clip" part="scrollbox-clip" flex="1"> <scrollbox part="scrollbox" flex="1"> @@ -29,7 +29,7 @@ </scrollbox> </box> <spacer part="overflow-end-indicator"/> - <toolbarbutton id="scrollbutton-down" part="scrollbutton-down" keyNav="false"/> + <toolbarbutton id="scrollbutton-down" part="scrollbutton-down" keyNav="false" data-l10n-id="overflow-scroll-button-down"/> `; } @@ -43,6 +43,8 @@ this._scrollButtonDown = this.shadowRoot.getElementById("scrollbutton-down"); + MozXULElement.insertFTLIfNeeded("toolkit/global/arrowscrollbox.ftl"); + this._arrowScrollAnim = { scrollbox: this, requestHandle: 0, @@ -134,6 +136,8 @@ } this.hasConnected = true; + document.l10n.connectRoot(this.shadowRoot); + if (!this.hasAttribute("smoothscroll")) { this.smoothScroll = Services.prefs.getBoolPref( "toolkit.scrollbox.smoothScroll", @@ -639,6 +643,7 @@ this._scrollTimer.cancel(); this._scrollTimer = null; } + document.l10n.disconnectRoot(this.shadowRoot); } on_wheel(event) { @@ -749,7 +754,7 @@ } } - on_touchend(event) { + on_touchend() { this._touchStart = -1; } @@ -804,12 +809,12 @@ this._updateScrollButtonsDisabledState(); } - on_scroll(event) { + on_scroll() { this._isScrolling = true; this._updateScrollButtonsDisabledState(); } - on_scrollend(event) { + on_scrollend() { this._isScrolling = false; this._destination = 0; this._direction = 0; diff --git a/toolkit/content/widgets/autocomplete-input.js b/toolkit/content/widgets/autocomplete-input.js index 36105ba4d7..a6fb4b5067 100644 --- a/toolkit/content/widgets/autocomplete-input.js +++ b/toolkit/content/widgets/autocomplete-input.js @@ -40,7 +40,7 @@ this.addEventListener( "compositionstart", - event => { + () => { if ( this.mController.input.wrappedJSObject == this.nsIAutocompleteInput ) { @@ -52,7 +52,7 @@ this.addEventListener( "compositionend", - event => { + () => { if ( this.mController.input.wrappedJSObject == this.nsIAutocompleteInput ) { @@ -64,7 +64,7 @@ this.addEventListener( "focus", - event => { + () => { this.attachController(); if ( window.gBrowser && @@ -82,7 +82,7 @@ this.addEventListener( "blur", - event => { + () => { if (!this._dontBlur) { if (this.forceComplete && this.mController.matchCount >= 1) { // If forceComplete is requested, we need to call the enter processing @@ -625,7 +625,7 @@ return value; } - onInput(aEvent) { + onInput() { if ( !this.mIgnoreInput && this.mController.input.wrappedJSObject == this.nsIAutocompleteInput diff --git a/toolkit/content/widgets/autocomplete-popup.js b/toolkit/content/widgets/autocomplete-popup.js index f033511e07..a13ba1bc62 100644 --- a/toolkit/content/widgets/autocomplete-popup.js +++ b/toolkit/content/widgets/autocomplete-popup.js @@ -572,7 +572,7 @@ } setListeners() { - this.addEventListener("popupshowing", event => { + this.addEventListener("popupshowing", () => { // If normalMaxRows wasn't already set by the input, then set it here // so that we restore the correct number when the popup is hidden. @@ -584,14 +584,14 @@ this.mPopupOpen = true; }); - this.addEventListener("popupshown", event => { + this.addEventListener("popupshown", () => { if (this._adjustHeightOnPopupShown) { this._adjustHeightOnPopupShown = false; this.adjustHeight(); } }); - this.addEventListener("popuphiding", event => { + this.addEventListener("popuphiding", () => { var isListActive = true; if (this.selectedIndex == -1) { isListActive = false; diff --git a/toolkit/content/widgets/autocomplete-richlistitem.js b/toolkit/content/widgets/autocomplete-richlistitem.js index ccbd37e132..fddd5b4029 100644 --- a/toolkit/content/widgets/autocomplete-richlistitem.js +++ b/toolkit/content/widgets/autocomplete-richlistitem.js @@ -21,7 +21,7 @@ * This overrides listitem's mousedown handler because we want to set the * selected item even when the shift or accel keys are pressed. */ - this.addEventListener("mousedown", event => { + this.addEventListener("mousedown", () => { // Call this.control only once since it's not a simple getter. let control = this.control; if (!control || control.disabled) { @@ -587,7 +587,7 @@ /** * Override _getSearchTokens to have the Learn More text emphasized */ - _getSearchTokens(aSearch) { + _getSearchTokens() { return [this._learnMoreString.toLowerCase()]; } } diff --git a/toolkit/content/widgets/browser-custom-element.js b/toolkit/content/widgets/browser-custom-element.js index e9b29034fa..887f59c742 100644 --- a/toolkit/content/widgets/browser-custom-element.js +++ b/toolkit/content/widgets/browser-custom-element.js @@ -872,7 +872,7 @@ this.webProgress.removeProgressListener(aListener); } - onPageHide(aEvent) { + onPageHide() { // If we're browsing from the tab crashed UI to a URI that keeps // this browser non-remote, we'll handle that here. lazy.SessionStore?.maybeExitCrashedState(this); @@ -1919,7 +1919,7 @@ // Called immediately after changing remoteness. If this method returns // `true`, Gecko will assume frontend handled resuming the load, and will // not attempt to resume the load itself. - afterChangeRemoteness(browser, redirectLoadSwitchId) { + afterChangeRemoteness() { /* no-op unless replaced */ return false; } diff --git a/toolkit/content/widgets/datetimebox.js b/toolkit/content/widgets/datetimebox.js index 28b32fddfa..04ed398bd7 100644 --- a/toolkit/content/widgets/datetimebox.js +++ b/toolkit/content/widgets/datetimebox.js @@ -5,7 +5,7 @@ "use strict"; // This is a UA widget. It runs in per-origin UA widget scope, -// to be loaded by UAWidgetsChild.jsm. +// to be loaded by UAWidgetsChild.sys.mjs. /* * This is the class of entry. It will construct the actual implementation diff --git a/toolkit/content/widgets/dialog.js b/toolkit/content/widgets/dialog.js index 52eb2168f8..c4b25c2f48 100644 --- a/toolkit/content/widgets/dialog.js +++ b/toolkit/content/widgets/dialog.js @@ -146,7 +146,7 @@ if (document.readyState == "complete") { this._postLoadInit(); } else { - window.addEventListener("load", event => this._postLoadInit()); + window.addEventListener("load", () => this._postLoadInit()); } } @@ -521,7 +521,7 @@ } var btn = this.getButton(this.defaultButton); - if (btn) { + if (btn && !btn.hidden) { this._doButtonCommand(this.defaultButton); } } diff --git a/toolkit/content/widgets/editor.js b/toolkit/content/widgets/editor.js index 9e5ffb542e..8e014f77af 100644 --- a/toolkit/content/widgets/editor.js +++ b/toolkit/content/widgets/editor.js @@ -16,13 +16,13 @@ "nsIURIContentListener", "nsISupportsWeakReference", ]), - doContent(contentType, isContentPreferred, request, contentHandler) { + doContent() { return false; }, - isPreferred(contentType, desiredContentType) { + isPreferred() { return false; }, - canHandleContent(contentType, isContentPreferred, desiredContentType) { + canHandleContent() { return false; }, loadCookie: null, diff --git a/toolkit/content/widgets/findbar.js b/toolkit/content/widgets/findbar.js index 3cbce11771..cdfbd315a7 100644 --- a/toolkit/content/widgets/findbar.js +++ b/toolkit/content/widgets/findbar.js @@ -164,7 +164,7 @@ window.addEventListener("unload", this.destroy); - this._findField.addEventListener("input", event => { + this._findField.addEventListener("input", () => { // We should do nothing during composition. E.g., composing string // before converting may matches a forward word of expected word. // After that, even if user converts the composition string to the @@ -230,17 +230,17 @@ } }); - this._findField.addEventListener("blur", event => { + this._findField.addEventListener("blur", () => { // Note: This code used to remove the selection // if it matched an editable. this.browser.finder.enableSelection(); }); - this._findField.addEventListener("focus", event => { + this._findField.addEventListener("focus", () => { this._updateBrowserWithState(); }); - this._findField.addEventListener("compositionstart", event => { + this._findField.addEventListener("compositionstart", () => { // Don't close the find toolbar while IME is composing. let findbar = this; findbar._isIMEComposing = true; @@ -251,7 +251,7 @@ } }); - this._findField.addEventListener("compositionend", event => { + this._findField.addEventListener("compositionend", () => { this._isIMEComposing = false; if (this.findMode != this.FIND_NORMAL) { this._setFindCloseTimeout(); @@ -1307,7 +1307,7 @@ } } - onHighlightFinished(result) { + onHighlightFinished() { // Noop. } diff --git a/toolkit/content/widgets/menu.js b/toolkit/content/widgets/menu.js index f787747a01..1a55d799b6 100644 --- a/toolkit/content/widgets/menu.js +++ b/toolkit/content/widgets/menu.js @@ -129,7 +129,7 @@ }; // The <menucaption> element is used for rendering <html:optgroup> inside of <html:select>, - // See SelectParentHelper.jsm. + // See SelectParentHelper.sys.mjs. class MozMenuCaption extends MozMenuBaseMixin(MozXULElement) { static get inheritedAttributes() { return { diff --git a/toolkit/content/widgets/menulist.js b/toolkit/content/widgets/menulist.js index 4e66c030f3..3672d4ccf1 100644 --- a/toolkit/content/widgets/menulist.js +++ b/toolkit/content/widgets/menulist.js @@ -289,6 +289,9 @@ } setInitialSelection() { + if (this.getAttribute("noinitialselection") === "true") { + return; + } var popup = this.menupopup; if (popup) { var arr = popup.getElementsByAttribute("selected", "true"); diff --git a/toolkit/content/widgets/menupopup.js b/toolkit/content/widgets/menupopup.js index 31801d6a33..c7448d4f05 100644 --- a/toolkit/content/widgets/menupopup.js +++ b/toolkit/content/widgets/menupopup.js @@ -11,16 +11,12 @@ "resource://gre/modules/AppConstants.sys.mjs" ); - // For the non-native context menu styling, we need to know if we need - // a gutter for checkboxes. To do this, check whether there are any - // radio/checkbox type menuitems in a menupopup when showing it. We use a - // system bubbling event listener to ensure we run *after* the "normal" - // popupshowing listeners, so (visibility) changes they make to their items - // take effect first, before we check for checkable menuitems. - Services.els.addSystemEventListener( - document, + document.addEventListener( "popupshowing", function (e) { + // For the non-native context menu styling, we need to know if we need + // a gutter for checkboxes. To do this, check whether there are any + // radio/checkbox type menuitems in a menupopup when showing it. if (e.target.nodeName == "menupopup") { let haveCheckableChild = e.target.querySelector( `:scope > menuitem:not([hidden]):is([type=checkbox],[type=radio]${ @@ -33,7 +29,10 @@ e.target.toggleAttribute("needsgutter", haveCheckableChild); } }, - false + // we use a system bubbling event listener to ensure we run *after* the + // "normal" popupshowing listeners, so (visibility) changes they make to + // their items take effect first, before we check for checkable menuitems. + { mozSystemGroup: true } ); class MozMenuPopup extends MozElements.MozElementMixin(XULPopupElement) { @@ -74,13 +73,13 @@ initShadowDOM() { // Retarget events from shadow DOM arrowscrollbox to the host. - this.scrollBox.addEventListener("scroll", ev => + this.scrollBox.addEventListener("scroll", () => this.dispatchEvent(new Event("scroll")) ); - this.scrollBox.addEventListener("overflow", ev => + this.scrollBox.addEventListener("overflow", () => this.dispatchEvent(new Event("overflow")) ); - this.scrollBox.addEventListener("underflow", ev => + this.scrollBox.addEventListener("underflow", () => this.dispatchEvent(new Event("underflow")) ); } diff --git a/toolkit/content/widgets/moz-button-group/moz-button-group.mjs b/toolkit/content/widgets/moz-button-group/moz-button-group.mjs index 0706f94762..8bf553c23d 100644 --- a/toolkit/content/widgets/moz-button-group/moz-button-group.mjs +++ b/toolkit/content/widgets/moz-button-group/moz-button-group.mjs @@ -44,7 +44,7 @@ export default class MozButtonGroup extends MozLitElement { } } - onSlotchange(e) { + onSlotchange() { for (let child of this.defaultSlotEl.assignedNodes()) { if (!(child instanceof Element)) { // Text nodes won't support classList or getAttribute. diff --git a/toolkit/content/widgets/moz-button/moz-button.css b/toolkit/content/widgets/moz-button/moz-button.css new file mode 100644 index 0000000000..47567df41d --- /dev/null +++ b/toolkit/content/widgets/moz-button/moz-button.css @@ -0,0 +1,142 @@ +/* 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/. */ + +:host { + display: inline-block; +} + +button { + appearance: none; + min-height: var(--button-min-height); + color: var(--button-text-color); + border: var(--button-border); + border-radius: var(--button-border-radius); + background-color: var(--button-background-color); + padding: var(--button-padding); + /* HTML button gets `font: -moz-button` from UA styles, + * but we want it to match the root font styling. */ + font: inherit; + font-weight: var(--button-font-weight); + /* Ensure font-size isn't overridden by widget styling (e.g. in forms.css) */ + font-size: var(--button-font-size); + width: 100%; + + &[size=small] { + min-height: var(--button-min-height-small); + font-size: var(--button-font-size-small); + } + + &:hover { + background-color: var(--button-background-color-hover); + border-color: var(--button-border-color-hover); + color: var(--button-text-color-hover); + } + + &:hover:active { + background-color: var(--button-background-color-active); + border-color: var(--button-border-color-active); + color: var(--button-text-color-active); + } + + &:disabled { + background-color: var(--button-background-color-disabled); + border-color: var(--button-border-color-disabled); + color: var(--button-text-color-disabled); + opacity: var(--button-opacity-disabled); + } + + &:focus-visible { + outline: var(--focus-outline); + outline-offset: var(--focus-outline-offset); + } + + &[type="primary"] { + background-color: var(--button-background-color-primary); + border-color: var(--button-border-color-primary); + color: var(--button-text-color-primary); + + &:hover { + background-color: var(--button-background-color-primary-hover); + border-color: var(--button-border-color-primary-hover); + color: var(--button-text-color-primary-hover); + } + + &:hover:active { + background-color: var(--button-background-color-primary-active); + border-color: var(--button-border-color-primary-active); + color: var(--button-text-color-primary-active); + } + + &:disabled { + background-color: var(--button-background-color-primary-disabled); + border-color: var(--button-border-color-primary-disabled); + color: var(--button-text-color-primary-disabled); + } + } + + &[type="destructive"] { + background-color: var(--button-background-color-destructive); + border-color: var(--button-border-color-destructive); + color: var(--button-text-color-destructive); + + &:hover { + background-color: var(--button-background-color-destructive-hover); + border-color: var(--button-border-color-destructive-hover); + color: var(--button-text-color-destructive-hover); + } + + &:hover:active { + background-color: var(--button-background-color-destructive-active); + border-color: var(--button-border-color-destructive-active); + color: var(--button-text-color-destructive-active); + } + + &:disabled { + background-color: var(--button-background-color-destructive-disabled); + border-color: var(--button-border-color-destructive-disabled); + color: var(--button-text-color-destructive-disabled); + } + } + + &[type~=ghost] { + background-color: var(--button-background-color-ghost); + border-color: var(--button-border-color-ghost); + color: var(--button-text-color-ghost); + + &:hover { + background-color: var(--button-background-color-ghost-hover); + border-color: var(--button-border-color-ghost-hover); + color: var(--button-text-color-ghost-hover); + } + + &:hover:active { + background-color: var(--button-background-color-ghost-active); + border-color: var(--button-border-color-ghost-active); + color: var(--button-text-color-ghost-active); + } + + &:disabled { + background-color: var(--button-background-color-ghost-disabled); + border-color: var(--button-border-color-ghost-disabled); + color: var(--button-text-color-ghost-disabled); + } + } + + &[type~=icon] { + background-size: var(--icon-size-default); + background-position: center; + background-repeat: no-repeat; + -moz-context-properties: fill, stroke; + fill: currentColor; + stroke: currentColor; + width: var(--button-size-icon); + height: var(--button-size-icon); + padding: var(--button-padding-icon); + + &[size=small] { + width: var(--button-size-icon-small); + height: var(--button-size-icon-small); + } + } +} diff --git a/toolkit/content/widgets/moz-button/moz-button.mjs b/toolkit/content/widgets/moz-button/moz-button.mjs new file mode 100644 index 0000000000..3e7c151e61 --- /dev/null +++ b/toolkit/content/widgets/moz-button/moz-button.mjs @@ -0,0 +1,90 @@ +/* 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 { html, ifDefined } from "../vendor/lit.all.mjs"; +import { MozLitElement } from "../lit-utils.mjs"; + +/** + * A button with multiple types and two sizes. + * + * @tagname moz-button + * @property {string} label - The button's label, will be overridden by slotted content. + * @property {string} type - The button type. + * Options: default, primary, destructive, icon, icon ghost, ghost. + * @property {string} size - The button size. + * Options: default, small. + * @property {boolean} disabled - The disabled state. + * @property {string} title - The button's title attribute, used in shadow DOM and therefore not as an attribute on moz-button. + * @property {string} titleAttribute - Internal, map title attribute to the title JS property. + * @property {string} tooltipText - Set the title property, the title attribute will be used first. + * @property {string} ariaLabel - The button's arial-label attribute, used in shadow DOM and therefore not as an attribute on moz-button. + * @property {string} ariaLabelAttribute - Internal, map aria-label attribute to the ariaLabel JS property. + * @property {HTMLButtonElement} buttonEl - The internal button element in the shadow DOM. + * @slot default - The button's content, overrides label property. + * @fires click - The click event. + */ +export default class MozButton extends MozLitElement { + static shadowRootOptions = { + ...MozLitElement.shadowRootOptions, + delegatesFocus: true, + }; + + static properties = { + label: { type: String, reflect: true }, + type: { type: String, reflect: true }, + size: { type: String, reflect: true }, + disabled: { type: Boolean, reflect: true }, + title: { type: String, state: true }, + titleAttribute: { type: String, attribute: "title", reflect: true }, + tooltipText: { type: String }, + ariaLabelAttribute: { + type: String, + attribute: "aria-label", + reflect: true, + }, + ariaLabel: { type: String, state: true }, + }; + + static queries = { + buttonEl: "button", + }; + + constructor() { + super(); + this.type = "default"; + this.size = "default"; + this.disabled = false; + } + + willUpdate(changes) { + if (changes.has("titleAttribute")) { + this.title = this.titleAttribute; + this.titleAttribute = null; + } + if (changes.has("ariaLabelAttribute")) { + this.ariaLabel = this.ariaLabelAttribute; + this.ariaLabelAttribute = null; + } + } + + render() { + return html` + <link + rel="stylesheet" + href="chrome://global/content/elements/moz-button.css" + /> + <button + type=${this.type} + size=${this.size} + ?disabled=${this.disabled} + title=${ifDefined(this.title || this.tooltipText)} + aria-label=${ifDefined(this.ariaLabel)} + part="button" + > + <slot>${this.label}</slot> + </button> + `; + } +} +customElements.define("moz-button", MozButton); diff --git a/toolkit/content/widgets/moz-button/moz-button.stories.mjs b/toolkit/content/widgets/moz-button/moz-button.stories.mjs new file mode 100644 index 0000000000..52a459e807 --- /dev/null +++ b/toolkit/content/widgets/moz-button/moz-button.stories.mjs @@ -0,0 +1,100 @@ +/* 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 { html } from "../vendor/lit.all.mjs"; +// eslint-disable-next-line import/no-unassigned-import +import "./moz-button.mjs"; + +export default { + title: "UI Widgets/Moz Button", + component: "moz-button", + argTypes: { + l10nId: { + options: [ + "moz-button-labelled", + "moz-button-titled", + "moz-button-aria-labelled", + ], + control: { type: "select" }, + }, + size: { + options: ["default", "small"], + control: { type: "radio" }, + }, + }, + parameters: { + actions: { + handles: ["click"], + }, + status: "in-development", + fluent: ` +moz-button-labelled = Button +moz-button-primary = Primary +moz-button-destructive = Destructive +moz-button-titled = + .title = View logins +moz-button-aria-labelled = + .aria-label = View logins +`, + }, +}; + +const Template = ({ type, size, l10nId, iconUrl, disabled }) => html` + <style> + moz-button[type~="icon"]::part(button) { + background-image: url("${iconUrl}"); + } + </style> + <moz-button + data-l10n-id=${l10nId} + type=${type} + size=${size} + ?disabled=${disabled} + ></moz-button> +`; + +export const Default = Template.bind({}); +Default.args = { + type: "default", + size: "default", + l10nId: "moz-button-labelled", + iconUrl: "chrome://global/skin/icons/more.svg", + disabled: false, +}; +export const DefaultSmall = Template.bind({}); +DefaultSmall.args = { + type: "default", + size: "small", + l10nId: "moz-button-labelled", + iconUrl: "chrome://global/skin/icons/more.svg", + disabled: false, +}; +export const Primary = Template.bind({}); +Primary.args = { + ...Default.args, + type: "primary", + l10nId: "moz-button-primary", +}; +export const Destructive = Template.bind({}); +Destructive.args = { + ...Default.args, + type: "destructive", + l10nId: "moz-button-destructive", +}; +export const Icon = Template.bind({}); +Icon.args = { + ...Default.args, + type: "icon", + l10nId: "moz-button-titled", +}; +export const IconSmall = Template.bind({}); +IconSmall.args = { + ...Icon.args, + size: "small", +}; +export const IconGhost = Template.bind({}); +IconGhost.args = { + ...Icon.args, + type: "icon ghost", +}; diff --git a/toolkit/content/widgets/moz-input-box.js b/toolkit/content/widgets/moz-input-box.js index 4704db6dc5..6e7b7b3f29 100644 --- a/toolkit/content/widgets/moz-input-box.js +++ b/toolkit/content/widgets/moz-input-box.js @@ -92,7 +92,7 @@ }); if (this.spellcheck) { - this.menupopup.addEventListener("popuphiding", event => { + this.menupopup.addEventListener("popuphiding", () => { if (this.spellCheckerUI) { this.spellCheckerUI.clearSuggestionsFromMenu(); this.spellCheckerUI.clearDictionaryListFromMenu(); diff --git a/toolkit/content/widgets/moz-label/README.stories.md b/toolkit/content/widgets/moz-label/README.stories.md index a3492ebefa..f5e4e2dd14 100644 --- a/toolkit/content/widgets/moz-label/README.stories.md +++ b/toolkit/content/widgets/moz-label/README.stories.md @@ -3,10 +3,10 @@ `moz-label` is an extension of the built-in `HTMLLabelElement` that provides accesskey styling and formatting as well as some click handling logic. ```html story -<label is="moz-label" accesskey="c" for="check"> +<label is="moz-label" accesskey="c" for="check" style={{ display: "inline-block" }}> This is a label with an accesskey: </label> -<input id="check" type="checkbox" defaultChecked /> +<input id="check" type="checkbox" defaultChecked style={{ display: "inline-block" }} /> ``` Accesskey underlining is enabled by default on Windows and Linux. It is also enabled in Storybook on Mac for demonstrative purposes, but is usually controlled by the `ui.key.menuAccessKey` preference. diff --git a/toolkit/content/widgets/moz-label/moz-label.mjs b/toolkit/content/widgets/moz-label/moz-label.mjs index 7812436ecd..cd9144e7cc 100644 --- a/toolkit/content/widgets/moz-label/moz-label.mjs +++ b/toolkit/content/widgets/moz-label/moz-label.mjs @@ -103,7 +103,7 @@ class MozTextLabel extends HTMLLabelElement { this.formatAccessKey(); } - _onClick(event) { + _onClick() { let controlElement = this.labeledControlElement; if (!controlElement || this.disabled) { return; diff --git a/toolkit/content/widgets/moz-message-bar/moz-message-bar.mjs b/toolkit/content/widgets/moz-message-bar/moz-message-bar.mjs index 58f41c28e4..d83de5d29f 100644 --- a/toolkit/content/widgets/moz-message-bar/moz-message-bar.mjs +++ b/toolkit/content/widgets/moz-message-bar/moz-message-bar.mjs @@ -69,7 +69,7 @@ export default class MozMessageBar extends MozLitElement { this.dismissable = false; } - onSlotchange(e) { + onSlotchange() { let actions = this.actionsSlotEl.assignedNodes(); this.actionsEl.classList.toggle("active", actions.length); } diff --git a/toolkit/content/widgets/moz-message-bar/moz-message-bar.stories.mjs b/toolkit/content/widgets/moz-message-bar/moz-message-bar.stories.mjs index 65803eed9f..6f19c45aee 100644 --- a/toolkit/content/widgets/moz-message-bar/moz-message-bar.stories.mjs +++ b/toolkit/content/widgets/moz-message-bar/moz-message-bar.stories.mjs @@ -121,3 +121,9 @@ WithSupportLink.args = { hasSupportLink: true, hasActionButton: false, }; + +export const WithHeading = Template.bind({}); +WithHeading.args = { + ...Default.args, + l10nId: "moz-message-bar-message-heading", +}; diff --git a/toolkit/content/widgets/moz-page-nav/moz-page-nav-button.css b/toolkit/content/widgets/moz-page-nav/moz-page-nav-button.css new file mode 100644 index 0000000000..2975bb1a7c --- /dev/null +++ b/toolkit/content/widgets/moz-page-nav/moz-page-nav-button.css @@ -0,0 +1,123 @@ +/* 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/. */ + +:host { + border-radius: var(--border-radius-small); + &:focus-visible { + outline-offset: var(--page-nav-focus-outline-inset); + } +} + +button { + background-color: var(--page-nav-button-background-color); + border: 1px solid var(--page-nav-border-color); + -moz-context-properties: fill, fill-opacity; + fill: currentColor; + display: grid; + grid-template-columns: min-content 1fr; + gap: 12px; + align-items: center; + font-size: inherit; + width: 100%; + font-weight: normal; + border-radius: var(--page-nav-button-border-radius); + color: var(--page-nav-button-text-color); + text-align: start; + transition: background-color 150ms; + padding: var(--page-nav-button-padding); +} + +button:hover { + cursor: pointer; +} + +@media not (prefers-contrast) { + button { + border-inline-start: 2px solid transparent; + border-inline-end: none; + border-block: none; + } + + button:hover, + button[selected]:hover { + background-color: var(--page-nav-button-background-color-hover); + } + + button[selected]:hover { + border-inline-start-color: inherit; + } + + button[selected], + button[selected]:hover { + border-inline-start: 2px solid; + } + + button[selected]:not(:focus-visible) { + border-start-start-radius: 0; + border-end-start-radius: 0; + } + + button[selected]:not(:hover) { + color: var(--color-accent-primary); + background-color: color-mix(in srgb, var(--page-nav-button-background-color-selected) 5%, transparent); + border-inline-start-color: var(--color-accent-primary); + } +} + +@media (prefers-color-scheme: dark) { + button[selected] { + background-color: color-mix(in srgb, var(--page-nav-button-background-color-selected) 12%, transparent); + } +} + +button:focus-visible, +button[selected]:focus-visible { + outline: var(--focus-outline); + outline-offset: var(--focus-outline-offset); + border-radius: var(--border-radius-small); +} + +.page-nav-icon { + height: 20px; + width: 20px; + -moz-context-properties: fill; + fill: currentColor; +} + +@media (prefers-contrast) { + button { + transition: none; + border-color: ButtonText; + background-color: var(--button-background-color); + } + + button:hover { + color: SelectedItem; + } + + button[selected] { + color: SelectedItemText; + background-color: SelectedItem; + border-color: SelectedItem; + } +} + +slot { + font-size: var(--font-size-large); + margin: 0; + padding-inline-start: 0; + user-select: none; +} + +@media (max-width: 52rem) { + button { + grid-template-columns: min-content; + justify-content: center; + margin-inline: 0; + } + + slot { + display: none; + } +} diff --git a/toolkit/content/widgets/moz-page-nav/moz-page-nav.css b/toolkit/content/widgets/moz-page-nav/moz-page-nav.css new file mode 100644 index 0000000000..49000f622d --- /dev/null +++ b/toolkit/content/widgets/moz-page-nav/moz-page-nav.css @@ -0,0 +1,76 @@ +/* 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/. */ + +:host { + --page-nav-button-border-radius: var(--button-border-radius); + --page-nav-button-text-color: var(--button-text-color); + --page-nav-button-background-color: transparent; + --page-nav-button-background-color-hover: var(--button-background-color-hover); + --page-nav-button-background-color-selected: var(--color-accent-primary); + --page-nav-button-padding: var(--space-small); + --page-nav-margin-top: 72px; + --page-nav-margin-bottom: 36px; + --page-nav-gap: 25px; + --page-nav-button-gap: var(--space-xsmall); + --page-nav-border-color: var(--border-color); + --page-nav-focus-outline-inset: var(--focus-outline-inset); + --page-nav-width: 240px; + margin-inline-start: 42px; + position: sticky; + top: 0; + height: 100vh; + width: var(--page-nav-width); + + @media (prefers-reduced-motion) { + /* (See Bug 1610081) Setting border-inline-end to add clear differentiation between side navigation and main content area */ + border-inline-end: 1px solid var(--page-nav-border-color); + } + + @media (max-width: 52rem) { + grid-template-rows: 1fr auto; + --page-nav-width: 118px; + } +} + +nav { + display: grid; + grid-template-rows: min-content 1fr auto; + gap: var(--page-nav-gap); + margin-block: var(--page-nav-margin-top) var(--page-nav-margin-bottom); + height: calc(100vh - var(--page-nav-margin-top) - var(--page-nav-margin-bottom)); + @media (max-width: 52rem) { + grid-template-rows: 1fr auto; + } +} + +.page-nav-header { + /* Align the header text/icon with the page nav button icons */ + margin-inline-start: var(--page-nav-button-padding); + font-size: var(--font-size-xlarge); + font-weight: var(--font-weight-bold); + margin-block: 0; + + @media (max-width: 52rem) { + display: none; + } +} + +.primary-nav-group, +#secondary-nav-group { + display: grid; + grid-template-columns: 1fr; + grid-auto-rows: min-content; + gap: var(--page-nav-button-gap); + + @media (max-width: 52rem) { + justify-content: center; + grid-template-columns: min-content; + } +} + +@media (prefers-contrast) { + .primary-nav-group { + gap: var(--space-small); + } +} diff --git a/toolkit/content/widgets/moz-page-nav/moz-page-nav.mjs b/toolkit/content/widgets/moz-page-nav/moz-page-nav.mjs new file mode 100644 index 0000000000..f998ee735f --- /dev/null +++ b/toolkit/content/widgets/moz-page-nav/moz-page-nav.mjs @@ -0,0 +1,170 @@ +/* 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 { html } from "chrome://global/content/vendor/lit.all.mjs"; +import { MozLitElement } from "chrome://global/content/lit-utils.mjs"; + +/** + * A grouping of navigation buttons that is displayed at the page level, + * intended to change the selected view, provide a heading, and have links + * to external resources. + * + * @tagname moz-page-nav + * @property {string} currentView - The currently selected view. + * @property {string} heading - A heading to be displayed at the top of the navigation. + * @slot [default] - Used to append moz-page-nav-button elements to the navigation. + */ +export default class MozPageNav extends MozLitElement { + static properties = { + currentView: { type: String }, + heading: { type: String }, + }; + + static queries = { + headingEl: "#page-nav-header", + primaryNavGroupSlot: ".primary-nav-group slot", + secondaryNavGroupSlot: "#secondary-nav-group slot", + }; + + get pageNavButtons() { + return this.primaryNavGroupSlot + .assignedNodes() + .filter( + node => node?.localName === "moz-page-nav-button" && !node.hidden + ); + } + + onChangeView(e) { + this.currentView = e.target.view; + } + + handleFocus(e) { + if (e.key == "ArrowDown" || e.key == "ArrowRight") { + e.preventDefault(); + this.focusNextView(); + } else if (e.key == "ArrowUp" || e.key == "ArrowLeft") { + e.preventDefault(); + this.focusPreviousView(); + } + } + + focusPreviousView() { + let pageNavButtons = this.pageNavButtons; + let currentIndex = pageNavButtons.findIndex(b => b.selected); + let prev = pageNavButtons[currentIndex - 1]; + if (prev) { + prev.activate(); + prev.buttonEl.focus(); + } + } + + focusNextView() { + let pageNavButtons = this.pageNavButtons; + let currentIndex = pageNavButtons.findIndex(b => b.selected); + let next = pageNavButtons[currentIndex + 1]; + if (next) { + next.activate(); + next.buttonEl.focus(); + } + } + + render() { + return html` + <link + rel="stylesheet" + href="chrome://global/content/elements/moz-page-nav.css" + /> + <nav> + <h1 class="page-nav-header" id="page-nav-header">${this.heading}</h1> + <div + class="primary-nav-group" + role="tablist" + aria-orientation="vertical" + aria-labelledby="page-nav-header" + > + <slot + @change-view=${this.onChangeView} + @keydown=${this.handleFocus} + ></slot> + </div> + <div id="secondary-nav-group" role="group"> + <slot name="secondary-nav" @keydown=${this.handleFocus}></slot> + </div> + </nav> + `; + } + + updated() { + let isViewSelected = false; + let assignedPageNavButtons = this.pageNavButtons; + for (let button of assignedPageNavButtons) { + button.selected = button.view == this.currentView; + isViewSelected = isViewSelected || button.selected; + } + if (!isViewSelected && assignedPageNavButtons.length) { + // Current page nav has no matching view, reset to the first view. + assignedPageNavButtons[0].activate(); + } + } +} +customElements.define("moz-page-nav", MozPageNav); + +/** + * A navigation button intended to change the selected view within a page. + * + * @tagname moz-page-nav-button + * @property {string} iconSrc - The chrome:// url for the icon used for the button. + * @property {string} l10nId - The fluent ID for the button's text + * @property {boolean} selected - Whether or not the button is currently selected. + * @slot [default] - Used to append the l10n string to the button. + */ +export class MozPageNavButton extends MozLitElement { + static properties = { + iconSrc: { type: String }, + l10nId: { type: String }, + selected: { type: Boolean }, + }; + + connectedCallback() { + super.connectedCallback(); + this.setAttribute("role", "none"); + } + + static queries = { + buttonEl: "button", + }; + + get view() { + return this.getAttribute("view"); + } + + activate() { + this.dispatchEvent( + new CustomEvent("change-view", { + bubbles: true, + composed: true, + }) + ); + } + + render() { + return html` + <link + rel="stylesheet" + href="chrome://global/content/elements/moz-page-nav-button.css" + /> + <button + aria-selected=${this.selected} + tabindex=${this.selected ? 0 : -1} + role="tab" + ?selected=${this.selected} + @click=${this.activate} + > + <img class="page-nav-icon" src=${this.iconSrc} /> + <slot></slot> + </button> + `; + } +} +customElements.define("moz-page-nav-button", MozPageNavButton); diff --git a/toolkit/content/widgets/moz-page-nav/moz-page-nav.stories.mjs b/toolkit/content/widgets/moz-page-nav/moz-page-nav.stories.mjs new file mode 100644 index 0000000000..4ac7b455cf --- /dev/null +++ b/toolkit/content/widgets/moz-page-nav/moz-page-nav.stories.mjs @@ -0,0 +1,77 @@ +/* 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 { html } from "../vendor/lit.all.mjs"; +// eslint-disable-next-line import/no-unassigned-import +import "./moz-page-nav.mjs"; + +export default { + title: "UI Widgets/Page Nav", + component: "moz-page-nav", + parameters: { + status: "in-development", + actions: { + handles: ["change-view"], + }, + fluent: ` +moz-page-nav-button-one = View 1 + .title = View 1 +moz-page-nav-button-two = View 2 + .title = View 2 +moz-page-nav-button-three = View 3 + .title = View 3 +moz-page-link-one = Support Page + .title = Support Page +moz-page-nav-heading = + .heading = Heading + `, + }, +}; + +const Template = () => html` + <style> + #page { + height: 100%; + display: flex; + + @media (max-width: 52rem) { + grid-template-columns: 82px 1fr; + } + } + moz-page-nav { + margin-inline-start: 10px; + --page-nav-margin-top: 10px; + + @media (max-width: 52rem) { + margin-inline-start: 0; + } + } + </style> + <div id="page"> + <moz-page-nav data-l10n-id="moz-page-nav-heading" data-l10n-attrs="heading"> + <moz-page-nav-button + view="view-one" + data-l10n-id="moz-page-nav-button-one" + iconSrc="chrome://browser/skin/preferences/category-general.svg" + > + </moz-page-nav-button> + <moz-page-nav-button + view="view-two" + data-l10n-id="moz-page-nav-button-two" + iconSrc="chrome://browser/skin/preferences/category-general.svg" + > + </moz-page-nav-button> + <moz-page-nav-button + view="view-three" + data-l10n-id="moz-page-nav-button-three" + iconSrc="chrome://browser/skin/preferences/category-general.svg" + > + </moz-page-nav-button> + </moz-page-nav> + <main></main> + </div> +`; + +export const Default = Template.bind({}); +Default.args = {}; diff --git a/toolkit/content/widgets/moz-support-link/moz-support-link.mjs b/toolkit/content/widgets/moz-support-link/moz-support-link.mjs index 23f18ac434..9d2d6ffac2 100644 --- a/toolkit/content/widgets/moz-support-link/moz-support-link.mjs +++ b/toolkit/content/widgets/moz-support-link/moz-support-link.mjs @@ -82,7 +82,7 @@ export default class MozSupportLink extends HTMLAnchorElement { } } - attributeChangedCallback(attrName, oldVal, newVal) { + attributeChangedCallback(attrName) { if (attrName === "support-page" || attrName === "utm-content") { this.#setHref(); } diff --git a/toolkit/content/widgets/moz-toggle/moz-toggle.css b/toolkit/content/widgets/moz-toggle/moz-toggle.css index 8b67a81878..2005544181 100644 --- a/toolkit/content/widgets/moz-toggle/moz-toggle.css +++ b/toolkit/content/widgets/moz-toggle/moz-toggle.css @@ -38,11 +38,15 @@ --toggle-background-color-pressed-hover: var(--color-accent-primary-hover); --toggle-background-color-pressed-active: var(--color-accent-primary-active); --toggle-border-color: var(--border-interactive-color); + --toggle-border-color-hover: var(--toggle-border-color); + --toggle-border-color-active: var(--toggle-border-color); --toggle-border-radius: var(--border-radius-circle); --toggle-border-width: var(--border-width); --toggle-height: var(--size-item-small); --toggle-width: var(--size-item-large); --toggle-dot-background-color: var(--toggle-border-color); + --toggle-dot-background-color-hover: var(--toggle-dot-background-color); + --toggle-dot-background-color-active: var(--toggle-dot-background-color); --toggle-dot-background-color-on-pressed: var(--color-canvas); --toggle-dot-margin: 1px; --toggle-dot-height: calc(var(--toggle-height) - 2 * var(--toggle-dot-margin) - 2 * var(--toggle-border-width)); @@ -141,17 +145,6 @@ background-color: var(--toggle-background-color-disabled); } - .toggle-button { - --toggle-dot-background-color: var(--color-accent-primary); - --toggle-dot-background-color-hover: var(--color-accent-primary-hover); - --toggle-dot-background-color-active: var(--color-accent-primary-active); - --toggle-dot-background-color-on-pressed: var(--button-background-color); - --toggle-background-color-disabled: var(--button-background-color-disabled); - --toggle-border-color-hover: var(--border-interactive-color-hover); - --toggle-border-color-active: var(--border-interactive-color-active); - --toggle-border-color-disabled: var(--border-interactive-color-disabled); - } - .toggle-button:enabled:hover { border-color: var(--toggle-border-color-hover); } @@ -175,6 +168,24 @@ border-color: var(--toggle-dot-background-color-hover); } + .toggle-button:hover::before, + .toggle-button:active::before { + background-color: var(--toggle-dot-background-color-hover); + } +} + +@media (forced-colors) { + .toggle-button { + --toggle-dot-background-color: var(--color-accent-primary); + --toggle-dot-background-color-hover: var(--color-accent-primary-hover); + --toggle-dot-background-color-active: var(--color-accent-primary-active); + --toggle-dot-background-color-on-pressed: var(--button-background-color); + --toggle-background-color-disabled: var(--button-background-color-disabled); + --toggle-border-color-hover: var(--border-interactive-color-hover); + --toggle-border-color-active: var(--border-interactive-color-active); + --toggle-border-color-disabled: var(--border-interactive-color-disabled); + } + .toggle-button[aria-pressed="true"]:enabled::after { border: 1px solid var(--button-background-color); content: ''; @@ -189,10 +200,4 @@ .toggle-button[aria-pressed="true"]:enabled:active::after { border-color: var(--toggle-border-color-active); } - - .toggle-button:hover::before, - .toggle-button:hover:active::before, - .toggle-button:active::before { - background-color: var(--toggle-dot-background-color-hover); - } } diff --git a/toolkit/content/widgets/named-deck.js b/toolkit/content/widgets/named-deck.js index 6b3b7a8835..42c96e278d 100644 --- a/toolkit/content/widgets/named-deck.js +++ b/toolkit/content/widgets/named-deck.js @@ -156,7 +156,7 @@ this.getRootNode().removeEventListener("keypress", this); } - attributeChangedCallback(name, oldVal, newVal) { + attributeChangedCallback(name) { if (name == "orientation") { if (this.isVertical) { this.setAttribute("aria-orientation", this.orientation); diff --git a/toolkit/content/widgets/notificationbox.js b/toolkit/content/widgets/notificationbox.js index f23fb03a74..0161588853 100644 --- a/toolkit/content/widgets/notificationbox.js +++ b/toolkit/content/widgets/notificationbox.js @@ -622,7 +622,7 @@ async function createNotificationMessageElement() { await window.ensureCustomElements("moz-message-bar"); - let MozMessageBar = customElements.get("moz-message-bar"); + let MozMessageBar = await customElements.whenDefined("moz-message-bar"); class NotificationMessage extends MozMessageBar { static queries = { ...MozMessageBar.queries, diff --git a/toolkit/content/widgets/panel-list/README.stories.md b/toolkit/content/widgets/panel-list/README.stories.md index b8800e2b5f..3e8617958e 100644 --- a/toolkit/content/widgets/panel-list/README.stories.md +++ b/toolkit/content/widgets/panel-list/README.stories.md @@ -6,7 +6,7 @@ children and optional `hr` elements as separators. The `panel-list` will anchor itself to the target of the initiating event when opened with `panelList.toggle(event)`. -Note: Nested menus are not currently supported. XUL is currently required to +Note: XUL is currently required to support accesskey underlining (although using `moz-label` could change that). Shortcuts are not displayed automatically in the `panel-item`. @@ -229,3 +229,23 @@ grow larger than its containing window if needed. </html:panel-list> </panel> ``` + +### Submenus + +`panel-list` supports nested submenus. Submenus can be created by nesting a second `panel-list` in a `panel-item`'s `submenu` slot and specifying a `submenu` attribute on that `panel-item` that points to the nested list's ID. For example: + +```html +<panel-list> + <panel-item>No submenu</panel-item> + <panel-item>No submenu</panel-item> + <panel-item submenu="example-submenu"> + Has a submenu + <panel-list slot="submenu" id="example-submenu"> + <panel-item>I'm a submenu item!</panel-item> + <panel-item>I'm also a submenu item!</panel-item> + </panel-list> + </panel-item> +</panel-list> +``` + +As of February 2024 submenus are only in use in Firefox View and support for nesting beyond one submenu may be limited. diff --git a/toolkit/content/widgets/panel-list/panel-list.css b/toolkit/content/widgets/panel-list/panel-list.css index 4358fc0cf8..619e6919a3 100644 --- a/toolkit/content/widgets/panel-list/panel-list.css +++ b/toolkit/content/widgets/panel-list/panel-list.css @@ -26,6 +26,10 @@ box-sizing: border-box; } +:host([has-submenu]) { + overflow-y: visible; +} + :host(:not([slot=submenu])) { max-height: 100%; } diff --git a/toolkit/content/widgets/panel-list/panel-list.js b/toolkit/content/widgets/panel-list/panel-list.js index 1cc1f865c3..2e93b4ddc3 100644 --- a/toolkit/content/widgets/panel-list/panel-list.js +++ b/toolkit/content/widgets/panel-list/panel-list.js @@ -308,7 +308,7 @@ } addHideListeners() { - if (this.hasAttribute("stay-open") && !this.lastAnchorNode.hasSubmenu) { + if (this.hasAttribute("stay-open") && !this.lastAnchorNode?.hasSubmenu) { // This is intended for inspection in Storybook. return; } @@ -631,31 +631,12 @@ this.#defaultSlot = document.createElement("slot"); this.#defaultSlot.style.display = "none"; - if (this.hasSubmenu) { - this.icon = document.createElement("div"); - this.icon.setAttribute("class", "submenu-icon"); - this.label.setAttribute("class", "submenu-label"); - - this.button.setAttribute("class", "submenu-container"); - this.button.appendChild(this.icon); - - this.submenuSlot = document.createElement("slot"); - this.submenuSlot.name = "submenu"; - - this.shadowRoot.append( - style, - this.button, - this.#defaultSlot, - this.submenuSlot - ); - } else { - this.shadowRoot.append( - style, - this.button, - supportLinkSlot, - this.#defaultSlot - ); - } + this.shadowRoot.append( + style, + this.button, + supportLinkSlot, + this.#defaultSlot + ); } connectedCallback() { @@ -664,6 +645,10 @@ this._l10nRootConnected = true; } + this.panel = + this.getRootNode()?.host?.closest("panel-list") || + this.closest("panel-list"); + if (!this.#initialized) { this.#initialized = true; // When click listeners are added to the panel-item it creates a node in @@ -683,18 +668,28 @@ }); if (this.hasSubmenu) { + this.panel.setAttribute("has-submenu", ""); + this.icon = document.createElement("div"); + this.icon.setAttribute("class", "submenu-icon"); + this.label.setAttribute("class", "submenu-label"); + + this.button.setAttribute("class", "submenu-container"); + this.button.appendChild(this.icon); + + this.submenuSlot = document.createElement("slot"); + this.submenuSlot.name = "submenu"; + + this.shadowRoot.append(this.submenuSlot); + this.setSubmenuContents(); } } - this.panel = - this.getRootNode()?.host?.closest("panel-list") || - this.closest("panel-list"); - if (this.panel) { this.panel.addEventListener("hidden", this); this.panel.addEventListener("shown", this); } + if (this.hasSubmenu) { this.addEventListener("mouseenter", this); this.addEventListener("mouseleave", this); @@ -762,7 +757,9 @@ setSubmenuContents() { this.submenuPanel = this.submenuSlot.assignedNodes()[0]; - this.shadowRoot.append(this.submenuPanel); + if (this.submenuPanel) { + this.shadowRoot.append(this.submenuPanel); + } } get disabled() { diff --git a/toolkit/content/widgets/panel-list/panel-list.stories.mjs b/toolkit/content/widgets/panel-list/panel-list.stories.mjs index 9c5a4cbe1f..db0ab7597c 100644 --- a/toolkit/content/widgets/panel-list/panel-list.stories.mjs +++ b/toolkit/content/widgets/panel-list/panel-list.stories.mjs @@ -22,6 +22,9 @@ panel-list-checked = Checked panel-list-badged = Badged, look at me panel-list-passwords = Passwords panel-list-settings = Settings +submenu-item-one = Submenu Item One +submenu-item-two = Submenu Item Two +submenu-item-three = Submenu Item Three `, }, }; @@ -36,7 +39,7 @@ function openMenu(event) { } } -const Template = ({ isOpen, items, wideAnchor }) => +const Template = ({ isOpen, items, wideAnchor, hasSubMenu }) => html` <style> panel-item[icon="passwords"]::part(button) { @@ -93,22 +96,36 @@ const Template = ({ isOpen, items, wideAnchor }) => ?open=${isOpen} ?min-width-from-anchor=${wideAnchor} > - ${items.map(i => - i == "<hr>" + ${items.map((item, index) => { + // Always showing submenu on the first item for simplicity. + let showSubMenu = hasSubMenu && index == 0; + let subMenuId = showSubMenu ? "example-sub-menu" : undefined; + return item == "<hr>" ? html` <hr /> ` : html` <panel-item - icon=${i.icon ?? ""} - ?checked=${i.checked} - ?badged=${i.badged} - accesskey=${ifDefined(i.accesskey)} - data-l10n-id=${i.l10nId ?? i} - ></panel-item> - ` - )} + icon=${item.icon ?? ""} + ?checked=${item.checked} + ?badged=${item.badged} + accesskey=${ifDefined(item.accesskey)} + data-l10n-id=${item.l10nId ?? item} + submenu=${ifDefined(subMenuId)} + > + ${showSubMenu ? subMenuTemplate() : ""} + </panel-item> + `; + })} </panel-list> `; +const subMenuTemplate = () => html` + <panel-list slot="submenu" id="example-sub-menu"> + <panel-item data-l10n-id="submenu-item-one"></panel-item> + <panel-item data-l10n-id="submenu-item-two"></panel-item> + <panel-item data-l10n-id="submenu-item-three"></panel-item> + </panel-list> +`; + export const Simple = Template.bind({}); Simple.args = { isOpen: false, @@ -145,3 +162,9 @@ Wide.args = { ...Simple.args, wideAnchor: true, }; + +export const SubMenu = Template.bind({}); +SubMenu.args = { + ...Simple.args, + hasSubMenu: true, +}; diff --git a/toolkit/content/widgets/radio.js b/toolkit/content/widgets/radio.js index 482323acb9..41e8a945ba 100644 --- a/toolkit/content/widgets/radio.js +++ b/toolkit/content/widgets/radio.js @@ -197,7 +197,7 @@ * @param {DOMNode} child * The <radio> element that got removed */ - radioUnattached(child) { + radioUnattached() { // Just invalidate the cache, next time it's fetched it'll get rebuilt. this._radioChildren = null; } @@ -481,13 +481,13 @@ constructor() { super(); - this.addEventListener("click", event => { + this.addEventListener("click", () => { if (!this.disabled) { this.control.selectedItem = this; } }); - this.addEventListener("mousedown", event => { + this.addEventListener("mousedown", () => { if (!this.disabled) { this.control.focusedItem = this; } diff --git a/toolkit/content/widgets/richlistbox.js b/toolkit/content/widgets/richlistbox.js index 904ef9ceec..01d970e6ed 100644 --- a/toolkit/content/widgets/richlistbox.js +++ b/toolkit/content/widgets/richlistbox.js @@ -126,7 +126,7 @@ } }); - this.addEventListener("focus", event => { + this.addEventListener("focus", () => { if (this.getRowCount() > 0) { if (this.currentIndex == -1) { this.currentIndex = this.getIndexOfFirstVisibleRow(); diff --git a/toolkit/content/widgets/search-textbox.js b/toolkit/content/widgets/search-textbox.js index abdcfa2999..b254b2796d 100644 --- a/toolkit/content/widgets/search-textbox.js +++ b/toolkit/content/widgets/search-textbox.js @@ -214,7 +214,7 @@ } } - on_mousedown(event) { + on_mousedown() { if (!this.hasAttribute("focused")) { this.setSelectionRange(0, 0); this.focus(); diff --git a/toolkit/content/widgets/tabbox.js b/toolkit/content/widgets/tabbox.js index 997e8413f2..b1b2ddecce 100644 --- a/toolkit/content/widgets/tabbox.js +++ b/toolkit/content/widgets/tabbox.js @@ -24,15 +24,15 @@ } connectedCallback() { - Services.els.addSystemEventListener(document, "keydown", this, false); + document.addEventListener("keydown", this, { mozSystemGroup: true }); window.addEventListener("unload", this.disconnectedCallback, { once: true, }); } disconnectedCallback() { + document.removeEventListener("keydown", this, { mozSystemGroup: true }); window.removeEventListener("unload", this.disconnectedCallback); - Services.els.removeSystemEventListener(document, "keydown", this, false); } set handleCtrlTab(val) { @@ -729,7 +729,7 @@ direction = 1, wrap = false, startWithAdjacent = true, - filter = tab => true, + filter = () => true, } = opts; let tab = startTab; @@ -804,7 +804,7 @@ } } - _canAdvanceToTab(aTab) { + _canAdvanceToTab() { return true; } diff --git a/toolkit/content/widgets/text.js b/toolkit/content/widgets/text.js index ca10f1489e..7bbf6db4cc 100644 --- a/toolkit/content/widgets/text.js +++ b/toolkit/content/widgets/text.js @@ -67,7 +67,7 @@ this.formatAccessKey(); } - _onClick(event) { + _onClick() { let controlElement = this.labeledControlElement; if (!controlElement || this.disabled) { return; diff --git a/toolkit/content/widgets/textrecognition.js b/toolkit/content/widgets/textrecognition.js index 887d576770..c517f7bfb1 100644 --- a/toolkit/content/widgets/textrecognition.js +++ b/toolkit/content/widgets/textrecognition.js @@ -4,7 +4,7 @@ "use strict"; // This is a UA widget. It runs in per-origin UA widget scope, -// to be loaded by UAWidgetsChild.jsm. +// to be loaded by UAWidgetsChild.sys.mjs. this.TextRecognitionWidget = class { /** diff --git a/toolkit/content/widgets/tree.js b/toolkit/content/widgets/tree.js index 322e42586e..4993bef0c2 100644 --- a/toolkit/content/widgets/tree.js +++ b/toolkit/content/widgets/tree.js @@ -515,7 +515,7 @@ } } - _onDragMouseUp(aEvent) { + _onDragMouseUp() { var col = document.treecolDragging; if (!col) { return; @@ -786,7 +786,7 @@ } }); - this.addEventListener("touchend", event => { + this.addEventListener("touchend", () => { this._touchY = -1; }); @@ -840,7 +840,7 @@ } }); - this.addEventListener("focus", event => { + this.addEventListener("focus", () => { this.focused = true; if (this.currentIndex == -1 && this.view.rowCount > 0) { this.currentIndex = this.getFirstVisibleRow(); @@ -1651,7 +1651,7 @@ this.ensureRowIsVisible(edge); } - _handleEnter(event) { + _handleEnter() { if (this._editingColumn) { this.stopEditing(true); this.focus(); diff --git a/toolkit/content/widgets/videocontrols.js b/toolkit/content/widgets/videocontrols.js index 21c8946e60..73a32164aa 100644 --- a/toolkit/content/widgets/videocontrols.js +++ b/toolkit/content/widgets/videocontrols.js @@ -5,7 +5,7 @@ "use strict"; // This is a UA widget. It runs in per-origin UA widget scope, -// to be loaded by UAWidgetsChild.jsm. +// to be loaded by UAWidgetsChild.sys.mjs. /* * This is the class of entry. It will construct the actual implementation @@ -64,11 +64,8 @@ this.VideoControlsWidget = class { // the underlying element state hasn't changed in ways that we // care about. This can happen if the property is set again // without a value change. - if ( - this.impl && - this.impl.constructor == newImpl && - this.impl.elementStateMatches(this.element) - ) { + if (this.impl && this.impl.constructor == newImpl) { + this.impl.onchange(); return; } if (this.impl) { @@ -458,10 +455,10 @@ this.VideoControlsImplWidget = class { this.statusIcon.setAttribute("type", "error"); this.updateErrorText(); this.setupStatusFader(true); - } else if (VideoControlsWidget.isPictureInPictureVideo(this.video)) { - this.setShowPictureInPictureMessage(true); } + this.updatePictureInPictureMessage(); + if (this.video.readyState >= this.video.HAVE_METADATA) { // According to the spec[1], at the HAVE_METADATA (or later) state, we know // the video duration and dimensions, which means we can calculate whether or @@ -934,6 +931,8 @@ this.VideoControlsImplWidget = class { // Since this event come from the layout, this is the only place // we are sure of that probing into layout won't trigger or force // reflow. + // FIXME(emilio): We should rewrite this to just use + // ResizeObserver, probably. this.reflowTriggeringCallValidator.isReflowTriggeringPropsAllowed = true; this.updateReflowedDimensions(); this.reflowTriggeringCallValidator.isReflowTriggeringPropsAllowed = false; @@ -1095,7 +1094,10 @@ this.VideoControlsImplWidget = class { ); }, - setShowPictureInPictureMessage(showMessage) { + updatePictureInPictureMessage() { + let showMessage = + !this.hasError() && + VideoControlsWidget.isPictureInPictureVideo(this.video); this.pictureInPictureOverlay.hidden = !showMessage; this.isShowingPictureInPictureMessage = showMessage; }, @@ -1188,7 +1190,7 @@ this.VideoControlsImplWidget = class { } }, - onScrubberInput(e) { + onScrubberInput() { const duration = Math.round(this.video.duration * 1000); // in ms let time = this.scrubber.value; @@ -1200,7 +1202,7 @@ this.VideoControlsImplWidget = class { this.pauseVideoDuringDragging(); }, - onScrubberChange(e) { + onScrubberChange() { this.scrubber.isDragging = false; if (this.isPausedByDragging) { @@ -1815,12 +1817,7 @@ this.VideoControlsImplWidget = class { updateMuteButtonState() { var muted = this.isEffectivelyMuted; - - if (muted) { - this.muteButton.setAttribute("muted", "true"); - } else { - this.muteButton.removeAttribute("muted"); - } + this.muteButton.toggleAttribute("muted", muted); var id = muted ? "videocontrols-unmute-button" @@ -2026,12 +2023,7 @@ this.VideoControlsImplWidget = class { }, setCastingButtonState() { - if (this.isCastingOn) { - this.castingButton.setAttribute("enabled", "true"); - } else { - this.castingButton.removeAttribute("enabled"); - } - + this.castingButton.toggleAttribute("enabled", this.isCastingOn); this.adjustControlSize(); }, @@ -2058,22 +2050,15 @@ this.VideoControlsImplWidget = class { }, setClosedCaptionButtonState() { - if (this.isClosedCaptionOn) { - this.closedCaptionButton.setAttribute("enabled", "true"); - } else { - this.closedCaptionButton.removeAttribute("enabled"); - } - + this.closedCaptionButton.toggleAttribute( + "enabled", + this.isClosedCaptionOn + ); let ttItems = this.textTrackList.childNodes; for (let tti of ttItems) { const idx = +tti.getAttribute("index"); - - if (idx == this.currentTextTrackIndex) { - tti.setAttribute("aria-checked", "true"); - } else { - tti.setAttribute("aria-checked", "false"); - } + tti.setAttribute("aria-checked", idx == this.currentTextTrackIndex); } this.adjustControlSize(); @@ -2804,10 +2789,6 @@ this.VideoControlsImplWidget = class { if (this.Utils.isTouchControls) { this.TouchUtils.init(this.shadowRoot, this.Utils); } - this.shadowRoot.firstChild.dispatchEvent( - new this.window.CustomEvent("VideoBindingAttached") - ); - this._setupEventListeners(); } @@ -2920,9 +2901,9 @@ this.VideoControlsImplWidget = class { this.l10n.translateRoots(); } - elementStateMatches(element) { - let elementInPiP = VideoControlsWidget.isPictureInPictureVideo(element); - return this.isShowingPictureInPictureMessage == elementInPiP; + onchange() { + this.Utils.updatePictureInPictureMessage(); + this.shadowRoot.firstChild.removeAttribute("flipped"); } teardown() { @@ -3080,14 +3061,9 @@ this.NoControlsMobileImplWidget = class { }, }; this.Utils.init(this.shadowRoot); - this.Utils.video.dispatchEvent( - new this.window.CustomEvent("MozNoControlsVideoBindingAttached") - ); } - elementStateMatches(element) { - return true; - } + onchange() {} teardown() { this.Utils.terminate(); @@ -3135,9 +3111,7 @@ this.NoControlsPictureInPictureImplWidget = class { this.shadowRoot.firstElementChild.setAttribute("localedir", direction); } - elementStateMatches(element) { - return true; - } + onchange() {} teardown() {} @@ -3312,9 +3286,7 @@ this.NoControlsDesktopImplWidget = class { this.Utils.init(this.shadowRoot, this.prefs); } - elementStateMatches(element) { - return true; - } + onchange() {} teardown() { this.Utils.terminate(); diff --git a/toolkit/content/widgets/wizard.js b/toolkit/content/widgets/wizard.js index 6eb4bcb517..c4285fada5 100644 --- a/toolkit/content/widgets/wizard.js +++ b/toolkit/content/widgets/wizard.js @@ -359,7 +359,7 @@ this._wizardButtons.onPageChange(); } - _advanceFocusToPage(aPage) { + _advanceFocusToPage() { if (!this._hasLoaded) { return; } |