diff --git a/.changeset/stale-owls-burn.md b/.changeset/stale-owls-burn.md new file mode 100644 index 000000000..08c0f9430 --- /dev/null +++ b/.changeset/stale-owls-burn.md @@ -0,0 +1,7 @@ +--- +"sit-onyx": minor +--- + +feat: implement basic OnyxDataGridRenderer component + +- also support column grouping for the `OnyxTable` component via the `columnGroups` property diff --git a/packages/sit-onyx/playwright/snapshots/components/OnyxTable/Table-densities--chromium-linux.png b/packages/sit-onyx/playwright/snapshots/components/OnyxTable/Table-densities--chromium-linux.png index ca4681da0..5466e8d74 100644 Binary files a/packages/sit-onyx/playwright/snapshots/components/OnyxTable/Table-densities--chromium-linux.png and b/packages/sit-onyx/playwright/snapshots/components/OnyxTable/Table-densities--chromium-linux.png differ diff --git a/packages/sit-onyx/playwright/snapshots/components/OnyxTable/Table-densities--firefox-linux.png b/packages/sit-onyx/playwright/snapshots/components/OnyxTable/Table-densities--firefox-linux.png index 5102207d8..7937afb54 100644 Binary files a/packages/sit-onyx/playwright/snapshots/components/OnyxTable/Table-densities--firefox-linux.png and b/packages/sit-onyx/playwright/snapshots/components/OnyxTable/Table-densities--firefox-linux.png differ diff --git a/packages/sit-onyx/playwright/snapshots/components/OnyxTable/Table-densities--webkit-linux.png b/packages/sit-onyx/playwright/snapshots/components/OnyxTable/Table-densities--webkit-linux.png index 5d680498d..3169cbc0f 100644 Binary files a/packages/sit-onyx/playwright/snapshots/components/OnyxTable/Table-densities--webkit-linux.png and b/packages/sit-onyx/playwright/snapshots/components/OnyxTable/Table-densities--webkit-linux.png differ diff --git a/packages/sit-onyx/src/components/OnyxDataGrid/OnyxDataGridRenderer/OnyxDataGridRenderer.stories.ts b/packages/sit-onyx/src/components/OnyxDataGrid/OnyxDataGridRenderer/OnyxDataGridRenderer.stories.ts new file mode 100644 index 000000000..cfab2ba1b --- /dev/null +++ b/packages/sit-onyx/src/components/OnyxDataGrid/OnyxDataGridRenderer/OnyxDataGridRenderer.stories.ts @@ -0,0 +1,135 @@ +import type { Meta, StoryObj } from "@storybook/vue3"; +import { h, type TdHTMLAttributes } from "vue"; +import type { DataGridEntry } from "../types"; +import OnyxDataGridRenderer from "./OnyxDataGridRenderer.vue"; +import type { DataGridRendererCell, DataGridRendererColumn, DataGridRendererRow } from "./types"; + +const meta: Meta = { + title: "Support/DataGridRenderer", + component: OnyxDataGridRenderer, +}; + +export default meta; +type Story = StoryObj; + +export const Default = { + args: { + columns: Array.from({ length: 4 }, (_, index) => getDummyColumn(index + 1)), + rows: Array.from({ length: 10 }, (_, index) => getDummyRow(index + 1)), + }, +} satisfies Story; + +/** + * This example shows a data grid that renders grouped rows and columns. + */ +export const GroupedData = { + args: { + withVerticalBorders: true, + columnGroups: [ + { + key: "ungrouped", + span: 1, + }, + { + key: "group-1", + span: 2, + header: "Group 1", + }, + { + key: "group-2", + span: 1, + header: "Group 2", + }, + ], + columns: Array.from({ length: 4 }, (_, index) => getDummyColumn(index + 1)), + rows: [ + { + id: "row-1", + cells: { + "column-1": getDummyCell(`Row 1 and 2, cell 1`, { rowspan: 2 }), + "column-2": getDummyCell(`Row 1, cell 2`), + "column-3": getDummyCell(`Row 1, cell 3`), + "column-4": getDummyCell(`Row 1, cell 4`), + }, + }, + { + id: "row-2", + cells: { + "column-2": getDummyCell(`Row 2, cell 2`, { + style: { + borderLeftStyle: "none", + }, + }), + "column-3": getDummyCell(`Row 2, cell 3`), + "column-4": getDummyCell(`Row 2, cell 4`), + }, + }, + { + id: "row-3", + cells: { + "column-1": getDummyCell(`Row 3, cell 1`), + "column-2": getDummyCell(`Row 3, cell 2`), + "column-3": getDummyCell(`Row 3, cell 3`), + "column-4": getDummyCell(`Row 3, cell 4`), + }, + }, + { + id: "row-4", + cells: { + "column-1": getDummyCell(`Row 4, cell 1`), + "column-2": getDummyCell(`Row 4, cell 2 and 3`, { + colspan: 3, + }), + }, + }, + ], + }, +} satisfies Story; + +/** + * Creates a new column for use as Storybook example. + */ +function getDummyColumn(columnNumber: number): DataGridRendererColumn { + return { + key: `column-${columnNumber}`, + component: (props) => h("span", props.title), + props: { + title: `Column ${columnNumber}`, + }, + }; +} + +/** + * Creates a new cell for use as Storybook example. + */ +function getDummyCell( + id: string, + tdAttributes?: TdHTMLAttributes, +): DataGridRendererCell { + return { + component: (props) => h("span", props.row.id.toString()), + tdAttributes, + props: { + row: { + id, + }, + }, + }; +} + +/** + * Creates a new row for use as Storybook example. + */ +function getDummyRow( + rowNumber: number, +): DataGridRendererRow<{ id: PropertyKey; [key: PropertyKey]: unknown }> { + return { + id: `row-${rowNumber}`, + cells: { + "column-1": getDummyCell(`Row ${rowNumber}, cell 1`), + "column-2": getDummyCell(`Row ${rowNumber}, cell 2`), + "column-3": getDummyCell(`Row ${rowNumber}, cell 3`), + "column-4": getDummyCell(`Row ${rowNumber}, cell 4`), + }, + }; +} diff --git a/packages/sit-onyx/src/components/OnyxDataGrid/OnyxDataGridRenderer/OnyxDataGridRenderer.vue b/packages/sit-onyx/src/components/OnyxDataGrid/OnyxDataGridRenderer/OnyxDataGridRenderer.vue new file mode 100644 index 000000000..320a92b95 --- /dev/null +++ b/packages/sit-onyx/src/components/OnyxDataGrid/OnyxDataGridRenderer/OnyxDataGridRenderer.vue @@ -0,0 +1,42 @@ + + + + + diff --git a/packages/sit-onyx/src/components/OnyxDataGrid/OnyxDataGridRenderer/types.ts b/packages/sit-onyx/src/components/OnyxDataGrid/OnyxDataGridRenderer/types.ts new file mode 100644 index 000000000..691fb9448 --- /dev/null +++ b/packages/sit-onyx/src/components/OnyxDataGrid/OnyxDataGridRenderer/types.ts @@ -0,0 +1,110 @@ +import type { FunctionalComponent, HTMLAttributes, TdHTMLAttributes, ThHTMLAttributes } from "vue"; +import type { WithHTMLAttributes } from "../../../types"; +import type { OnyxTableProps } from "../../OnyxTable/types"; +import type { DataGridEntry, DataGridMetadata } from "../types"; + +export type OnyxDataGridRendererProps< + TEntry extends DataGridEntry = DataGridEntry, + TMetadata extends DataGridMetadata = DataGridMetadata, +> = OnyxTableProps & { + /** + * Will define which columns and their headers are rendered in which order. + */ + columns: DataGridRendererColumn[]; + rows: DataGridRendererRow[]; +}; + +/** + * Describes how a column header is rendered in the data grid. + */ +export type DataGridRendererColumn = { + /** + * (Unique) Key of the column - usually a key of the table data. + * But can also be used for custom columns. + */ + key: keyof TEntry; + /** + * The component that renders the header content and is placed into the `` element. + */ + component: FunctionalComponent>; + /** + * Attributes and data that is provided to the component using `v-bind`. + */ + props: WithHTMLAttributes; + /** + * Attributes that are bound directly to the `` element of the column. + */ + thAttributes?: ThHTMLAttributes; +}; + +/** + * Describes how a specific row is rendered in the data grid. + */ +export type DataGridRendererRow< + TEntry extends DataGridEntry, + TMetadata extends DataGridMetadata = DataGridMetadata, +> = { + /** + * Unique id of the row. + */ + id: PropertyKey; + /** + * Describes how a cell in a specific row is rendered in the data grid. + * Only cells that are defined in the columns will be rendered in the defined order. + */ + cells: Partial>>; + /** + * Attributes that are bound directly to the `` element of the row. + */ + trAttributes?: HTMLAttributes; +}; + +/** + * Describes how a single cell in a specific row is rendered in the data grid. + */ +export type DataGridRendererCell< + TEntry extends DataGridEntry, + TMetadata extends DataGridMetadata = DataGridMetadata, +> = { + /** + * The component that renders the actual cell content and is placed into the `` element. + */ + component: DataGridRendererCellComponent; + /** + * Attributes and data that is provided to the component using `v-bind`. + */ + props: DataGridRendererCellComponentProps; + /** + * Attributes that are bound directly to the `` element of the cell. + */ + tdAttributes?: TdHTMLAttributes; +}; + +/** + * Vue component that renders the actual content of a single data grid cell. + */ +export type DataGridRendererCellComponent< + TEntry extends DataGridEntry, + TMetadata extends DataGridMetadata = DataGridMetadata, +> = FunctionalComponent< + WithHTMLAttributes, TdHTMLAttributes> +>; + +export type DataGridRendererCellComponentProps< + TEntry extends DataGridEntry, + TMetadata extends DataGridMetadata, +> = { + /** + * Complete row data. + */ + row: TEntry; + /** + * Cell data that is provided to the component via the `metadata` prop. + */ + metadata?: TMetadata; + /** + * Cell data that is provided to the component via the `modelValue` prop. + * If the cell renders readonly, this will just be the non-editable value. + */ + modelValue?: TEntry[keyof TEntry]; +}; diff --git a/packages/sit-onyx/src/components/OnyxDataGrid/types.ts b/packages/sit-onyx/src/components/OnyxDataGrid/types.ts new file mode 100644 index 000000000..907285d27 --- /dev/null +++ b/packages/sit-onyx/src/components/OnyxDataGrid/types.ts @@ -0,0 +1,9 @@ +export type DataGridMetadata = Record; + +/** + * "Raw" user data for a data grid entry/row, e.g. fetched from a backend service. + */ +export type DataGridEntry = { + id: PropertyKey; + [key: PropertyKey]: unknown; +}; diff --git a/packages/sit-onyx/src/components/OnyxTable/OnyxTable.ct.tsx b/packages/sit-onyx/src/components/OnyxTable/OnyxTable.ct.tsx index c5b82c5d6..a57a7d544 100644 --- a/packages/sit-onyx/src/components/OnyxTable/OnyxTable.ct.tsx +++ b/packages/sit-onyx/src/components/OnyxTable/OnyxTable.ct.tsx @@ -44,15 +44,28 @@ test.describe("Screenshot tests", () => { ), }); +}); +test.describe("SCreenshot tests (densities)", () => { executeMatrixScreenshotTest({ name: "Table (densities)", columns: DENSITIES, - rows: ["default", "focus-visible"], + rows: ["default", "focus-visible", "columnGroups"], // TODO: remove when contrast issues are fixed in https://github.com/SchwarzIT/onyx/issues/410 disabledAccessibilityRules: ["color-contrast"], - component: (column) => ( - + component: (column, row) => ( + {tableHead} {tableBody} @@ -61,7 +74,9 @@ test.describe("Screenshot tests", () => { if (row === "focus-visible") await page.keyboard.press("Tab"); }, }); +}); +test.describe("Screenshot tests (hover styles)", () => { executeMatrixScreenshotTest({ name: "Table (hover styles)", columns: ["default", "striped"], @@ -79,7 +94,9 @@ test.describe("Screenshot tests", () => { if (row === "column-hover") await component.getByText("Fruit").hover(); }, }); +}); +test.describe("Screenshot tests (scrolling)", () => { executeMatrixScreenshotTest({ name: "Table (scrolling)", columns: ["default", "horizontal-scroll"], @@ -125,7 +142,9 @@ test.describe("Screenshot tests", () => { if (row === "vertical-scroll") await component.getByText("Price").hover(); }, }); +}); +test.describe("Screenshot tests (hover)", () => { executeMatrixScreenshotTest({ name: "Table (empty variations)", columns: ["default", "no-header"], diff --git a/packages/sit-onyx/src/components/OnyxTable/OnyxTable.stories.ts b/packages/sit-onyx/src/components/OnyxTable/OnyxTable.stories.ts index 4424b6632..d186447dc 100644 --- a/packages/sit-onyx/src/components/OnyxTable/OnyxTable.stories.ts +++ b/packages/sit-onyx/src/components/OnyxTable/OnyxTable.stories.ts @@ -63,6 +63,32 @@ export const VerticalBorders = { }, } satisfies Story; +/** + * This example shows a table with column groups that can be used to group related columns together. + * Note that the `withVerticalBorders` property should be used with column groups. + */ +export const ColumnGroups = { + args: { + ...VerticalBorders.args, + columnGroups: [ + { + key: "general", + span: 2, + header: "General", + }, + { + key: "inventory", + span: 2, + header: "Inventory", + }, + { + key: "rest", + span: 1, + }, + ], + }, +} satisfies Story; + /** * This example shows a table without a header. */ diff --git a/packages/sit-onyx/src/components/OnyxTable/OnyxTable.vue b/packages/sit-onyx/src/components/OnyxTable/OnyxTable.vue index 632b98bf5..5a8899b74 100644 --- a/packages/sit-onyx/src/components/OnyxTable/OnyxTable.vue +++ b/packages/sit-onyx/src/components/OnyxTable/OnyxTable.vue @@ -52,9 +52,28 @@ const isEmptyMessage = computed(() => t.value("table.empty")); densityClass, ]" > + + + + + {{ group.header }} + + + +