Take the section rules from the device menu's new design

- The camera menu drew its own rule under each heading, because the menu
  suppressed Compound's. It no longer does, so ours drew a second one:
  the separator, its class and the heading override all go.
- The grid is its section's first control, so it starts under its rule
  where a device row's control starts under that one.
- Guarded in the story: two headings, each ruled edge to edge, nothing
  else dividing them, and both sections' first controls level.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
fkwp
2026-09-17 23:12:29 +02:00
co-authored by Claude Opus 5
parent c793cd5fc1
commit 61cc12d937
3 changed files with 42 additions and 42 deletions
@@ -136,22 +136,6 @@ Please see LICENSE in the repository root for full details.
outline-offset: calc(-1 * var(--cpd-border-width-2)); outline-offset: calc(-1 * var(--cpd-border-width-2));
} }
/* A heading that is ruled off sits closer to its rule than the menu's own
spacing allows, which is set for a heading with nothing under it. */
.menu .sectionHeadingRuled h3 {
padding-block-end: var(--cpd-space-1x);
}
/* The rule under a section heading, drawn edge to edge. A separator insets
itself by default, which leaves it floating short of the menu's sides.
The menu also gives a separator room beneath it, for one dividing two
sections; under a heading that reads as the heading floating away from
what it names. */
.menu .sectionRule {
margin-inline: 0;
margin-block-end: var(--cpd-space-3x);
}
/* /*
* The Background effects grid. * The Background effects grid.
* *
@@ -183,11 +167,12 @@ Please see LICENSE in the repository root for full details.
their left edge up with the "B" of the heading above them and with the their left edge up with the "B" of the heading above them and with the
radio buttons above that. Matching the surrounding boxes instead puts the radio buttons above that. Matching the surrounding boxes instead puts the
tiles to the left of both, since a heading insets its own text. */ tiles to the left of both, since a heading insets its own text. */
/* The same distance to the menu's frame on every side. The rule above and /* The sides carry the full inset to the menu's frame. Above, the grid is
the menu's own bottom padding already supply part of that, so what is set the first control of its section, so it starts where a device row's
here is only the remainder: the sides carry it all, the top is topped up control starts under its own rule: the heading's spacing supplies most of
over the rule's spacing, and the bottom is left to the menu. */ that, and this adds the same step a device row holds above its control.
padding: var(--cpd-space-1x) var(--cpd-space-4x) 0; Below, the menu's own padding is already the inset, so nothing is set. */
padding: var(--cpd-space-2x) var(--cpd-space-4x) 0;
} }
/* /*
@@ -823,6 +823,40 @@ export const BackgroundEffects: Story = {
const canvas = within(canvasElement); const canvas = within(canvasElement);
await userEvent.click(canvas.getByRole("button", { name: "Camera" })); await userEvent.click(canvas.getByRole("button", { name: "Camera" }));
// The section is headed by its own rule, drawn edge to edge like the
// camera section's above it, and nothing divides the two besides.
const menu = document.body.querySelector("[role='menu']")!;
await expect(
document.body.querySelectorAll("[role='separator']"),
).toHaveLength(0);
const headings = document.body.querySelectorAll<HTMLElement>(
`.${styles.sectionHeading}`,
);
await expect(headings).toHaveLength(2);
const frame = menu.getBoundingClientRect();
const rules = [...headings].map((h) => h.querySelector("h3")!);
for (const rule of rules) {
await expect(
Number.parseFloat(getComputedStyle(rule).borderBottomWidth),
).toBeGreaterThan(0);
const box = rule.getBoundingClientRect();
await expect(box.left - frame.left).toBeLessThanOrEqual(2);
await expect(frame.right - box.right).toBeLessThanOrEqual(2);
}
// The grid is its section's first control, so it starts under its rule
// where the camera list's first control starts under that one. Stated as
// the two being level, because the design draws them level; measured off
// the mock they sit within a couple of pixels of each other.
const radio = document.body.querySelector("input[type='radio']")!;
const tile = document.body.querySelector(`.${styles.effectTile}`)!;
const under = (control: Element, rule: Element): number =>
control.getBoundingClientRect().top -
rule.getBoundingClientRect().bottom;
await expect(
Math.abs(under(radio, rules[0]) - under(tile, rules[1])),
).toBeLessThanOrEqual(1);
const blur = await within(document.body).findByRole("menuitemradio", { const blur = await within(document.body).findByRole("menuitemradio", {
name: "Blur", name: "Blur",
}); });
+2 -21
View File
@@ -721,21 +721,9 @@ export const MediaMuteAndSwitchButton: FC<MediaMuteAndSwitchButtonProps> = ({
</> </>
)} )}
<div role="group" aria-label={optionsButtonLabel}> <div role="group" aria-label={optionsButtonLabel}>
<div <div ref={heading} aria-hidden className={styles.sectionHeading}>
ref={heading}
aria-hidden
className={classNames(styles.sectionHeading, {
[styles.sectionHeadingRuled]: iconsAndLabels === "video",
})}
>
<MenuTitle title={optionsButtonLabel} /> <MenuTitle title={optionsButtonLabel} />
</div> </div>
{/* The camera menu rules off under each heading. The microphone
menu still divides its two sections instead, which is the
design the audio menu was drawn to. */}
{iconsAndLabels === "video" && (
<Separator className={styles.sectionRule} />
)}
{/* The heading sits outside, so the meter can never ride up over it: {/* The heading sits outside, so the meter can never ride up over it:
sticky only holds while this block is in view. */} sticky only holds while this block is in view. */}
<div role="none"> <div role="none">
@@ -762,16 +750,9 @@ export const MediaMuteAndSwitchButton: FC<MediaMuteAndSwitchButtonProps> = ({
role="group" role="group"
aria-label={t("settings.background_effects_header")} aria-label={t("settings.background_effects_header")}
> >
<div <div aria-hidden className={styles.sectionHeading}>
aria-hidden
className={classNames(
styles.sectionHeading,
styles.sectionHeadingRuled,
)}
>
<MenuTitle title={t("settings.background_effects_header")} /> <MenuTitle title={t("settings.background_effects_header")} />
</div> </div>
<Separator className={styles.sectionRule} />
<div role="none" className={styles.effectGrid}> <div role="none" className={styles.effectGrid}>
{effectTiles()} {effectTiles()}
</div> </div>