Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement OnyxDataGridRenderer component #1890

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
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
7 changes: 7 additions & 0 deletions .changeset/stale-owls-burn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"sit-onyx": minor
---

feat: implement basic OnyxDataGridRenderer component

- also support column grouping for the `OnyxTable` component via the `columnGroups` property
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
@@ -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<typeof OnyxDataGridRenderer> = {
title: "Support/DataGridRenderer",
component: OnyxDataGridRenderer,
};

export default meta;
type Story = StoryObj<typeof OnyxDataGridRenderer>;

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<DataGridEntry, object> {
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<DataGridEntry> {
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`),
},
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<script lang="ts" setup generic="TEntry extends DataGridEntry, TMetadata extends DataGridMetadata">
import OnyxTable from "../../OnyxTable/OnyxTable.vue";
import type { DataGridEntry, DataGridMetadata } from "../types";
import type { OnyxDataGridRendererProps } from "./types";

const props = defineProps<OnyxDataGridRendererProps<TEntry, TMetadata>>();
</script>

<template>
<OnyxTable class="onyx-data-grid" v-bind="props">
<template #head>
<tr>
<th
v-for="column in props.columns"
:key="column.key"
v-bind="column.thAttributes"
scope="col"
>
<component :is="column.component" v-bind="column.props" />
</th>
</tr>
</template>

<tr v-for="row in props.rows" :key="row.id" v-bind="row.trAttributes">
<template v-for="column in props.columns" :key="column.key">
<!-- We are safe to use the Non-Null Assertion operator ("!") here, as we check beforehand with "v-if" -->
<td v-if="row.cells[column.key]" v-bind="row.cells[column.key]!.tdAttributes">
<component :is="row.cells[column.key]!.component" v-bind="row.cells[column.key]!.props" />
</td>
</template>
</tr>
</OnyxTable>
</template>

<style lang="scss">
@use "../../../styles/mixins/layers.scss";

.onyx-data-grid {
@include layers.component() {
}
}
</style>
Original file line number Diff line number Diff line change
@@ -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<TEntry, object>[];
rows: DataGridRendererRow<TEntry, TMetadata>[];
};

/**
* Describes how a column header is rendered in the data grid.
*/
export type DataGridRendererColumn<TEntry extends DataGridEntry, TProps extends object> = {
/**
* (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 `<th>` element.
*/
component: FunctionalComponent<WithHTMLAttributes<TProps>>;
/**
* Attributes and data that is provided to the component using `v-bind`.
*/
props: WithHTMLAttributes<TProps>;
/**
* Attributes that are bound directly to the `<th>` 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<Record<keyof TEntry, DataGridRendererCell<TEntry, TMetadata>>>;
/**
* Attributes that are bound directly to the `<tr>` 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 `<td>` element.
*/
component: DataGridRendererCellComponent<TEntry, TMetadata>;
/**
* Attributes and data that is provided to the component using `v-bind`.
*/
props: DataGridRendererCellComponentProps<TEntry, TMetadata>;
/**
* Attributes that are bound directly to the `<td>` 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<DataGridRendererCellComponentProps<TEntry, TMetadata>, 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];
};
9 changes: 9 additions & 0 deletions packages/sit-onyx/src/components/OnyxDataGrid/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export type DataGridMetadata = Record<string, unknown>;

/**
* "Raw" user data for a data grid entry/row, e.g. fetched from a backend service.
*/
export type DataGridEntry = {
id: PropertyKey;
[key: PropertyKey]: unknown;
};
25 changes: 22 additions & 3 deletions packages/sit-onyx/src/components/OnyxTable/OnyxTable.ct.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,28 @@ test.describe("Screenshot tests", () => {
</OnyxTable>
),
});
});

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) => (
<OnyxTable density={column}>
component: (column, row) => (
<OnyxTable
density={column}
withVerticalBorders={row === "columnGroups"}
columnGroups={
row === "columnGroups"
? [
{ key: "1", span: 2, header: "Group 1" },
{ key: "", span: 1 },
]
: undefined
}
>
{tableHead}
{tableBody}
</OnyxTable>
Expand All @@ -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"],
Expand All @@ -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"],
Expand Down Expand Up @@ -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"],
Expand Down
Loading
Loading