Skip to content
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