Bug 1792238 - Create a moz-button-group component r=hjones

Differential Revision: https://phabricator.services.mozilla.com/D163141
This commit is contained in:
Mark Striemer 2022-12-07 15:46:07 +00:00
parent eac0c55546
commit 568be762eb
8 changed files with 306 additions and 13 deletions

View File

@ -24,7 +24,7 @@ window.MozXULElement = {
// This should be browser, locales-preview or toolkit.
let [root, ...rest] = name.split("/");
let ftlContents;
//
// TODO(mstriemer): These seem like they could be combined but I don't want
// to fight with webpack anymore.
if (root == "toolkit") {

View File

@ -92,6 +92,8 @@ toolkit.jar:
content/global/elements/message-bar.js (widgets/message-bar.js)
content/global/elements/menu.js (widgets/menu.js)
content/global/elements/menupopup.js (widgets/menupopup.js)
content/global/elements/moz-button-group.css (widgets/moz-button-group/moz-button-group.css)
content/global/elements/moz-button-group.mjs (widgets/moz-button-group/moz-button-group.mjs)
content/global/elements/moz-input-box.js (widgets/moz-input-box.js)
content/global/elements/named-deck.js (widgets/named-deck.js)
content/global/elements/notificationbox.js (widgets/notificationbox.js)

View File

@ -23,6 +23,7 @@ run-if = os == "mac" && os_version != "10.15" # Mac only feature, requires > 10.
[test_label_checkbox.xhtml]
[test_menubar.xhtml]
skip-if = os == 'mac'
[test_moz_button_group.html]
[test_popupanchor.xhtml]
skip-if = os == 'linux' || (verify && (os == 'win')) # Bug 1335894 perma-fail on linux 16.04
[test_popupreflows.xhtml]

View File

@ -0,0 +1,156 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>moz-button-group tests</title>
<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
<link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
<!-- TODO: Bug 1798404 - in-content/common.css can be removed once we have a better
solution for token variables for the new widgets -->
<link rel="stylesheet" href="chrome://global/skin/in-content/common.css">
<script type="module" src="chrome://global/content/elements/moz-button-group.mjs"></script>
</head>
<body>
<p id="display"></p>
<div id="content">
<button id="before-button">Before</button>
<div id="render"></div>
<button id="after-button">After</button>
</div>
<!-- This is here to ensure the stylesheet is loaded. It gets removed in setup. -->
<moz-button-group></moz-button-group>
<pre id="test">
</pre>
<script>
const { Assert } = ChromeUtils.import("resource://testing-common/Assert.jsm");
let html;
let render;
let renderArea = document.getElementById("render");
let beforeButton = document.getElementById("before-button");
let afterButton = document.getElementById("after-button");
async function checkButtons(first, second) {
let firstBounds = first.getBoundingClientRect();
let secondBounds = second.getBoundingClientRect();
ok(firstBounds.x < secondBounds.x, `First button comes first`);
let locationDiff = Math.abs(secondBounds.x - firstBounds.x - firstBounds.width - 8);
ok(locationDiff < 1, `Second button is 8px after first (${locationDiff})`);
beforeButton.focus();
is(document.activeElement, beforeButton, "Before button is focused");
synthesizeKey("VK_TAB");
is(document.activeElement, first, "First button is first in tab order");
synthesizeKey("VK_TAB");
is(document.activeElement, second, "Second button is next in tab order");
synthesizeKey("VK_TAB");
is(document.activeElement, afterButton, "After button is at the end in tab order");
}
add_setup(async function setup() {
({ html, render} = await import("chrome://global/content/vendor/lit.all.mjs"));
document.querySelector("moz-button-group").remove();
});
add_task(async function testButtonOrderingSlot() {
render(
html`
<moz-button-group>
<button slot="primary" id="primary-button">Primary</button>
<button id="secondary-button">Secondary</button>
</moz-button-group>
`,
renderArea
);
let buttonGroup = document.querySelector("moz-button-group");
let primaryButton = document.getElementById("primary-button");
let secondaryButton = document.getElementById("secondary-button");
buttonGroup.platform = "win";
await buttonGroup.updateComplete;
await checkButtons(primaryButton, secondaryButton);
buttonGroup.platform = "macosx";
await buttonGroup.updateComplete;
await checkButtons(secondaryButton, primaryButton);
});
add_task(async function testPrimaryButtonAutoSlotting() {
render(
html`
<moz-button-group>
<button class="primary">Primary</button>
<button class="secondary">Secondary</button>
</moz-button-group>
`,
renderArea
);
let buttonGroup = document.querySelector("moz-button-group");
let primaryButton = buttonGroup.querySelector(".primary");
let secondaryButton = buttonGroup.querySelector(".secondary");
buttonGroup.platform = "win";
await buttonGroup.updateComplete;
is(primaryButton.slot, "primary", "primary button was auto-slotted")
await checkButtons(primaryButton, secondaryButton);
buttonGroup.platform = "macosx";
await buttonGroup.updateComplete;
await checkButtons(secondaryButton, primaryButton);
});
add_task(async function testSubmitButtonAutoSlotting() {
render(
html`
<moz-button-group>
<button type="submit">Submit</button>
<button class="secondary">Secondary</button>
</moz-button-group>
`,
renderArea
);
let buttonGroup = document.querySelector("moz-button-group");
let submitButton = buttonGroup.querySelector("[type=submit]");
let secondaryButton = buttonGroup.querySelector(".secondary");
buttonGroup.platform = "win";
await buttonGroup.updateComplete;
is(submitButton.slot, "primary", "submit button was auto-slotted")
await checkButtons(submitButton, secondaryButton);
buttonGroup.platform = "macosx";
await buttonGroup.updateComplete;
await checkButtons(secondaryButton, submitButton);
});
add_task(async function testAutofocusButtonAutoSlotting() {
render(
html`
<moz-button-group>
<button autofocus>First</button>
<button class="secondary">Secondary</button>
</moz-button-group>
`,
renderArea
);
let buttonGroup = document.querySelector("moz-button-group");
let autofocusButton = buttonGroup.querySelector("[autofocus]");
let secondaryButton = buttonGroup.querySelector(".secondary");
buttonGroup.platform = "win";
await buttonGroup.updateComplete;
is(autofocusButton.slot, "primary", "autofocus button was auto-slotted")
await checkButtons(autofocusButton, secondaryButton);
buttonGroup.platform = "macosx";
await buttonGroup.updateComplete;
await checkButtons(secondaryButton, autofocusButton);
});
</script>
</body>
</html>

View File

@ -102,24 +102,17 @@ export class MozLitElement extends LitElement {
/**
* The URL for this component's styles. To make development in Storybook
* easier this will use the chrome:// URL when in product (feature detected
* by AppConstants existing) and a relative path for Storybook.
* easier this will use the chrome:// URL when in product and a relative path
* for Storybook.
*
* LOCAL_NAME should be the kebab-cased name of the element. It is added by
* the `./mach addwidget` command.
*/
static get stylesheetUrl() {
if (this.useChromeStylesheet) {
return `chrome://global/content/elements/${this.LOCAL_NAME}.css`;
if (window.IS_STORYBOOK) {
return `./${this.LOCAL_NAME}/${this.LOCAL_NAME}.css`;
}
return `./${this.LOCAL_NAME}/${this.LOCAL_NAME}.css`;
}
static get useChromeStylesheet() {
return (
typeof AppConstants != "undefined" ||
(typeof Cu != "undefined" && Cu.isInAutomation)
);
return `chrome://global/content/elements/${this.LOCAL_NAME}.css`;
}
connectedCallback() {

View File

@ -0,0 +1,13 @@
/* 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: flex;
gap: 8px;
justify-content: flex-end;
}
::slotted(button) {
margin: 0 !important;
}

View File

@ -0,0 +1,80 @@
/* 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";
import { MozLitElement } from "../lit-utils.mjs";
export const PLATFORM_LINUX = "linux";
export const PLATFORM_MACOS = "macosx";
export const PLATFORM_WINDOWS = "win";
export default class MozButtonGroup extends MozLitElement {
static queries = {
defaultSlotEl: "slot:not([name])",
primarySlotEl: "slot[name=primary]",
};
static properties = {
platform: { state: true },
};
// Use a relative URL in storybook to get faster reloads on style changes.
static stylesheetUrl = window.IS_STORYBOOK
? "./moz-button-group/moz-button-group.css"
: "chrome://global/content/elements/moz-button-group.css";
constructor() {
super();
this.#detectPlatform();
}
#detectPlatform() {
if (typeof AppConstants !== "undefined") {
this.platform = AppConstants.platform;
} else if (navigator.platform.includes("Linux")) {
this.platform = PLATFORM_LINUX;
} else if (navigator.platform.includes("Mac")) {
this.platform = PLATFORM_MACOS;
} else {
this.platform = PLATFORM_WINDOWS;
}
}
onSlotchange(e) {
for (let child of this.defaultSlotEl.assignedNodes()) {
if (!(child instanceof Element)) {
// Text nodes won't support classList or getAttribute.
continue;
}
// Bug 1791816: These should check moz-button instead of button.
if (
child.localName == "button" &&
(child.classList.contains("primary") ||
child.getAttribute("type") == "submit" ||
child.hasAttribute("autofocus"))
) {
child.slot = "primary";
}
}
}
render() {
let slots = [
html`
<slot @slotchange=${this.onSlotchange}></slot>
`,
html`
<slot name="primary"></slot>
`,
];
if (this.platform == PLATFORM_WINDOWS) {
slots = [slots[1], slots[0]];
}
return html`
<link rel="stylesheet" href=${this.constructor.stylesheetUrl} />
${slots}
`;
}
}
customElements.define("moz-button-group", MozButtonGroup);

View File

@ -0,0 +1,48 @@
/* 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";
import {
PLATFORM_LINUX,
PLATFORM_MACOS,
PLATFORM_WINDOWS,
} from "./moz-button-group.mjs";
export default {
title: "Design System/Experiments/MozButtonGroup",
argTypes: {
platform: {
options: [PLATFORM_LINUX, PLATFORM_MACOS, PLATFORM_WINDOWS],
control: { type: "select" },
},
},
};
const Template = ({ platform }) => html`
<div class="card card-no-hover" style="max-width: 400px">
<p>The button group is below. Card for emphasis.</p>
<moz-button-group .platform=${platform}>
<button class="primary">OK</button>
<button>Cancel</button>
</moz-button-group>
</div>
`;
export const Default = Template.bind({});
Default.args = {
// Platform will auto-detected.
};
export const Windows = Template.bind({});
Windows.args = {
platform: PLATFORM_WINDOWS,
};
export const Mac = Template.bind({});
Mac.args = {
platform: PLATFORM_MACOS,
};
export const Linux = Template.bind({});
Linux.args = {
platform: PLATFORM_LINUX,
};