From dd43224119905c3a26a2369f836338c18fcbafba Mon Sep 17 00:00:00 2001 From: Eric Liu Date: Mon, 11 Nov 2024 21:10:45 -0800 Subject: [PATCH] feat(data-table): support generics (#1954) Co-authored-by: K.Kiyokawa Co-authored-by: brunnerh --- COMPONENT_INDEX.md | 69 +++++++++--------- docs/src/COMPONENT_API.json | 66 ++++++++--------- src/DataTable/DataTable.svelte | 58 ++++++++++----- src/DataTable/DataTableTypes.d.ts | 18 +++++ tests/DataTable.test.svelte | 69 +++++++++++++++++- tests/DataTableAppendColumns.test.svelte | 2 +- tests/DataTableBatchSelection.test.svelte | 2 +- ...DataTableBatchSelectionToolbar.test.svelte | 2 +- tests/DataTableNestedHeaders.test.svelte | 30 ++++++++ tests/RadioSelectableDataTable.test.svelte | 2 +- tests/SelectableDataTable.test.svelte | 2 +- types/DataTable/DataTable.svelte.d.ts | 72 +++++++++---------- types/DataTable/DataTableTypes.d.ts | 18 +++++ 13 files changed, 278 insertions(+), 132 deletions(-) create mode 100644 src/DataTable/DataTableTypes.d.ts create mode 100644 tests/DataTableNestedHeaders.test.svelte create mode 100644 types/DataTable/DataTableTypes.d.ts diff --git a/COMPONENT_INDEX.md b/COMPONENT_INDEX.md index 78306b8767..fee5e9a8ce 100644 --- a/COMPONENT_INDEX.md +++ b/COMPONENT_INDEX.md @@ -926,31 +926,34 @@ None. ### Types ```ts -export type DataTableKey = string; +export type DataTableKey = + import("./DataTableTypes.d.ts").PropertyPath; export type DataTableValue = any; -export interface DataTableEmptyHeader { - key: DataTableKey; +export interface DataTableEmptyHeader { + key: DataTableKey | (string & {}); empty: boolean; - display?: (item: DataTableValue, row: DataTableRow) => DataTableValue; + display?: (item: DataTableValue, row: Row) => DataTableValue; sort?: false | ((a: DataTableValue, b: DataTableValue) => number); columnMenu?: boolean; width?: string; minWidth?: string; } -export interface DataTableNonEmptyHeader { - key: DataTableKey; +export interface DataTableNonEmptyHeader { + key: DataTableKey; value: DataTableValue; - display?: (item: DataTableValue, row: DataTableRow) => DataTableValue; + display?: (item: DataTableValue, row: Row) => DataTableValue; sort?: false | ((a: DataTableValue, b: DataTableValue) => number); columnMenu?: boolean; width?: string; minWidth?: string; } -export type DataTableHeader = DataTableNonEmptyHeader | DataTableEmptyHeader; +export type DataTableHeader = + | DataTableNonEmptyHeader + | DataTableEmptyHeader; export interface DataTableRow { id: any; @@ -959,8 +962,8 @@ export interface DataTableRow { export type DataTableRowId = any; -export interface DataTableCell { - key: DataTableKey; +export interface DataTableCell { + key: DataTableKey | (string & {}); value: DataTableValue; display?: (item: DataTableValue, row: DataTableRow) => DataTableValue; } @@ -975,9 +978,9 @@ export interface DataTableCell { | expandedRowIds | No | let | Yes | ReadonlyArray | [] | Specify the row ids to be expanded | | expandable | No | let | Yes | boolean | false | Set to `true` for the expandable variant
Automatically set to `true` if `batchExpansion` is `true` | | sortDirection | No | let | Yes | "none" | "ascending" | "descending" | "none" | Specify the sort direction | -| sortKey | No | let | Yes | DataTableKey | null | Specify the header key to sort by | -| headers | No | let | No | ReadonlyArray | [] | Specify the data table headers | -| rows | No | let | No | ReadonlyArray | [] | Specify the rows the data table should render
keys defined in `headers` are used for the row ids | +| sortKey | No | let | Yes | DataTableKey | null | Specify the header key to sort by | +| headers | No | let | No | ReadonlyArray> | [] | Specify the data table headers | +| rows | No | let | No | ReadonlyArray | [] | Specify the rows the data table should render
keys defined in `headers` are used for the row ids | | size | No | let | No | "compact" | "short" | "medium" | "tall" | undefined | Set the size of the data table | | title | No | let | No | string | "" | Specify the title of the data table | | description | No | let | No | string | "" | Specify the description of the data table | @@ -995,29 +998,29 @@ export interface DataTableCell { ### Slots -| Slot name | Default | Props | Fallback | -| :----------- | :------ | :--------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------- | -| -- | Yes | -- | -- | -| cell | No | { row: DataTableRow; cell: DataTableCell; rowIndex: number; cellIndex: number; } | {cell.display ? cell.display(cell.value, row) : cell.value} | -| cell-header | No | { header: DataTableNonEmptyHeader; } | {header.value} | -| description | No | -- | {description} | -| expanded-row | No | { row: DataTableRow; } | -- | -| title | No | -- | {title} | +| Slot name | Default | Props | Fallback | +| :----------- | :------ | :----------------------------------------------------------------------------------------- | :----------------------------------------------------------------------- | +| -- | Yes | -- | -- | +| cell | No | { row: Row; cell: DataTableCell; rowIndex: number; cellIndex: number; } | {cell.display ? cell.display(cell.value, row) : cell.value} | +| cell-header | No | { header: DataTableNonEmptyHeader; } | {header.value} | +| description | No | -- | {description} | +| expanded-row | No | { row: Row; } | -- | +| title | No | -- | {title} | ### Events -| Event name | Type | Detail | -| :------------------- | :--------- | :------------------------------------------------------------------------------------------------------ | -| click | dispatched | { header?: DataTableHeader; row?: DataTableRow; cell?: DataTableCell; } | -| click:header--expand | dispatched | { expanded: boolean; } | -| click:header | dispatched | { header: DataTableHeader; sortDirection?: "ascending" | "descending" | "none" } | -| click:header--select | dispatched | { indeterminate: boolean; selected: boolean; } | -| click:row | dispatched | DataTableRow | -| mouseenter:row | dispatched | DataTableRow | -| mouseleave:row | dispatched | DataTableRow | -| click:row--expand | dispatched | { expanded: boolean; row: DataTableRow; } | -| click:row--select | dispatched | { selected: boolean; row: DataTableRow; } | -| click:cell | dispatched | DataTableCell | +| Event name | Type | Detail | +| :------------------- | :--------- | :----------------------------------------------------------------------------------------------------------- | +| click | dispatched | { header?: DataTableHeader; row?: Row; cell?: DataTableCell; } | +| click:header--expand | dispatched | { expanded: boolean; } | +| click:header | dispatched | { header: DataTableHeader; sortDirection?: "ascending" | "descending" | "none" } | +| click:header--select | dispatched | { indeterminate: boolean; selected: boolean; } | +| click:row | dispatched | Row | +| mouseenter:row | dispatched | Row | +| mouseleave:row | dispatched | Row | +| click:row--expand | dispatched | { expanded: boolean; row: Row; } | +| click:row--select | dispatched | { selected: boolean; row: Row; } | +| click:cell | dispatched | DataTableCell | ## `DataTableSkeleton` diff --git a/docs/src/COMPONENT_API.json b/docs/src/COMPONENT_API.json index 0f8d88fd77..4cf6377a30 100644 --- a/docs/src/COMPONENT_API.json +++ b/docs/src/COMPONENT_API.json @@ -2381,7 +2381,7 @@ "name": "headers", "kind": "let", "description": "Specify the data table headers", - "type": "ReadonlyArray", + "type": "ReadonlyArray>", "value": "[]", "isFunction": false, "isFunctionDeclaration": false, @@ -2393,7 +2393,7 @@ "name": "rows", "kind": "let", "description": "Specify the rows the data table should render\nkeys defined in `headers` are used for the row ids", - "type": "ReadonlyArray", + "type": "ReadonlyArray", "value": "[]", "isFunction": false, "isFunctionDeclaration": false, @@ -2464,7 +2464,7 @@ "name": "sortKey", "kind": "let", "description": "Specify the header key to sort by", - "type": "DataTableKey", + "type": "DataTableKey", "value": "null", "isFunction": false, "isFunctionDeclaration": false, @@ -2648,7 +2648,7 @@ "name": "cell", "default": false, "fallback": "{cell.display ? cell.display(cell.value, row) : cell.value}", - "slot_props": "{ row: DataTableRow; cell: DataTableCell; rowIndex: number; cellIndex: number; }" + "slot_props": "{ row: Row; cell: DataTableCell; rowIndex: number; cellIndex: number; }" }, { "name": "cell-header", @@ -2665,7 +2665,7 @@ { "name": "expanded-row", "default": false, - "slot_props": "{ row: DataTableRow; }" + "slot_props": "{ row: Row; }" }, { "name": "title", @@ -2678,7 +2678,7 @@ { "type": "dispatched", "name": "click", - "detail": "{ header?: DataTableHeader; row?: DataTableRow; cell?: DataTableCell; }" + "detail": "{ header?: DataTableHeader; row?: Row; cell?: DataTableCell; }" }, { "type": "dispatched", @@ -2688,45 +2688,37 @@ { "type": "dispatched", "name": "click:header", - "detail": "{ header: DataTableHeader; sortDirection?: \"ascending\" | \"descending\" | \"none\" }" + "detail": "{ header: DataTableHeader; sortDirection?: \"ascending\" | \"descending\" | \"none\" }" }, { "type": "dispatched", "name": "click:header--select", "detail": "{ indeterminate: boolean; selected: boolean; }" }, - { "type": "dispatched", "name": "click:row", "detail": "DataTableRow" }, - { - "type": "dispatched", - "name": "mouseenter:row", - "detail": "DataTableRow" - }, - { - "type": "dispatched", - "name": "mouseleave:row", - "detail": "DataTableRow" - }, + { "type": "dispatched", "name": "click:row", "detail": "Row" }, + { "type": "dispatched", "name": "mouseenter:row", "detail": "Row" }, + { "type": "dispatched", "name": "mouseleave:row", "detail": "Row" }, { "type": "dispatched", "name": "click:row--expand", - "detail": "{ expanded: boolean; row: DataTableRow; }" + "detail": "{ expanded: boolean; row: Row; }" }, { "type": "dispatched", "name": "click:row--select", - "detail": "{ selected: boolean; row: DataTableRow; }" + "detail": "{ selected: boolean; row: Row; }" }, { "type": "dispatched", "name": "click:cell", - "detail": "DataTableCell" + "detail": "DataTableCell" } ], "typedefs": [ { - "type": "string", - "name": "DataTableKey", - "ts": "type DataTableKey = string" + "type": "import('./DataTableTypes.d.ts').PropertyPath", + "name": "DataTableKey", + "ts": "type DataTableKey = import('./DataTableTypes.d.ts').PropertyPath" }, { "type": "any", @@ -2734,19 +2726,19 @@ "ts": "type DataTableValue = any" }, { - "type": "{ key: DataTableKey; empty: boolean; display?: (item: DataTableValue, row: DataTableRow) => DataTableValue; sort?: false | ((a: DataTableValue, b: DataTableValue) => number); columnMenu?: boolean; width?: string; minWidth?: string; }", - "name": "DataTableEmptyHeader", - "ts": "interface DataTableEmptyHeader { key: DataTableKey; empty: boolean; display?: (item: DataTableValue, row: DataTableRow) => DataTableValue; sort?: false | ((a: DataTableValue, b: DataTableValue) => number); columnMenu?: boolean; width?: string; minWidth?: string; }" + "type": "{\n key: DataTableKey | (string & {});\n empty: boolean;\n display?: (item: DataTableValue, row: Row) => DataTableValue;\n sort?: false | ((a: DataTableValue, b: DataTableValue) => number);\n columnMenu?: boolean;\n width?: string;\n minWidth?: string;\n}", + "name": "DataTableEmptyHeader", + "ts": "interface DataTableEmptyHeader {\n key: DataTableKey | (string & {});\n empty: boolean;\n display?: (item: DataTableValue, row: Row) => DataTableValue;\n sort?: false | ((a: DataTableValue, b: DataTableValue) => number);\n columnMenu?: boolean;\n width?: string;\n minWidth?: string;\n}" }, { - "type": "{ key: DataTableKey; value: DataTableValue; display?: (item: DataTableValue, row: DataTableRow) => DataTableValue; sort?: false | ((a: DataTableValue, b: DataTableValue) => number); columnMenu?: boolean; width?: string; minWidth?: string; }", - "name": "DataTableNonEmptyHeader", - "ts": "interface DataTableNonEmptyHeader { key: DataTableKey; value: DataTableValue; display?: (item: DataTableValue, row: DataTableRow) => DataTableValue; sort?: false | ((a: DataTableValue, b: DataTableValue) => number); columnMenu?: boolean; width?: string; minWidth?: string; }" + "type": "{\n key: DataTableKey;\n value: DataTableValue;\n display?: (item: DataTableValue, row: Row) => DataTableValue;\n sort?: false | ((a: DataTableValue, b: DataTableValue) => number);\n columnMenu?: boolean;\n width?: string;\n minWidth?: string;\n}", + "name": "DataTableNonEmptyHeader", + "ts": "interface DataTableNonEmptyHeader {\n key: DataTableKey;\n value: DataTableValue;\n display?: (item: DataTableValue, row: Row) => DataTableValue;\n sort?: false | ((a: DataTableValue, b: DataTableValue) => number);\n columnMenu?: boolean;\n width?: string;\n minWidth?: string;\n}" }, { - "type": "DataTableNonEmptyHeader | DataTableEmptyHeader", - "name": "DataTableHeader", - "ts": "type DataTableHeader = DataTableNonEmptyHeader | DataTableEmptyHeader" + "type": "DataTableNonEmptyHeader | DataTableEmptyHeader", + "name": "DataTableHeader", + "ts": "type DataTableHeader = DataTableNonEmptyHeader | DataTableEmptyHeader" }, { "type": "{ id: any; [key: string]: DataTableValue; }", @@ -2759,12 +2751,12 @@ "ts": "type DataTableRowId = any" }, { - "type": "{ key: DataTableKey; value: DataTableValue; display?: (item: DataTableValue, row: DataTableRow) => DataTableValue; }", - "name": "DataTableCell", - "ts": "interface DataTableCell { key: DataTableKey; value: DataTableValue; display?: (item: DataTableValue, row: DataTableRow) => DataTableValue; }" + "type": "{\n key: DataTableKey | (string & {});\n value: DataTableValue;\n display?: (item: DataTableValue, row: DataTableRow) => DataTableValue;\n}", + "name": "DataTableCell", + "ts": "interface DataTableCell {\n key: DataTableKey | (string & {});\n value: DataTableValue;\n display?: (item: DataTableValue, row: DataTableRow) => DataTableValue;\n}" } ], - "generics": null, + "generics": ["Row", "Row extends DataTableRow = DataTableRow"], "rest_props": { "type": "Element", "name": "div" } }, { diff --git a/src/DataTable/DataTable.svelte b/src/DataTable/DataTable.svelte index b82f4f002d..01755445ba 100644 --- a/src/DataTable/DataTable.svelte +++ b/src/DataTable/DataTable.svelte @@ -1,39 +1,61 @@ + + + + {cell.key === "rule.name"} + {#if cell.key === "name"} + {row.name} {row.id} + {:else} + {cell.value} + {/if} + + diff --git a/tests/RadioSelectableDataTable.test.svelte b/tests/RadioSelectableDataTable.test.svelte index f4c2fdaaac..9225f573a4 100644 --- a/tests/RadioSelectableDataTable.test.svelte +++ b/tests/RadioSelectableDataTable.test.svelte @@ -5,7 +5,7 @@ { key: "name", value: "Name" }, { key: "port", value: "Port" }, { key: "rule", value: "Rule" }, - ]; + ] as const; const rows = [ { id: "a", name: "Load Balancer 3", port: 3000, rule: "Round robin" }, diff --git a/tests/SelectableDataTable.test.svelte b/tests/SelectableDataTable.test.svelte index 2424c7a2d8..ec6fec8440 100644 --- a/tests/SelectableDataTable.test.svelte +++ b/tests/SelectableDataTable.test.svelte @@ -5,7 +5,7 @@ { key: "name", value: "Name" }, { key: "port", value: "Port" }, { key: "rule", value: "Rule" }, - ]; + ] as const; const rows = [ { id: "a", name: "Load Balancer 3", port: 3000, rule: "Round robin" }, diff --git a/types/DataTable/DataTable.svelte.d.ts b/types/DataTable/DataTable.svelte.d.ts index 7697e41d42..89dd9303a8 100644 --- a/types/DataTable/DataTable.svelte.d.ts +++ b/types/DataTable/DataTable.svelte.d.ts @@ -1,31 +1,34 @@ import type { SvelteComponentTyped } from "svelte"; import type { SvelteHTMLElements } from "svelte/elements"; -export type DataTableKey = string; +export type DataTableKey = + import("./DataTableTypes.d.ts").PropertyPath; export type DataTableValue = any; -export interface DataTableEmptyHeader { - key: DataTableKey; +export interface DataTableEmptyHeader { + key: DataTableKey | (string & {}); empty: boolean; - display?: (item: DataTableValue, row: DataTableRow) => DataTableValue; + display?: (item: DataTableValue, row: Row) => DataTableValue; sort?: false | ((a: DataTableValue, b: DataTableValue) => number); columnMenu?: boolean; width?: string; minWidth?: string; } -export interface DataTableNonEmptyHeader { - key: DataTableKey; +export interface DataTableNonEmptyHeader { + key: DataTableKey; value: DataTableValue; - display?: (item: DataTableValue, row: DataTableRow) => DataTableValue; + display?: (item: DataTableValue, row: Row) => DataTableValue; sort?: false | ((a: DataTableValue, b: DataTableValue) => number); columnMenu?: boolean; width?: string; minWidth?: string; } -export type DataTableHeader = DataTableNonEmptyHeader | DataTableEmptyHeader; +export type DataTableHeader = + | DataTableNonEmptyHeader + | DataTableEmptyHeader; export interface DataTableRow { id: any; @@ -34,27 +37,27 @@ export interface DataTableRow { export type DataTableRowId = any; -export interface DataTableCell { - key: DataTableKey; +export interface DataTableCell { + key: DataTableKey | (string & {}); value: DataTableValue; display?: (item: DataTableValue, row: DataTableRow) => DataTableValue; } type $RestProps = SvelteHTMLElements["div"]; -type $Props = { +type $Props = { /** * Specify the data table headers * @default [] */ - headers?: ReadonlyArray; + headers?: ReadonlyArray>; /** * Specify the rows the data table should render * keys defined in `headers` are used for the row ids * @default [] */ - rows?: ReadonlyArray; + rows?: ReadonlyArray; /** * Set the size of the data table @@ -90,7 +93,7 @@ type $Props = { * Specify the header key to sort by * @default null */ - sortKey?: DataTableKey; + sortKey?: DataTableKey; /** * Specify the sort direction @@ -181,49 +184,46 @@ type $Props = { [key: `data-${string}`]: any; }; -export type DataTableProps = Omit<$RestProps, keyof $Props> & $Props; +export type DataTableProps = Omit<$RestProps, keyof $Props> & + $Props; -export default class DataTable extends SvelteComponentTyped< - DataTableProps, +export default class DataTable< + Row extends DataTableRow = DataTableRow, +> extends SvelteComponentTyped< + DataTableProps, { click: CustomEvent<{ - header?: DataTableHeader; - row?: DataTableRow; - cell?: DataTableCell; + header?: DataTableHeader; + row?: Row; + cell?: DataTableCell; }>; ["click:header--expand"]: CustomEvent<{ expanded: boolean }>; ["click:header"]: CustomEvent<{ - header: DataTableHeader; + header: DataTableHeader; sortDirection?: "ascending" | "descending" | "none"; }>; ["click:header--select"]: CustomEvent<{ indeterminate: boolean; selected: boolean; }>; - ["click:row"]: CustomEvent; - ["mouseenter:row"]: CustomEvent; - ["mouseleave:row"]: CustomEvent; - ["click:row--expand"]: CustomEvent<{ - expanded: boolean; - row: DataTableRow; - }>; - ["click:row--select"]: CustomEvent<{ - selected: boolean; - row: DataTableRow; - }>; - ["click:cell"]: CustomEvent; + ["click:row"]: CustomEvent; + ["mouseenter:row"]: CustomEvent; + ["mouseleave:row"]: CustomEvent; + ["click:row--expand"]: CustomEvent<{ expanded: boolean; row: Row }>; + ["click:row--select"]: CustomEvent<{ selected: boolean; row: Row }>; + ["click:cell"]: CustomEvent>; }, { default: {}; cell: { - row: DataTableRow; - cell: DataTableCell; + row: Row; + cell: DataTableCell; rowIndex: number; cellIndex: number; }; ["cell-header"]: { header: DataTableNonEmptyHeader }; description: {}; - ["expanded-row"]: { row: DataTableRow }; + ["expanded-row"]: { row: Row }; title: {}; } > {} diff --git a/types/DataTable/DataTableTypes.d.ts b/types/DataTable/DataTableTypes.d.ts new file mode 100644 index 0000000000..02fd5de9e4 --- /dev/null +++ b/types/DataTable/DataTableTypes.d.ts @@ -0,0 +1,18 @@ +type PathDepth = [never, 0, 1, 2, ...0[]]; + +type Join = K extends string | number + ? P extends string | number + ? `${K}${"" extends P ? "" : "."}${P}` + : never + : never; + +// For performance, the maximum traversal depth is 10. +export type PropertyPath = [D] extends [never] + ? never + : T extends object + ? { + [K in keyof T]-?: K extends string | number + ? `${K}` | Join> + : never; + }[keyof T] + : "";