@semcore/data-table 1.5.3 → 2.1.0

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.
@@ -0,0 +1,428 @@
1
+ import React from 'react';
2
+ import createComponent, { Component, sstyled, Root, PropGetterFn } from '@semcore/core';
3
+ import { Box, IBoxProps, IFlexProps } from '@semcore/flex-box';
4
+ import syncScroll from '@semcore/utils/lib/syncScroll';
5
+ import { callAllEventHandlers } from '@semcore/utils/lib/assignProps';
6
+ import fire from '@semcore/utils/lib/fire';
7
+ import { flattenColumns } from './utils';
8
+ import type {
9
+ RowData,
10
+ SortDirection,
11
+ PseudoChildPropsGetter,
12
+ PropsLayer,
13
+ NestedCells,
14
+ Column,
15
+ } from './types';
16
+ import Head from './Head';
17
+ import Body from './Body';
18
+
19
+ import style from './style/data-table.shadow.css';
20
+
21
+ const REVERSED_SORT_DIRECTION: { [direction in SortDirection]: SortDirection } = {
22
+ desc: 'asc',
23
+ asc: 'desc',
24
+ };
25
+ const DEFAULT_SORT_DIRECTION: SortDirection = 'desc';
26
+
27
+ const ROW_GROUP = Symbol('ROW_GROUP');
28
+
29
+ const cssVarReg = /[:;]/g;
30
+
31
+ const createCssVarForWidth = (name: string) => {
32
+ return `--${name.replace(cssVarReg, '_')}_width`;
33
+ };
34
+
35
+ type AsProps = {
36
+ use: 'primary' | 'secondary';
37
+ sort: SortDirection[];
38
+ data: RowData[];
39
+ };
40
+
41
+ type HeadAsProps = {
42
+ children: React.ReactChild;
43
+ };
44
+ type BodyAsProps = {
45
+ children: React.ReactChild;
46
+ };
47
+
48
+ /* utils type */
49
+ type CProps<Props, Ctx = {}, UCProps = {}> = Props & {
50
+ children?: ((props: Props & Ctx, handlers: UCProps) => React.ReactNode) | React.ReactNode;
51
+ };
52
+ type ReturnEl = React.ReactElement | null;
53
+ type ChildRenderFn<Props> = Props & {
54
+ children?: (props: Props, column: DataTableData, index: number) => { [key: string]: unknown };
55
+ };
56
+ /* utils type */
57
+
58
+ export type DataTableData = { [key: string]: unknown };
59
+ export type DataTableSort = [string, 'desc' | 'asc'];
60
+ export type DataTableTheme = 'muted' | 'info' | 'success' | 'warning' | 'danger';
61
+ export type DataTableUse = 'primary' | 'secondary';
62
+ export type DataTableRow = DataTableCell[];
63
+ export type DataTableCell = {
64
+ /** Name of column */
65
+ name: string;
66
+ /** Data of column */
67
+ data: React.ReactNode;
68
+ [key: string]: unknown;
69
+ };
70
+
71
+ export interface IDataTableProps extends IBoxProps {
72
+ /** Theme for table
73
+ * @default primary
74
+ * */
75
+ use?: DataTableUse;
76
+ /** Data for table */
77
+ data?: DataTableData[];
78
+ /** Active sort object */
79
+ sort?: DataTableSort;
80
+ /** Handler call when will request change sort */
81
+ onSortChange?: (sort: DataTableSort, e?: React.SyntheticEvent) => void;
82
+ }
83
+
84
+ export interface IDataTableHeadProps extends IBoxProps {
85
+ /** Sticky header table
86
+ * @deprecated
87
+ * */
88
+ sticky?: boolean;
89
+
90
+ /** Hidden header */
91
+ hidden?: boolean;
92
+ }
93
+
94
+ export interface IDataTableColumnProps extends IFlexProps {
95
+ /** Unique name column */
96
+ name?: string;
97
+ /** Enable sort for column also if you pass string you can set default sort */
98
+ sortable?: boolean | 'desc' | 'asc';
99
+ /** Enable resize for column
100
+ * @ignore */
101
+ resizable?: boolean;
102
+ /** Fixed column on the left/right */
103
+ fixed?: 'left' | 'right';
104
+ }
105
+
106
+ export interface IDataTableBodyProps extends IBoxProps {
107
+ /** Rows table */
108
+ rows?: DataTableRow[];
109
+ }
110
+
111
+ export interface IDataTableRowProps extends IBoxProps {
112
+ /** Theme for row */
113
+ theme?: DataTableTheme;
114
+ /** Displays row as active/hover */
115
+ active?: boolean;
116
+ }
117
+
118
+ export interface IDataTableCellProps extends IFlexProps {
119
+ /** Unique name column or columns separated by / */
120
+ name: string;
121
+ /** Theme for cell */
122
+ theme?: DataTableTheme;
123
+ }
124
+
125
+ class RootDefinitionTable extends Component<AsProps> {
126
+ static displayName = 'DefinitionTable';
127
+
128
+ static style = style;
129
+
130
+ static defaultProps = {
131
+ use: 'primary',
132
+ sort: [],
133
+ data: [],
134
+ } as AsProps;
135
+
136
+ columns: Column[] = [];
137
+
138
+ tableRef = React.createRef<HTMLElement>();
139
+ scrollBodyRef: null | ReturnType<ReturnType<typeof syncScroll>> = null;
140
+ scrollHeadRef: null | ReturnType<ReturnType<typeof syncScroll>> = null;
141
+
142
+ constructor(props: AsProps) {
143
+ super(props);
144
+
145
+ const createRef = syncScroll();
146
+ // first create body ref for master scroll
147
+ this.scrollBodyRef = createRef('body');
148
+ this.scrollHeadRef = createRef('head');
149
+ }
150
+
151
+ handlerSortClick = (name: string, event: React.MouseEvent) => {
152
+ const column = this.columns.find((column) => column.name === name)!;
153
+ return fire(
154
+ this,
155
+ 'onSortChange',
156
+ [
157
+ column.name,
158
+ column.active ? REVERSED_SORT_DIRECTION[column.sortDirection] : column.sortDirection,
159
+ ],
160
+ event,
161
+ );
162
+ };
163
+
164
+ handlerResize = () => {
165
+ this.forceUpdate();
166
+ };
167
+
168
+ scrollToUp = () => {
169
+ this.tableRef?.current?.scrollIntoView({
170
+ block: 'nearest',
171
+ inline: 'nearest',
172
+ behavior: 'smooth',
173
+ });
174
+ };
175
+
176
+ setVarStyle(columns: Column[]) {
177
+ for (const column of columns) {
178
+ if (Array.isArray(column.cssVar)) {
179
+ for (const cssVar of column.cssVar) {
180
+ this.tableRef.current?.style.setProperty(cssVar, `${column.width}px`);
181
+ }
182
+ } else {
183
+ this.tableRef.current?.style.setProperty(column.cssVar, `${column.width}px`);
184
+ }
185
+ }
186
+ }
187
+
188
+ childrenToColumns(
189
+ children: React.ReactNode,
190
+ options: { fixed?: 'left' | 'right' } = { fixed: undefined },
191
+ ) {
192
+ const { sort } = this.asProps;
193
+ const columnsChildren: Column[] = [];
194
+ React.Children.forEach(children, (child) => {
195
+ if (!React.isValidElement(child)) return;
196
+ if (child.type !== DefinitionTable.Column) return;
197
+
198
+ let {
199
+ children,
200
+ name,
201
+ fixed = options.fixed,
202
+ resizable,
203
+ sortable,
204
+ ...props
205
+ } = child.props as Column['props'];
206
+ const isGroup = !name;
207
+ let columns: Column[] = [];
208
+
209
+ if (isGroup) {
210
+ columns = this.childrenToColumns(children, { fixed });
211
+ name = flattenColumns(columns)
212
+ .map(({ name }) => name)
213
+ .join('/');
214
+ if (!columns.length) return;
215
+ children = React.Children.toArray(children).filter(
216
+ (child) => !(React.isValidElement(child) && child.type === DefinitionTable.Column),
217
+ );
218
+ }
219
+
220
+ const column = this.columns.find((column) => column.name === name);
221
+
222
+ columnsChildren.push({
223
+ get width() {
224
+ return this.props.ref.current?.getBoundingClientRect().width || 0;
225
+ },
226
+ name,
227
+ cssVar: createCssVarForWidth(name),
228
+ fixed,
229
+ resizable,
230
+ active: sort[0] === name,
231
+ sortable,
232
+ sortDirection:
233
+ sort[0] === name
234
+ ? sort[1]
235
+ : column?.sortDirection ||
236
+ (typeof sortable == 'string' ? sortable : DEFAULT_SORT_DIRECTION),
237
+ columns,
238
+ props: {
239
+ name,
240
+ ref: column?.props?.ref || React.createRef(),
241
+ children,
242
+ ...props,
243
+ },
244
+ });
245
+ });
246
+ return columnsChildren;
247
+ }
248
+
249
+ getHeadProps(props: HeadAsProps) {
250
+ const { use } = this.asProps;
251
+ const columnsChildren = this.childrenToColumns(props.children);
252
+ this.columns = flattenColumns(columnsChildren);
253
+ return {
254
+ $onSortClick: callAllEventHandlers(this.handlerSortClick, this.scrollToUp),
255
+ columnsChildren,
256
+ use,
257
+ onResize: this.handlerResize,
258
+ $scrollRef: this.scrollHeadRef,
259
+ };
260
+ }
261
+
262
+ getBodyProps(props: BodyAsProps) {
263
+ const { data, use } = this.asProps;
264
+
265
+ const cellPropsLayers: { [columnName: string]: PropsLayer[] } = {};
266
+ const rowPropsLayers: PropsLayer[] = [];
267
+
268
+ React.Children.forEach(props.children, (child) => {
269
+ if (React.isValidElement(child)) {
270
+ const { name, children, ...other } = child.props as {
271
+ name?: string;
272
+ children?: PseudoChildPropsGetter;
273
+ } & { [propName: string]: unknown };
274
+ if (child.type === DefinitionTable.Cell && name) {
275
+ name.split('/').forEach((name) => {
276
+ cellPropsLayers[name] = cellPropsLayers[name] || [];
277
+ cellPropsLayers[name].push({
278
+ ...other,
279
+ childrenPropsGetter: children,
280
+ });
281
+ });
282
+ }
283
+ if (child.type === DefinitionTable.Row) {
284
+ rowPropsLayers.push({
285
+ ...other,
286
+ childrenPropsGetter: children,
287
+ });
288
+ }
289
+ }
290
+ });
291
+
292
+ return {
293
+ columns: this.columns,
294
+ rows: this.dataToRows(data, cellPropsLayers),
295
+ use,
296
+ rowPropsLayers,
297
+ $scrollRef: this.scrollBodyRef,
298
+ };
299
+ }
300
+
301
+ dataToRows(data: RowData[], cellPropsLayers: { [columnName: string]: PropsLayer[] }) {
302
+ const parseData = (data: RowData[], exclude: { [columnName: string]: true }) =>
303
+ data.map((row) => {
304
+ const groupByName: {
305
+ [columnName: string]: {
306
+ groupedColumns: string[];
307
+ groupData: { [columnName: string]: unknown };
308
+ };
309
+ } = {};
310
+ const groupedColumns: { [columnname: string]: true } = {};
311
+ const ungroupedColumns: { [columnname: string]: true } = {};
312
+ for (const rowKey in row) {
313
+ const columnNames = rowKey.split('/');
314
+ if (columnNames.length >= 2) {
315
+ for (const column of columnNames) {
316
+ groupByName[column] = {
317
+ groupedColumns: columnNames,
318
+ groupData: row[rowKey] as { [columnName: string]: unknown },
319
+ };
320
+ groupedColumns[rowKey] = true;
321
+ }
322
+ } else {
323
+ ungroupedColumns[rowKey] = true;
324
+ }
325
+ }
326
+ const rowsGroup = row[ROW_GROUP] || [];
327
+ const rowsGroupedNames = Object.fromEntries(
328
+ rowsGroup
329
+ .map((subRow) => Object.keys(subRow))
330
+ .flat()
331
+ .map((key) => [key, true]),
332
+ );
333
+
334
+ let isGroup = false;
335
+
336
+ const cells: NestedCells = this.columns
337
+ .map((column) => {
338
+ if (groupByName[column.name]) {
339
+ const { groupedColumns, groupData } = groupByName[column.name];
340
+ if (groupedColumns[0] === column.name) {
341
+ return {
342
+ name: groupedColumns.join('/'),
343
+ cssVar: groupedColumns.map(createCssVarForWidth),
344
+ fixed: column.fixed,
345
+ data: groupData,
346
+ cellPropsLayers: cellPropsLayers[column.name] || [],
347
+ };
348
+ }
349
+ } else if (column.name in row) {
350
+ return {
351
+ name: column.name,
352
+ cssVar: column.cssVar,
353
+ fixed: column.fixed,
354
+ data: row[column.name],
355
+ cellPropsLayers: cellPropsLayers[column.name] || [],
356
+ };
357
+ } else if (!isGroup && rowsGroupedNames[column.name]) {
358
+ // TODO: make it work not only with first group
359
+ isGroup = true;
360
+ return parseData(rowsGroup, {
361
+ ...ungroupedColumns,
362
+ ...groupedColumns,
363
+ });
364
+ } else if (!exclude[column.name] && !rowsGroupedNames[column.name]) {
365
+ return {
366
+ name: column.name,
367
+ cssVar: column.cssVar,
368
+ fixed: column.fixed,
369
+ data: null,
370
+ cellPropsLayers: cellPropsLayers[column.name] || [],
371
+ };
372
+ }
373
+ })
374
+ .filter((column) => column !== undefined)
375
+ .map((column) => column!);
376
+
377
+ cells.flatRowData = row;
378
+ return cells;
379
+ });
380
+
381
+ return parseData(data, {});
382
+ }
383
+
384
+ componentDidUpdate() {
385
+ this.setVarStyle(this.columns);
386
+ }
387
+
388
+ render() {
389
+ const SDataTable = Root;
390
+ const { Children, styles } = this.asProps;
391
+
392
+ return sstyled(styles)(
393
+ <SDataTable render={Box} __excludeProps={['data']} ref={this.tableRef}>
394
+ <Children />
395
+ </SDataTable>,
396
+ );
397
+ }
398
+ }
399
+
400
+ interface IDataTableCtx {
401
+ getHeadProps: PropGetterFn;
402
+ getBodyProps: PropGetterFn;
403
+ }
404
+
405
+ function ComponentDefinition() {
406
+ return null;
407
+ }
408
+
409
+ const DefinitionTable = createComponent(
410
+ RootDefinitionTable,
411
+ {
412
+ Head,
413
+ Body,
414
+ Column: ComponentDefinition,
415
+ Cell: ComponentDefinition,
416
+ Row: ComponentDefinition,
417
+ },
418
+ {},
419
+ ) as (<T>(props: CProps<IDataTableProps & T, IDataTableCtx>) => ReturnEl) & {
420
+ Head: <T>(props: IDataTableHeadProps & T) => ReturnEl;
421
+ Body: <T>(props: IDataTableBodyProps & T) => ReturnEl;
422
+ Column: <T>(props: IDataTableColumnProps & T) => ReturnEl;
423
+ Cell: <T>(props: ChildRenderFn<IDataTableCellProps & T>) => ReturnEl;
424
+ Row: <T>(props: ChildRenderFn<IDataTableRowProps & T>) => ReturnEl;
425
+ };
426
+
427
+ export { ROW_GROUP };
428
+ export default DefinitionTable;
@@ -6,46 +6,64 @@ import SortDesc from '@semcore/icon/SortDesc/m';
6
6
  import SortAsc from '@semcore/icon/SortAsc/m';
7
7
  import { callAllEventHandlers } from '@semcore/utils/lib/assignProps';
8
8
  import { flattenColumns, getFixedStyle, getScrollOffsetValue } from './utils';
9
+ import type { Column } from './types';
9
10
  import logger from '@semcore/utils/lib/logger';
11
+ import type ResizeObserverCallback from 'resize-observer-polyfill';
10
12
 
11
13
  import scrollStyles from './style/scroll-area.shadow.css';
12
14
 
13
15
  const SORTING_ICON = {
14
16
  desc: SortDesc,
15
17
  asc: SortAsc,
18
+ } as const;
19
+
20
+ type AsProps = {
21
+ $onSortClick: (name: string, event: React.MouseEvent | React.KeyboardEvent) => void;
22
+ $scrollRef: (instance: unknown) => void;
23
+ use: 'primary' | 'secondary';
24
+ columnsChildren: Column[];
25
+ onResize: ResizeObserverCallback;
26
+ sticky: boolean;
27
+ ['data-ui-name']: string;
16
28
  };
17
29
 
18
- class Head extends Component {
19
- columns = [];
30
+ class Head extends Component<AsProps> {
31
+ columns: Column[] = [];
32
+
33
+ static displayName: string;
20
34
 
21
- bindHandlerSortClick = (name) => (e) => {
22
- this.asProps.$onSortClick(name, e);
35
+ bindHandlerSortClick = (name: string) => (event: React.MouseEvent) => {
36
+ this.asProps.$onSortClick(name, event);
23
37
  };
24
38
 
25
- bindHandlerKeyDown = (name) => (e) => {
26
- if (e.keyCode === 13) {
27
- this.asProps.$onSortClick(name, e);
39
+ bindHandlerKeyDown = (name: string) => (event: React.KeyboardEvent) => {
40
+ if (event.code === 'Enter') {
41
+ this.asProps.$onSortClick(name, event);
28
42
  }
29
43
  };
30
44
 
31
- renderColumns(columns, width) {
45
+ renderColumns(columns: Column[], width: number) {
32
46
  return columns.map((column) => this.renderColumn(column, width));
33
47
  }
34
48
 
35
- renderColumn(column, width) {
49
+ renderColumn(column: Column, width: number) {
36
50
  const { styles, use, hidden } = this.asProps;
37
51
  const SColumn = Flex;
38
52
  const SHead = Box;
39
53
  const SSortIcon = SORTING_ICON[column.sortDirection];
40
- const isGroup = !!column.columns;
54
+ const isGroup = column.columns?.length > 0;
41
55
  const cSize = isGroup ? flattenColumns(column.columns).length : 1;
42
56
  const [name, value] = getFixedStyle(column, this.columns);
57
+
43
58
  const style = {
44
- [name]: value,
45
59
  flexBasis: column.props.flex === undefined && `${width * cSize}%`,
46
60
  ...column.props.style,
47
61
  };
48
62
 
63
+ if (name !== undefined && value !== undefined) {
64
+ style[name] = value;
65
+ }
66
+
49
67
  return sstyled(styles)(
50
68
  <SColumn
51
69
  key={column.name}
File without changes
@@ -211,7 +211,7 @@ SRow[theme='danger']:hover SCell:not([theme]) {
211
211
 
212
212
  SCell {
213
213
  display: flex;
214
- flex: 0 0 auto;
214
+ flex: 1;
215
215
  font-size: 14px;
216
216
  color: var(--gray20);
217
217
  line-height: 20px;
package/src/types.ts ADDED
@@ -0,0 +1,54 @@
1
+ import React from 'react';
2
+ import { ROW_GROUP } from './DataTable';
3
+
4
+ export type PseudoChildPropsGetter = (
5
+ props: { [propName: string]: unknown },
6
+ rowData: { [columnName: string]: unknown },
7
+ index: number,
8
+ ) => { [propName: string]: unknown };
9
+ export type PropsLayer = {
10
+ childrenPropsGetter?: PseudoChildPropsGetter;
11
+ [propName: string]: unknown;
12
+ };
13
+
14
+ export type SortDirection = 'asc' | 'desc';
15
+ export type Column<
16
+ Props extends { [propName: string]: unknown } = { [propName: string]: unknown },
17
+ > = {
18
+ name: string;
19
+ active: boolean;
20
+ width: number;
21
+ fixed?: 'left' | 'right';
22
+ resizable?: boolean;
23
+ sortable?: boolean | SortDirection;
24
+ sortDirection: SortDirection;
25
+ cssVar: string | string[];
26
+ data?: unknown;
27
+ props: {
28
+ name: string;
29
+ } & Partial<{
30
+ onClick: (event: React.MouseEvent) => void;
31
+ onKeyDown: (event: React.KeyboardEvent) => void;
32
+ ref: React.RefObject<HTMLElement>;
33
+ style: React.CSSProperties;
34
+ fixed: 'left' | 'right';
35
+ children: React.ReactNode[];
36
+
37
+ resizable: boolean;
38
+ sortable: boolean | SortDirection;
39
+ sortDirection: SortDirection;
40
+ }> &
41
+ Props;
42
+ columns: Column[];
43
+ };
44
+ export type Cell = Pick<Column, 'name' | 'cssVar' | 'fixed' | 'data'> & {
45
+ cellPropsLayers: PropsLayer[];
46
+ };
47
+ export type RowData<
48
+ Data extends { [columnName: string]: unknown } = { [columnName: string]: unknown },
49
+ > = Data &
50
+ Partial<{
51
+ name: string;
52
+ [ROW_GROUP]: RowData[];
53
+ }>;
54
+ export type NestedCells = (Cell | NestedCells)[] & { flatRowData?: RowData };
package/src/utils.ts ADDED
@@ -0,0 +1,55 @@
1
+ import type { Column } from './types';
2
+
3
+ export const getScrollOffsetValue = (columns: Column[]) =>
4
+ columns.reduce(
5
+ (acc, column) => {
6
+ if (column.fixed === 'left') {
7
+ acc[0] += column.width;
8
+ }
9
+ if (column.fixed === 'right') {
10
+ acc[1] += column.width;
11
+ }
12
+ return acc;
13
+ },
14
+ [0, 0] as [leftOffset: number, rightOffset: number],
15
+ );
16
+
17
+ export const flattenColumns = (columns: Column[]) =>
18
+ columns.reduce((acc, column) => {
19
+ const hasNestedColumns = 'columns' in column && column.columns.length > 0;
20
+ const columns: Column[] = hasNestedColumns ? flattenColumns(column.columns) : [column];
21
+ acc = acc.concat(columns);
22
+ return acc;
23
+ }, [] as Column[]);
24
+
25
+ export const getFixedStyle = (
26
+ cell: Pick<Column, 'name' | 'fixed'>,
27
+ columns: Column[],
28
+ ): [side: 'left' | 'right', style: string | number] | [side: undefined, style: undefined] => {
29
+ const side = cell.fixed;
30
+ if (!side) return [undefined, undefined];
31
+ const names = cell.name.split('/');
32
+ const nameSideMap = {
33
+ left: names[0],
34
+ right: names[names.length - 1],
35
+ };
36
+ const name = nameSideMap[side];
37
+ const index = columns.findIndex((column) => column.name === name);
38
+
39
+ if (index === -1) return [undefined, undefined];
40
+
41
+ const startIndexSideMap = {
42
+ left: 0,
43
+ right: index,
44
+ };
45
+ const endIndexSideMap = {
46
+ left: index,
47
+ right: columns.length - 1,
48
+ };
49
+ const columnsFixed = columns.slice(startIndexSideMap[side], endIndexSideMap[side]);
50
+
51
+ if (columnsFixed.length < 1) return [side, 0];
52
+
53
+ const vars = columnsFixed.map((column) => `var(--${column.name}_width)`);
54
+ return [side, vars.length === 1 ? vars[0] : `calc(${vars.join(' + ')})`];
55
+ };