Skip to content

Commit

Permalink
feat(OnyxTable): implement empty state + define focus outline (#1437)
Browse files Browse the repository at this point in the history
- refactor(OnyxTable): split default slot to distinguish thead and tbody
- feat(OnyxTable): implement empty state + define focus outline
  • Loading branch information
BoppLi authored Jul 1, 2024
1 parent cf1aba1 commit 760bb76
Show file tree
Hide file tree
Showing 18 changed files with 226 additions and 95 deletions.
10 changes: 10 additions & 0 deletions .changeset/large-zebras-kneel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"sit-onyx": major
---

refactor(OnyxTable): split default slot to distinguish thead and tbody

Including new features:

- implement empty state when no table data exists
- define focus outline
34 changes: 23 additions & 11 deletions apps/alpha-test-app/src/views/HomeView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -263,15 +263,15 @@ const toast = useToast();
:skeleton="useSkeleton"
/>

<OnyxTable v-if="show('OnyxTable')">
<thead>
<tr>
<th>Fruit</th>
<th>Price (€/kg)</th>
<th>Inventory (kg)</th>
</tr>
</thead>
<tbody>
<template v-if="show('OnyxTable')">
<OnyxTable>
<template #head>
<tr>
<th>Fruit</th>
<th>Price (€/kg)</th>
<th>Inventory (kg)</th>
</tr>
</template>
<tr>
<td>Strawberry</td>
<td>4.50</td>
Expand All @@ -287,8 +287,20 @@ const toast = useToast();
<td>3.75</td>
<td>18000</td>
</tr>
</tbody>
</OnyxTable>
</OnyxTable>
<OnyxTable>
<template #head>
<tr>
<th>Empty</th>
<th>Table</th>
<th>Example</th>
</tr>
</template>

<!-- this demonstrates that the empty state works even when v-for is used -->
<tr v-for="(_row, index) in []" :key="index"></tr>
</OnyxTable>
</template>

<OnyxTag v-if="show('OnyxTag')" label="Example tag" :icon="emojiHappy2" />

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.
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
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
Expand Up @@ -13,7 +13,7 @@ const slots = defineSlots<{
/**
* OnyxListItems to show
*/
options?(): VNode[];
options?(): unknown;
/**
* Optional header content to display above the options.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
<script lang="ts" setup>
import { isExternalLink } from "../../utils";
import arrowSmallUpRight from "@sit-onyx/icons/arrow-small-up-right.svg?raw";
import { isExternalLink } from "../../utils";
import OnyxFlyoutMenu from "../OnyxFlyoutMenu/future/OnyxFlyoutMenu.vue";
import OnyxIcon from "../OnyxIcon/OnyxIcon.vue";
import { type OnyxNavButtonProps } from "./types";
import { type VNode } from "vue";
const props = withDefaults(defineProps<OnyxNavButtonProps>(), {
active: false,
Expand All @@ -19,7 +18,7 @@ const slots = defineSlots<{
/**
* An optional slot to render nested children.
*/
children?(): VNode[];
children?(): unknown;
}>();
const emit = defineEmits<{
Expand Down
141 changes: 92 additions & 49 deletions packages/sit-onyx/src/components/OnyxTable/OnyxTable.ct.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,30 @@ import { DENSITIES } from "../../composables/density";
import { expect, test } from "../../playwright/a11y";
import { executeMatrixScreenshotTest } from "../../playwright/screenshots";
import OnyxButton from "../OnyxButton/OnyxButton.vue";
import OnyxEmpty from "../OnyxEmpty/OnyxEmpty.vue";
import OnyxTable from "./OnyxTable.vue";

const tableHead = (
<thead>
<template v-slot:head>
<tr>
<th>Fruit</th>
<th>Price (€/kg)</th>
<th>Inventory (kg)</th>
</tr>
</thead>
</template>
);

const tableBody = (
<tbody>
<tr>
<td>Strawberry</td> <td>4.50</td> <td>200</td>
</tr>
<tr>
<td>Apple</td> <td>1.99</td> <td>3000</td>
</tr>
<tr>
<td>Banana</td> <td>3.75</td> <td>18000</td>
</tr>
</tbody>
);
const tableBody = [
<tr>
<td>Strawberry</td> <td>4.50</td> <td>200</td>
</tr>,
<tr>
<td>Apple</td> <td>1.99</td> <td>3000</td>
</tr>,
<tr>
<td>Banana</td> <td>3.75</td> <td>18000</td>
</tr>,
];

test.describe("Screenshot tests", () => {
executeMatrixScreenshotTest({
Expand All @@ -49,7 +48,7 @@ test.describe("Screenshot tests", () => {
executeMatrixScreenshotTest({
name: "Table (densities)",
columns: DENSITIES,
rows: ["default"],
rows: ["default", "focus-visible"],
// TODO: remove when contrast issues are fixed in https://github.com/SchwarzIT/onyx/issues/410
disabledAccessibilityRules: ["color-contrast"],
component: (column) => (
Expand All @@ -58,6 +57,9 @@ test.describe("Screenshot tests", () => {
{tableBody}
</OnyxTable>
),
beforeScreenshot: async (_component, page, _column, row) => {
if (row === "focus-visible") await page.keyboard.press("Tab");
},
});

executeMatrixScreenshotTest({
Expand Down Expand Up @@ -91,31 +93,30 @@ test.describe("Screenshot tests", () => {
maxHeight: row !== "default" ? "12rem" : "fit-content",
}}
>
<thead>
<template v-slot:head>
<tr>
<th>Fruit</th>
<th>Price</th>
<th>Inventory</th>
<th>Rating</th>
</tr>
</thead>
<tbody>
<tr>
<td>Strawberry</td> <td>4.50</td> <td>200</td> <td>5</td>
</tr>
<tr>
<td>Apple</td> <td>1.99</td> <td>3000</td> <td>4</td>
</tr>
<tr>
<td>Banana</td> <td>3.75</td> <td>18000</td> <td>3</td>
</tr>
<tr>
<td>Pinia</td> <td>7.00</td> <td>250</td> <td>5</td>
</tr>
<tr>
<td>Jackfruit</td> <td>3.50</td> <td>1000</td> <td>2</td>
</tr>
</tbody>
</template>

<tr>
<td>Strawberry</td> <td>4.50</td> <td>200</td> <td>5</td>
</tr>
<tr>
<td>Apple</td> <td>1.99</td> <td>3000</td> <td>4</td>
</tr>
<tr>
<td>Banana</td> <td>3.75</td> <td>18000</td> <td>3</td>
</tr>
<tr>
<td>Pinia</td> <td>7.00</td> <td>250</td> <td>5</td>
</tr>
<tr>
<td>Jackfruit</td> <td>3.50</td> <td>1000</td> <td>2</td>
</tr>
</OnyxTable>
),

Expand All @@ -124,33 +125,75 @@ test.describe("Screenshot tests", () => {
if (row === "vertical-scroll") await component.getByText("Price").hover();
},
});

executeMatrixScreenshotTest({
name: "Table (empty variations)",
columns: ["default", "no-header"],
rows: ["default", "custom-empty"],
// TODO: remove when contrast issues are fixed in https://github.com/SchwarzIT/onyx/issues/410
disabledAccessibilityRules: ["color-contrast"],
component: (column, row) => (
<OnyxTable style="width: 20rem;">
{column === "default" ? tableHead : undefined}
{row === "custom-empty" ? (
<template v-slot:empty>
<OnyxEmpty>Custom empty</OnyxEmpty>
</template>
) : undefined}
</OnyxTable>
),
});

executeMatrixScreenshotTest({
name: "Table (empty blocks hover)",
columns: ["row-hover", "column-hover"],
rows: ["default", "empty-body"],
// TODO: remove when contrast issues are fixed in https://github.com/SchwarzIT/onyx/issues/410
disabledAccessibilityRules: ["color-contrast"],
component: (_column, row) => (
<OnyxTable>
{tableHead}
{row === "default" ? tableBody : undefined}
</OnyxTable>
),
beforeScreenshot: async (_component, page, column, row) => {
if (column === "row-hover") {
// this is needed to demonstrate that a row hover has no effect when empty.
await page.mouse.move(32, 132);
}
if (column === "column-hover" && ["default", "empty-body"].includes(row)) {
// this is needed to demonstrate that a column hover has no effect when empty.
// selecting the header label does not work because we prevent pointer-events.
await page.mouse.move(32, 32);
}
},
});
});

test("should focus components with active column hover effect", async ({ page, mount }) => {
let buttonClickCount = 0;

const component = await mount(
<OnyxTable>
<thead>
<template v-slot:head>
<tr>
<th>
<OnyxButton label="Header button" onClick={() => buttonClickCount++} />
</th>
<th>Column 2</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<OnyxButton label="Row button" onClick={() => buttonClickCount++} />
</td>
<td>Test 2</td>
</tr>
<tr>
<td>Test 3</td>
<td>Test 4</td>
</tr>
</tbody>
</template>

<tr>
<td>
<OnyxButton label="Row button" onClick={() => buttonClickCount++} />
</td>
<td>Test 2</td>
</tr>
<tr>
<td>Test 3</td>
<td>Test 4</td>
</tr>
</OnyxTable>,
);

Expand Down
55 changes: 29 additions & 26 deletions packages/sit-onyx/src/components/OnyxTable/OnyxTable.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,42 +12,38 @@ const meta: Meta<typeof OnyxTable> = {
component: OnyxTable,
events: [],
argTypes: {
default: {
control: { disable: true },
},
default: { control: { disable: true } },
head: { control: { disable: true } },
empty: { control: { disable: true } },
},
}),
};

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

const getTableHeader = () => {
return h("thead", [
h("tr", [
h("th", "Fruit"),
h("th", "Price (€/kg)"),
h("th", "Inventory (kg)"),
h("th", "Inventory (pieces)"),
h("th", "Rating"),
]),
const getTableHeaderRow = () =>
h("tr", [
h("th", "Fruit"),
h("th", "Price (€/kg)"),
h("th", "Inventory (kg)"),
h("th", "Inventory (pieces)"),
h("th", "Rating"),
]);
};

const getTableBodyRows = () => {
return [
h("tr", [h("td", "Strawberry"), h("td", "4.50"), h("td", "200"), h("td", "100"), h("td", "5")]),
h("tr", [h("td", "Apple"), h("td", "1.99"), h("td", "3000"), h("td", "200"), h("td", "3")]),
h("tr", [h("td", "Banana"), h("td", "3.75"), h("td", "18000"), h("td", "300"), h("td", "4")]),
];
};
const getTableBodyRows = () => [
h("tr", [h("td", "Strawberry"), h("td", "4.50"), h("td", "200"), h("td", "100"), h("td", "5")]),
h("tr", [h("td", "Apple"), h("td", "1.99"), h("td", "3000"), h("td", "200"), h("td", "3")]),
h("tr", [h("td", "Banana"), h("td", "3.75"), h("td", "18000"), h("td", "300"), h("td", "4")]),
];

/**
* This example shows a default table.
*/
export const Default = {
args: {
default: () => [getTableHeader(), h("tbody", getTableBodyRows())],
default: () => getTableBodyRows(),
head: () => getTableHeaderRow(),
},
} satisfies Story;

Expand Down Expand Up @@ -76,7 +72,7 @@ export const VerticalBorders = {
*/
export const WithoutHeader = {
args: {
default: () => h("tbody", getTableBodyRows()),
default: Default.args.default,
},
} satisfies Story;

Expand All @@ -86,10 +82,8 @@ export const WithoutHeader = {
export const LimitedHeight = {
args: {
style: "max-height: 16rem",
default: [
getTableHeader(),
h("tbody", [...getTableBodyRows(), ...getTableBodyRows(), ...getTableBodyRows()]),
],
head: Default.args.head,
default: () => [...getTableBodyRows(), ...getTableBodyRows(), ...getTableBodyRows()],
},
} satisfies Story;

Expand All @@ -112,3 +106,12 @@ export const LimitedHeightAndWidth = {
style: "max-width: 20rem; max-height: 16rem",
},
} satisfies Story;

/**
* This example shows a table without a `tbody`.
*/
export const Empty = {
args: {
head: () => getTableHeaderRow(),
},
} satisfies Story;
Loading

0 comments on commit 760bb76

Please sign in to comment.