diff --git a/CHANGELOG.md b/CHANGELOG.md index 750d3d7..f7c92b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,31 @@ Versioning follows the policy in [CONTRIBUTING.md](CONTRIBUTING.md#versioning). ## [Unreleased] +### Added + +- **`Board` draws an item's title row and description.** `BoardItem` takes `title` + and `description`, each a string or a node: a string title renders as a + level-5 heading, a string description as text, a node (a `BoardItemTitle` + with a tooltip and actions, say) as is. The drag handle sits in the inline + padding on the title row's first line, where it stays when the title wraps + or grows; without a title there is no header row and the description takes + the whole item. `itemPadding` sets every item's padding, a number (px) or + a CSS length for every side or one per side (default `--spacing-5` top + and bottom, `--spacing-8` left and right); the CSS knobs behind it are + `--board-item-padding-top` / `-right` / `-bottom` / `-left`. + +### Changed + +- **`BoardItemTitle` is a `Board` item's `title` node**, no longer a + self-padded, sticky row: the board pads and places it. + +### Removed + +- **`Board`'s `renderItem`**, with `BoardItemActions`, `removeItem`, + `removedItem`, `liveAnnouncementItemRemoved` and the `uic.Board.itemRemoved` + catalog key. An item is its `title` and `description`; removing one is the + consumer's, by passing `items` without it. + ## [0.2.0-alpha.19] Astryx 0.6.5 → 0.7.0 (`@astryxdesign/lab` 0.7.0-canary.97a0d31). Astryx's diff --git a/README.md b/README.md index edfbbe0..96aa040 100644 --- a/README.md +++ b/README.md @@ -294,7 +294,7 @@ import { Modal } from "@lablup/ui-common/Modal"; | `DeleteConfirmModal` | Confirms a deletion, with typed confirmation when needed | `/components/DeleteConfirmModal` | | `StepNumberInput`, `NumberStepper` | A number field that steps along a list of values | `/components/StepNumberInput` | | `Board` | A dashboard grid of movable, resizable items | `/components/Board` | -| `BoardItemTitle` | A dashboard panel's sticky title row | `/components/BoardItemTitle` | +| `BoardItemTitle` | A Board item's title row: heading, tooltip, actions | `/components/BoardItemTitle` | | `Statistic` | A metric with a caption, a large value and a notched bar | `/components/Statistic` | | `DividedRow` | A wrapping row with dividers between neighbours on a line | `/components/DividedRow` | | `TokenList` | Values inline, the rest behind `+N` on hover | `/components/TokenList` | diff --git a/astryx/components/Board.doc.mjs b/astryx/components/Board.doc.mjs index 4e9dfc9..4195364 100644 --- a/astryx/components/Board.doc.mjs +++ b/astryx/components/Board.doc.mjs @@ -26,27 +26,28 @@ export default { name: "items", type: "ReadonlyArray>", description: - "The items: id, data, optional rowSpan (from 2) and columnSpan (from 1), definition (minRowSpan, minColumnSpan, defaultRowSpan, defaultColumnSpan) and columnOffset, a column per column count ({ 4: 2, 6: 3 }) the board writes back.", + "The items: id, data, optional title and description (each a string or a node), rowSpan (from 2) and columnSpan (from 1), definition (minRowSpan, minColumnSpan, defaultRowSpan, defaultColumnSpan) and columnOffset, a column per column count ({ 4: 2, 6: 3 }) the board writes back. A string title renders as a level-5 heading in a header row, with the drag handle on its first line; a node title (a BoardItemTitle, say) renders there as is, so a help tooltip or end-of-row actions are the node's to include. Without a title there is no header row and the description takes the whole item. A string description renders as text; a node as is, and scrolls.", required: true, }, { - name: "renderItem", - type: "(item: BoardItem, actions: { removeItem: () => void }) => ReactNode", + name: "itemPadding", + type: "number | string | { top?: number | string; right?: …; bottom?: …; left?: … }", description: - "An item's content. The board draws the surface, border and handles around it; the content area scrolls, so a BoardItemTitle inside stays at the top. removeItem reports the board without the item.", - required: true, + "The padding of every item's title row and description: a number (px) or a CSS length ('1rem', 'var(--spacing-3)') for every side, or one per side. The drag handle is not padded; it keeps the item's top-left corner, so the left padding is what clears it.", + default: "--spacing-5 top and bottom, --spacing-8 left and right", }, { name: "onItemsChange", type: "(detail: BoardItemsChangeDetail) => void", description: - "Called when the user moves, resizes or removes an item, with items (the whole board in its new order, spans and columnOffset updated) and the movedItem, resizedItem or removedItem. A move that ends where it started does not fire.", + "Called when the user moves or resizes an item, with items (the whole board in its new order, spans and columnOffset updated) and the movedItem or resizedItem. A move that ends where it started does not fire. Removing an item is the consumer's: pass items without it.", required: true, }, { name: "isMovable", type: "boolean", - description: "Renders a drag handle at each item's top-left corner.", + description: + "Renders a drag handle at each item's top-left corner, in the inline padding on the title row's first line (the block padding from the top, --spacing-1 from the start).", default: "false", }, { @@ -102,21 +103,19 @@ export default { description: "Live-region announcement at each step; the placement is 0-based.", default: "the catalog's uic.Board.itemMoved / itemResized", }, - { - name: "liveAnnouncementItemRemoved", - type: "(detail: { item: BoardItem }) => string", - description: "Live-region announcement when an item is removed.", - default: "the catalog's uic.Board.itemRemoved", - }, ], usage: { description: - "Persist what onItemsChange reports (minus data) and pass it back as items; the board snaps back otherwise. Drag a handle to move or resize by pointer (a resized item follows the pointer's size and snaps to the grid on release). Click a handle, or press Enter or Space on it, to activate it: direction buttons appear and they or the arrow keys step one cell, Enter, Space or leaving the handle commits, Escape discards. A step that would overlap an item only partly, in the direction of the move, is a conflict and does not commit. The drag handle overlays the item's top-left corner, so give the content a horizontal inset. A pointer drag held near an edge of the nearest scroll container scrolls it. CSS knobs on the root or above: --board-row-height (96px), --board-gap (--spacing-5), --board-item-radius (--radius-container), --board-transition-duration (200ms, off under prefers-reduced-motion), --board-handle-z (51; BoardItemTitle's sticky row is 50). Other div attributes reach the root.", + "Persist what onItemsChange reports (minus data) and pass it back as items; the board snaps back otherwise. Drag a handle to move or resize by pointer (a resized item follows the pointer's size and snaps to the grid on release). Click a handle, or press Enter or Space on it, to activate it: direction buttons appear and they or the arrow keys step one cell, Enter, Space or leaving the handle commits, Escape discards. A step that would overlap an item only partly, in the direction of the move, is a conflict and does not commit. The drag handle is absolute at the item's top-left corner, in the inline padding on the title row's first line, and stays there when a title wraps or a node title grows; an item without a title keeps it in the same spot. A pointer drag held near an edge of the nearest scroll container scrolls it. CSS knobs on the root or above: --board-row-height (96px), --board-gap (--spacing-5), --board-item-radius (--radius-container), --board-transition-duration (200ms, off under prefers-reduced-motion), --board-handle-z (51), --board-item-padding-top / -right / -bottom / -left (the title row's and the description's padding, --spacing-5 top and bottom, --spacing-8 left and right; itemPadding sets them per board). Other div attributes reach the root.", }, examples: [ { label: "A movable, resizable dashboard", - code: ' (\n \n \n {item.data.content}\n \n )}\n onItemsChange={({ items }) => setItems(items)}\n/>', + code: ', data: {} },\n {\n id: "usage",\n title: } />,\n description: ,\n data: {},\n },\n ]}\n isMovable\n isResizable\n variant="bordered"\n onItemsChange={({ items }) => setItems(items)}\n/>', + }, + { + label: "Title-less tiles with a tighter inset", + code: " ({ id: a.id, description: , data: a }))}\n isMovable\n itemPadding={{ top: 16, right: 20, bottom: 16, left: 20 }}\n onItemsChange={({ items }) => setItems(items)}\n/>", }, ], }; diff --git a/astryx/components/BoardItemTitle.doc.mjs b/astryx/components/BoardItemTitle.doc.mjs index 92c16f3..9180bef 100644 --- a/astryx/components/BoardItemTitle.doc.mjs +++ b/astryx/components/BoardItemTitle.doc.mjs @@ -9,9 +9,9 @@ export default { displayName: "BoardItemTitle", import: "@lablup/ui-common", category: "Layout", - keywords: ["title", "panel", "dashboard", "board", "sticky", "header"], + keywords: ["title", "panel", "dashboard", "board", "header"], description: - "The title row of a dashboard panel: a heading with an optional help tooltip, and actions at the end. It sticks to the top of the panel's scroll area on the surface colour, and its two groups wrap onto separate lines when the panel is narrow.", + "A Board item's title node: a heading with an optional help tooltip, and actions at the end. The board pads and places the row, and its two groups wrap onto separate lines when the item is narrow.", props: [ { name: "title", @@ -40,7 +40,7 @@ export default { ], usage: { description: - "At the top of a dashboard panel or a board item. Other div attributes (className, style, data-*) reach the row. The --board-item-title-z property sets its z-index (default 50).", + "As a Board item's title. Other div attributes (className, style, data-*) reach the row.", }, examples: [ { diff --git a/docs/storybook.md b/docs/storybook.md index 41fe573..c2ad097 100644 --- a/docs/storybook.md +++ b/docs/storybook.md @@ -75,6 +75,10 @@ type Story = StoryObj; still renders and stays in the props table; only the edit field goes. - Import the component relatively (`./StatCard`), never from `@lablup/ui-common`. +- A `render` function that puts elements it creates into a prop's data (an + `items` array with node titles, say) hangs the Docs page's JSX snippet, which + walks every prop. Give that story + `parameters: { docs: { source: { type: "code" } } }`. - A generic component takes its type argument in both places: `component: DataGrid` and `satisfies Meta>`. - If `tsc` reports TS2742 on `meta` (the inferred type names an Astryx internal diff --git a/src/components/Board/Board.css b/src/components/Board/Board.css index dc07390..cf9c51c 100644 --- a/src/components/Board/Board.css +++ b/src/components/Board/Board.css @@ -6,8 +6,14 @@ * --board-gap between cells (default --spacing-5) * --board-item-radius an item's corners (default --radius-container) * --board-transition-duration how long items take to slide (default 200ms) - * --board-handle-z the handles' z-index inside an item (default 51; - * BoardItemTitle's sticky row sits at 50) + * --board-handle-z the handles' z-index inside an item (default 51) + * --board-item-padding-top / -right / -bottom / -left + * the title row's and the description's padding + * (default --spacing-5 top and bottom, --spacing-8 + * left and right; the drag handle sits in the left + * padding, and its top offset is the top padding, + * so it sits on the title row's first line). The + * `itemPadding` prop sets them per board. */ @layer ui-common { .uic-board { @@ -34,9 +40,14 @@ } .uic-board-item { + --_board-item-padding-top: var(--board-item-padding-top, var(--spacing-5)); + --_board-item-padding-right: var(--board-item-padding-right, var(--spacing-8)); + --_board-item-padding-bottom: var(--board-item-padding-bottom, var(--spacing-5)); + --_board-item-padding-left: var(--board-item-padding-left, var(--spacing-8)); + position: relative; - /* Its own stacking context: the handles' z-index and a sticky title's - stay inside the item, and an active item lifts whole. */ + /* Its own stacking context: the handles' z-index stays inside the item, + and an active item lifts whole. */ isolation: isolate; display: flex; flex-direction: column; @@ -76,13 +87,54 @@ align-self: start; } + /* The header row: the item's padding (the top value above and below the + title), and a title slot as tall as the sm drag handle, which sits in the + left padding on the row's first line. Outside the scrolling content, so + it needs no sticky. */ + .uic-board-item__header { + display: flex; + flex-shrink: 0; + align-items: center; + min-width: 0; + padding: var(--_board-item-padding-top) var(--_board-item-padding-right) + var(--_board-item-padding-top) var(--_board-item-padding-left); + } + + .uic-board-item__title { + display: flex; + flex: 1; + align-items: flex-start; + min-width: 0; + min-block-size: var(--size-element-sm); + } + + .uic-board-item__title > * { + flex: 1; + min-width: 0; + min-block-size: var(--size-element-sm); + } + + /* A string title: its first line is centered on the handle, the rest wrap below. */ + .uic-board-item__title > .uic-board-item__heading { + min-block-size: 0; + padding-block: calc((var(--size-element-sm) - 1lh) / 2); + } + + /* The description: the item's padding; after a header row, none on top. + Without a title it is the whole item. */ .uic-board-item__content { flex: 1; min-height: 0; overflow: auto; + padding: var(--_board-item-padding-top) var(--_board-item-padding-right) + var(--_board-item-padding-bottom) var(--_board-item-padding-left); border-radius: inherit; } + .uic-board-item__header + .uic-board-item__content { + padding-block-start: 0; + } + .uic-board-item__handle { position: absolute; z-index: var(--board-handle-z, 51); @@ -90,7 +142,7 @@ } .uic-board-item__handle--drag { - inset-block-start: var(--spacing-2); + inset-block-start: var(--_board-item-padding-top); inset-inline-start: var(--spacing-1); } diff --git a/src/components/Board/Board.messages.ts b/src/components/Board/Board.messages.ts index 0e4d07d..d14a745 100644 --- a/src/components/Board/Board.messages.ts +++ b/src/components/Board/Board.messages.ts @@ -46,10 +46,6 @@ export const boardMessages = defineMessages({ defaultMessage: "Resize discarded.", description: "Screen reader announcement when resizing a board item is cancelled", }, - "uic.Board.itemRemoved": { - defaultMessage: "Item removed.", - description: "Screen reader announcement when a board item is removed", - }, "uic.Board.directionUp": { defaultMessage: "Up", description: diff --git a/src/components/Board/Board.stories.tsx b/src/components/Board/Board.stories.tsx index 430a8bc..73d000a 100644 --- a/src/components/Board/Board.stories.tsx +++ b/src/components/Board/Board.stories.tsx @@ -1,60 +1,52 @@ -import type { ReactNode } from "react"; +import { useState } from "react"; import type { Meta, StoryObj } from "@storybook/react-vite"; import { fn } from "storybook/test"; import { Button } from "../../astryx/Button"; +import { HStack, Stack } from "../../astryx/Stack"; import { BoardItemTitle } from "../BoardItemTitle"; import { useArgState } from "../../stories/helpers"; -import { Board } from "./Board"; +import { Board, type BoardItemsChangeDetail } from "./Board"; import type { BoardItem } from "./layout"; -interface Widget { - title: string; - body: string; -} +/** The items carry nothing of their own: `title` and `description` are the whole content. */ +type Widget = null; const WIDGETS: BoardItem[] = [ { id: "sessions", columnSpan: 2, - rowSpan: 3, - data: { title: "Active sessions", body: "1,284 running across 3 regions." }, + rowSpan: 2, + title: "Active sessions", + description: "1,284 running across 3 regions.", + data: null, }, { - id: "usage", + id: "resources", columnSpan: 2, - rowSpan: 3, - data: { title: "Resource usage", body: "CPU 62%, memory 48%, GPU 71%." }, + rowSpan: 2, + title: "Resource usage", + description: "CPU 62%, memory 48%, GPU 71%.", + data: null, }, { id: "queue", columnSpan: 1, rowSpan: 2, - data: { title: "Queue", body: "12 pending jobs." }, + title: "Queue", + description: "12 pending jobs.", + data: null, }, { id: "storage", columnSpan: 1, rowSpan: 2, - data: { title: "Storage", body: "3.2 TiB of 8 TiB used." }, + title: "Storage", + description: "3.2 TiB of 8 TiB used.", + data: null, }, ]; -function WidgetCard({ - widget, - endContent, -}: { - widget: Widget; - endContent?: ReactNode; -}) { - return ( -
- -

{widget.body}

-
- ); -} - const meta = { title: "Components/Board", component: Board, @@ -63,10 +55,10 @@ const meta = { emptyContent: { control: false }, dragHandleIcon: { control: false }, resizeHandleIcon: { control: false }, + itemPadding: { control: false }, }, args: { items: WIDGETS, - renderItem: (item) => , onItemsChange: fn(), variant: "bordered", }, @@ -76,8 +68,10 @@ export default meta; type Story = StoryObj; +/** Each item renders its `title` as a header row and its `description` under it. */ export const Default: Story = {}; +/** The drag handle sits on the header row's first line, in the inline padding. */ export const Editable: Story = { args: { isMovable: true, isResizable: true }, render: function Render(args) { @@ -86,14 +80,6 @@ export const Editable: Story = { {...args} items={items} - renderItem={(item, { removeItem }) => ( - - } - /> - )} onItemsChange={(detail) => { args.onItemsChange(detail); setItems(detail.items); @@ -103,6 +89,106 @@ export const Editable: Story = { }, }; +/** A `BoardItemTitle` node as the title: a help tooltip and end-of-row actions. Removing is the consumer's: filter the items. */ +export const TitleWithActions: Story = { + args: { isMovable: true, isResizable: true }, + // Storybook's JSX snippet walks a rendered element's props; node titles made + // in a render function hang it, so this story shows its code instead. + parameters: { docs: { source: { type: "code" } } }, + render: function Render(args) { + const [items, setItems] = useArgState(args.items); + const remove = (id: string) => { + const next = items.filter((item) => item.id !== id); + const detail: BoardItemsChangeDetail = { items: next }; + args.onItemsChange(detail); + setItems(next); + }; + return ( + + {...args} + items={items.map((item) => ({ + ...item, + title: ( + remove(item.id)} + /> + } + /> + ), + }))} + onItemsChange={(detail) => { + args.onItemsChange(detail); + setItems(detail.items); + }} + /> + ); + }, +}; + +function ExpandableTitle({ title, detail }: { title: string; detail: string }) { + const [isOpen, setIsOpen] = useState(false); + return ( + + + {title} + - - )} - onItemsChange={onItemsChange} - {...props} - />, + items={THREE} onItemsChange={onItemsChange} {...props} />, ); return { ...view, onItemsChange }; } @@ -106,28 +94,32 @@ describe("Board", () => { rowSpan: 3, columnSpan: 1, columnOffset: { 4: 0 }, - data: { title: "Alpha" }, + title: "Alpha", + data: null, }, { id: "b", rowSpan: 3, columnSpan: 1, columnOffset: { 4: 1 }, - data: { title: "Beta" }, + title: "Beta", + data: null, }, { id: "c", rowSpan: 3, columnSpan: 1, columnOffset: { 4: 3 }, - data: { title: "Gamma" }, + title: "Gamma", + data: null, }, { id: "d", rowSpan: 2, columnSpan: 2, columnOffset: { 4: 1 }, - data: { title: "Delta" }, + title: "Delta", + data: null, }, ], }); @@ -176,14 +168,7 @@ describe("Board", () => { screen.queryByRole("button", { name: "Resize handle" }), ).not.toBeInTheDocument(); - rerender( - - items={THREE} - isMovable - renderItem={(item) =>
{item.data.title}
} - onItemsChange={() => {}} - />, - ); + rerender( items={THREE} isMovable onItemsChange={() => {}} />); expect(screen.getAllByRole("button", { name: "Drag handle" })).toHaveLength(3); expect( screen.queryByRole("button", { name: "Resize handle" }), @@ -195,7 +180,6 @@ describe("Board", () => { isResizable dragHandleLabel="Move" resizeHandleLabel="Grow" - renderItem={(item) =>
{item.data.title}
} onItemsChange={() => {}} />, ); @@ -217,7 +201,6 @@ describe("Board", () => { const { container } = render( null} onItemsChange={() => {}} className="extra" data-testid="board" @@ -282,7 +265,7 @@ describe("Board", () => { ]); expect(detail.movedItem?.id).toBe("b"); expect(detail.resizedItem).toBeUndefined(); - expect(detail.items[0]?.data).toBe(THREE[1]?.data); + expect(detail.items[0]?.id).toBe("b"); expect(shell("Beta")).not.toHaveClass("uic-board-item--active"); expect(directionButtons("Beta")).toEqual([]); }); @@ -366,14 +349,14 @@ describe("Board", () => { isMovable: true, liveAnnouncementDndStarted: (op) => `start ${op}`, liveAnnouncementDndItemMoved: ({ item, placement }) => - `${item.data.title} at ${placement.x},${placement.y}`, + `${item.id} at ${placement.x},${placement.y}`, liveAnnouncementDndCommitted: (op) => `done ${op}`, }); dragHandleOf("Beta").focus(); await userEvent.keyboard(" "); expect(liveText()).toBe("start move"); await userEvent.keyboard("{ArrowLeft}{ArrowLeft}"); - expect(liveText()).toBe("Beta at 0,0"); + expect(liveText()).toBe("b at 0,0"); await userEvent.keyboard("{Enter}"); expect(liveText()).toBe("done move"); }); @@ -615,7 +598,8 @@ describe("Board", () => { rowSpan: 3, columnSpan: 2, definition: { minColumnSpan: 2 }, - data: { title: "Alpha" }, + title: "Alpha", + data: null, }, ], }); @@ -630,29 +614,13 @@ describe("Board", () => { }); }); - it("removes an item through the render actions and re-lays the rest out", async () => { - const { onItemsChange } = renderBoard(); - await userEvent.click(screen.getByRole("button", { name: "Remove Alpha" })); - expect(onItemsChange).toHaveBeenCalledTimes(1); - const detail = onItemsChange.mock.calls[0]?.[0] as BoardItemsChangeDetail; - expect(detail.removedItem?.id).toBe("a"); - expect(detail.items.map((it) => it.id)).toEqual(["b", "c"]); - expect(detail.items.map((it) => it.columnOffset)).toEqual([{ 4: 2 }, { 4: 0 }]); - expect(liveText()).toBe("Item removed."); - }); - it("is controlled: renders what the parent passes after a change", async () => { const { onItemsChange, rerender } = renderBoard({ isMovable: true }); dragHandleOf("Beta").focus(); await userEvent.keyboard("{Enter}{ArrowLeft}{ArrowLeft}{Enter}"); const detail = onItemsChange.mock.calls[0]?.[0] as BoardItemsChangeDetail; rerender( - - items={detail.items} - isMovable - renderItem={(item) =>
{item.data.title}
} - onItemsChange={onItemsChange} - />, + items={detail.items} isMovable onItemsChange={onItemsChange} />, ); expect(placement("Beta")).toBe("1 / span 2 | 1 / span 2"); expect(placement("Alpha")).toBe("3 / span 2 | 1 / span 2"); @@ -663,3 +631,132 @@ describe("Board", () => { ]); }); }); + +describe("Board item title and description", () => { + const item = (overrides: Partial>): BoardItem => ({ + id: "a", + rowSpan: 2, + columnSpan: 2, + data: null, + ...overrides, + }); + const header = () => document.querySelector(".uic-board-item__header"); + const dragHandle = () => screen.getByRole("button", { name: "Drag handle" }); + + it("renders a string title as a heading in a header row and the description as the body", () => { + render( + + items={[item({ title: "Alpha", description: "The first one." })]} + isMovable + onItemsChange={vi.fn()} + />, + ); + const heading = within(header() as HTMLElement).getByRole("heading", { + name: "Alpha", + }); + expect(heading.classList.contains("uic-board-item__heading")).toBe(true); + expect( + within( + document.querySelector(".uic-board-item__content") as HTMLElement, + ).getByText("The first one."), + ).toBeTruthy(); + expect( + document.querySelector(".uic-board-item__header + .uic-board-item__content"), + ).not.toBeNull(); + }); + + it("keeps the drag handle on the item itself, outside the header row", () => { + render( + + items={[item({ title: "Alpha", description: "The first one." })]} + isMovable + onItemsChange={vi.fn()} + />, + ); + expect(dragHandle().closest(".uic-board-item__header")).toBeNull(); + expect(dragHandle().closest(".uic-board-item__handle--drag")?.parentElement).toBe( + document.querySelector(".uic-board-item"), + ); + }); + + it("renders a node title as is", () => { + render( + + items={[ + item({ + title: ( + + Alpha + + ), + description: The first one., + }), + ]} + onItemsChange={vi.fn()} + />, + ); + expect( + within(header() as HTMLElement).getByRole("button", { name: "Refresh" }), + ).toBeTruthy(); + expect(screen.queryByRole("heading")).toBeNull(); + }); + + it("draws no header row without a title: the description takes the item", () => { + render( + + items={[item({ description: "Body only." })]} + isMovable + onItemsChange={vi.fn()} + />, + ); + expect(header()).toBeNull(); + expect(screen.queryByRole("heading")).toBeNull(); + expect(dragHandle().closest(".uic-board-item__handle--drag")?.parentElement).toBe( + document.querySelector(".uic-board-item"), + ); + expect(screen.getByText("Body only.")).toBeTruthy(); + }); + + it("sets the padding custom properties from itemPadding", () => { + const titled = [item({ title: "Alpha", description: "The first one." })]; + const { rerender } = render( + + items={titled} + itemPadding={{ top: 4, left: "1rem" }} + onItemsChange={vi.fn()} + />, + ); + const prop = (name: string) => + (document.querySelector(".uic-board-item") as HTMLElement).style.getPropertyValue( + `--board-item-padding-${name}`, + ); + expect([prop("top"), prop("right"), prop("bottom"), prop("left")]).toEqual([ + "4px", + "", + "", + "1rem", + ]); + rerender( items={titled} itemPadding={13} onItemsChange={vi.fn()} />); + expect([prop("top"), prop("right"), prop("bottom"), prop("left")]).toEqual([ + "13px", + "13px", + "13px", + "13px", + ]); + rerender( + + items={titled} + itemPadding="var(--spacing-3)" + onItemsChange={vi.fn()} + />, + ); + expect(prop("left")).toBe("var(--spacing-3)"); + rerender( items={titled} onItemsChange={vi.fn()} />); + expect([prop("top"), prop("right"), prop("bottom"), prop("left")]).toEqual([ + "", + "", + "", + "", + ]); + }); +}); diff --git a/src/components/Board/Board.tsx b/src/components/Board/Board.tsx index 52c9c04..abe129c 100644 --- a/src/components/Board/Board.tsx +++ b/src/components/Board/Board.tsx @@ -8,8 +8,11 @@ * controlled: `onItemsChange` reports the whole board in its new order, and * what the parent passes back is what renders. * - * A drag handle at an item's top-left corner moves it, a handle at the - * bottom-right resizes it. Dragging a handle moves or resizes by pointer: the + * An item renders a header row with its `title` and its `description` + * under it; without a title the description takes the whole item. Both + * are padded by `itemPadding`. A drag handle at the item's top-left corner + * moves it, on the header row's first line; a handle at the bottom-right + * resizes it. Dragging a handle moves or resizes by pointer: the * item follows the pointer, the rest of the board previews where everything * lands, and releasing snaps to the grid. Clicking a handle, or pressing * Enter or Space on it, activates it instead: direction buttons appear around @@ -24,9 +27,10 @@ * isMovable * isResizable * variant="bordered" - * renderItem={(item) => } * onItemsChange={({ items }) => setItems(items)} * /> + * // items: [{ id: "sessions", title: "Active sessions", description: , data: {} }] + * // itemPadding={12}, itemPadding="1rem" or itemPadding={{ top: 12, left: 16 }} set the content padding. */ import { useEffect, @@ -38,14 +42,15 @@ import { type PointerEvent, type ReactNode, } from "react"; +import { Heading } from "@astryxdesign/core/Heading"; import { IconButton } from "@astryxdesign/core/IconButton"; +import { Text } from "@astryxdesign/core/Text"; import { VisuallyHidden } from "@astryxdesign/core/VisuallyHidden"; import { useUicTranslator } from "../../i18n/useUicTranslator"; import { appendPath, LayoutEngine, - removeItem as removeFromLayout, type Direction, type LayoutShift, type Position, @@ -85,22 +90,25 @@ export interface BoardDndDetail { placement: BoardItemPlacement; } -export interface BoardItemRemovedDetail { - item: BoardItem; -} - export interface BoardItemsChangeDetail { /** The whole board in its new order, spans and column offsets updated. */ items: ReadonlyArray>; movedItem?: BoardItem; resizedItem?: BoardItem; - removedItem?: BoardItem; } -export interface BoardItemActions { - /** Removes the item: `onItemsChange` fires with the rest, re-laid out. */ - removeItem: () => void; -} +/** A length: a number is px, a string any CSS length (`"1rem"`, `"var(--spacing-3)"`). */ +export type BoardLength = number | string; + +/** An item's content padding: one length for every side, or a length per side. */ +export type BoardItemPadding = + | BoardLength + | { + top?: BoardLength; + right?: BoardLength; + bottom?: BoardLength; + left?: BoardLength; + }; export interface BoardColumnBreakpoint { /** The least content width, in px, at which the board has `columns` columns. */ @@ -124,9 +132,14 @@ export interface BoardProps extends Omit< "children" | "onChange" > { items: ReadonlyArray>; - /** An item's content. The board draws the surface, border and handles around it. */ - renderItem: (item: BoardItem, actions: BoardItemActions) => ReactNode; - /** Called when the user moves, resizes or removes an item. */ + /** + * The padding of every item's title row and description: a number (px) or + * a CSS length for every side, or one per side. The drag handle is not + * padded: it keeps the item's top-left corner, so the left padding is what + * clears it. @default `--spacing-5` top and bottom, `--spacing-8` left and right + */ + itemPadding?: BoardItemPadding; + /** Called when the user moves or resizes an item. */ onItemsChange: (detail: BoardItemsChangeDetail) => void; /** Whether items have a drag handle and can be moved. @default false */ isMovable?: boolean; @@ -158,8 +171,24 @@ export interface BoardProps extends Omit< liveAnnouncementDndCommitted?: (operation: BoardOperation) => string; /** Announced when a move or resize is discarded. Default: `uic.Board.moveDiscarded` / `resizeDiscarded` */ liveAnnouncementDndDiscarded?: (operation: BoardOperation) => string; - /** Announced when an item is removed. Default: `uic.Board.itemRemoved` */ - liveAnnouncementItemRemoved?: (detail: BoardItemRemovedDetail) => string; +} + +const toLength = (value: BoardLength) => + typeof value === "number" ? `${value}px` : value; + +/** The custom properties `itemPadding` sets on an item; empty when unset. */ +export function itemPaddingStyle(padding: BoardItemPadding | undefined): CSSProperties { + if (padding === undefined) return {}; + const sides = + typeof padding === "object" + ? padding + : { top: padding, right: padding, bottom: padding, left: padding }; + const style: Record = {}; + for (const side of ["top", "right", "bottom", "left"] as const) { + const value = sides[side]; + if (value !== undefined) style[`--board-item-padding-${side}`] = toLength(value); + } + return style as CSSProperties; } export interface BoardMetrics { @@ -384,7 +413,6 @@ const cx = (...names: Array) => export function Board({ items, - renderItem, onItemsChange, isMovable = false, isResizable = false, @@ -401,7 +429,7 @@ export function Board({ liveAnnouncementDndItemResized, liveAnnouncementDndCommitted, liveAnnouncementDndDiscarded, - liveAnnouncementItemRemoved, + itemPadding, className, ...divProps }: BoardProps): ReactNode { @@ -654,18 +682,6 @@ export function Board({ return () => window.removeEventListener("keydown", onKeyDown); }, [isPointerDrag]); - const removeItemAction = (item: BoardItem) => { - const current = interpretItems(itemsRef.current, columns); - const rest = itemsRef.current.filter((it) => it.id !== item.id); - onItemsChange({ - items: transformItems(rest, removeFromLayout(current, item.id)), - removedItem: item, - }); - setAnnouncement( - liveAnnouncementItemRemoved?.({ item }) ?? t("uic.Board.itemRemoved"), - ); - }; - /** One keyboard or direction-button step of the active transition. */ const stepBy = (tr: Transition, direction: Direction) => { const dx = direction === "left" ? -1 : direction === "right" ? 1 : 0; @@ -967,6 +983,8 @@ export function Board({ cells * (cell + (transition?.metrics.gap ?? 0)); /** The direction buttons of an activated handle. */ + const paddingStyle = itemPaddingStyle(itemPadding); + const renderDirections = (operation: BoardOperation, itemId: string) => { const shown = transition !== null && @@ -1042,6 +1060,7 @@ export function Board({ } } const style: CSSProperties = { + ...paddingStyle, gridColumn: column, gridRow: row, transform, @@ -1050,6 +1069,25 @@ export function Board({ }; const extra = typeof itemClassName === "function" ? itemClassName(item) : itemClassName; + const hasHeader = item.title != null && item.title !== false; + const dragHandle = isMovable ? ( + + } + onPointerDown={onHandlePointerDown("move", item.id)} + onPointerMove={onHandlePointerMove} + onPointerUp={onHandlePointerUp} + onPointerCancel={onHandlePointerCancel} + onKeyDown={onHandleKeyDown("move", item.id)} + onBlur={onHandleBlur} + /> + {renderDirections("move", item.id)} + + ) : null; return (
({ style={style} data-board-item-id={item.id} > - {isMovable ? ( - - } - onPointerDown={onHandlePointerDown("move", item.id)} - onPointerMove={onHandlePointerMove} - onPointerUp={onHandlePointerUp} - onPointerCancel={onHandlePointerCancel} - onKeyDown={onHandleKeyDown("move", item.id)} - onBlur={onHandleBlur} - /> - {renderDirections("move", item.id)} - + {dragHandle} + {hasHeader ? ( +
+
+ {typeof item.title === "string" ? ( + + {item.title} + + ) : ( + item.title + )} +
+
) : null}
- {renderItem(item, { removeItem: () => removeItemAction(item) })} + {typeof item.description === "string" ? ( + {item.description} + ) : ( + item.description + )}
{isResizable ? ( diff --git a/src/components/Board/index.ts b/src/components/Board/index.ts index d32cad6..36125a6 100644 --- a/src/components/Board/index.ts +++ b/src/components/Board/index.ts @@ -4,9 +4,9 @@ export type { BoardOperation, BoardItemPlacement, BoardDndDetail, - BoardItemRemovedDetail, BoardItemsChangeDetail, - BoardItemActions, + BoardItemPadding, + BoardLength, BoardColumnBreakpoint, } from "./Board"; export { interpretItems, transformItems } from "./layout"; diff --git a/src/components/Board/layout.ts b/src/components/Board/layout.ts index 5a0c777..e83a539 100644 --- a/src/components/Board/layout.ts +++ b/src/components/Board/layout.ts @@ -9,6 +9,8 @@ * (the board this one replaces), so a stored dashboard renders identically. */ +import type { ReactNode } from "react"; + export interface BoardItemDefinition { /** The least rows the item may take. Floor 2. @default 2 */ minRowSpan?: number; @@ -33,6 +35,14 @@ export interface BoardItem { rowSpan?: number; /** Columns, from 1. Written back by `onItemsChange` after a resize. */ columnSpan?: number; + /** + * The header row's content. A string renders as a level-5 heading; a node + * renders as is. Without one there is no header row: the description takes + * the whole item. + */ + title?: ReactNode; + /** The content under the title. A string renders as text; a node renders as is. */ + description?: ReactNode; data: D; } diff --git a/src/components/BoardItemTitle/BoardItemTitle.css b/src/components/BoardItemTitle/BoardItemTitle.css index d43dbde..42e39a8 100644 --- a/src/components/BoardItemTitle/BoardItemTitle.css +++ b/src/components/BoardItemTitle/BoardItemTitle.css @@ -1,17 +1,8 @@ -/* - * BoardItemTitle: the sticky title row. --board-item-title-z lifts it - * over the panel's own content (sticky table headers and fixed columns); - * a product with a z-index ladder sets it from there. - */ +/* BoardItemTitle: a Board item's title row. The board pads and places it. */ @layer ui-common { .uic-board-item-title { - position: sticky; - top: 0; - z-index: var(--board-item-title-z, 50); flex-shrink: 0; min-width: 0; - padding-block: var(--spacing-5); - background-color: var(--color-background-surface); } .uic-board-item-title__group { diff --git a/src/components/BoardItemTitle/BoardItemTitle.tsx b/src/components/BoardItemTitle/BoardItemTitle.tsx index 3b94b6f..cb6ba96 100644 --- a/src/components/BoardItemTitle/BoardItemTitle.tsx +++ b/src/components/BoardItemTitle/BoardItemTitle.tsx @@ -1,17 +1,23 @@ /** * BoardItemTitle * - * The title row of a dashboard panel: a heading with an optional help - * tooltip, and actions at the end. It sticks to the top of the panel's - * scroll area on the surface colour, so a long panel keeps its title in view. - * The two groups wrap onto separate lines when the panel is narrow. + * A Board item's `title` node: a heading with an optional help tooltip, and + * actions at the end. The board pads and places the row; the two groups wrap + * onto separate lines when the item is narrow. * * @example - * } - * /> + * items={[{ + * id: "sessions", + * title: ( + * } + * /> + * ), + * description: , + * data: {}, + * }]} */ import type { HTMLAttributes, ReactNode } from "react"; import { Heading } from "@astryxdesign/core/Heading"; diff --git a/src/i18n/locales/de-DE.json b/src/i18n/locales/de-DE.json index 1e8884f..0b6dfbf 100644 --- a/src/i18n/locales/de-DE.json +++ b/src/i18n/locales/de-DE.json @@ -63,10 +63,6 @@ "defaultMessage": "Größenänderung verworfen.", "description": "Screen reader announcement when resizing a board item is cancelled" }, - "uic.Board.itemRemoved": { - "defaultMessage": "Element entfernt.", - "description": "Screen reader announcement when a board item is removed" - }, "uic.ColorPicker.label": { "defaultMessage": "Farbe auswählen", "description": "Accessible name of a color picker whose caller gave it no label" diff --git a/src/i18n/locales/el-GR.json b/src/i18n/locales/el-GR.json index 7821004..5d02b89 100644 --- a/src/i18n/locales/el-GR.json +++ b/src/i18n/locales/el-GR.json @@ -63,10 +63,6 @@ "defaultMessage": "Η αλλαγή μεγέθους ακυρώθηκε.", "description": "Screen reader announcement when resizing a board item is cancelled" }, - "uic.Board.itemRemoved": { - "defaultMessage": "Το στοιχείο αφαιρέθηκε.", - "description": "Screen reader announcement when a board item is removed" - }, "uic.ColorPicker.label": { "defaultMessage": "Επιλογή χρώματος", "description": "Accessible name of a color picker whose caller gave it no label" diff --git a/src/i18n/locales/es-ES.json b/src/i18n/locales/es-ES.json index 0f47c45..90d8a0e 100644 --- a/src/i18n/locales/es-ES.json +++ b/src/i18n/locales/es-ES.json @@ -63,10 +63,6 @@ "defaultMessage": "Redimensión descartada.", "description": "Screen reader announcement when resizing a board item is cancelled" }, - "uic.Board.itemRemoved": { - "defaultMessage": "Elemento eliminado.", - "description": "Screen reader announcement when a board item is removed" - }, "uic.ColorPicker.label": { "defaultMessage": "Seleccionar color", "description": "Accessible name of a color picker whose caller gave it no label" diff --git a/src/i18n/locales/fi-FI.json b/src/i18n/locales/fi-FI.json index 65fbc95..0a09a64 100644 --- a/src/i18n/locales/fi-FI.json +++ b/src/i18n/locales/fi-FI.json @@ -63,10 +63,6 @@ "defaultMessage": "Koonmuutos hylättiin.", "description": "Screen reader announcement when resizing a board item is cancelled" }, - "uic.Board.itemRemoved": { - "defaultMessage": "Kohde poistettiin.", - "description": "Screen reader announcement when a board item is removed" - }, "uic.ColorPicker.label": { "defaultMessage": "Valitse väri", "description": "Accessible name of a color picker whose caller gave it no label" diff --git a/src/i18n/locales/fr-FR.json b/src/i18n/locales/fr-FR.json index fd7f2a5..bc68e8e 100644 --- a/src/i18n/locales/fr-FR.json +++ b/src/i18n/locales/fr-FR.json @@ -63,10 +63,6 @@ "defaultMessage": "Redimensionnement annulé.", "description": "Screen reader announcement when resizing a board item is cancelled" }, - "uic.Board.itemRemoved": { - "defaultMessage": "Élément supprimé.", - "description": "Screen reader announcement when a board item is removed" - }, "uic.ColorPicker.label": { "defaultMessage": "Sélectionner une couleur", "description": "Accessible name of a color picker whose caller gave it no label" diff --git a/src/i18n/locales/id-ID.json b/src/i18n/locales/id-ID.json index c16720b..6515c13 100644 --- a/src/i18n/locales/id-ID.json +++ b/src/i18n/locales/id-ID.json @@ -63,10 +63,6 @@ "defaultMessage": "Perubahan ukuran dibatalkan.", "description": "Screen reader announcement when resizing a board item is cancelled" }, - "uic.Board.itemRemoved": { - "defaultMessage": "Item dihapus.", - "description": "Screen reader announcement when a board item is removed" - }, "uic.ColorPicker.label": { "defaultMessage": "Pilih warna", "description": "Accessible name of a color picker whose caller gave it no label" diff --git a/src/i18n/locales/it-IT.json b/src/i18n/locales/it-IT.json index 5ee93a2..5115c3f 100644 --- a/src/i18n/locales/it-IT.json +++ b/src/i18n/locales/it-IT.json @@ -63,10 +63,6 @@ "defaultMessage": "Ridimensionamento annullato.", "description": "Screen reader announcement when resizing a board item is cancelled" }, - "uic.Board.itemRemoved": { - "defaultMessage": "Elemento rimosso.", - "description": "Screen reader announcement when a board item is removed" - }, "uic.ColorPicker.label": { "defaultMessage": "Seleziona colore", "description": "Accessible name of a color picker whose caller gave it no label" diff --git a/src/i18n/locales/ja-JP.json b/src/i18n/locales/ja-JP.json index bd8c81f..6650d06 100644 --- a/src/i18n/locales/ja-JP.json +++ b/src/i18n/locales/ja-JP.json @@ -63,10 +63,6 @@ "defaultMessage": "サイズ変更を取り消しました。", "description": "Screen reader announcement when resizing a board item is cancelled" }, - "uic.Board.itemRemoved": { - "defaultMessage": "項目を削除しました。", - "description": "Screen reader announcement when a board item is removed" - }, "uic.BooleanToken.true": { "defaultMessage": "はい", "description": "Default label of a token showing a boolean value that is true" diff --git a/src/i18n/locales/ko-KR.json b/src/i18n/locales/ko-KR.json index e606ad0..36c6ee4 100644 --- a/src/i18n/locales/ko-KR.json +++ b/src/i18n/locales/ko-KR.json @@ -63,10 +63,6 @@ "defaultMessage": "크기 조절을 취소했습니다.", "description": "Screen reader announcement when resizing a board item is cancelled" }, - "uic.Board.itemRemoved": { - "defaultMessage": "항목을 제거했습니다.", - "description": "Screen reader announcement when a board item is removed" - }, "uic.BooleanToken.true": { "defaultMessage": "예", "description": "Default label of a token showing a boolean value that is true" diff --git a/src/i18n/locales/mn-MN.json b/src/i18n/locales/mn-MN.json index bb13f4e..081c3d7 100644 --- a/src/i18n/locales/mn-MN.json +++ b/src/i18n/locales/mn-MN.json @@ -59,10 +59,6 @@ "defaultMessage": "Хэмжээний өөрчлөлтийг цуцаллаа.", "description": "Screen reader announcement when resizing a board item is cancelled" }, - "uic.Board.itemRemoved": { - "defaultMessage": "Элементийг хаслаа.", - "description": "Screen reader announcement when a board item is removed" - }, "uic.ColorPicker.label": { "defaultMessage": "Өнгө сонгох", "description": "Accessible name of a color picker whose caller gave it no label" diff --git a/src/i18n/locales/ms-MY.json b/src/i18n/locales/ms-MY.json index 33e45af..840f77e 100644 --- a/src/i18n/locales/ms-MY.json +++ b/src/i18n/locales/ms-MY.json @@ -63,10 +63,6 @@ "defaultMessage": "Perubahan saiz dibatalkan.", "description": "Screen reader announcement when resizing a board item is cancelled" }, - "uic.Board.itemRemoved": { - "defaultMessage": "Item dialih keluar.", - "description": "Screen reader announcement when a board item is removed" - }, "uic.ColorPicker.label": { "defaultMessage": "Pilih warna", "description": "Accessible name of a color picker whose caller gave it no label" diff --git a/src/i18n/locales/pl-PL.json b/src/i18n/locales/pl-PL.json index d3e3846..b017c93 100644 --- a/src/i18n/locales/pl-PL.json +++ b/src/i18n/locales/pl-PL.json @@ -63,10 +63,6 @@ "defaultMessage": "Zmianę rozmiaru odrzucono.", "description": "Screen reader announcement when resizing a board item is cancelled" }, - "uic.Board.itemRemoved": { - "defaultMessage": "Element usunięto.", - "description": "Screen reader announcement when a board item is removed" - }, "uic.ColorPicker.label": { "defaultMessage": "Wybierz kolor", "description": "Accessible name of a color picker whose caller gave it no label" diff --git a/src/i18n/locales/pt-BR.json b/src/i18n/locales/pt-BR.json index 46a8d35..466a22f 100644 --- a/src/i18n/locales/pt-BR.json +++ b/src/i18n/locales/pt-BR.json @@ -63,10 +63,6 @@ "defaultMessage": "Redimensionamento descartado.", "description": "Screen reader announcement when resizing a board item is cancelled" }, - "uic.Board.itemRemoved": { - "defaultMessage": "Item removido.", - "description": "Screen reader announcement when a board item is removed" - }, "uic.ColorPicker.label": { "defaultMessage": "Selecionar cor", "description": "Accessible name of a color picker whose caller gave it no label" diff --git a/src/i18n/locales/pt-PT.json b/src/i18n/locales/pt-PT.json index 19441da..7a1ed10 100644 --- a/src/i18n/locales/pt-PT.json +++ b/src/i18n/locales/pt-PT.json @@ -63,10 +63,6 @@ "defaultMessage": "Redimensionamento rejeitado.", "description": "Screen reader announcement when resizing a board item is cancelled" }, - "uic.Board.itemRemoved": { - "defaultMessage": "Item removido.", - "description": "Screen reader announcement when a board item is removed" - }, "uic.ColorPicker.label": { "defaultMessage": "Selecionar cor", "description": "Accessible name of a color picker whose caller gave it no label" diff --git a/src/i18n/locales/ru-RU.json b/src/i18n/locales/ru-RU.json index 362e478..f8aca32 100644 --- a/src/i18n/locales/ru-RU.json +++ b/src/i18n/locales/ru-RU.json @@ -63,10 +63,6 @@ "defaultMessage": "Изменение размера отменено.", "description": "Screen reader announcement when resizing a board item is cancelled" }, - "uic.Board.itemRemoved": { - "defaultMessage": "Элемент удалён.", - "description": "Screen reader announcement when a board item is removed" - }, "uic.ColorPicker.label": { "defaultMessage": "Выбрать цвет", "description": "Accessible name of a color picker whose caller gave it no label" diff --git a/src/i18n/locales/th-TH.json b/src/i18n/locales/th-TH.json index 04182a6..c7429d1 100644 --- a/src/i18n/locales/th-TH.json +++ b/src/i18n/locales/th-TH.json @@ -63,10 +63,6 @@ "defaultMessage": "ยกเลิกการปรับขนาดแล้ว", "description": "Screen reader announcement when resizing a board item is cancelled" }, - "uic.Board.itemRemoved": { - "defaultMessage": "นำรายการออกแล้ว", - "description": "Screen reader announcement when a board item is removed" - }, "uic.ColorPicker.label": { "defaultMessage": "เลือกสี", "description": "Accessible name of a color picker whose caller gave it no label" diff --git a/src/i18n/locales/tr-TR.json b/src/i18n/locales/tr-TR.json index 1534f1c..9154cc6 100644 --- a/src/i18n/locales/tr-TR.json +++ b/src/i18n/locales/tr-TR.json @@ -63,10 +63,6 @@ "defaultMessage": "Boyutlandırma iptal edildi.", "description": "Screen reader announcement when resizing a board item is cancelled" }, - "uic.Board.itemRemoved": { - "defaultMessage": "Öğe kaldırıldı.", - "description": "Screen reader announcement when a board item is removed" - }, "uic.ColorPicker.label": { "defaultMessage": "Renk seç", "description": "Accessible name of a color picker whose caller gave it no label" diff --git a/src/i18n/locales/vi-VN.json b/src/i18n/locales/vi-VN.json index 2a8de89..bfa10fd 100644 --- a/src/i18n/locales/vi-VN.json +++ b/src/i18n/locales/vi-VN.json @@ -63,10 +63,6 @@ "defaultMessage": "Đã hủy đổi cỡ.", "description": "Screen reader announcement when resizing a board item is cancelled" }, - "uic.Board.itemRemoved": { - "defaultMessage": "Đã xóa mục.", - "description": "Screen reader announcement when a board item is removed" - }, "uic.ColorPicker.label": { "defaultMessage": "Chọn màu", "description": "Accessible name of a color picker whose caller gave it no label" diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index 2a475d1..33290d3 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -63,10 +63,6 @@ "defaultMessage": "已放弃大小调整。", "description": "Screen reader announcement when resizing a board item is cancelled" }, - "uic.Board.itemRemoved": { - "defaultMessage": "已移除项目。", - "description": "Screen reader announcement when a board item is removed" - }, "uic.ColorPicker.label": { "defaultMessage": "选择颜色", "description": "Accessible name of a color picker whose caller gave it no label" diff --git a/src/i18n/locales/zh-TW.json b/src/i18n/locales/zh-TW.json index 69b1d95..e39ca76 100644 --- a/src/i18n/locales/zh-TW.json +++ b/src/i18n/locales/zh-TW.json @@ -63,10 +63,6 @@ "defaultMessage": "已捨棄大小調整。", "description": "Screen reader announcement when resizing a board item is cancelled" }, - "uic.Board.itemRemoved": { - "defaultMessage": "已移除項目。", - "description": "Screen reader announcement when a board item is removed" - }, "uic.ColorPicker.label": { "defaultMessage": "選擇顏色", "description": "Accessible name of a color picker whose caller gave it no label" diff --git a/src/index.ts b/src/index.ts index bfbeb31..42f60c8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1407,11 +1407,11 @@ export type { BoardColumnBreakpoint, BoardDndDetail, BoardItem, - BoardItemActions, BoardItemDefinition, + BoardItemPadding, BoardItemPlacement, - BoardItemRemovedDetail, BoardItemsChangeDetail, + BoardLength, BoardOperation, BoardProps, GridLayout,