Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions pages/board/row-height.page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import { useState } from "react";

import Box from "@cloudscape-design/components/box";
import Header from "@cloudscape-design/components/header";
import SpaceBetween from "@cloudscape-design/components/space-between";

import { Board, BoardItem, BoardProps } from "../../lib/components";
import { ScreenshotArea } from "../screenshot-area";
import { boardI18nStrings, boardItemI18nStrings } from "../shared/i18n";
import { ItemData } from "../shared/interfaces";

const rowHeightOptions: Array<undefined | number> = [undefined, 32, 48, 64, 96];

const initialItems: ReadonlyArray<BoardProps.Item<ItemData>> = [
{
id: "one",
columnSpan: 2,
rowSpan: 2,
definition: {},
data: { title: "Content-light widget", description: "", content: "Two rows tall" },
},
{
id: "two",
columnSpan: 2,
rowSpan: 4,
definition: {},
data: { title: "Taller widget", description: "", content: "Four rows tall" },
},
{
id: "three",
columnSpan: 2,
rowSpan: 3,
definition: {},
data: { title: "Medium widget", description: "", content: "Three rows tall" },
},
];

export default function BoardRowHeightPage() {
const [rowHeight, setRowHeight] = useState<undefined | number>(undefined);
const [items, setItems] = useState(initialItems);

return (
<ScreenshotArea>
<Box margin="l">
<SpaceBetween size="m">
<Header variant="h1" description="Use the buttons to change the board row height.">
Board row height (AWSUI-61568)
</Header>

<div>
<SpaceBetween size="xs" direction="horizontal">
{rowHeightOptions.map((option) => (
<button
key={String(option)}
type="button"
data-testid={`row-height-${option ?? "default"}`}
aria-pressed={rowHeight === option}
onClick={() => setRowHeight(option)}
>
{option === undefined ? "Default (96px)" : `${option}px`}
</button>
))}
</SpaceBetween>
</div>

<Box data-testid="current-row-height">
Current rowHeight: {rowHeight === undefined ? "default" : rowHeight}
</Box>

<Board
items={items}
rowHeight={rowHeight}
renderItem={(item) => (
<BoardItem header={<Header>{item.data.title}</Header>} i18nStrings={boardItemI18nStrings}>
{item.data.content}
</BoardItem>
)}
i18nStrings={boardI18nStrings}
onItemsChange={(event) => setItems(event.detail.items)}
empty="No items"
/>
</SpaceBetween>
</Box>
</ScreenshotArea>
);
}
11 changes: 11 additions & 0 deletions src/__tests__/__snapshots__/documenter.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,17 @@ The function takes the item and its associated actions (BoardProps.ItemActions)
"optional": false,
"type": "(item: BoardProps.Item<D>, actions: BoardProps.ItemActions) => JSX.Element",
},
{
"description": "Overrides the height, in pixels, of a single board row. Board items span a whole number of
rows, so this value controls the granularity of their vertical sizing (for example, a smaller
row height lets content-light items occupy less vertical space and increases sizing precision).

When not set, the density-based default is used (96px in comfortable mode, 76px in compact mode),
which preserves the existing behavior. Values that are not positive finite numbers are ignored.",
"name": "rowHeight",
"optional": true,
"type": "number",
},
],
"regions": [
{
Expand Down
49 changes: 49 additions & 0 deletions src/board/__tests__/board-row-height.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import { cleanup, render } from "@testing-library/react";
import { afterEach, beforeAll, expect, test } from "vitest";

import Board from "../../../lib/components/board";
import { defaultProps } from "./utils";

import gridStyles from "../../../lib/components/internal/grid/styles.css.js";

const ROW_HEIGHT_CSS_PROPERTY = "--awsui-board-row-height";

beforeAll(() => {
// jsdom does not support this function
document.elementFromPoint = () => null;
});

afterEach(() => {
cleanup();
});

function getGridRoot(container: HTMLElement) {
return container.querySelector<HTMLElement>(`.${gridStyles.grid}`)!;
}

test("uses the default row height when rowHeight is not provided", () => {
const { container } = render(<Board {...defaultProps} />);

expect(getGridRoot(container).style.getPropertyValue(ROW_HEIGHT_CSS_PROPERTY)).toBe("96px");
});

test("forwards a custom rowHeight to the underlying grid", () => {
const { container } = render(<Board {...defaultProps} rowHeight={32} />);

expect(getGridRoot(container).style.getPropertyValue(ROW_HEIGHT_CSS_PROPERTY)).toBe("32px");
});

test("ignores a non-positive rowHeight and keeps the default", () => {
const { container } = render(<Board {...defaultProps} rowHeight={0} />);

expect(getGridRoot(container).style.getPropertyValue(ROW_HEIGHT_CSS_PROPERTY)).toBe("96px");
});

test("does not leak rowHeight to the root element as a data attribute", () => {
const { container } = render(<Board {...defaultProps} rowHeight={48} />);

// The root board element must not receive a `rowHeight` attribute.
expect(container.firstElementChild?.hasAttribute("rowheight")).toBe(false);
});
10 changes: 10 additions & 0 deletions src/board/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,16 @@ export interface BoardProps<D = DataFallbackType> {
* When items are loading the slot can be used to render the loading indicator.
*/
empty: ReactNode;

/**
* Overrides the height, in pixels, of a single board row. Board items span a whole number of
* rows, so this value controls the granularity of their vertical sizing (for example, a smaller
* row height lets content-light items occupy less vertical space and increases sizing precision).
*
* When not set, the density-based default is used (96px in comfortable mode, 76px in compact mode),
* which preserves the existing behavior. Values that are not positive finite numbers are ignored.
*/
rowHeight?: number;
}

export namespace BoardProps {
Expand Down
2 changes: 2 additions & 0 deletions src/board/internal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
onItemsChange,
empty,
i18nStrings,
rowHeight,
__internalRootRef,
...rest
}: BoardProps<D> & InternalBaseComponentProps) {
Expand Down Expand Up @@ -237,6 +238,7 @@
isRtl={isRtl}
columns={itemsLayout.columns}
layout={[...placeholdersLayout.items, ...itemsLayout.items]}
rowHeight={rowHeight}
>
{(gridContext) => {
const layoutShift = transition?.layoutShift ?? removeTransition?.layoutShift;
Expand All @@ -254,7 +256,7 @@
children.push(
<Placeholder
key={placeholder.id}
id={placeholder.id}

Check warning on line 259 in src/board/internal.tsx

View workflow job for this annotation

GitHub Actions / build / build

Prop "id" is forbidden on Components

Check warning on line 259 in src/board/internal.tsx

View workflow job for this annotation

GitHub Actions / dry-run / Build board components

Prop "id" is forbidden on Components
state={transition ? (transition.collisionIds?.has(placeholder.id) ? "hover" : "active") : "default"}
gridContext={gridContext}
columns={itemsLayout.columns}
Expand Down
25 changes: 25 additions & 0 deletions src/internal/grid/__tests__/grid.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ const defaultProps: GridProps = {
),
};

const ROW_HEIGHT_CSS_PROPERTY = "--awsui-board-row-height";

function getGridRoot(container: HTMLElement) {
return container.querySelector<HTMLElement>(`.${gridStyles.grid}`)!;
}

test("renders children content", async () => {
const result = render(<Grid {...defaultProps} />);

Expand Down Expand Up @@ -52,3 +58,22 @@ test("assigns styles on individual elements", () => {
"grid-column-end": "span 2",
});
});

test("uses the default row height when rowHeight is not provided", () => {
const { container } = render(<Grid {...defaultProps} />);

// Defaults to the comfortable-mode row height (96px).
expect(getGridRoot(container).style.getPropertyValue(ROW_HEIGHT_CSS_PROPERTY)).toBe("96px");
});

test("applies a custom row height when rowHeight is provided", () => {
const { container } = render(<Grid {...defaultProps} rowHeight={32} />);

expect(getGridRoot(container).style.getPropertyValue(ROW_HEIGHT_CSS_PROPERTY)).toBe("32px");
});

test.each([0, -10, NaN, Infinity])("ignores invalid rowHeight value %s and falls back to default", (rowHeight) => {
const { container } = render(<Grid {...defaultProps} rowHeight={rowHeight} />);

expect(getGridRoot(container).style.getPropertyValue(ROW_HEIGHT_CSS_PROPERTY)).toBe("96px");
});
25 changes: 21 additions & 4 deletions src/internal/grid/grid.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import { Children, useRef } from "react";
import { Children, CSSProperties, useRef } from "react";
import clsx from "clsx";

import { useContainerQuery } from "@cloudscape-design/component-toolkit";
Expand All @@ -19,12 +19,21 @@ const GRID_GAP = { comfortable: 20, compact: 16 };
/* Matches grid-auto-rows in CSS. */
const ROWSPAN_HEIGHT = { comfortable: 96, compact: 76 };

export default function Grid({ layout, children: render, columns, isRtl }: GridProps) {
/* Custom property consumed by grid-auto-rows in CSS to allow overriding the row height. */
const ROW_HEIGHT_CSS_PROPERTY = "--awsui-board-row-height";

function isValidRowHeight(rowHeight?: number): rowHeight is number {
return typeof rowHeight === "number" && isFinite(rowHeight) && rowHeight > 0;
}

export default function Grid({ layout, children: render, columns, isRtl, rowHeight }: GridProps) {
const gridRef = useRef<HTMLDivElement>(null);
const [gridWidth, containerQueryRef] = useContainerQuery((entry) => entry.contentBoxWidth, []);
const densityMode = useDensityMode(gridRef);
const gridGap = GRID_GAP[densityMode];
const rowspanHeight = ROWSPAN_HEIGHT[densityMode];
// An explicit row height, when provided, overrides the density-based default so that
// consumers can achieve finer-grained vertical sizing.
const rowspanHeight = isValidRowHeight(rowHeight) ? rowHeight : ROWSPAN_HEIGHT[densityMode];

// The below getters translate relative grid units into size/offset values in pixels.
const getWidth = (colspan: number) => {
Expand All @@ -44,9 +53,17 @@ export default function Grid({ layout, children: render, columns, isRtl }: GridP

const zipped = zipTwoArrays(layout, Children.toArray(children));

// Keep the rendered CSS row height in sync with the pixel math above. The custom property is
// always set to the resolved value so grid-auto-rows matches getHeight() exactly.
const gridStyle = { [ROW_HEIGHT_CSS_PROPERTY]: `${rowspanHeight}px` } as CSSProperties;

const ref = useMergeRefs(gridRef, containerQueryRef);
return (
<div ref={ref} className={clsx(styles.grid, styles[`grid-${densityMode}`], styles[`columns-${columns}`])}>
<div
ref={ref}
className={clsx(styles.grid, styles[`grid-${densityMode}`], styles[`columns-${columns}`])}
style={gridStyle}
>
{zipped.map(([item, children]) => (
<GridItem key={item.id} item={item}>
{children}
Expand Down
6 changes: 6 additions & 0 deletions src/internal/grid/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ export interface GridProps {
columns: number;
children?: (context: GridContext) => ReactNode;
isRtl?: () => boolean;
/**
* Overrides the default height (in pixels) of a single grid row. When not set, the
* density-based default is used (96px in comfortable mode, 76px in compact mode).
* Values that are not positive finite numbers are ignored and the default is used.
*/
rowHeight?: number;
}

export interface GridContext {
Expand Down
7 changes: 4 additions & 3 deletions src/internal/grid/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ $grid-col-width: minmax(0, 1fr);
display: grid;
/* Matches GRID_GAP constant used for calculations. */
gap: 20px;
/* Matches ROWSPAN_HEIGHT constant used for calculations. */
grid-auto-rows: 96px;
/* Matches ROWSPAN_HEIGHT constant used for calculations. The custom property is set by the
Grid component and defaults to the comfortable-mode row height when absent. */
grid-auto-rows: var(--awsui-board-row-height, 96px);

&-compact {
gap: 16px;
grid-auto-rows: 76px;
grid-auto-rows: var(--awsui-board-row-height, 76px);
}
}

Expand Down
Loading