Skip to content

feat(AnalyticalTableV2): introduce initial experimental draft #7134

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 23 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/cypress-commands/TestSetup.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ UI5 Web Components for React is using [Cypress](https://www.cypress.io/) as pref
When launching Cypress the first time you're guided through the setup, which then will create a [configuration file](https://docs.cypress.io/guides/references/configuration) for you. You can use any configuration you like, but since we're heavily relying on web-components, we recommend traversing the shadow DOM per default:

```js
includeShadowDom: true
includeShadowDom: true;
```

[Here](https://docs.cypress.io/guides/component-testing/react/overview) you can find the Cypress Quickstart tutorial for React.
Expand Down
1 change: 1 addition & 0 deletions packages/main/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"watch:css": "yarn build:css --watch"
},
"dependencies": {
"@tanstack/react-table": "^8.21.3",
"@tanstack/react-virtual": "~3.13.0",
"@ui5/webcomponents-react-base": "workspace:~",
"clsx": "2.1.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*todo scroll margin for interactive elements (scroll into view when focused)*/
/*todo: use will-change: transform?*/
.sticky {
position: sticky;
z-index: 1;
}

.cell {
box-sizing: border-box;
display: flex;
overflow: hidden;
/*todo: dev*/
border-inline: solid 1px black;
}

/* ============================================================= */
/* Container */
/* ============================================================= */

.tableContainer {
overflow: auto;
position: relative;
background-color: var(--sapList_Background);
font-size: var(--sapFontSize);
box-sizing: border-box;
overscroll-behavior: none;
}

/* ============================================================= */
/* Table */
/* ============================================================= */

.table {
/*todo check if we really require grid here*/
display: grid;
}

/* ============================================================= */
/* RowGroup */
/* ============================================================= */

.headerGroups {
inset-block-start: 0;
font-family: var(--_ui5wcr-AnalyticalTable-HeaderFontFamily);
z-index: 2;

> [data-component-name='AnalyticalTableV2HeaderRow']:last-child {
/*todo: box shadow or border --> specs*/
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
}

.topRowsGroup {
inset-block-start: calc(var(--_ui5WcrAnalyticalTableHeaderGroups) * var(--_ui5WcrAnalyticalTableControlledRowHeight));
height: calc(var(--_ui5WcrAnalyticalTableTopRows) * var(--_ui5WcrAnalyticalTableControlledRowHeight));

> [data-component-name='AnalyticalTableV2TopRow']:last-child {
/*todo: box shadow or border --> specs*/
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
}

.bottomRowsGroup {
inset-block-end: 0;
height: calc(var(--_ui5WcrAnalyticalTableBottomRows) * var(--_ui5WcrAnalyticalTableControlledRowHeight));

> [data-component-name='AnalyticalTableV2BottomRow']:first-child {
/*todo: box shadow or border --> specs*/
box-shadow: 0 -2px 4px rgba(0, 0, 0, 0.1);
}
}

/* ============================================================= */
/* Row */
/* ============================================================= */

.row {
box-sizing: border-box;
display: flex;
width: 100%;
height: var(--_ui5WcrAnalyticalTableControlledRowHeight);
background-color: var(--sapList_Background);

&.selectable {
cursor: pointer;
}
&.selected {
border-block-end: 1px solid var(--sapList_SelectionBorderColor);
background-color: var(--sapList_SelectionBackgroundColor);
}
}

/* ============================================================= */
/* Header */
/* ============================================================= */

/*.headerCell {*/
/* display: flex;*/
/*}*/

.headerRow {
background-color: var(--sapList_HeaderBackground);
}

.headerInteractive {
cursor: pointer;
/* todo:remove*/
background: lightgrey;
}

/* ============================================================= */
/* Body */
/* ============================================================= */

.virtualizedRow {
position: absolute;
inset-inline-start: 0;
inset-block-start: 0;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import dataLarge from '@sb/mockData/Friends500.json';
import type { Meta, StoryObj } from '@storybook/react';
import type { ColumnDef } from '@tanstack/react-table';
import { Button, Input } from '@ui5/webcomponents-react';
import { Profiler, useReducer } from 'react';
import { AnalyticalTableV2 } from './index.js';

//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

//todo: any
const columns: ColumnDef<any>[] = [
{
header: 'Person',
id: 'A',
columns: [
{ header: 'Name', accessorKey: 'name', id: 'B' },
{ header: 'Age', accessorKey: 'age', id: 'C' }
]
},
{
id: 'D',
header: 'Friend',
columns: [
{ header: 'Friend Name', accessorKey: 'friend.name', id: 'E' },
{ header: 'Friend Age', accessorKey: 'friend.age', id: 'F' }
]
},
{
id: 'G',
header: 'Pinnable',
columns: [
{
maxSize: 100,
header: 'Column Pinned',
id: 'c_pinned',
cell: ({ row }) => {
return 'Pinned';
}
},
{
header: 'Pin Row',
id: 'r_pinned',
size: 300,
cell: ({ row }) => {
return (
<>
<Button
onClick={() => {
row.pin('top');
}}
>
Pin Top
</Button>
<Button
onClick={() => {
row.pin('bottom');
}}
>
Pin Bottom
</Button>
<Button
onClick={() => {
row.pin(false);
}}
>
Reset Pin
</Button>
</>
);
}
},
{ header: 'Input', cell: () => <Input />, id: 'input' }
]
}
];

const data = dataLarge.map((item, index) => ({ ...item, friend: { ...item.friend, age: index } })).slice(0);
const data5k = [
...dataLarge,
...dataLarge,
...dataLarge,
...dataLarge,
...dataLarge,
...dataLarge,
...dataLarge,
...dataLarge,
...dataLarge,
...dataLarge
];
const data20k = [...data5k, ...data5k, ...data5k, ...data5k];
const data100k = [...data20k, ...data20k, ...data20k, ...data20k, ...data20k];

const data500k = [...data100k, ...data100k, ...data100k, ...data100k, ...data100k];
console.log(data20k.length);
const meta = {
title: 'Data Display / AnalyticalTableV2',
component: AnalyticalTableV2,
args: {
data: data100k,
columns,
visibleRows: 5,
selectionMode: 'Single'
},
argTypes: { data: { control: { disable: true } }, columns: { control: { disable: true } } }
} satisfies Meta<typeof AnalyticalTableV2>;
export default meta;
type Story = StoryObj<typeof meta>;

export const Default: Story = {
render(args) {
const [sortable, toggleSortable] = useReducer((prev) => !prev, true);
return (
<>
<div style={{ height: '300px' }}></div>
<button onClick={toggleSortable}>toggle sortable</button>
{/*<Profiler id="content" onRender={console.log}>*/}
<AnalyticalTableV2 {...args} sortable={sortable} />
{/*</Profiler>*/}
</>
);
}
};
98 changes: 98 additions & 0 deletions packages/main/src/components/AnalyticalTableV2/core/Cell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import type { Column, CoreCell, CoreHeader } from '@tanstack/react-table';
import { flexRender } from '@tanstack/react-table';
import { clsx } from 'clsx';
import type { CSSProperties, HTMLAttributes } from 'react';
import { useId, useState } from 'react';
import { classNames } from '../AnalyticalTableV2.module.css.js';
import { ColumnPopover } from './ColumnPopover.js';

//todo type
const getCommonPinningStyles = (column: Column<any>): CSSProperties => {
const isPinned = column.getIsPinned();
const isLastLeftPinnedColumn = isPinned === 'left' && column.getIsLastColumn('left');
const isFirstRightPinnedColumn = isPinned === 'right' && column.getIsFirstColumn('right');

return {
boxShadow: isLastLeftPinnedColumn
? '-4px 0 4px -4px gray inset'
: isFirstRightPinnedColumn
? '4px 0 4px -4px gray inset'
: undefined,
insetInlineStart: isPinned === 'left' ? `${column.getStart('left')}px` : undefined,
insetInlineEnd: isPinned === 'right' ? `${column.getAfter('right')}px` : undefined,
position: isPinned ? 'sticky' : 'relative',
width: column.getSize(),
zIndex: isPinned ? 1 : 0
};
};

interface CellProps<TData, TValue> {
style?: CSSProperties;
role: HTMLAttributes<HTMLDivElement>['role'];
/**
* cell object (e.g. `header`, `cell`)
*/
cell: CoreCell<TData, TValue> | CoreHeader<TData, TValue>;
//todo type
renderable: any;
startIndex: number;
isFirstFocusableCell?: boolean;
isSortable?: boolean;
isSelectionCell: boolean;
isSelectableCell?: boolean;
}

//todo: create own component for header cells or handle this via props?
export function Cell<TData, TValue>(props: CellProps<TData, TValue>) {
const {
style = {},
role,
cell,
renderable,
startIndex,
isFirstFocusableCell,
isSortable,
isSelectionCell,
isSelectableCell,
...rest
} = props;
const cellContext = cell.getContext();
const isInteractive = isSortable;
const openerId = `${useId()}-opener`;

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

const openPopover = () => {
setPopoverOpen(true);
};

return (
<>
<div
{...rest}
id={openerId}
role={role}
style={{
...getCommonPinningStyles(cell.column),
...style
}}
className={clsx(classNames.cell, isInteractive && classNames.headerInteractive)}
aria-colindex={startIndex + 1}
data-cell={'true'}
tabIndex={isFirstFocusableCell ? 0 : undefined}
//todo: keydown (Enter) keyup(Space) required as well
onClick={isInteractive ? openPopover : undefined}
data-selection-cell={isSelectionCell ? 'true' : undefined}
data-selectable-cell={isSelectableCell ? 'true' : undefined}
>
{flexRender(renderable, cellContext)}
</div>
{/*`id` as opener is simpler than Ref, because we can't add a ref directly as prop (React18)*/}
{popoverOpen && (
<ColumnPopover isSortable={isSortable} openerId={openerId} setOpen={setPopoverOpen} column={cell.column} />
)}
</>
);
}

Cell.displayName = 'AnalyticalTableV2Cell';
Loading
Loading