@hyphen/hyphen-components 9.0.0-beta.0 → 9.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -55,7 +55,7 @@ declare type ResponsiveProp<T> = {
55
55
  };
56
56
  type Row = UnknownPropertiesObjType;
57
57
  type Cell = Row[string];
58
- declare type Column = {
58
+ declare type Column<TRow extends object = Row> = {
59
59
  /**
60
60
  * Text alignment for column cells (including header alignment). Cells will default to left if not defined.
61
61
  */
@@ -63,11 +63,11 @@ declare type Column = {
63
63
  /**
64
64
  * CSS Class to be applied uniformly to all individual cells in a column.
65
65
  */
66
- cellClassName?: string | ((cell?: Cell, row?: Row, rowIndex?: number) => string);
66
+ cellClassName?: string | ((cell?: Cell, row?: TRow, rowIndex?: number) => string);
67
67
  /**
68
68
  * The key value to be rendered based on the table `rows`.
69
69
  */
70
- dataKey?: string;
70
+ dataKey?: Extract<keyof TRow, string>;
71
71
  /**
72
72
  * Placeholder for empty cells Applies only to the cells of the particular column with this prop.
73
73
  */
@@ -93,7 +93,7 @@ declare type Column = {
93
93
  * Render method for column cell data. Provides ability to render any aspect of the cell/row with custom
94
94
  * markup.
95
95
  */
96
- render?: (cell?: Cell, row?: Row, rowIndex?: number) => ReactNode;
96
+ render?: (cell?: Cell, row?: TRow, rowIndex?: number) => ReactNode;
97
97
  /**
98
98
  * Whether the column is stuck to the left or right.
99
99
  */
@@ -2401,20 +2401,20 @@ interface SwitchProps {
2401
2401
  }
2402
2402
  declare const Switch: FC<SwitchProps>;
2403
2403
 
2404
- interface TableProps {
2404
+ interface TableProps<TRow extends object = Row> {
2405
2405
  /**
2406
2406
  * Columns for the table. See Column definition below for details.
2407
2407
  */
2408
- columns: Column[];
2408
+ columns: Column<TRow>[];
2409
2409
  /**
2410
2410
  * The data rows to be displayed
2411
2411
  */
2412
- rows: Row[];
2412
+ rows: TRow[];
2413
2413
  /**
2414
2414
  * Key that represents a unique value for a row. This is necessary in
2415
2415
  * order to supply React with a node key on each row.
2416
2416
  */
2417
- rowKey: string;
2417
+ rowKey: Extract<keyof TRow, string>;
2418
2418
  /**
2419
2419
  * Additional classes to add.
2420
2420
  */
@@ -2484,7 +2484,7 @@ interface TableProps {
2484
2484
  */
2485
2485
  truncateOverflow?: boolean;
2486
2486
  }
2487
- declare const Table: FC<TableProps>;
2487
+ declare const Table: <TRow extends object = Row>({ columns, rows, rowKey, align, className, emptyCellPlaceholder, hoverableRows, isBorderless, isCompact, hasStickyHeader, isLoading, isScrollable, isStriped, onSort, sortedColumn, useFixedTableLayout, truncateOverflow, }: TableProps<TRow>) => React__default.JSX.Element;
2488
2488
 
2489
2489
  type Theme = 'dark' | 'light' | 'system';
2490
2490
  type ThemeProviderProps = {
@@ -2729,7 +2729,7 @@ declare function generateResponsiveClasses(classRoot: string, value: ResponsiveP
2729
2729
 
2730
2730
  declare function getAutoCompleteValue(value: string | boolean): string;
2731
2731
 
2732
- declare const getColumnKeys: (columns: Column[]) => Key[];
2732
+ declare const getColumnKeys: <TRow extends object = Row>(columns: Column<TRow>[]) => Key[];
2733
2733
 
2734
2734
  declare function getDimensionStyles(dimension: CssDimensionAbbreviation, value?: DimensionSize | ResponsiveProp<DimensionSize> | string): {
2735
2735
  [key: string]: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyphen/hyphen-components",
3
- "version": "9.0.0-beta.0",
3
+ "version": "9.0.0-beta.1",
4
4
  "license": "MIT",
5
5
  "author": {
6
6
  "name": "@hyphen"
@@ -56,7 +56,7 @@
56
56
  "react-select": "^5.10.0",
57
57
  "react-select-event": "^5.5.1",
58
58
  "uuid": "^11.1.1",
59
- "@hyphen/hyphen-design-tokens": "^9.0.0-beta.0"
59
+ "@hyphen/hyphen-design-tokens": "^9.0.0-beta.1"
60
60
  },
61
61
  "devDependencies": {
62
62
  "@babel/core": "^7.27.4",
@@ -30,6 +30,26 @@ An example showcasing the props required to render a list of customers in a tabl
30
30
 
31
31
  <Canvas of={Stories.CommonExample} />
32
32
 
33
+ ## TypeScript row types
34
+
35
+ `Table` accepts interfaces and infers its row type from `rows`. Type separately declared columns with `Column<TRow>[]` to validate `dataKey` values and type the `row` argument passed to render callbacks.
36
+
37
+ ```tsx
38
+ interface Customer {
39
+ id: string;
40
+ name: string;
41
+ }
42
+
43
+ const columns: Column<Customer>[] = [
44
+ {
45
+ dataKey: 'name',
46
+ render: (_cell, row) => row?.name,
47
+ },
48
+ ];
49
+
50
+ <Table columns={columns} rows={customers} rowKey="id" />;
51
+ ```
52
+
33
53
  ## Loading
34
54
 
35
55
  Use the `isLoading` prop on the `<Table />` component to display an overlay that prevents user interaction. Useful for when
@@ -1,4 +1,4 @@
1
- import React, { FC } from 'react';
1
+ import React from 'react';
2
2
  import classNames from 'classnames';
3
3
  import { Column, Row, EventWithColumnKey } from '../../types';
4
4
  import { Spinner } from '../Spinner/Spinner';
@@ -6,20 +6,20 @@ import styles from './Table.module.scss';
6
6
  import { TableBody } from './TableBody/TableBody';
7
7
  import { TableHead } from './TableHead/TableHead';
8
8
 
9
- export interface TableProps {
9
+ export interface TableProps<TRow extends object = Row> {
10
10
  /**
11
11
  * Columns for the table. See Column definition below for details.
12
12
  */
13
- columns: Column[];
13
+ columns: Column<TRow>[];
14
14
  /**
15
15
  * The data rows to be displayed
16
16
  */
17
- rows: Row[];
17
+ rows: TRow[];
18
18
  /**
19
19
  * Key that represents a unique value for a row. This is necessary in
20
20
  * order to supply React with a node key on each row.
21
21
  */
22
- rowKey: string;
22
+ rowKey: Extract<keyof TRow, string>;
23
23
  /**
24
24
  * Additional classes to add.
25
25
  */
@@ -90,7 +90,7 @@ export interface TableProps {
90
90
  truncateOverflow?: boolean;
91
91
  }
92
92
 
93
- export const Table: FC<TableProps> = ({
93
+ export const Table = <TRow extends object = Row,>({
94
94
  columns,
95
95
  rows,
96
96
  rowKey,
@@ -108,7 +108,7 @@ export const Table: FC<TableProps> = ({
108
108
  sortedColumn = undefined,
109
109
  useFixedTableLayout = false,
110
110
  truncateOverflow = false,
111
- }) => {
111
+ }: TableProps<TRow>) => {
112
112
  const containerClasses = classNames(styles.container, {
113
113
  [styles['full-height']]: !!isScrollable?.y,
114
114
  });
@@ -1,5 +1,6 @@
1
1
  import React from 'react';
2
- import { render, screen } from '@testing-library/react';
2
+ import { fireEvent, render, screen } from '@testing-library/react';
3
+ import type { Column } from '../../../types';
3
4
  import { TableBody } from './TableBody';
4
5
 
5
6
  const columns = [
@@ -14,6 +15,18 @@ const rows = [
14
15
  { id: 3, color: 'green', flavor: 'strawberry' },
15
16
  ];
16
17
 
18
+ const inputColumns: Column<(typeof rows)[number]>[] = [
19
+ {
20
+ dataKey: 'color',
21
+ render: (_cell, row) => (
22
+ <input
23
+ aria-label={`Color for row ${row?.id}`}
24
+ defaultValue={row?.color}
25
+ />
26
+ ),
27
+ },
28
+ ];
29
+
17
30
  describe('TableBody', () => {
18
31
  test("It renders with striped rows if passed 'isStriped' prop", () => {
19
32
  render(<TableBody columns={columns} rows={rows} rowKey="id" isStriped />);
@@ -35,4 +48,79 @@ describe('TableBody', () => {
35
48
  const tableBody = screen.getByRole('rowgroup');
36
49
  expect(tableBody).toHaveClass('my-custom-class');
37
50
  });
51
+
52
+ test('It preserves row identity when rows are reordered', () => {
53
+ const { rerender } = render(
54
+ <table>
55
+ <TableBody columns={inputColumns} rows={rows} rowKey="id" />
56
+ </table>
57
+ );
58
+
59
+ fireEvent.change(screen.getByLabelText('Color for row 1'), {
60
+ target: { value: 'purple' },
61
+ });
62
+
63
+ rerender(
64
+ <table>
65
+ <TableBody
66
+ columns={inputColumns}
67
+ rows={[...rows].reverse()}
68
+ rowKey="id"
69
+ />
70
+ </table>
71
+ );
72
+
73
+ expect(screen.getByLabelText('Color for row 1')).toHaveValue('purple');
74
+ });
75
+
76
+ test('It avoids duplicate keys when row key values cannot be used', () => {
77
+ const consoleError = jest.spyOn(console, 'error').mockImplementation();
78
+ const rowsWithoutUsableKeys = [
79
+ { id: undefined, color: 'red' },
80
+ { id: undefined, color: 'blue' },
81
+ { id: null, color: 'green' },
82
+ { id: null, color: 'yellow' },
83
+ { id: {}, color: 'black' },
84
+ { id: {}, color: 'white' },
85
+ ];
86
+
87
+ render(
88
+ <table>
89
+ <TableBody
90
+ columns={[{ dataKey: 'color' }]}
91
+ rows={rowsWithoutUsableKeys}
92
+ rowKey="id"
93
+ />
94
+ </table>
95
+ );
96
+
97
+ const duplicateKeyWarnings = consoleError.mock.calls.filter((call) =>
98
+ call.some(
99
+ (argument) =>
100
+ typeof argument === 'string' &&
101
+ argument.includes('Encountered two children with the same key')
102
+ )
103
+ );
104
+ consoleError.mockRestore();
105
+
106
+ expect(duplicateKeyWarnings).toHaveLength(0);
107
+ });
108
+
109
+ test('It handles nullish row values at runtime', () => {
110
+ const rowsWithNullishValues = [undefined, null] as unknown as typeof rows;
111
+
112
+ expect(() =>
113
+ render(
114
+ <table>
115
+ <TableBody
116
+ columns={columns}
117
+ rows={rowsWithNullishValues}
118
+ rowKey="id"
119
+ />
120
+ </table>
121
+ )
122
+ ).not.toThrow();
123
+
124
+ expect(screen.getAllByRole('row')).toHaveLength(2);
125
+ });
38
126
  });
@@ -1,22 +1,22 @@
1
- import React, { FC, Key } from 'react';
1
+ import React from 'react';
2
2
  import classNames from 'classnames';
3
3
  import styles from './TableBody.module.scss';
4
4
  import { Column, Row } from '../../../types';
5
5
  import { TableRow } from '../common/TableRow/TableRow';
6
6
 
7
- export interface TableBodyProps {
7
+ export interface TableBodyProps<TRow extends object = Row> {
8
8
  /**
9
9
  * The table columns to be rendered
10
10
  */
11
- columns: Column[];
11
+ columns: Column<TRow>[];
12
12
  /**
13
13
  * The unique key to identify a React node for each row.
14
14
  */
15
- rowKey: Key;
15
+ rowKey: Extract<keyof TRow, string>;
16
16
  /**
17
17
  * The table rows to be rendered
18
18
  */
19
- rows: Row[];
19
+ rows: TRow[];
20
20
  /**
21
21
  * Text alignment for all table cells. Can be superseded by passing the same prop into the `Column` object
22
22
  * for a specific column.
@@ -54,9 +54,10 @@ export interface TableBodyProps {
54
54
  truncateOverflow?: boolean;
55
55
  }
56
56
 
57
- export const TableBody: FC<TableBodyProps> = ({
57
+ export const TableBody = <TRow extends object = Row,>({
58
58
  columns,
59
59
  rows,
60
+ rowKey,
60
61
  align = 'left',
61
62
  className = '',
62
63
  emptyCellPlaceholder = '',
@@ -65,7 +66,7 @@ export const TableBody: FC<TableBodyProps> = ({
65
66
  isCompact = false,
66
67
  isStriped = false,
67
68
  truncateOverflow = false,
68
- }) => {
69
+ }: TableBodyProps<TRow>) => {
69
70
  const tableBodyClasses = classNames(
70
71
  styles['table-body'],
71
72
  {
@@ -75,6 +76,20 @@ export const TableBody: FC<TableBodyProps> = ({
75
76
  className
76
77
  );
77
78
 
79
+ const getRowKey = (row: TRow, rowIndex: number): string => {
80
+ const keyValue = row?.[rowKey];
81
+
82
+ if (
83
+ typeof keyValue === 'string' ||
84
+ typeof keyValue === 'number' ||
85
+ typeof keyValue === 'bigint'
86
+ ) {
87
+ return `row-key-${String(keyValue)}`;
88
+ }
89
+
90
+ return `row-index-${rowIndex}`;
91
+ };
92
+
78
93
  return (
79
94
  <tbody className={tableBodyClasses}>
80
95
  {rows.map((row, rowIndex) => (
@@ -83,7 +98,7 @@ export const TableBody: FC<TableBodyProps> = ({
83
98
  row={row}
84
99
  rowIndex={rowIndex}
85
100
  align={align}
86
- key={rowIndex}
101
+ key={getRowKey(row, rowIndex)}
87
102
  emptyCellPlaceholder={emptyCellPlaceholder}
88
103
  truncateOverflow={truncateOverflow}
89
104
  isBorderless={isBorderless}
@@ -1,13 +1,13 @@
1
- import React, { FC } from 'react';
1
+ import React from 'react';
2
2
  import classNames from 'classnames';
3
- import { Column, EventWithColumnKey } from '../../../types';
3
+ import { Column, EventWithColumnKey, Row } from '../../../types';
4
4
  import { TableRow } from '../common/TableRow/TableRow';
5
5
 
6
- export interface TableHeadProps {
6
+ export interface TableHeadProps<TRow extends object = Row> {
7
7
  /**
8
8
  * The table columns to be rendered
9
9
  */
10
- columns: Column[];
10
+ columns: Column<TRow>[];
11
11
  /**
12
12
  * Text alignment for all table cells. Can be superseded by passing the same prop into the `Column` object
13
13
  * for a specific column.
@@ -47,7 +47,7 @@ export interface TableHeadProps {
47
47
  truncateOverflow?: boolean;
48
48
  }
49
49
 
50
- export const TableHead: FC<TableHeadProps> = ({
50
+ export const TableHead = <TRow extends object = Row,>({
51
51
  columns,
52
52
  align = 'left',
53
53
  className = '',
@@ -57,7 +57,7 @@ export const TableHead: FC<TableHeadProps> = ({
57
57
  onSort = undefined,
58
58
  sortedColumn = undefined,
59
59
  truncateOverflow = false,
60
- }) => {
60
+ }: TableHeadProps<TRow>) => {
61
61
  const tableHeadClasses = classNames(className);
62
62
 
63
63
  return (
@@ -1,15 +1,15 @@
1
- import React, { FC, Key, KeyboardEvent, MouseEvent, ReactNode } from 'react';
1
+ import React, { Key, KeyboardEvent, MouseEvent, ReactNode } from 'react';
2
2
  import classNames from 'classnames';
3
3
  import { Box } from '../../../Box/Box';
4
4
  import { Icon } from '../../../Icon/Icon';
5
- import { Column, EventWithColumnKey } from '../../../../types';
5
+ import { Column, EventWithColumnKey, Row } from '../../../../types';
6
6
  import styles from './TableHeaderCell.module.scss';
7
7
 
8
- export interface TableHeaderCellProps {
8
+ export interface TableHeaderCellProps<TRow extends object = Row> {
9
9
  /**
10
10
  * Title to display for the column.
11
11
  */
12
- column: Column;
12
+ column: Column<TRow>;
13
13
  /**
14
14
  * Text alignment for all table cells. Can be superseded by passing the same prop into the `Column` object
15
15
  * for a specific column.
@@ -69,7 +69,7 @@ export interface TableHeaderCellProps {
69
69
  }
70
70
 
71
71
  // eslint-disable-line import/prefer-default-export
72
- export const TableHeaderCell: FC<TableHeaderCellProps> = ({
72
+ export const TableHeaderCell = <TRow extends object = Row,>({
73
73
  column,
74
74
  align = 'left',
75
75
  className = undefined,
@@ -83,7 +83,7 @@ export const TableHeaderCell: FC<TableHeaderCellProps> = ({
83
83
  sticky = undefined,
84
84
  truncateOverflow = false,
85
85
  width = undefined,
86
- }) => {
86
+ }: TableHeaderCellProps<TRow>) => {
87
87
  const isColumnSorted = (columnDataKey: Key | undefined): boolean =>
88
88
  !!sortedColumn && sortedColumn.dataKey === columnDataKey;
89
89
 
@@ -33,7 +33,7 @@ describe('TableRow', () => {
33
33
  render(
34
34
  <TableRow row={row} rowIndex={rowIndex} columns={customClassCell} />
35
35
  );
36
- expect(func).toHaveBeenCalledWith(customClassCell[0], row, rowIndex);
36
+ expect(func).toHaveBeenCalledWith(row.id, row, rowIndex);
37
37
  const element = document.getElementsByClassName('my-custom-class')[0];
38
38
  expect(element).toBeInTheDocument();
39
39
  });
@@ -1,4 +1,4 @@
1
- import React, { FC, ReactNode } from 'react';
1
+ import React, { ReactNode } from 'react';
2
2
  import classNames from 'classnames';
3
3
  import styles from './TableRow.module.scss';
4
4
  import { Column, EventWithColumnKey, Row } from '../../../../types';
@@ -6,11 +6,11 @@ import { getColumnKeys } from '../../../../lib/getColumnKeys';
6
6
  import TableBodyCell from '../../TableBody/TableBodyCell/TableBodyCell';
7
7
  import { TableHeaderCell } from '../../TableHead/TableHeaderCell/TableHeaderCell';
8
8
 
9
- export interface TableRowProps {
9
+ export interface TableRowProps<TRow extends object = Row> {
10
10
  /**
11
11
  * The table columns to be rendered
12
12
  */
13
- columns: Column[];
13
+ columns: Column<TRow>[];
14
14
  /**
15
15
  * Text alignment for all table cells. Can be superseded by passing the same prop into the `Column` object
16
16
  * for a specific column.
@@ -52,7 +52,7 @@ export interface TableRowProps {
52
52
  /**
53
53
  * The specific row to be rendered.
54
54
  */
55
- row?: Row;
55
+ row?: TRow;
56
56
  /**
57
57
  * The unique key to identify a React node for each row.
58
58
  */
@@ -71,7 +71,7 @@ export interface TableRowProps {
71
71
  truncateOverflow?: boolean;
72
72
  }
73
73
 
74
- export const TableRow: FC<TableRowProps> = ({
74
+ export const TableRow = <TRow extends object = Row,>({
75
75
  columns,
76
76
  align = 'left',
77
77
  className = '',
@@ -86,7 +86,7 @@ export const TableRow: FC<TableRowProps> = ({
86
86
  row = undefined,
87
87
  rowIndex = undefined,
88
88
  truncateOverflow = false,
89
- }) => {
89
+ }: TableRowProps<TRow>) => {
90
90
  const tableRowClasses = classNames(
91
91
  styles['table-row'],
92
92
  { [styles.hoverable]: isHoverable },
@@ -110,7 +110,7 @@ export const TableRow: FC<TableRowProps> = ({
110
110
  return React.isValidElement(value);
111
111
  };
112
112
 
113
- const renderCellContent = (column: Column): ReactNode => {
113
+ const renderCellContent = (column: Column<TRow>): ReactNode => {
114
114
  if (column.render) {
115
115
  const cellValue = column.dataKey && row ? row[column.dataKey] : undefined;
116
116
  return column.render(cellValue, row, rowIndex);
@@ -120,10 +120,12 @@ export const TableRow: FC<TableRowProps> = ({
120
120
  return isRenderableCell(cellValue) ? cellValue : null;
121
121
  };
122
122
 
123
- const getCellClassName = (column: Column): string | undefined => {
123
+ const getCellClassName = (column: Column<TRow>): string | undefined => {
124
124
  if (column.cellClassName) {
125
125
  if (typeof column.cellClassName === 'function') {
126
- return column.cellClassName(column, row, rowIndex);
126
+ const cellValue =
127
+ column.dataKey && row ? row[column.dataKey] : undefined;
128
+ return column.cellClassName(cellValue, row, rowIndex);
127
129
  }
128
130
  return column.cellClassName;
129
131
  }
@@ -1,10 +1,12 @@
1
1
  import { Key } from 'react';
2
- import { Column } from '../types';
2
+ import { Column, Row } from '../types';
3
3
 
4
4
  // eslint-disable-next-line import/prefer-default-export
5
- export const getColumnKeys = (columns: Column[]): Key[] => {
5
+ export const getColumnKeys = <TRow extends object = Row>(
6
+ columns: Column<TRow>[]
7
+ ): Key[] => {
6
8
  const INTERNAL_KEY_PREFIX = 'columnKeyPrefix';
7
- const columnKeys: React.Key[] = [];
9
+ const columnKeys: Key[] = [];
8
10
  const keys: Record<string, boolean> = {};
9
11
 
10
12
  columns.forEach((column) => {
@@ -149,7 +149,7 @@ export type Row = UnknownPropertiesObjType;
149
149
 
150
150
  export type Cell = Row[string];
151
151
 
152
- export declare type Column = {
152
+ export declare type Column<TRow extends object = Row> = {
153
153
  /**
154
154
  * Text alignment for column cells (including header alignment). Cells will default to left if not defined.
155
155
  */
@@ -159,11 +159,11 @@ export declare type Column = {
159
159
  */
160
160
  cellClassName?:
161
161
  | string
162
- | ((cell?: Cell, row?: Row, rowIndex?: number) => string);
162
+ | ((cell?: Cell, row?: TRow, rowIndex?: number) => string);
163
163
  /**
164
164
  * The key value to be rendered based on the table `rows`.
165
165
  */
166
- dataKey?: string;
166
+ dataKey?: Extract<keyof TRow, string>;
167
167
  /**
168
168
  * Placeholder for empty cells Applies only to the cells of the particular column with this prop.
169
169
  */
@@ -189,7 +189,7 @@ export declare type Column = {
189
189
  * Render method for column cell data. Provides ability to render any aspect of the cell/row with custom
190
190
  * markup.
191
191
  */
192
- render?: (cell?: Cell, row?: Row, rowIndex?: number) => ReactNode;
192
+ render?: (cell?: Cell, row?: TRow, rowIndex?: number) => ReactNode;
193
193
  /**
194
194
  * Whether the column is stuck to the left or right.
195
195
  */