commit 1b69823731aed5e7ff2d4d192653a1fbbe964723
parent 9c8a99ad31717c852f89bc6d4597ccaa33a5f8cf
Author: hannajones <hjones@mozilla.com>
Date: Tue, 18 Nov 2025 20:03:02 +0000
Bug 1998446 - fix visual issues for scrollable moz-box-group with wrapped items r=desktop-theme-reviewers,jules
Differential Revision: https://phabricator.services.mozilla.com/D271508
Diffstat:
5 files changed, 209 insertions(+), 42 deletions(-)
diff --git a/browser/components/storybook/component-status/components.json b/browser/components/storybook/component-status/components.json
@@ -1,5 +1,5 @@
{
- "generatedAt": "2025-11-12T19:18:35.085Z",
+ "generatedAt": "2025-11-18T18:36:31.823Z",
"count": 29,
"items": [
{
diff --git a/toolkit/content/widgets/moz-box-common.css b/toolkit/content/widgets/moz-box-common.css
@@ -13,8 +13,10 @@
--box-icon-size: var(--icon-size);
--box-icon-fill: var(--icon-color);
- border: var(--box-border);
- border-block-end: var(--box-border-width-end, var(--box-border-width)) solid var(--box-border-color);
+ border-inline-start: var(--box-border-inline-start, var(--box-border));
+ border-inline-end: var(--box-border-inline-end, var(--box-border));
+ border-block-start: var(--box-border-block-start, var(--box-border));
+ border-block-end: var(--box-border-block-end, var(--box-border));
border-start-start-radius: var(--box-border-radius-start, var(--box-border-radius));
border-start-end-radius: var(--box-border-radius-start, var(--box-border-radius));
border-end-start-radius: var(--box-border-radius-end, var(--box-border-radius));
@@ -90,7 +92,7 @@
&:focus-visible {
outline: var(--focus-outline);
- outline-offset: var(--focus-outline-offset);
+ outline-offset: var(--focus-outline-inset);
}
&:hover {
diff --git a/toolkit/content/widgets/moz-box-group/moz-box-group.css b/toolkit/content/widgets/moz-box-group/moz-box-group.css
@@ -7,35 +7,44 @@
--box-group-border-radius-inner: calc(var(--border-radius-medium) - var(--border-width));
display: block;
- border: var(--box-group-border);
+ outline: var(--box-group-border);
border-radius: var(--border-radius-medium);
overflow: hidden;
}
::slotted(*) {
- border: none;
+ --box-border-inline-end: none;
+ --box-border-inline-start: none;
}
::slotted(*:not(.last)) {
--box-border-radius-end: 0;
- --box-border-width-end: 0;
+ --box-border-block-end: 0;
}
::slotted(*:not(.first, [position="0"])) {
--box-border-radius-start: 0;
- border-block-start: var(--box-group-border);
+}
+
+/* targets the first element when we have a header, since the element at
+position 0 won't have the .first class */
+::slotted([position="0"]:not(.first)) {
+ --box-border-radius-start: 0;
+ --box-border-block-start: none;
}
::slotted(.first) {
--box-border-radius-start: var(--box-group-border-radius-inner);
+ --box-border-block-start: none;
}
::slotted(.last) {
--box-border-radius-end: var(--box-group-border-radius-inner);
+ --box-border-block-end: none;
}
slot[name="header"]::slotted(:first-child) {
- border-block-end: var(--box-group-border);
+ --box-border-block-end: var(--box-group-border);
}
.list {
diff --git a/toolkit/content/widgets/moz-box-group/moz-box-group.mjs b/toolkit/content/widgets/moz-box-group/moz-box-group.mjs
@@ -73,6 +73,7 @@ export default class MozBoxGroup extends MozLitElement {
let listTag =
this.type == GROUP_TYPES.reorderable ? literal`ol` : literal`ul`;
return staticHtml`<${listTag}
+ tabindex="-1"
class="list scroll-container"
aria-orientation="vertical"
@keydown=${this.handleKeydown}
@@ -87,7 +88,9 @@ export default class MozBoxGroup extends MozLitElement {
</${listTag}>
<slot hidden></slot>`;
}
- return html`<div class="scroll-container"><slot></slot></div>`;
+ return html`<div class="scroll-container">
+ <slot></slot>
+ </div>`;
}
handleReorder(event) {
diff --git a/toolkit/content/widgets/moz-box-group/moz-box-group.stories.mjs b/toolkit/content/widgets/moz-box-group/moz-box-group.stories.mjs
@@ -4,6 +4,8 @@
import { html, ifDefined } from "../vendor/lit.all.mjs";
import { GROUP_TYPES } from "./moz-box-group.mjs";
+// eslint-disable-next-line mozilla/no-browser-refs-in-toolkit
+import "chrome://browser/content/preferences/widgets/setting-control.mjs";
export default {
title: "UI Widgets/Box Group",
@@ -53,10 +55,34 @@ moz-box-button-footer =
},
};
+function basicTemplate({ type, hasHeader, hasFooter, wrapped }) {
+ return html`<moz-box-group type=${ifDefined(type)}>
+ ${hasHeader
+ ? html`<moz-box-item
+ slot="header"
+ data-l10n-id="moz-box-item-header"
+ ></moz-box-item>`
+ : ""}
+ ${getInnerElements(type, wrapped)}
+ ${hasFooter
+ ? html`<moz-box-button
+ slot="footer"
+ data-l10n-id="moz-box-button-footer"
+ ></moz-box-button>`
+ : ""}
+ </moz-box-group>
+ ${type == "list"
+ ? html`<moz-button class="delete" @click=${appendItem}>
+ Add an item
+ </moz-button>`
+ : ""}`;
+}
+
function getInnerElements(type) {
if (type == GROUP_TYPES.reorderable) {
return reorderableElements();
}
+
return basicElements();
}
@@ -116,38 +142,6 @@ function basicElements() {
<moz-box-button data-l10n-id="moz-box-button-2"></moz-box-button>`;
}
-const Template = ({ type, hasHeader, hasFooter, scrollable }) => html`
- <style>
- moz-box-group {
- --box-group-max-height: ${scrollable ? "200px" : "unset"};
- }
-
- .delete {
- margin-top: var(--space-medium);
- }
- </style>
- <moz-box-group type=${ifDefined(type)}>
- ${hasHeader
- ? html`<moz-box-item
- slot="header"
- data-l10n-id="moz-box-item-header"
- ></moz-box-item>`
- : ""}
- ${getInnerElements(type)}
- ${hasFooter
- ? html`<moz-box-button
- slot="footer"
- data-l10n-id="moz-box-button-footer"
- ></moz-box-button>`
- : ""}
- </moz-box-group>
- ${type == "list"
- ? html`<moz-button class="delete" @click=${appendItem}>
- Add an item
- </moz-button>`
- : ""}
-`;
-
const appendItem = event => {
let group = event.target.getRootNode().querySelector("moz-box-group");
@@ -165,12 +159,165 @@ const appendItem = event => {
group.prepend(boxItem);
};
+// Example with all child elements wrapped in setting-control/setting-group,
+// which is the most common use case in Firefox preferences.
+function wrappedTemplate({ type, hasHeader, hasFooter }) {
+ return html`<setting-control
+ .config=${getConfig({ type, hasHeader, hasFooter })}
+ .setting=${DEFAULT_SETTING}
+ .getSetting=${getSetting}
+ ></setting-control>`;
+}
+
+const getConfig = ({ type, hasHeader, hasFooter }) => ({
+ id: "exampleWrapped",
+ control: "moz-box-group",
+ controlAttrs: {
+ type,
+ },
+ items: [
+ ...(hasHeader
+ ? [
+ {
+ id: "header",
+ control: "moz-box-item",
+ l10nId: "moz-box-item-header",
+ controlAttrs: { slot: "header " },
+ },
+ ]
+ : []),
+ {
+ id: "item1",
+ control: "moz-box-item",
+ l10nId: "moz-box-item",
+ options: [
+ {
+ id: "slotted-button",
+ control: "moz-button",
+ l10nId: "moz-box-edit-action",
+ iconSrc: "chrome://global/skin/icons/edit-outline.svg",
+ controlAttrs: {
+ type: "ghost",
+ slot: "actions",
+ },
+ },
+ {
+ id: "slotted-toggle",
+ control: "moz-toggle",
+ l10nId: "moz-box-toggle-action",
+ controlAttrs: {
+ slot: "actions",
+ },
+ },
+ {
+ id: "slotted-icon-button",
+ control: "moz-button",
+ l10nId: "moz-box-more-action",
+ iconSrc: "chrome://global/skin/icons/more.svg",
+ controlAttrs: {
+ slot: "actions",
+ },
+ },
+ ],
+ },
+ {
+ id: "link1",
+ control: "moz-box-link",
+ l10nId: "moz-box-link",
+ },
+ {
+ id: "button1",
+ control: "moz-box-button",
+ l10nId: "moz-box-button-1",
+ },
+ {
+ id: "item2",
+ control: "moz-box-item",
+ l10nId: "moz-box-item",
+ options: [
+ {
+ id: "slotted-button-start",
+ control: "moz-button",
+ l10nId: "moz-box-edit-action",
+ iconSrc: "chrome://global/skin/icons/edit-outline.svg",
+ controlAttrs: {
+ type: "ghost",
+ slot: "actions-start",
+ },
+ },
+ {
+ id: "slotted-icon-button-start",
+ control: "moz-button",
+ l10nId: "moz-box-more-action",
+ iconSrc: "chrome://global/skin/icons/more.svg",
+ controlAttrs: {
+ slot: "actions-start",
+ },
+ },
+ ],
+ },
+ {
+ id: "button2",
+ control: "moz-box-button",
+ l10nId: "moz-box-button-2",
+ },
+ ...(hasFooter
+ ? [
+ {
+ id: "footer",
+ control: "moz-box-button",
+ l10nId: "moz-box-button-footer",
+ controlAttrs: { slot: "footer " },
+ },
+ ]
+ : []),
+ ],
+});
+
+const DEFAULT_SETTING = {
+ value: 1,
+ on() {},
+ off() {},
+ userChange() {},
+ getControlConfig: c => c,
+ controllingExtensionInfo: {},
+ visible: true,
+};
+
+function getSetting() {
+ return {
+ value: true,
+ on() {},
+ off() {},
+ userChange() {},
+ visible: () => true,
+ getControlConfig: c => c,
+ controllingExtensionInfo: {},
+ };
+}
+
+const Template = ({ type, hasHeader, hasFooter, scrollable, wrapped }) => html`
+ <style>
+ moz-box-group {
+ --box-group-max-height: ${scrollable ? "250px" : "unset"};
+ }
+
+ .delete {
+ margin-top: var(--space-medium);
+ }
+ </style>
+ ${wrapped
+ ? wrappedTemplate({ type, hasHeader, hasFooter })
+ : basicTemplate({ type, hasHeader, hasFooter, wrapped })}
+`;
+
export const Default = Template.bind({});
Default.args = {
type: "default",
hasHeader: false,
hasFooter: false,
scrollable: false,
+ wrapped: false,
};
export const List = Template.bind({});
@@ -197,3 +344,9 @@ Scrollable.args = {
...ListWithHeaderAndFooter.args,
scrollable: true,
};
+
+export const Wrapped = Template.bind({});
+Wrapped.args = {
+ ...ListWithHeaderAndFooter.args,
+ wrapped: true,
+};