Skip to content
Open
6 changes: 4 additions & 2 deletions packages/components/PATTERNS.md
Original file line number Diff line number Diff line change
Expand Up @@ -532,10 +532,12 @@ and consistency enforced by tooling, not maintained by hand.
`src/components/LayoutCard/LayoutCard.module.scss:16`
- ✓ a layout must style standalone Flow classes unreachable via props/context.
- ✗ normal composition → PropsContext classes.
- **Container-query ownership** `[undocumented]` — inline-size container at the
semantic boundary. `src/components/Section/Section.module.scss:5`
- **Container-query ownership** `[undocumented]` — the component that queries a
container declares it itself.
`src/components/ColumnLayout/ColumnLayout.module.scss:2`
- ✓ descendants adapt to this component's available space.
- ✗ viewport-wide adaptation → responsive primitives/media.
- ✗ a container nothing queries → no `container-type` at all.
- **Logical CSS properties** `[undocumented]` — `padding-inline`,
`margin-inline-start`. `src/components/Button/Button.module.scss:13`
- ✓ direction-safe spacing/border/size.
Expand Down
70 changes: 69 additions & 1 deletion packages/components/src/components/List/List.browser.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
} from "@/components/List";
import type { AsyncDataLoader } from "@/components/List/model/loading/types";
import { use, useState, type ReactNode } from "react";
import { test, type Mock } from "vitest";
import { expect, test, type Mock } from "vitest";
import { page, userEvent } from "vitest/browser";
import { RouterProvider } from "react-aria-components";
import {
Expand All @@ -20,6 +20,13 @@ import {
} from "../SettingsProvider";
import { FilterValue } from "./model/filter/FilterValue";
import Content from "../Content";
import { Table } from "./setupComponents/Table";
import { TableHeader } from "./setupComponents/TableHeader";
import { TableColumn } from "./setupComponents/TableColumn";
import { TableBody } from "./setupComponents/TableBody";
import { TableRow } from "./setupComponents/TableRow";
import { TableCell } from "./setupComponents/TableCell";
import { ColumnLayout } from "../ColumnLayout";
import { Heading } from "../Heading";
import { ContextMenu, MenuItem } from "../ContextMenu";

Expand Down Expand Up @@ -656,6 +663,67 @@ describe("Item rendering", () => {
});
});

/*
* `.list` needs `min-width: 0` now that it declares no containment (#2655):
* with neither, a list with unbreakable content pushes its own `Nfr` grid
* track open and equal columns stop being equal.
*/
test.each(["list", "tiles", "table"] as const)(
"does not blow out an equal-width grid track (%s view)",
async (viewMode) => {
await render(
<div style={{ width: 400 }}>
<ColumnLayout s={[1, 1]} gap="s">
<List aria-label="Wide" defaultViewMode={viewMode}>
<ListStaticData<Data> data={[{ num: 1 }]} />
<ListItem<Data> textValue={() => "wide"}>
{() => (
<span style={{ whiteSpace: "nowrap" }}>
unbreakable-item-content-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
</span>
)}
</ListItem>
<Table<Data>>
<TableHeader<Data>>
<TableColumn<Data>>
Averyverylongcolumnheadingnobreaks
</TableColumn>
<TableColumn<Data>>
Anotherverylongcolumnheadingnobreaks
</TableColumn>
</TableHeader>
<TableBody<Data>>
<TableRow<Data>>
<TableCell<Data>>
{() => "unbreakable-cell-content-aaaaaaaaaaaaaaaa"}
</TableCell>
<TableCell<Data>>
{() => "unbreakable-cell-content-bbbbbbbbbbbbbbbb"}
</TableCell>
</TableRow>
</TableBody>
</Table>
</List>
<span data-testid="sibling">sibling</span>
</ColumnLayout>
</div>,
);
const sibling = page.getByTestId("sibling").element();
const listElement = sibling.previousElementSibling as HTMLElement;

// The items load asynchronously; measuring before they are in the DOM
// measures an empty list, which cannot blow anything out.
await vitest.waitUntil(() =>
listElement.textContent?.includes("unbreakable"),
);

expect(listElement.getBoundingClientRect().width).toBeCloseTo(
sibling.getBoundingClientRect().width,
0,
);
},
);

describe("Linked items", () => {
const itemHref = `${location.origin}/domains/42`;

Expand Down
2 changes: 1 addition & 1 deletion packages/components/src/components/List/List.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
display: flex;
flex-direction: column;
row-gap: var(--list--block-to-block-spacing);
container-type: inline-size;
min-width: 0;

:has(:global(.flow--list--list-summary--top)) {
:global(.flow--list--items),
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
@use "@/styles/mixins/containerBreakpointSizes";

.view {
display: flex;
flex-direction: column;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
display: flex;
flex-direction: column;
row-gap: var(--section--spacing);
container-type: inline-size;
width: 100%;
min-width: 0;

> form {
display: flex;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,72 @@ test.each(testEnvironments)(
await testScreenshot("Section with secondary button");
},
);

/*
* Guards the height path of the reporter's structure from #2655 (LayoutCard >
* Section > Section > Switch revealing taller content). The bug itself is
* Blink-specific and cannot reproduce here — this suite runs WebKit and Firefox.
*/
test.each(testEnvironments)(
"Section growing inside a LayoutCard (%s)",
async ({
testScreenshot,
render,
components: {
LayoutCard,
Section,
Heading,
Text,
Switch,
Label,
Alert,
TextField,
ColumnLayout,
},
}) => {
const TestComponent: FC = () => {
const [showDetails, setShowDetails] = useState(false);

return (
<LayoutCard>
<Section>
<Heading>Image compression</Heading>
<Section>
<Switch
data-testid="details-switch"
onChange={(isSelected) => setShowDetails(isSelected)}
>
<Label>Compress uploaded images</Label>
</Switch>
{showDetails && (
<>
<Alert>
<Heading>Recompression is not reversible</Heading>
<Text>
Images already stored keep their current quality. Only
uploads from now on are compressed.
</Text>
</Alert>
<ColumnLayout>
<TextField>
<Label>Quality</Label>
</TextField>
<TextField>
<Label>Max. width</Label>
</TextField>
</ColumnLayout>
</>
)}
</Section>
</Section>
</LayoutCard>
);
};

await render(<TestComponent />);
await testScreenshot("Section growing inside a LayoutCard - collapsed");

await page.getByTestId("details-switch").click();
await testScreenshot("Section growing inside a LayoutCard - expanded");
},
);
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading