Skip to content

Commit 4bfbb55

Browse files
committed
Add selection feature
1 parent ba28afc commit 4bfbb55

File tree

10 files changed

+361
-37
lines changed

10 files changed

+361
-37
lines changed

packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.module.css

+10-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
.cell {
99
box-sizing: border-box;
1010
display: flex;
11-
background-color: var(--sapList_Background);
1211
overflow: hidden;
1312
/*todo: dev*/
1413
border-inline: solid 1px black;
@@ -76,9 +75,19 @@
7675
/* ============================================================= */
7776

7877
.row {
78+
box-sizing: border-box;
7979
display: flex;
8080
width: 100%;
8181
height: var(--_ui5WcrAnalyticalTableControlledRowHeight);
82+
background-color: var(--sapList_Background);
83+
84+
&.selectable {
85+
cursor: pointer;
86+
}
87+
&.selected {
88+
border-block-end: 1px solid var(--sapList_SelectionBorderColor);
89+
background-color: var(--sapList_SelectionBackgroundColor);
90+
}
8291
}
8392

8493
/* ============================================================= */

packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.stories.tsx

+25-4
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import dataLarge from '@sb/mockData/Friends500.json';
22
import type { Meta, StoryObj } from '@storybook/react';
33
import type { ColumnDef } from '@tanstack/react-table';
44
import { Button, Input } from '@ui5/webcomponents-react';
5-
import { useReducer } from 'react';
5+
import { Profiler, useReducer } from 'react';
66
import { AnalyticalTableV2 } from './index.js';
77

88
//todo make id mandatory, or take this into account for custom implementations: https://tanstack.com/table/latest/docs/api/core/column-def --> imo id mandatory is the easiest way
@@ -74,13 +74,32 @@ const columns: ColumnDef<any>[] = [
7474
}
7575
];
7676

77+
const data = dataLarge.map((item, index) => ({ ...item, friend: { ...item.friend, age: index } })).slice(0);
78+
const data5k = [
79+
...dataLarge,
80+
...dataLarge,
81+
...dataLarge,
82+
...dataLarge,
83+
...dataLarge,
84+
...dataLarge,
85+
...dataLarge,
86+
...dataLarge,
87+
...dataLarge,
88+
...dataLarge
89+
];
90+
const data20k = [...data5k, ...data5k, ...data5k, ...data5k];
91+
const data100k = [...data20k, ...data20k, ...data20k, ...data20k, ...data20k];
92+
93+
const data500k = [...data100k, ...data100k, ...data100k, ...data100k, ...data100k];
94+
console.log(data20k.length);
7795
const meta = {
7896
title: 'Data Display / AnalyticalTableV2',
7997
component: AnalyticalTableV2,
8098
args: {
81-
data: dataLarge.map((item, index) => ({ ...item, friend: { ...item.friend, age: index } })).slice(0),
99+
data: data100k,
82100
columns,
83-
visibleRows: 5
101+
visibleRows: 5,
102+
selectionMode: 'Single'
84103
},
85104
argTypes: { data: { control: { disable: true } }, columns: { control: { disable: true } } }
86105
} satisfies Meta<typeof AnalyticalTableV2>;
@@ -89,12 +108,14 @@ type Story = StoryObj<typeof meta>;
89108

90109
export const Default: Story = {
91110
render(args) {
92-
const [sortable, toggleSortable] = useReducer((prev) => !prev, false);
111+
const [sortable, toggleSortable] = useReducer((prev) => !prev, true);
93112
return (
94113
<>
95114
<div style={{ height: '300px' }}></div>
96115
<button onClick={toggleSortable}>toggle sortable</button>
116+
{/*<Profiler id="content" onRender={console.log}>*/}
97117
<AnalyticalTableV2 {...args} sortable={sortable} />
118+
{/*</Profiler>*/}
98119
</>
99120
);
100121
}

packages/main/src/components/AnalyticalTableV2/core/Cell.tsx

+18-3
Original file line numberDiff line numberDiff line change
@@ -38,18 +38,31 @@ interface CellProps<TData, TValue> {
3838
startIndex: number;
3939
isFirstFocusableCell?: boolean;
4040
isSortable?: boolean;
41+
isSelectionCell: boolean;
42+
isSelectableCell?: boolean;
4143
}
4244

4345
//todo: create own component for header cells or handle this via props?
4446
export function Cell<TData, TValue>(props: CellProps<TData, TValue>) {
45-
const { style = {}, role, cell, renderable, startIndex, isFirstFocusableCell, isSortable, ...rest } = props;
47+
const {
48+
style = {},
49+
role,
50+
cell,
51+
renderable,
52+
startIndex,
53+
isFirstFocusableCell,
54+
isSortable,
55+
isSelectionCell,
56+
isSelectableCell,
57+
...rest
58+
} = props;
4659
const cellContext = cell.getContext();
4760
const isInteractive = isSortable;
4861
const openerId = `${useId()}-opener`;
4962

5063
const [popoverOpen, setPopoverOpen] = useState(false);
5164

52-
const openPopover = (e) => {
65+
const openPopover = () => {
5366
setPopoverOpen(true);
5467
};
5568

@@ -65,10 +78,12 @@ export function Cell<TData, TValue>(props: CellProps<TData, TValue>) {
6578
}}
6679
className={clsx(classNames.cell, isInteractive && classNames.headerInteractive)}
6780
aria-colindex={startIndex + 1}
68-
data-cell={true}
81+
data-cell={'true'}
6982
tabIndex={isFirstFocusableCell ? 0 : undefined}
7083
//todo: keydown (Enter) keyup(Space) required as well
7184
onClick={isInteractive ? openPopover : undefined}
85+
data-selection-cell={isSelectionCell ? 'true' : undefined}
86+
data-selectable-cell={isSelectableCell ? 'true' : undefined}
7287
>
7388
{flexRender(renderable, cellContext)}
7489
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import type { Row, RowData } from '@tanstack/react-table';
2+
import type { HTMLAttributes } from 'react';
3+
import type { FeaturesList } from '../types/index.js';
4+
import type { FeatureRowProps } from './rowProps.js';
5+
import { rowProps } from './rowProps.js';
6+
7+
type RowProps = Partial<ReturnType<FeatureRowProps[keyof FeatureRowProps]>>;
8+
9+
/**
10+
* Creates an object of (merged) React props by features.
11+
*/
12+
export function createRowProps(features: FeaturesList, row: Row<RowData>): RowProps {
13+
const propsList: HTMLAttributes<HTMLDivElement>[] = features
14+
.map((feature) => rowProps[feature]?.(row))
15+
.filter(Boolean);
16+
17+
if (!propsList.length) {
18+
return {};
19+
}
20+
21+
if (propsList.length === 1) {
22+
return propsList[0];
23+
}
24+
25+
const mergedProps: HTMLAttributes<HTMLDivElement> = {};
26+
const classNames: string[] = [];
27+
28+
for (const props of propsList) {
29+
for (const prop of Object.keys(props)) {
30+
const next = props[prop];
31+
const prev = mergedProps[prop];
32+
if (typeof prev === 'function' && typeof next === 'function') {
33+
// merge handlers of identical event
34+
mergedProps[prop] = (e) => {
35+
prev(e);
36+
next(e);
37+
};
38+
} else if (typeof next === 'function') {
39+
// single handler
40+
mergedProps[prop] = next;
41+
//todo: extend this for other props if required
42+
} else if (prop === 'className' && typeof next === 'string') {
43+
// add className to merge later
44+
classNames.push(next);
45+
} else {
46+
mergedProps[prop] = next;
47+
}
48+
}
49+
}
50+
51+
if (classNames.length) {
52+
mergedProps.className = classNames.join(' ');
53+
}
54+
return mergedProps;
55+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import type { Row, RowData } from '@tanstack/react-table';
2+
import { clsx } from 'clsx';
3+
import type { KeyboardEventHandler, MouseEvent, KeyboardEvent, HTMLAttributes } from 'react';
4+
import { classNames } from './../AnalyticalTableV2.module.css.js';
5+
6+
interface SelectionProps extends Pick<HTMLAttributes<HTMLDivElement>, 'className' | 'aria-selected' | 'onClick'> {
7+
/**
8+
* ENTER press
9+
*/
10+
onKeyDown: KeyboardEventHandler<HTMLDivElement>;
11+
/**
12+
* SPACE release (default prevented)
13+
*/
14+
onKeyUp: KeyboardEventHandler<HTMLDivElement>;
15+
}
16+
export interface FeatureRowProps {
17+
selection: (row: Row<RowData>) => SelectionProps;
18+
}
19+
20+
function selectionHandler(e: MouseEvent<HTMLDivElement> | KeyboardEvent<HTMLDivElement>, row: Row<RowData>) {
21+
if (e.currentTarget.querySelector('[data-selectable-cell]')) {
22+
//todo: check what is better for our use case
23+
row.getToggleSelectedHandler()(e);
24+
// row.toggleSelected()
25+
}
26+
}
27+
28+
export const rowProps: FeatureRowProps = {
29+
selection: (row) => {
30+
const isSelected = row.getIsSelected?.() ?? false;
31+
return {
32+
className: clsx(classNames.selectable, isSelected && classNames.selected),
33+
'aria-selected': `${isSelected}`,
34+
onClick: (e) => {
35+
selectionHandler(e, row);
36+
},
37+
onKeyDown: (e) => {
38+
if (e.key === 'Enter') {
39+
selectionHandler(e, row);
40+
}
41+
},
42+
onKeyUp: (e) => {
43+
if (e.code === 'Space') {
44+
selectionHandler(e, row);
45+
e.preventDefault();
46+
}
47+
}
48+
};
49+
},
50+
//todo: remove
51+
//@ts-expect-error: will be removed
52+
test: () => ({
53+
className: 'testClassName',
54+
onClick: () => console.log('test'),
55+
onKeyDown: () => console.log('test'),
56+
onKeyUp: () => console.log('test')
57+
})
58+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
//todo remove
2+
/* eslint-disable */
3+
//@ts-nocheck
4+
5+
import type { TableFeature, Row, Updater, RowData } from '@tanstack/react-table';
6+
import { KeyboardEvent } from 'react';
7+
8+
//todo: maybe apply handlers directly and come back to this once this PR made it into the table implementation:https://github.com/TanStack/table/pull/5927
9+
export const SelectionFeature: TableFeature<RowData> = {
10+
createRow: (row: Row<RowData>, table) => {
11+
row.selectionBehavior = table.options.selectionBehavior;
12+
13+
if (table.options.enableRowSelection) {
14+
row.getRowProps = () => ({
15+
onClick: (e) => {
16+
if (table.options.enableRowSelection) {
17+
if (row.selectionBehavior !== 'RowSelector') {
18+
console.log(e);
19+
row.toggleSelected!();
20+
}
21+
}
22+
},
23+
onKeyDown: (e: KeyboardEvent) => {
24+
if (e.key === 'Enter' && table.options.enableRowSelection) {
25+
row.toggleSelected!();
26+
}
27+
},
28+
'data-selection-behavior': row.selectionBehavior
29+
});
30+
}
31+
}
32+
};

0 commit comments

Comments
 (0)