From ad308af51181b6ed1144ce864ba9e999374f3df7 Mon Sep 17 00:00:00 2001 From: Lukas Harbarth Date: Wed, 15 Jan 2025 16:46:18 +0100 Subject: [PATCH 01/17] add dep --- package.json | 1 + yarn.lock | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/package.json b/package.json index a0ab68955a7..a5aa151ae6e 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "@storybook/react": "8.4.5", "@storybook/react-vite": "8.4.5", "@storybook/theming": "8.4.5", + "@tanstack/react-table": "^8.20.6", "@ui5/webcomponents": "2.6.2", "@ui5/webcomponents-compat": "2.6.2", "@ui5/webcomponents-fiori": "2.6.2", diff --git a/yarn.lock b/yarn.lock index 25c94ee5b52..7709f74743e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4754,6 +4754,18 @@ __metadata: languageName: node linkType: hard +"@tanstack/react-table@npm:^8.20.6": + version: 8.20.6 + resolution: "@tanstack/react-table@npm:8.20.6" + dependencies: + "@tanstack/table-core": "npm:8.20.5" + peerDependencies: + react: ">=16.8" + react-dom: ">=16.8" + checksum: 10c0/3213dc146f647fbd571f4e347007b969320819e588439b2ee95dd3a65efcbe30d097c24426dd82617041ed1e186182a5b303382bcebed5d61a1c6045a55c58d3 + languageName: node + linkType: hard + "@tanstack/react-virtual@npm:~3.11.0": version: 3.11.2 resolution: "@tanstack/react-virtual@npm:3.11.2" @@ -4766,6 +4778,13 @@ __metadata: languageName: node linkType: hard +"@tanstack/table-core@npm:8.20.5": + version: 8.20.5 + resolution: "@tanstack/table-core@npm:8.20.5" + checksum: 10c0/3c27b5debd61b6bd9bfbb40bfc7c5d5af90873ae1a566b20e3bf2d2f4f2e9a78061c081aacc5259a00e256f8df506ec250eb5472f5c01ff04baf9918b554982b + languageName: node + linkType: hard + "@tanstack/virtual-core@npm:3.11.2": version: 3.11.2 resolution: "@tanstack/virtual-core@npm:3.11.2" @@ -23107,6 +23126,7 @@ __metadata: "@storybook/react": "npm:8.4.5" "@storybook/react-vite": "npm:8.4.5" "@storybook/theming": "npm:8.4.5" + "@tanstack/react-table": "npm:^8.20.6" "@testing-library/cypress": "npm:^10.0.0" "@types/eslint__js": "npm:^8.42.3" "@types/jscodeshift": "npm:^0.12.0" From a7193fb2a36a752ac7e106fc3f4681d71a8c4140 Mon Sep 17 00:00:00 2001 From: Lukas Harbarth Date: Wed, 15 Jan 2025 16:47:32 +0100 Subject: [PATCH 02/17] add story --- .../AnalyticalTableV2.stories.tsx | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.stories.tsx diff --git a/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.stories.tsx b/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.stories.tsx new file mode 100644 index 00000000000..1be5b7a4264 --- /dev/null +++ b/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.stories.tsx @@ -0,0 +1,70 @@ +import dataLarge from '@sb/mockData/Friends500.json'; +import type { Meta, StoryObj } from '@storybook/react'; +import type { ColumnDef } from '@tanstack/react-table'; +import { Button } from '@ui5/webcomponents-react'; +import { AnalyticalTableV2 } from './index.js'; + +const columns: ColumnDef[] = [ + { header: 'Name', accessorKey: 'name' }, + { header: 'Age', accessorKey: 'age' }, + { header: 'Friend Name', accessorKey: 'friend.name' }, + { header: 'Friend Age', accessorKey: 'friend.age' }, + { + header: 'Column Pinned', + id: 'c_pinned', + cell: ({ row }) => { + return 'Pinned'; + } + }, + { + header: 'Pin Row', + id: 'r_pinned', + size: 300, + cell: ({ row }) => { + return ( + <> + + + + + ); + } + } +]; + +const meta = { + title: 'Data Display / AnalyticalTableV2', + component: AnalyticalTableV2, + args: { data: dataLarge.map((item, index) => ({ ...item, friend: { ...item.friend, age: index } })), columns } +} satisfies Meta; +export default meta; +type Story = StoryObj; + +export const Default: Story = { + render(args) { + return ( + <> +
+ + + ); + } +}; From 504fac746a17400b7e249397f79ff94d9c8764c0 Mon Sep 17 00:00:00 2001 From: Lukas Harbarth Date: Wed, 15 Jan 2025 16:50:53 +0100 Subject: [PATCH 03/17] initial row virtualization & pin feature (no-verify) --- .../AnalyticalTableV2.stories.tsx | 1 + .../components/AnalyticalTableV2/index.tsx | 295 ++++++++++++++++++ 2 files changed, 296 insertions(+) create mode 100644 packages/main/src/components/AnalyticalTableV2/index.tsx diff --git a/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.stories.tsx b/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.stories.tsx index 1be5b7a4264..8b5ff74a526 100644 --- a/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.stories.tsx +++ b/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.stories.tsx @@ -4,6 +4,7 @@ import type { ColumnDef } from '@tanstack/react-table'; import { Button } from '@ui5/webcomponents-react'; import { AnalyticalTableV2 } from './index.js'; +//todo: any const columns: ColumnDef[] = [ { header: 'Name', accessorKey: 'name' }, { header: 'Age', accessorKey: 'age' }, diff --git a/packages/main/src/components/AnalyticalTableV2/index.tsx b/packages/main/src/components/AnalyticalTableV2/index.tsx new file mode 100644 index 00000000000..c61224f086f --- /dev/null +++ b/packages/main/src/components/AnalyticalTableV2/index.tsx @@ -0,0 +1,295 @@ +import { Column, flexRender, getCoreRowModel, Row, useReactTable } from '@tanstack/react-table'; +import { useVirtualizer } from '@tanstack/react-virtual'; +import { CSSProperties, ReactNode, useEffect, useRef, useState } from 'react'; + +interface AnalyticalTableV2Props { + data?: any[]; + columns?: any[]; +} + +const ROW_HEIGHT = 44; + +const getCommonPinningStyles = (column: Column): 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, + left: isPinned === 'left' ? `${column.getStart('left')}px` : undefined, + right: isPinned === 'right' ? `${column.getAfter('right')}px` : undefined, + opacity: isPinned ? 0.95 : 1, + position: isPinned ? 'sticky' : 'relative', + width: column.getSize(), + zIndex: isPinned ? 1 : 0, + background: isPinned ? 'lightgreen' : 'transparent' + }; +}; + +//todo forwardRef or React19? --> prob forwardRef +function AnalyticalTableV2(props: AnalyticalTableV2Props): JSX.Element { + const { columns, data } = props; + const tableContainerRef = useRef(null); + const [tableWidth, setTableWidth] = useState(0); + + useEffect(() => { + const tableContainer = tableContainerRef.current; + + const resizeObserver = new ResizeObserver((entries) => { + for (let entry of entries) { + if (entry.borderBoxSize) { + const borderBoxWidth = entry.borderBoxSize[0].inlineSize; + setTableWidth(borderBoxWidth); + } + } + }); + + if (tableContainer) { + resizeObserver.observe(tableContainer, { box: 'border-box' }); + } + + return () => { + if (tableContainer) { + resizeObserver.unobserve(tableContainer); + } + }; + }, []); + + const reactTable = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + //todo: remove + debugTable: true, + //todo: optional + enableColumnPinning: true, + initialState: { + columnPinning: { + left: ['c_pinned'], + right: ['friend_age'] + }, + rowPinning: { + bottom: ['0', '1'], + top: ['499', '498'] + } + }, + enableRowPinning: true + }); + + // not pinned + const centerRows = reactTable.getCenterRows(); + const bottomRows = reactTable.getBottomRows(); + const topRows = reactTable.getTopRows(); + const headerGroups = reactTable.getHeaderGroups(); + + console.log(topRows); + + //todo: fixed height - can be updated to use dynamic height => implement virtualizer so it can be extended + const rowVirtualizer = useVirtualizer({ + count: centerRows.length, + getScrollElement: () => tableContainerRef.current, + estimateSize: () => ROW_HEIGHT, + overscan: 5 + // paddingStart: topRows.length ? topRows.length * ROW_HEIGHT : 0 + // paddingEnd: bottomRows.length ? bottomRows.length * ROW_HEIGHT : 0 + }); + + console.log(rowVirtualizer.getTotalSize()); + + const virtualizedContainerHeight = rowVirtualizer.getSize(); + const tableHeight = 11 * ROW_HEIGHT; + + const { rows } = reactTable.getRowModel(); + + return ( + <> +
adjust if multiple headers are rendered + background: 'lightblue' + }} + > +
+
+ {headerGroups.map((headerGroup) => { + return ( +
+ {headerGroup.headers.map((header) => { + return ( +
+ {flexRender(header.column.columnDef.header, header.getContext())} +
+ ); + })} +
+ ); + })} +
+ {topRows.length > 0 && ( +
+ {topRows.map((row, index, arr) => { + return ( +
+ {row.getVisibleCells().map((cell) => { + return ( +
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
+ ); + })} +
+ ); + })} +
+ )} + {/* todo: if only the table body should be scrolled (scrollbar not over header) then we can't use this approach*/} +
+ {rowVirtualizer.getVirtualItems().map((virtualRow) => { + const row = centerRows[virtualRow.index] as Row; //todo: check if type can be inferred from `data` + return ( +
+ {row.getVisibleCells().map((cell) => { + return ( +
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
+ ); + })} +
+ ); + })} +
+ {bottomRows.length > 0 && ( +
+ {bottomRows.map((row) => { + return ( +
+ {row.getVisibleCells().map((cell) => { + return ( +
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
+ ); + })} +
+ ); + })} +
+ )} +
+
+ + ); +} + +export type { AnalyticalTableV2Props }; +export { AnalyticalTableV2 }; From 2c9c98dbf73a6cf315a21aa8f6cad7c2708917ca Mon Sep 17 00:00:00 2001 From: Lukas Harbarth Date: Thu, 16 Jan 2025 17:46:21 +0100 Subject: [PATCH 04/17] outsource components/hooks, add css, allow dynamic row height change --- .../AnalyticalTableV2.module.css | 97 +++++++ .../AnalyticalTableV2.stories.tsx | 6 +- .../AnalyticalTableV2/core/Cell.tsx | 53 ++++ .../components/AnalyticalTableV2/core/Row.tsx | 19 ++ .../components/AnalyticalTableV2/index.tsx | 248 ++++++------------ .../AnalyticalTableV2/useRowVirtualizer.ts | 44 ++++ .../utils/useComputedCssVariable.ts | 19 ++ 7 files changed, 320 insertions(+), 166 deletions(-) create mode 100644 packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.module.css create mode 100644 packages/main/src/components/AnalyticalTableV2/core/Cell.tsx create mode 100644 packages/main/src/components/AnalyticalTableV2/core/Row.tsx create mode 100644 packages/main/src/components/AnalyticalTableV2/useRowVirtualizer.ts create mode 100644 packages/main/src/components/AnalyticalTableV2/utils/useComputedCssVariable.ts diff --git a/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.module.css b/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.module.css new file mode 100644 index 00000000000..7512f9a43d3 --- /dev/null +++ b/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.module.css @@ -0,0 +1,97 @@ +/*todo scroll margin for interactive elements (scroll into view when focused)*/ +.sticky { + position: sticky; + z-index: 1; +} + +.cell { + display: flex; + background-color: var(--sapList_Background); +} + +/* ============================================================= */ +/* Container */ +/* ============================================================= */ + +.tableContainer { + overflow: auto; + position: relative; + background-color: var(--sapList_Background); + font-size: var(--sapFontSize); +} + +/* ============================================================= */ +/* Table Body Container */ +/* ============================================================= */ + +.tableBodyContainer { + /*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 { + display: flex; + width: 100%; + height: var(--_ui5WcrAnalyticalTableControlledRowHeight); +} + +/* ============================================================= */ +/* Header */ +/* ============================================================= */ + +/*.headerCell {*/ +/* display: flex;*/ +/*}*/ + +.headerRow { + background-color: var(--sapList_HeaderBackground); +} + +/* ============================================================= */ +/* Body */ +/* ============================================================= */ + +.virtualizedRow { + position: absolute; + inset-inline-start: 0; + inset-block-start: 0; +} diff --git a/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.stories.tsx b/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.stories.tsx index 8b5ff74a526..4f1f4b9f8f3 100644 --- a/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.stories.tsx +++ b/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.stories.tsx @@ -54,7 +54,11 @@ const columns: ColumnDef[] = [ const meta = { title: 'Data Display / AnalyticalTableV2', component: AnalyticalTableV2, - args: { data: dataLarge.map((item, index) => ({ ...item, friend: { ...item.friend, age: index } })), columns } + args: { + rowHeight: 200, + data: dataLarge.map((item, index) => ({ ...item, friend: { ...item.friend, age: index } })), + columns + } } satisfies Meta; export default meta; type Story = StoryObj; diff --git a/packages/main/src/components/AnalyticalTableV2/core/Cell.tsx b/packages/main/src/components/AnalyticalTableV2/core/Cell.tsx new file mode 100644 index 00000000000..785285a6c22 --- /dev/null +++ b/packages/main/src/components/AnalyticalTableV2/core/Cell.tsx @@ -0,0 +1,53 @@ +import type { Column, CoreCell, CoreHeader } from '@tanstack/react-table'; +import { flexRender } from '@tanstack/react-table'; +import type { CSSProperties, HTMLAttributes } from 'react'; +import { classNames } from '../AnalyticalTableV2.module.css.js'; + +const getCommonPinningStyles = (column: Column): 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 { + style?: CSSProperties; + role: HTMLAttributes['role']; + /** + * cell object (e.g. `header`, `cell`) + */ + cell: CoreCell | CoreHeader; + //todo type + renderable: any; +} + +export function Cell(props: CellProps) { + const { style = {}, role, cell, renderable } = props; + + return ( +
+ {flexRender(renderable, cell.getContext())} +
+ ); +} + +Cell.displayName = 'AnalyticalTableV2Cell'; diff --git a/packages/main/src/components/AnalyticalTableV2/core/Row.tsx b/packages/main/src/components/AnalyticalTableV2/core/Row.tsx new file mode 100644 index 00000000000..a959703ad70 --- /dev/null +++ b/packages/main/src/components/AnalyticalTableV2/core/Row.tsx @@ -0,0 +1,19 @@ +import type { CommonProps } from '@ui5/webcomponents-react'; +import { clsx } from 'clsx'; +import { classNames } from '../AnalyticalTableV2.module.css.js'; + +interface RowProps extends CommonProps { + //todo children type + children: any; +} + +export function Row(props: RowProps) { + const { children, className, ...rest } = props; + return ( +
+ {children} +
+ ); +} + +Row.displayName = 'AnalyticalTableRow'; diff --git a/packages/main/src/components/AnalyticalTableV2/index.tsx b/packages/main/src/components/AnalyticalTableV2/index.tsx index c61224f086f..db30c584f0e 100644 --- a/packages/main/src/components/AnalyticalTableV2/index.tsx +++ b/packages/main/src/components/AnalyticalTableV2/index.tsx @@ -1,46 +1,41 @@ -import { Column, flexRender, getCoreRowModel, Row, useReactTable } from '@tanstack/react-table'; -import { useVirtualizer } from '@tanstack/react-virtual'; -import { CSSProperties, ReactNode, useEffect, useRef, useState } from 'react'; +import { getCoreRowModel, useReactTable } from '@tanstack/react-table'; +import { CssSizeVariables, useStylesheet } from '@ui5/webcomponents-react-base'; +import { clsx } from 'clsx'; +import type { CSSProperties, ReactElement } from 'react'; +import { useEffect, useRef, useState } from 'react'; +import { classNames, styleData } from './AnalyticalTableV2.module.css.js'; +import { Cell } from './core/Cell.js'; +import { Row } from './core/Row.js'; +import { useRowVirtualizer } from '@/packages/main/src/components/AnalyticalTableV2/useRowVirtualizer.js'; interface AnalyticalTableV2Props { data?: any[]; columns?: any[]; + rowHeight?: number; + visibleRows?: number; } -const ROW_HEIGHT = 44; - -const getCommonPinningStyles = (column: Column): CSSProperties => { - const isPinned = column.getIsPinned(); - const isLastLeftPinnedColumn = isPinned === 'left' && column.getIsLastColumn('left'); - const isFirstRightPinnedColumn = isPinned === 'right' && column.getIsFirstColumn('right'); +interface CSSPropertiesWithVars extends CSSProperties { + '--_ui5WcrAnalyticalTableControlledRowHeight': string; + '--_ui5WcrAnalyticalTableHeaderGroups': number; + '--_ui5WcrAnalyticalTableTopRows': number; + '--_ui5WcrAnalyticalTableBottomRows': number; +} - return { - boxShadow: isLastLeftPinnedColumn - ? '-4px 0 4px -4px gray inset' - : isFirstRightPinnedColumn - ? '4px 0 4px -4px gray inset' - : undefined, - left: isPinned === 'left' ? `${column.getStart('left')}px` : undefined, - right: isPinned === 'right' ? `${column.getAfter('right')}px` : undefined, - opacity: isPinned ? 0.95 : 1, - position: isPinned ? 'sticky' : 'relative', - width: column.getSize(), - zIndex: isPinned ? 1 : 0, - background: isPinned ? 'lightgreen' : 'transparent' - }; -}; +const ROW_HEIGHT_VAR = 'var(--_ui5WcrAnalyticalTableControlledRowHeight)'; //todo forwardRef or React19? --> prob forwardRef -function AnalyticalTableV2(props: AnalyticalTableV2Props): JSX.Element { - const { columns, data } = props; +function AnalyticalTableV2(props: AnalyticalTableV2Props): ReactElement { + const { columns, data, rowHeight, visibleRows = 15 } = props; + useStylesheet(styleData, AnalyticalTableV2.displayName); const tableContainerRef = useRef(null); - const [tableWidth, setTableWidth] = useState(0); + const [_tableWidth, setTableWidth] = useState(0); useEffect(() => { const tableContainer = tableContainerRef.current; const resizeObserver = new ResizeObserver((entries) => { - for (let entry of entries) { + for (const entry of entries) { if (entry.borderBoxSize) { const borderBoxWidth = entry.borderBoxSize[0].inlineSize; setTableWidth(borderBoxWidth); @@ -80,207 +75,128 @@ function AnalyticalTableV2(props: AnalyticalTableV2Props): JSX.Element { enableRowPinning: true }); - // not pinned + const headerGroups = reactTable.getHeaderGroups(); + const topRows = reactTable.getTopRows(); const centerRows = reactTable.getCenterRows(); const bottomRows = reactTable.getBottomRows(); - const topRows = reactTable.getTopRows(); - const headerGroups = reactTable.getHeaderGroups(); - console.log(topRows); + const rowVirtualizer = useRowVirtualizer(rowHeight, tableContainerRef, { count: centerRows.length }); - //todo: fixed height - can be updated to use dynamic height => implement virtualizer so it can be extended - const rowVirtualizer = useVirtualizer({ - count: centerRows.length, - getScrollElement: () => tableContainerRef.current, - estimateSize: () => ROW_HEIGHT, - overscan: 5 - // paddingStart: topRows.length ? topRows.length * ROW_HEIGHT : 0 - // paddingEnd: bottomRows.length ? bottomRows.length * ROW_HEIGHT : 0 - }); - - console.log(rowVirtualizer.getTotalSize()); - - const virtualizedContainerHeight = rowVirtualizer.getSize(); - const tableHeight = 11 * ROW_HEIGHT; - - const { rows } = reactTable.getRowModel(); + // const { rows } = reactTable.getRowModel(); return ( <>
adjust if multiple headers are rendered - background: 'lightblue' - }} + style={ + { + '--_ui5WcrAnalyticalTableControlledRowHeight': + typeof rowHeight === 'number' ? `${rowHeight}px` : CssSizeVariables.ui5WcrAnalyticalTableRowHeight, + '--_ui5WcrAnalyticalTableHeaderGroups': headerGroups.length, + '--_ui5WcrAnalyticalTableTopRows': topRows.length, + '--_ui5WcrAnalyticalTableBottomRows': bottomRows.length, + height: `calc(${headerGroups.length} * ${ROW_HEIGHT_VAR} + ${visibleRows} * ${ROW_HEIGHT_VAR})` + } as CSSPropertiesWithVars + } + className={classNames.tableContainer} > -
-
+
+
{headerGroups.map((headerGroup) => { return ( -
{headerGroup.headers.map((header) => { return ( -
- {flexRender(header.column.columnDef.header, header.getContext())} -
+ role="columnheader" + style={{ width: header.getSize() }} + renderable={header.column.columnDef.header} + cell={header} + /> ); })} -
+ ); })}
{topRows.length > 0 && ( -
- {topRows.map((row, index, arr) => { +
+ {topRows.map((row) => { return ( -
+ {row.getVisibleCells().map((cell) => { return ( -
- {flexRender(cell.column.columnDef.cell, cell.getContext())} -
+ renderable={cell.column.columnDef.cell} + cell={cell} + // style={{ width: cell.column.getSize() }} + /> ); })} -
+ ); })}
)} - {/* todo: if only the table body should be scrolled (scrollbar not over header) then we can't use this approach*/}
{rowVirtualizer.getVirtualItems().map((virtualRow) => { - const row = centerRows[virtualRow.index] as Row; //todo: check if type can be inferred from `data` + const row = centerRows[virtualRow.index]; return ( -
{row.getVisibleCells().map((cell) => { return ( -
- {flexRender(cell.column.columnDef.cell, cell.getContext())} -
+ role="gridcell" + renderable={cell.column.columnDef.cell} + cell={cell} + style={{ width: cell.column.getSize() }} + /> ); })} -
+ ); })}
{bottomRows.length > 0 && ( -
+
{bottomRows.map((row) => { return ( -
+ {row.getVisibleCells().map((cell) => { return ( -
- {flexRender(cell.column.columnDef.cell, cell.getContext())} -
+ renderable={cell.column.columnDef.cell} + cell={cell} + style={{ width: cell.column.getSize() }} + /> ); })} -
+ ); })}
@@ -291,5 +207,7 @@ function AnalyticalTableV2(props: AnalyticalTableV2Props): JSX.Element { ); } +AnalyticalTableV2.displayName = 'AnalyticalTableV2'; + export type { AnalyticalTableV2Props }; export { AnalyticalTableV2 }; diff --git a/packages/main/src/components/AnalyticalTableV2/useRowVirtualizer.ts b/packages/main/src/components/AnalyticalTableV2/useRowVirtualizer.ts new file mode 100644 index 00000000000..d9b716845ed --- /dev/null +++ b/packages/main/src/components/AnalyticalTableV2/useRowVirtualizer.ts @@ -0,0 +1,44 @@ +import { useVirtualizer } from '@tanstack/react-virtual'; +import type { RefObject } from 'react'; +import { useRef, useEffect } from 'react'; +import { useComputedCssVariable } from './utils/useComputedCssVariable.js'; + +export function useRowVirtualizer( + rowHeight: number | undefined, + containerRef: RefObject, + virtualizerOptions: { + count: number; + overscan?: number; + // paddingStart?: number; + // paddingEnd?: number; + } +) { + const { count, overscan = 5 } = virtualizerOptions; + const computedRowHeight = useComputedCssVariable(containerRef, '--_ui5WcrAnalyticalTableControlledRowHeight'); + //todo parse int + const appliedRowHeight = rowHeight ?? computedRowHeight; + + const rowVirtualizer = useVirtualizer({ + count, + getScrollElement: () => containerRef.current, + estimateSize: () => appliedRowHeight, + overscan + // paddingStart: options.paddingStart, + // paddingEnd: options.paddingEnd + }); + + const prevAppliedRowHeight = useRef(null); + + useEffect(() => { + if (prevAppliedRowHeight.current !== appliedRowHeight) { + if (prevAppliedRowHeight.current !== null) { + console.log('measure'); + rowVirtualizer.measure(); + } else { + prevAppliedRowHeight.current = appliedRowHeight; + } + } + }, [appliedRowHeight, rowVirtualizer]); + + return rowVirtualizer; +} diff --git a/packages/main/src/components/AnalyticalTableV2/utils/useComputedCssVariable.ts b/packages/main/src/components/AnalyticalTableV2/utils/useComputedCssVariable.ts new file mode 100644 index 00000000000..4c8d83b1799 --- /dev/null +++ b/packages/main/src/components/AnalyticalTableV2/utils/useComputedCssVariable.ts @@ -0,0 +1,19 @@ +import type { RefObject } from 'react'; +import { useEffect, useState } from 'react'; + +export function useComputedCssVariable(ref: RefObject, variableName: string): number | null { + const [value, setValue] = useState(null); + + useEffect(() => { + if (ref.current) { + const computedStyles = getComputedStyle(ref.current); + const variableValue = computedStyles.getPropertyValue(variableName).trim(); + + // Attempt to parse the value as a number + const parsedValue = parseFloat(variableValue); + setValue(isNaN(parsedValue) ? null : parsedValue); + } + }, [variableName, ref]); + + return value; +} From 240aa6b5f95c95ca1bad3e11a0d337aaefb96ea0 Mon Sep 17 00:00:00 2001 From: Lukas Harbarth Date: Fri, 17 Jan 2025 11:47:15 +0100 Subject: [PATCH 05/17] update table-height with horizontal scrollbar height --- .../AnalyticalTableV2.module.css | 3 ++ .../AnalyticalTableV2.stories.tsx | 7 ++-- .../components/AnalyticalTableV2/index.tsx | 32 +++------------ .../AnalyticalTableV2/useRowVirtualizer.ts | 7 ++-- .../utils/useTableContainerResizeObserver.ts | 41 +++++++++++++++++++ 5 files changed, 56 insertions(+), 34 deletions(-) create mode 100644 packages/main/src/components/AnalyticalTableV2/utils/useTableContainerResizeObserver.ts diff --git a/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.module.css b/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.module.css index 7512f9a43d3..7aca0fb8961 100644 --- a/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.module.css +++ b/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.module.css @@ -1,4 +1,5 @@ /*todo scroll margin for interactive elements (scroll into view when focused)*/ +/*todo: use will-change: transform?*/ .sticky { position: sticky; z-index: 1; @@ -18,6 +19,8 @@ position: relative; background-color: var(--sapList_Background); font-size: var(--sapFontSize); + box-sizing: border-box; + overscroll-behavior: none; } /* ============================================================= */ diff --git a/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.stories.tsx b/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.stories.tsx index 4f1f4b9f8f3..6b9f07eea83 100644 --- a/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.stories.tsx +++ b/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.stories.tsx @@ -55,10 +55,11 @@ const meta = { title: 'Data Display / AnalyticalTableV2', component: AnalyticalTableV2, args: { - rowHeight: 200, data: dataLarge.map((item, index) => ({ ...item, friend: { ...item.friend, age: index } })), - columns - } + columns, + visibleRows: 5 + }, + argTypes: { data: { control: { disable: true } }, columns: { control: { disable: true } } } } satisfies Meta; export default meta; type Story = StoryObj; diff --git a/packages/main/src/components/AnalyticalTableV2/index.tsx b/packages/main/src/components/AnalyticalTableV2/index.tsx index db30c584f0e..fbfe093ebd5 100644 --- a/packages/main/src/components/AnalyticalTableV2/index.tsx +++ b/packages/main/src/components/AnalyticalTableV2/index.tsx @@ -2,11 +2,12 @@ import { getCoreRowModel, useReactTable } from '@tanstack/react-table'; import { CssSizeVariables, useStylesheet } from '@ui5/webcomponents-react-base'; import { clsx } from 'clsx'; import type { CSSProperties, ReactElement } from 'react'; -import { useEffect, useRef, useState } from 'react'; +import { useRef } from 'react'; import { classNames, styleData } from './AnalyticalTableV2.module.css.js'; import { Cell } from './core/Cell.js'; import { Row } from './core/Row.js'; -import { useRowVirtualizer } from '@/packages/main/src/components/AnalyticalTableV2/useRowVirtualizer.js'; +import { useRowVirtualizer } from './useRowVirtualizer.js'; +import { useTableContainerResizeObserver } from './utils/useTableContainerResizeObserver.js'; interface AnalyticalTableV2Props { data?: any[]; @@ -29,30 +30,7 @@ function AnalyticalTableV2(props: AnalyticalTableV2Props): ReactElement(null); - const [_tableWidth, setTableWidth] = useState(0); - - useEffect(() => { - const tableContainer = tableContainerRef.current; - - const resizeObserver = new ResizeObserver((entries) => { - for (const entry of entries) { - if (entry.borderBoxSize) { - const borderBoxWidth = entry.borderBoxSize[0].inlineSize; - setTableWidth(borderBoxWidth); - } - } - }); - - if (tableContainer) { - resizeObserver.observe(tableContainer, { box: 'border-box' }); - } - - return () => { - if (tableContainer) { - resizeObserver.unobserve(tableContainer); - } - }; - }, []); + const { tableWidth: _tableWidth, horizontalScrollbarHeight } = useTableContainerResizeObserver(tableContainerRef); const reactTable = useReactTable({ data, @@ -95,7 +73,7 @@ function AnalyticalTableV2(props: AnalyticalTableV2Props): ReactElement( rowHeight: number | undefined, containerRef: RefObject, @@ -15,8 +16,7 @@ export function useRowVirtualizer( ) { const { count, overscan = 5 } = virtualizerOptions; const computedRowHeight = useComputedCssVariable(containerRef, '--_ui5WcrAnalyticalTableControlledRowHeight'); - //todo parse int - const appliedRowHeight = rowHeight ?? computedRowHeight; + const appliedRowHeight = rowHeight ?? computedRowHeight ?? 32; const rowVirtualizer = useVirtualizer({ count, @@ -28,11 +28,10 @@ export function useRowVirtualizer( }); const prevAppliedRowHeight = useRef(null); - useEffect(() => { if (prevAppliedRowHeight.current !== appliedRowHeight) { if (prevAppliedRowHeight.current !== null) { - console.log('measure'); + // remeasure if rowHeight changes rowVirtualizer.measure(); } else { prevAppliedRowHeight.current = appliedRowHeight; diff --git a/packages/main/src/components/AnalyticalTableV2/utils/useTableContainerResizeObserver.ts b/packages/main/src/components/AnalyticalTableV2/utils/useTableContainerResizeObserver.ts new file mode 100644 index 00000000000..ed76e94bbca --- /dev/null +++ b/packages/main/src/components/AnalyticalTableV2/utils/useTableContainerResizeObserver.ts @@ -0,0 +1,41 @@ +import type { RefObject } from 'react'; +import { useState, useEffect } from 'react'; + +export const useTableContainerResizeObserver = (tableContainerRef: RefObject) => { + const [tableWidth, setTableWidth] = useState(0); + const [horizontalScrollbarHeight, setHorizontalScrollbarHeight] = useState(0); + + useEffect(() => { + const tableContainer = tableContainerRef.current; + + const resizeObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + if (entry.borderBoxSize) { + const { borderBoxSize, contentBoxSize } = entry; + const borderBoxHeight = borderBoxSize[0].blockSize; + const contentBoxHeight = contentBoxSize[0].blockSize; + if (borderBoxHeight > contentBoxHeight) { + setHorizontalScrollbarHeight(borderBoxHeight - contentBoxHeight); + } else { + setHorizontalScrollbarHeight(0); + } + + const borderBoxWidth = borderBoxSize[0].inlineSize; + setTableWidth(borderBoxWidth); + } + } + }); + + if (tableContainer) { + resizeObserver.observe(tableContainer, { box: 'border-box' }); + } + + return () => { + if (tableContainer) { + resizeObserver.unobserve(tableContainer); + } + }; + }, [tableContainerRef]); + + return { tableWidth, horizontalScrollbarHeight }; +}; From 075369a739b6c636f02ba4f2c1c7c3d97308fa7a Mon Sep 17 00:00:00 2001 From: Lukas Harbarth Date: Wed, 22 Jan 2025 17:25:28 +0100 Subject: [PATCH 06/17] Update yarn.lock --- yarn.lock | 626 +++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 434 insertions(+), 192 deletions(-) diff --git a/yarn.lock b/yarn.lock index 7709f74743e..d5da2c96d51 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12,6 +12,13 @@ __metadata: languageName: node linkType: hard +"@adobe/css-tools@npm:^4.4.0": + version: 4.4.1 + resolution: "@adobe/css-tools@npm:4.4.1" + checksum: 10c0/1a68ad9af490f45fce7b6e50dd2d8ac0c546d74431649c0d42ee4ceb1a9fa057fae0a7ef1e148effa12d84ec00ed71869ebfe0fb1dcdcc80bfcb6048c12abcc0 + languageName: node + linkType: hard + "@ampproject/remapping@npm:^2.2.0": version: 2.2.0 resolution: "@ampproject/remapping@npm:2.2.0" @@ -3101,21 +3108,19 @@ __metadata: languageName: node linkType: hard -"@joshwooding/vite-plugin-react-docgen-typescript@npm:0.3.0": - version: 0.3.0 - resolution: "@joshwooding/vite-plugin-react-docgen-typescript@npm:0.3.0" +"@joshwooding/vite-plugin-react-docgen-typescript@npm:0.4.2": + version: 0.4.2 + resolution: "@joshwooding/vite-plugin-react-docgen-typescript@npm:0.4.2" dependencies: - glob: "npm:^7.2.0" - glob-promise: "npm:^4.2.0" magic-string: "npm:^0.27.0" react-docgen-typescript: "npm:^2.2.2" peerDependencies: typescript: ">= 4.3.x" - vite: ^3.0.0 || ^4.0.0 || ^5.0.0 + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 peerDependenciesMeta: typescript: optional: true - checksum: 10c0/31098ad8fcc2440437534599c111d9f2951dd74821e8ba46c521b969bae4c918d830b7bb0484efbad29a51711bb62d3bc623d5a1ed5b1695b5b5594ea9dd4ca0 + checksum: 10c0/355d13ad92a9da786b561a25250e6c94a8e51d235ced345e54ebfe709abc45ab60c2a8d06599df6ec0441fba01720f189883429943cb62dff9a4c31b59f0939c languageName: node linkType: hard @@ -4411,21 +4416,23 @@ __metadata: languageName: node linkType: hard -"@storybook/addon-a11y@npm:8.4.5": - version: 8.4.5 - resolution: "@storybook/addon-a11y@npm:8.4.5" +"@storybook/addon-a11y@npm:8.5.0": + version: 8.5.0 + resolution: "@storybook/addon-a11y@npm:8.5.0" dependencies: - "@storybook/addon-highlight": "npm:8.4.5" + "@storybook/addon-highlight": "npm:8.5.0" + "@storybook/test": "npm:8.5.0" axe-core: "npm:^4.2.0" + vitest-axe: "npm:^0.1.0" peerDependencies: - storybook: ^8.4.5 - checksum: 10c0/3e9abdcc60b8029652a24559ba4f89a67da2572211b71f5f12d7ab1806aff790c0e5b19c7fc3d01e655c87947002d59c6ab798c2f575a6cd7d01a790979a6c6b + storybook: ^8.5.0 + checksum: 10c0/69b1fbd9b1912ad6dec29bceec17601cd80eb85cde10252b0e7309e1432ce9dddf46c2d6ac16ec488f01e7de8765230fadb73ad2fc49faa241ea89ae6aeec1a3 languageName: node linkType: hard -"@storybook/addon-actions@npm:8.4.5": - version: 8.4.5 - resolution: "@storybook/addon-actions@npm:8.4.5" +"@storybook/addon-actions@npm:8.5.0": + version: 8.5.0 + resolution: "@storybook/addon-actions@npm:8.5.0" dependencies: "@storybook/global": "npm:^5.0.0" "@types/uuid": "npm:^9.0.1" @@ -4433,177 +4440,177 @@ __metadata: polished: "npm:^4.2.2" uuid: "npm:^9.0.0" peerDependencies: - storybook: ^8.4.5 - checksum: 10c0/b689c16a01302c4d64f24dc777b666456bddc1ab820aaf9b6b6f9d3ab7081d6f573a6641bc2dcb9ee8c3ec9425f36426737abd6735da6fcfc670ee6b9f3d8280 + storybook: ^8.5.0 + checksum: 10c0/153e808afb6b13e24bc3ef916bf7324c050c8deac83b8e1a2cbea82de627f4d6c99cffbe735d5f47b38f9c07dda3a5921aca1c2333d9d9a5df2e4872d1de7f8a languageName: node linkType: hard -"@storybook/addon-backgrounds@npm:8.4.5": - version: 8.4.5 - resolution: "@storybook/addon-backgrounds@npm:8.4.5" +"@storybook/addon-backgrounds@npm:8.5.0": + version: 8.5.0 + resolution: "@storybook/addon-backgrounds@npm:8.5.0" dependencies: "@storybook/global": "npm:^5.0.0" memoizerific: "npm:^1.11.3" ts-dedent: "npm:^2.0.0" peerDependencies: - storybook: ^8.4.5 - checksum: 10c0/863c4cb60957c1113231a5bedf833de8ba86846509b950e878acd788d1e1ad13e07ad9b2e183c96a8bee8c01442b22ee8cdf2f324e6fca297d88f43b26b3fef1 + storybook: ^8.5.0 + checksum: 10c0/b8dc09a0afa4132388c38d27107f3c2ab5365643b8df59d4342b5aa796f0bc41706e196336abcb6b16184a9972906298962b23317f68556c5331102107a03720 languageName: node linkType: hard -"@storybook/addon-controls@npm:8.4.5": - version: 8.4.5 - resolution: "@storybook/addon-controls@npm:8.4.5" +"@storybook/addon-controls@npm:8.5.0": + version: 8.5.0 + resolution: "@storybook/addon-controls@npm:8.5.0" dependencies: "@storybook/global": "npm:^5.0.0" dequal: "npm:^2.0.2" ts-dedent: "npm:^2.0.0" peerDependencies: - storybook: ^8.4.5 - checksum: 10c0/1ca92a32d5ff018f2120d8a8787b834d1ee2bbf2423b422ccb6f2a9f1ce0f66ad5f67de6e268330434d47734dbe0cec8b130678392705db36510d13770ce6616 + storybook: ^8.5.0 + checksum: 10c0/b06697ff75f3984fb1888629260ac49ff3fc6127825f30121781d828d3f75090c39074eba681c6165420d746ce22c892bbde84e41c2bead1749577c08af38bd5 languageName: node linkType: hard -"@storybook/addon-docs@npm:8.4.5": - version: 8.4.5 - resolution: "@storybook/addon-docs@npm:8.4.5" +"@storybook/addon-docs@npm:8.5.0": + version: 8.5.0 + resolution: "@storybook/addon-docs@npm:8.5.0" dependencies: "@mdx-js/react": "npm:^3.0.0" - "@storybook/blocks": "npm:8.4.5" - "@storybook/csf-plugin": "npm:8.4.5" - "@storybook/react-dom-shim": "npm:8.4.5" + "@storybook/blocks": "npm:8.5.0" + "@storybook/csf-plugin": "npm:8.5.0" + "@storybook/react-dom-shim": "npm:8.5.0" react: "npm:^16.8.0 || ^17.0.0 || ^18.0.0" react-dom: "npm:^16.8.0 || ^17.0.0 || ^18.0.0" ts-dedent: "npm:^2.0.0" peerDependencies: - storybook: ^8.4.5 - checksum: 10c0/cb3731d6cc738ea01094acc83dfe92b918aed4dde3d0251b61aaa2105fd5b692686be06c092f27d37653855f2ffae86d24888a6066e121f6fc97c92b86dfd2c1 + storybook: ^8.5.0 + checksum: 10c0/876be7abebfa83dda8ba263d757962b9e290733c40c7560263705daf707b1a50f04ca129042f6bed1ee98452fb3f05c2e92dc74744cd1c9b324402c3c156653d languageName: node linkType: hard -"@storybook/addon-essentials@npm:8.4.5": - version: 8.4.5 - resolution: "@storybook/addon-essentials@npm:8.4.5" - dependencies: - "@storybook/addon-actions": "npm:8.4.5" - "@storybook/addon-backgrounds": "npm:8.4.5" - "@storybook/addon-controls": "npm:8.4.5" - "@storybook/addon-docs": "npm:8.4.5" - "@storybook/addon-highlight": "npm:8.4.5" - "@storybook/addon-measure": "npm:8.4.5" - "@storybook/addon-outline": "npm:8.4.5" - "@storybook/addon-toolbars": "npm:8.4.5" - "@storybook/addon-viewport": "npm:8.4.5" +"@storybook/addon-essentials@npm:8.5.0": + version: 8.5.0 + resolution: "@storybook/addon-essentials@npm:8.5.0" + dependencies: + "@storybook/addon-actions": "npm:8.5.0" + "@storybook/addon-backgrounds": "npm:8.5.0" + "@storybook/addon-controls": "npm:8.5.0" + "@storybook/addon-docs": "npm:8.5.0" + "@storybook/addon-highlight": "npm:8.5.0" + "@storybook/addon-measure": "npm:8.5.0" + "@storybook/addon-outline": "npm:8.5.0" + "@storybook/addon-toolbars": "npm:8.5.0" + "@storybook/addon-viewport": "npm:8.5.0" ts-dedent: "npm:^2.0.0" peerDependencies: - storybook: ^8.4.5 - checksum: 10c0/fed258f3bbe6b380d61dd14f77b22049f8b2c38ac63cb08b66aa368301be7209cc7d7f2dea57caeed4f7021bedc6d35468ba42fda3e1e1cfe67a91713c0e0564 + storybook: ^8.5.0 + checksum: 10c0/def71ca485678073a89c32ed73d5cafa67cf8f7ed9c86602e6e23b5603d5c5b5033ecaad215492bcc301081bce4ac577cee6e6c47beeeeb4b4f8da3a174af09e languageName: node linkType: hard -"@storybook/addon-highlight@npm:8.4.5": - version: 8.4.5 - resolution: "@storybook/addon-highlight@npm:8.4.5" +"@storybook/addon-highlight@npm:8.5.0": + version: 8.5.0 + resolution: "@storybook/addon-highlight@npm:8.5.0" dependencies: "@storybook/global": "npm:^5.0.0" peerDependencies: - storybook: ^8.4.5 - checksum: 10c0/ba0b3f824e17279339ddafdf9c6b5e50158601c0f48185e24d26ff4070537d5e095452ad632402bd8d48a886a1a696c70bf996dd74637158858d3e98c18de44f + storybook: ^8.5.0 + checksum: 10c0/f9ef6073d980e125aa246488adb4968895e62df8152b89297e687d623e1b0d4ef1965f2a34c7310dabc19c23c18d4f6d5cc8e757757fbe33fb9620d6621fd1d6 languageName: node linkType: hard -"@storybook/addon-measure@npm:8.4.5": - version: 8.4.5 - resolution: "@storybook/addon-measure@npm:8.4.5" +"@storybook/addon-measure@npm:8.5.0": + version: 8.5.0 + resolution: "@storybook/addon-measure@npm:8.5.0" dependencies: "@storybook/global": "npm:^5.0.0" tiny-invariant: "npm:^1.3.1" peerDependencies: - storybook: ^8.4.5 - checksum: 10c0/793670594ac4154b8456f2aba6bb113dd4afbf5079c547d54092ea91f8d0d5139d61a15180b5e24402b309874249c628f3e1ff389cba446abd98be31153bb917 + storybook: ^8.5.0 + checksum: 10c0/1869a685b1ca0756362674d04df458f6e8c99a555beb3a7942167ce7f555930e4d72f648d7b96d511ffe7fedc86e51d9c440987bcfb70e5d717fafd750770ab4 languageName: node linkType: hard -"@storybook/addon-outline@npm:8.4.5": - version: 8.4.5 - resolution: "@storybook/addon-outline@npm:8.4.5" +"@storybook/addon-outline@npm:8.5.0": + version: 8.5.0 + resolution: "@storybook/addon-outline@npm:8.5.0" dependencies: "@storybook/global": "npm:^5.0.0" ts-dedent: "npm:^2.0.0" peerDependencies: - storybook: ^8.4.5 - checksum: 10c0/14f3993aa88a33035a048ea00713d936f0025055eb205c3033a65e5d885012c54b77a7363f994faeaefa362e6f88aad1f174bf01a1f6b0c85b3f96fbe8332772 + storybook: ^8.5.0 + checksum: 10c0/a8fd18dafa33afabc5bf2b568dd4a6115ac5e3a26f808144630f2a0d4e8b9553afcec467171893820cae9cfee15cc3e42563386275b1409b1967a721430f9d68 languageName: node linkType: hard -"@storybook/addon-toolbars@npm:8.4.5": - version: 8.4.5 - resolution: "@storybook/addon-toolbars@npm:8.4.5" +"@storybook/addon-toolbars@npm:8.5.0": + version: 8.5.0 + resolution: "@storybook/addon-toolbars@npm:8.5.0" peerDependencies: - storybook: ^8.4.5 - checksum: 10c0/dbb76bad06d5c7ded93881d6195f2e63a744a006011cf177cd316e51cc42de323aa47b5d9c5c9a374f4b1c8c13c1dd503b079501bf0379672bc6be83df0863f0 + storybook: ^8.5.0 + checksum: 10c0/0d2fc5f3098d9cbc0732b67666327671b3169ed4dc98ec839ba6d107658638dd4ad73e2e53e452523a3736ed06b953c2da2a96b8ce049e09c4afe1f3bd907bc6 languageName: node linkType: hard -"@storybook/addon-viewport@npm:8.4.5": - version: 8.4.5 - resolution: "@storybook/addon-viewport@npm:8.4.5" +"@storybook/addon-viewport@npm:8.5.0": + version: 8.5.0 + resolution: "@storybook/addon-viewport@npm:8.5.0" dependencies: memoizerific: "npm:^1.11.3" peerDependencies: - storybook: ^8.4.5 - checksum: 10c0/4d828612602605f13fcac83e8f5095713ecfe791a47f36a301e306d15892ce038c15e9fb075a1ea18974cdf5a7a8da2f81e85260fcbb9ebd9b0ac1f2e60eae35 + storybook: ^8.5.0 + checksum: 10c0/01aa02b0467f6df4638dea02f8f316c230b7b0a8afec161cad4cbf6e9c409b125e5b2e81d4b7ef5a1047c54bfc50844d4d56daa281f84c6df4b346ff12860e7c languageName: node linkType: hard -"@storybook/blocks@npm:8.4.5": - version: 8.4.5 - resolution: "@storybook/blocks@npm:8.4.5" +"@storybook/blocks@npm:8.5.0": + version: 8.5.0 + resolution: "@storybook/blocks@npm:8.5.0" dependencies: - "@storybook/csf": "npm:^0.1.11" + "@storybook/csf": "npm:0.1.12" "@storybook/icons": "npm:^1.2.12" ts-dedent: "npm:^2.0.0" peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^8.4.5 + storybook: ^8.5.0 peerDependenciesMeta: react: optional: true react-dom: optional: true - checksum: 10c0/6839e8439e0cec41c8562ed2b68641780ad017dd5ff45ea7414df00a85dc168cb06dc339523be46997827f630225ea77afdbbf859ed5a322d974a4aa92a14522 + checksum: 10c0/3fa4814c64b26014192747b8387a18bd84ddae694bf145cbd8cf4123c55fcb85b395dfd2e67f4a7d7c01d1ee18a72cf3264f2fc9524b62f66fb41ef8bd800fef languageName: node linkType: hard -"@storybook/builder-vite@npm:8.4.5": - version: 8.4.5 - resolution: "@storybook/builder-vite@npm:8.4.5" +"@storybook/builder-vite@npm:8.5.0": + version: 8.5.0 + resolution: "@storybook/builder-vite@npm:8.5.0" dependencies: - "@storybook/csf-plugin": "npm:8.4.5" + "@storybook/csf-plugin": "npm:8.5.0" browser-assert: "npm:^1.2.1" ts-dedent: "npm:^2.0.0" peerDependencies: - storybook: ^8.4.5 - vite: ^4.0.0 || ^5.0.0 - checksum: 10c0/4588ac40606ac20ae523cda8c0c073e892e19ff5eecba4e39a04f84b7f7986587b3ce8386356bb423994e30903fd8100cd9d163d875c5c1a748854fc63ac3ec9 + storybook: ^8.5.0 + vite: ^4.0.0 || ^5.0.0 || ^6.0.0 + checksum: 10c0/a20b051206327bf4c61a82caf019cc1df78141cd84de235133e8abef12e6184f1143614b8b47e6befb0fbb0c3bd8fc1ab3350e7b9d018742b2e5b132f7ffe28b languageName: node linkType: hard -"@storybook/components@npm:8.4.5": - version: 8.4.5 - resolution: "@storybook/components@npm:8.4.5" +"@storybook/components@npm:8.5.0": + version: 8.5.0 + resolution: "@storybook/components@npm:8.5.0" peerDependencies: storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 - checksum: 10c0/b166a73e79fee2747360d4e49a5d7171c3b45869dcc5a5bc72475bb711fc3d0bdf7dd1264ec248b69bf9a9afc7d85a1077616036ccb05e2f5c219aecab077176 + checksum: 10c0/d49934f6ceae0691e8258b0804789829de5746cb839f35382948aecc00d037fbacf402ee6e365208c3703b9fd16e28efc1e0de5e47dbd014d0b3a128447cbb63 languageName: node linkType: hard -"@storybook/core@npm:8.4.5": - version: 8.4.5 - resolution: "@storybook/core@npm:8.4.5" +"@storybook/core@npm:8.5.0": + version: 8.5.0 + resolution: "@storybook/core@npm:8.5.0" dependencies: - "@storybook/csf": "npm:^0.1.11" + "@storybook/csf": "npm:0.1.12" better-opn: "npm:^3.0.2" browser-assert: "npm:^1.2.1" esbuild: "npm:^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0" @@ -4619,18 +4626,27 @@ __metadata: peerDependenciesMeta: prettier: optional: true - checksum: 10c0/426327ebb7042c3f574fd076fa80c20662b26bdfab3f75c752f6facc03fa9100dfa7afda9c026c04dfe8a7f426524650423c644d1e511cbc96bdbc6c8c4c20e4 + checksum: 10c0/378d205527256ef9acaed3e6dc3e27f3669929ebcfd0c567de49c0b2ce1a7fc88588f7f3bd0c9786f9d9e1d68699ee483afadd150c3c04832e5cb503aa8a27be languageName: node linkType: hard -"@storybook/csf-plugin@npm:8.4.5": - version: 8.4.5 - resolution: "@storybook/csf-plugin@npm:8.4.5" +"@storybook/csf-plugin@npm:8.5.0": + version: 8.5.0 + resolution: "@storybook/csf-plugin@npm:8.5.0" dependencies: unplugin: "npm:^1.3.1" peerDependencies: - storybook: ^8.4.5 - checksum: 10c0/c23b423740820679a4fcef9df8b077b24a047f250d1710e87a2fd6918b71bfeb513749eb41c0c072f1ac86e5888e666486cd7f58105d44e5e5bd727653ca1401 + storybook: ^8.5.0 + checksum: 10c0/e1b8969f5887687a585d80a8b892a9a496e7340c476298dbf71d24480c7c6b52a0668c8e14039b3c119d0f6681746516df42a1ad0452f682f9d90e97b609eb21 + languageName: node + linkType: hard + +"@storybook/csf@npm:0.1.12": + version: 0.1.12 + resolution: "@storybook/csf@npm:0.1.12" + dependencies: + type-fest: "npm:^2.19.0" + checksum: 10c0/3d96a976ada67eb683279338d1eb6aa730b228107d4c4f6616ea7b94061899c1fdc11957a756e7bc0708d18cb39af0010c865d124efd84559cd82dcb2d8bc959 languageName: node linkType: hard @@ -4660,88 +4676,122 @@ __metadata: languageName: node linkType: hard -"@storybook/manager-api@npm:8.4.5": - version: 8.4.5 - resolution: "@storybook/manager-api@npm:8.4.5" +"@storybook/instrumenter@npm:8.5.0": + version: 8.5.0 + resolution: "@storybook/instrumenter@npm:8.5.0" + dependencies: + "@storybook/global": "npm:^5.0.0" + "@vitest/utils": "npm:^2.1.1" + peerDependencies: + storybook: ^8.5.0 + checksum: 10c0/a60370789b9039dbea4b604c1d003c73be5f97f1f17448f69293d0f2a752cdbdc16b5adbe62c91d844a3632ccdcdb66996e0329cb1d0269bc965041adbe7cd72 + languageName: node + linkType: hard + +"@storybook/manager-api@npm:8.5.0": + version: 8.5.0 + resolution: "@storybook/manager-api@npm:8.5.0" peerDependencies: storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 - checksum: 10c0/bf75ad329d7bcc66e810b34930ea39bf22d8fb052c6c8e26d113f0531b7e294374cd6b1c7250a9e3f0fb668a9026627a13a80f61f2e3991facf7a288020589ad + checksum: 10c0/93702011fdd8d0590cfc859b57a21d348e477bf6fedd11e30df362b3f5fc1eab0154ee5d4bbd9c452fea294cf17b14238a4a6787d6483726ccd8a5ed27009d98 languageName: node linkType: hard -"@storybook/preview-api@npm:8.4.5": - version: 8.4.5 - resolution: "@storybook/preview-api@npm:8.4.5" +"@storybook/preview-api@npm:8.5.0": + version: 8.5.0 + resolution: "@storybook/preview-api@npm:8.5.0" peerDependencies: storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 - checksum: 10c0/e00955596f28e12ae19060d4e0c04c7b4e39f31293200afe861dfd94f5da1a3389a1a223afe3cf01dc5552c1dac46b23d88b07eee6a7d2be36ecc90aa98f8af8 + checksum: 10c0/5aee35e8daf2c0dc0ee317e997cf0131e9c5c92759d74d0e9183093e1bd25bab29956a39e2b01f2b2968cca7bcdd8c51b819602ecff543786603028f39b8d03d languageName: node linkType: hard -"@storybook/react-dom-shim@npm:8.4.5": - version: 8.4.5 - resolution: "@storybook/react-dom-shim@npm:8.4.5" +"@storybook/react-dom-shim@npm:8.5.0": + version: 8.5.0 + resolution: "@storybook/react-dom-shim@npm:8.5.0" peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^8.4.5 - checksum: 10c0/358bdb85346517128acca483ffad9110e79c4d279d64b40929256158190f5d5b774b16631c84b121ab39b616ac893468d7172c19d542dd53368456bb649ebb52 + storybook: ^8.5.0 + checksum: 10c0/cf9bda1eb7be135658541a382e14b62fdaa939b6a331ac7c065dbcc80de735ec404347fd5d1aa325b550bf3d67544e069813835e60c4c53adf875371a96d2a48 languageName: node linkType: hard -"@storybook/react-vite@npm:8.4.5": - version: 8.4.5 - resolution: "@storybook/react-vite@npm:8.4.5" +"@storybook/react-vite@npm:8.5.0": + version: 8.5.0 + resolution: "@storybook/react-vite@npm:8.5.0" dependencies: - "@joshwooding/vite-plugin-react-docgen-typescript": "npm:0.3.0" + "@joshwooding/vite-plugin-react-docgen-typescript": "npm:0.4.2" "@rollup/pluginutils": "npm:^5.0.2" - "@storybook/builder-vite": "npm:8.4.5" - "@storybook/react": "npm:8.4.5" + "@storybook/builder-vite": "npm:8.5.0" + "@storybook/react": "npm:8.5.0" find-up: "npm:^5.0.0" magic-string: "npm:^0.30.0" react-docgen: "npm:^7.0.0" resolve: "npm:^1.22.8" tsconfig-paths: "npm:^4.2.0" peerDependencies: + "@storybook/test": 8.5.0 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^8.4.5 - vite: ^4.0.0 || ^5.0.0 - checksum: 10c0/667ca7c7d8309ff36e96b6820c00bddfe11b634fd591f7ed0d467613ceba84d89b518215c87070d0a27d5be4b332c0c8320a05cc1a19ad7d3071902cfbfe8e14 + storybook: ^8.5.0 + vite: ^4.0.0 || ^5.0.0 || ^6.0.0 + peerDependenciesMeta: + "@storybook/test": + optional: true + checksum: 10c0/81287767b39d97f488d792fc91463592390e65a5135b50345d2853716a6bf05235b93388dd93ea4ca425fcb80ba01dbd8209c04b927716c03801be60da9030b3 languageName: node linkType: hard -"@storybook/react@npm:8.4.5": - version: 8.4.5 - resolution: "@storybook/react@npm:8.4.5" +"@storybook/react@npm:8.5.0": + version: 8.5.0 + resolution: "@storybook/react@npm:8.5.0" dependencies: - "@storybook/components": "npm:8.4.5" + "@storybook/components": "npm:8.5.0" "@storybook/global": "npm:^5.0.0" - "@storybook/manager-api": "npm:8.4.5" - "@storybook/preview-api": "npm:8.4.5" - "@storybook/react-dom-shim": "npm:8.4.5" - "@storybook/theming": "npm:8.4.5" + "@storybook/manager-api": "npm:8.5.0" + "@storybook/preview-api": "npm:8.5.0" + "@storybook/react-dom-shim": "npm:8.5.0" + "@storybook/theming": "npm:8.5.0" peerDependencies: - "@storybook/test": 8.4.5 + "@storybook/test": 8.5.0 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^8.4.5 + storybook: ^8.5.0 typescript: ">= 4.2.x" peerDependenciesMeta: "@storybook/test": optional: true typescript: optional: true - checksum: 10c0/207e03c3dfcabb0b11d3a2440166d8eeb4f76318e32dd274c87a9503af7f2bedee255a13d358d653654f6eca2b81fb579c88f909f3e86f6f167187ca0aaadba9 + checksum: 10c0/55f4abf7a9b9d9f91c4f0fae57250db72853df79648a4fcc4cb5029cfeba592da51e651b878a4613f01ee24a58fd5286192b65a92e1f631bf664f38d61a1b229 + languageName: node + linkType: hard + +"@storybook/test@npm:8.5.0": + version: 8.5.0 + resolution: "@storybook/test@npm:8.5.0" + dependencies: + "@storybook/csf": "npm:0.1.12" + "@storybook/global": "npm:^5.0.0" + "@storybook/instrumenter": "npm:8.5.0" + "@testing-library/dom": "npm:10.4.0" + "@testing-library/jest-dom": "npm:6.5.0" + "@testing-library/user-event": "npm:14.5.2" + "@vitest/expect": "npm:2.0.5" + "@vitest/spy": "npm:2.0.5" + peerDependencies: + storybook: ^8.5.0 + checksum: 10c0/55c63426bc4dc06bb6389f29cb193241fe9c2f65f4f8b8fa0458831a76a5f2720dc8dbc64902f9dece5021b22cccd86d86a8c992cc6224781aade5b791e3f45a languageName: node linkType: hard -"@storybook/theming@npm:8.4.5": - version: 8.4.5 - resolution: "@storybook/theming@npm:8.4.5" +"@storybook/theming@npm:8.5.0": + version: 8.5.0 + resolution: "@storybook/theming@npm:8.5.0" peerDependencies: storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 - checksum: 10c0/9dbb92605f88eef3a5d4ca3b01a8815939e9a08c9eb3cef55e05c8f196c6bcd1a92ab1592ff0a489256382e172587c385a7cfdac227feb64e21cba65017fa818 + checksum: 10c0/c1c69d9f38b828f24c55f7c83ae6439c9254dc613301f0a67530766b0f43d09c65b2b0fbdf00912e6935f91f450ecbb534126a2a09d228b21175bc2126671dc0 languageName: node linkType: hard @@ -4804,6 +4854,22 @@ __metadata: languageName: node linkType: hard +"@testing-library/dom@npm:10.4.0": + version: 10.4.0 + resolution: "@testing-library/dom@npm:10.4.0" + dependencies: + "@babel/code-frame": "npm:^7.10.4" + "@babel/runtime": "npm:^7.12.5" + "@types/aria-query": "npm:^5.0.1" + aria-query: "npm:5.3.0" + chalk: "npm:^4.1.0" + dom-accessibility-api: "npm:^0.5.9" + lz-string: "npm:^1.5.0" + pretty-format: "npm:^27.0.2" + checksum: 10c0/0352487720ecd433400671e773df0b84b8268fb3fe8e527cdfd7c11b1365b398b4e0eddba6e7e0c85e8d615f48257753283fccec41f6b986fd6c85f15eb5f84f + languageName: node + linkType: hard + "@testing-library/dom@npm:^10.1.0": version: 10.1.0 resolution: "@testing-library/dom@npm:10.1.0" @@ -4820,6 +4886,30 @@ __metadata: languageName: node linkType: hard +"@testing-library/jest-dom@npm:6.5.0": + version: 6.5.0 + resolution: "@testing-library/jest-dom@npm:6.5.0" + dependencies: + "@adobe/css-tools": "npm:^4.4.0" + aria-query: "npm:^5.0.0" + chalk: "npm:^3.0.0" + css.escape: "npm:^1.5.1" + dom-accessibility-api: "npm:^0.6.3" + lodash: "npm:^4.17.21" + redent: "npm:^3.0.0" + checksum: 10c0/fd5936a547f04608d8de15a7de3ae26516f21023f8f45169b10c8c8847015fd20ec259b7309f08aa1031bcbc37c6e5e6f532d1bb85ef8f91bad654193ec66a4c + languageName: node + linkType: hard + +"@testing-library/user-event@npm:14.5.2": + version: 14.5.2 + resolution: "@testing-library/user-event@npm:14.5.2" + peerDependencies: + "@testing-library/dom": ">=7.21.4" + checksum: 10c0/68a0c2aa28a3c8e6eb05cafee29705438d7d8a9427423ce5064d44f19c29e89b5636de46dd2f28620fb10abba75c67130185bbc3aa23ac1163a227a5f36641e1 + languageName: node + linkType: hard + "@tootallnate/once@npm:2": version: 2.0.0 resolution: "@tootallnate/once@npm:2.0.0" @@ -5148,16 +5238,6 @@ __metadata: languageName: node linkType: hard -"@types/glob@npm:^7.1.3": - version: 7.2.0 - resolution: "@types/glob@npm:7.2.0" - dependencies: - "@types/minimatch": "npm:*" - "@types/node": "npm:*" - checksum: 10c0/a8eb5d5cb5c48fc58c7ca3ff1e1ddf771ee07ca5043da6e4871e6757b4472e2e73b4cfef2644c38983174a4bc728c73f8da02845c28a1212f98cabd293ecae98 - languageName: node - linkType: hard - "@types/hast@npm:^2.0.0": version: 2.3.10 resolution: "@types/hast@npm:2.3.10" @@ -5324,13 +5404,6 @@ __metadata: languageName: node linkType: hard -"@types/minimatch@npm:*": - version: 5.1.2 - resolution: "@types/minimatch@npm:5.1.2" - checksum: 10c0/83cf1c11748891b714e129de0585af4c55dd4c2cafb1f1d5233d79246e5e1e19d1b5ad9e8db449667b3ffa2b6c80125c429dbee1054e9efb45758dbc4e118562 - languageName: node - linkType: hard - "@types/minimatch@npm:^3.0.3": version: 3.0.5 resolution: "@types/minimatch@npm:3.0.5" @@ -6210,6 +6283,68 @@ __metadata: languageName: node linkType: hard +"@vitest/expect@npm:2.0.5": + version: 2.0.5 + resolution: "@vitest/expect@npm:2.0.5" + dependencies: + "@vitest/spy": "npm:2.0.5" + "@vitest/utils": "npm:2.0.5" + chai: "npm:^5.1.1" + tinyrainbow: "npm:^1.2.0" + checksum: 10c0/08cb1b0f106d16a5b60db733e3d436fa5eefc68571488eb570dfe4f599f214ab52e4342273b03dbe12331cc6c0cdc325ac6c94f651ad254cd62f3aa0e3d185aa + languageName: node + linkType: hard + +"@vitest/pretty-format@npm:2.0.5": + version: 2.0.5 + resolution: "@vitest/pretty-format@npm:2.0.5" + dependencies: + tinyrainbow: "npm:^1.2.0" + checksum: 10c0/236c0798c5170a0b5ad5d4bd06118533738e820b4dd30079d8fbcb15baee949d41c60f42a9f769906c4a5ce366d7ef11279546070646c0efc03128c220c31f37 + languageName: node + linkType: hard + +"@vitest/pretty-format@npm:2.1.8": + version: 2.1.8 + resolution: "@vitest/pretty-format@npm:2.1.8" + dependencies: + tinyrainbow: "npm:^1.2.0" + checksum: 10c0/1dc5c9b1c7c7e78e46a2a16033b6b20be05958bbebc5a5b78f29e32718c80252034804fccd23f34db6b3583239db47e68fc5a8e41942c54b8047cc3b4133a052 + languageName: node + linkType: hard + +"@vitest/spy@npm:2.0.5": + version: 2.0.5 + resolution: "@vitest/spy@npm:2.0.5" + dependencies: + tinyspy: "npm:^3.0.0" + checksum: 10c0/70634c21921eb271b54d2986c21d7ab6896a31c0f4f1d266940c9bafb8ac36237846d6736638cbf18b958bd98e5261b158a6944352742accfde50b7818ff655e + languageName: node + linkType: hard + +"@vitest/utils@npm:2.0.5": + version: 2.0.5 + resolution: "@vitest/utils@npm:2.0.5" + dependencies: + "@vitest/pretty-format": "npm:2.0.5" + estree-walker: "npm:^3.0.3" + loupe: "npm:^3.1.1" + tinyrainbow: "npm:^1.2.0" + checksum: 10c0/0d1de748298f07a50281e1ba058b05dcd58da3280c14e6f016265e950bd79adab6b97822de8f0ea82d3070f585654801a9b1bcf26db4372e51cf7746bf86d73b + languageName: node + linkType: hard + +"@vitest/utils@npm:^2.1.1": + version: 2.1.8 + resolution: "@vitest/utils@npm:2.1.8" + dependencies: + "@vitest/pretty-format": "npm:2.1.8" + loupe: "npm:^3.1.2" + tinyrainbow: "npm:^1.2.0" + checksum: 10c0/d4a29ecd8f6c24c790e4c009f313a044d89e664e331bc9c3cfb57fe1380fb1d2999706dbbfc291f067d6c489602e76d00435309fbc906197c0d01f831ca17d64 + languageName: node + linkType: hard + "@vue/compiler-core@npm:3.5.13": version: 3.5.13 resolution: "@vue/compiler-core@npm:3.5.13" @@ -7183,6 +7318,13 @@ __metadata: languageName: node linkType: hard +"assertion-error@npm:^2.0.1": + version: 2.0.1 + resolution: "assertion-error@npm:2.0.1" + checksum: 10c0/bbbcb117ac6480138f8c93cf7f535614282dea9dc828f540cdece85e3c665e8f78958b96afac52f29ff883c72638e6a87d469ecc9fe5bc902df03ed24a55dba8 + languageName: node + linkType: hard + "ast-types@npm:0.14.2, ast-types@npm:^0.14.1": version: 0.14.2 resolution: "ast-types@npm:0.14.2" @@ -7273,6 +7415,13 @@ __metadata: languageName: node linkType: hard +"axe-core@npm:^4.4.2": + version: 4.10.2 + resolution: "axe-core@npm:4.10.2" + checksum: 10c0/0e20169077de96946a547fce0df39d9aeebe0077f9d3eeff4896518b96fde857f80b98f0d4279274a7178791744dd5a54bb4f322de45b4f561ffa2586ff9a09d + languageName: node + linkType: hard + "axios@npm:^1.7.4": version: 1.7.7 resolution: "axios@npm:1.7.7" @@ -7957,6 +8106,19 @@ __metadata: languageName: node linkType: hard +"chai@npm:^5.1.1": + version: 5.1.2 + resolution: "chai@npm:5.1.2" + dependencies: + assertion-error: "npm:^2.0.1" + check-error: "npm:^2.1.1" + deep-eql: "npm:^5.0.1" + loupe: "npm:^3.1.0" + pathval: "npm:^2.0.0" + checksum: 10c0/6c04ff8495b6e535df9c1b062b6b094828454e9a3c9493393e55b2f4dbff7aa2a29a4645133cad160fb00a16196c4dc03dc9bb37e1f4ba9df3b5f50d7533a736 + languageName: node + linkType: hard + "chalk@npm:4.1.0": version: 4.1.0 resolution: "chalk@npm:4.1.0" @@ -8001,6 +8163,16 @@ __metadata: languageName: node linkType: hard +"chalk@npm:^3.0.0": + version: 3.0.0 + resolution: "chalk@npm:3.0.0" + dependencies: + ansi-styles: "npm:^4.1.0" + supports-color: "npm:^7.1.0" + checksum: 10c0/ee650b0a065b3d7a6fda258e75d3a86fc8e4effa55871da730a9e42ccb035bf5fd203525e5a1ef45ec2582ecc4f65b47eb11357c526b84dd29a14fb162c414d2 + languageName: node + linkType: hard + "chalk@npm:^5.0.1": version: 5.3.0 resolution: "chalk@npm:5.3.0" @@ -8050,6 +8222,13 @@ __metadata: languageName: node linkType: hard +"check-error@npm:^2.1.1": + version: 2.1.1 + resolution: "check-error@npm:2.1.1" + checksum: 10c0/979f13eccab306cf1785fa10941a590b4e7ea9916ea2a4f8c87f0316fc3eab07eabefb6e587424ef0f88cbcd3805791f172ea739863ca3d7ce2afc54641c7f0e + languageName: node + linkType: hard + "check-more-types@npm:^2.24.0": version: 2.24.0 resolution: "check-more-types@npm:2.24.0" @@ -8929,6 +9108,13 @@ __metadata: languageName: node linkType: hard +"css.escape@npm:^1.5.1": + version: 1.5.1 + resolution: "css.escape@npm:1.5.1" + checksum: 10c0/5e09035e5bf6c2c422b40c6df2eb1529657a17df37fda5d0433d722609527ab98090baf25b13970ca754079a0f3161dd3dfc0e743563ded8cfa0749d861c1525 + languageName: node + linkType: hard + "cssesc@npm:^3.0.0": version: 3.0.0 resolution: "cssesc@npm:3.0.0" @@ -9498,6 +9684,13 @@ __metadata: languageName: node linkType: hard +"deep-eql@npm:^5.0.1": + version: 5.0.2 + resolution: "deep-eql@npm:5.0.2" + checksum: 10c0/7102cf3b7bb719c6b9c0db2e19bf0aa9318d141581befe8c7ce8ccd39af9eaa4346e5e05adef7f9bd7015da0f13a3a25dcfe306ef79dc8668aedbecb658dd247 + languageName: node + linkType: hard + "deep-equal@npm:^2.0.5": version: 2.2.0 resolution: "deep-equal@npm:2.2.0" @@ -9851,13 +10044,20 @@ __metadata: languageName: node linkType: hard -"dom-accessibility-api@npm:^0.5.9": +"dom-accessibility-api@npm:^0.5.14, dom-accessibility-api@npm:^0.5.9": version: 0.5.16 resolution: "dom-accessibility-api@npm:0.5.16" checksum: 10c0/b2c2eda4fae568977cdac27a9f0c001edf4f95a6a6191dfa611e3721db2478d1badc01db5bb4fa8a848aeee13e442a6c2a4386d65ec65a1436f24715a2f8d053 languageName: node linkType: hard +"dom-accessibility-api@npm:^0.6.3": + version: 0.6.3 + resolution: "dom-accessibility-api@npm:0.6.3" + checksum: 10c0/10bee5aa514b2a9a37c87cd81268db607a2e933a050074abc2f6fa3da9080ebed206a320cbc123567f2c3087d22292853bdfdceaffdd4334ffe2af9510b29360 + languageName: node + linkType: hard + "dom-helpers@npm:^5.0.1": version: 5.2.1 resolution: "dom-helpers@npm:5.2.1" @@ -11362,6 +11562,15 @@ __metadata: languageName: node linkType: hard +"estree-walker@npm:^3.0.3": + version: 3.0.3 + resolution: "estree-walker@npm:3.0.3" + dependencies: + "@types/estree": "npm:^1.0.0" + checksum: 10c0/c12e3c2b2642d2bcae7d5aa495c60fa2f299160946535763969a1c83fc74518ffa9c2cd3a8b69ac56aea547df6a8aac25f729a342992ef0bbac5f1c73e78995d + languageName: node + linkType: hard + "esutils@npm:^2.0.2": version: 2.0.3 resolution: "esutils@npm:2.0.3" @@ -12532,17 +12741,6 @@ __metadata: languageName: node linkType: hard -"glob-promise@npm:^4.2.0": - version: 4.2.2 - resolution: "glob-promise@npm:4.2.2" - dependencies: - "@types/glob": "npm:^7.1.3" - peerDependencies: - glob: ^7.1.6 - checksum: 10c0/3eb01bed2901539365df6a4d27800afb8788840647d01f9bf3500b3de756597f2ff4b8c823971ace34db228c83159beca459dc42a70968d4e9c8200ed2cc96bd - languageName: node - linkType: hard - "glob@npm:7.2.0": version: 7.2.0 resolution: "glob@npm:7.2.0" @@ -15579,6 +15777,13 @@ __metadata: languageName: node linkType: hard +"loupe@npm:^3.1.0, loupe@npm:^3.1.1, loupe@npm:^3.1.2": + version: 3.1.2 + resolution: "loupe@npm:3.1.2" + checksum: 10c0/b13c02e3ddd6a9d5f8bf84133b3242de556512d824dddeea71cce2dbd6579c8f4d672381c4e742d45cf4423d0701765b4a6e5fbc24701def16bc2b40f8daa96a + languageName: node + linkType: hard + "lowercase-keys@npm:^2.0.0": version: 2.0.0 resolution: "lowercase-keys@npm:2.0.0" @@ -18783,6 +18988,13 @@ __metadata: languageName: node linkType: hard +"pathval@npm:^2.0.0": + version: 2.0.0 + resolution: "pathval@npm:2.0.0" + checksum: 10c0/602e4ee347fba8a599115af2ccd8179836a63c925c23e04bd056d0674a64b39e3a081b643cc7bc0b84390517df2d800a46fcc5598d42c155fe4977095c2f77c5 + languageName: node + linkType: hard + "pause-stream@npm:0.0.11": version: 0.0.11 resolution: "pause-stream@npm:0.0.11" @@ -21834,11 +22046,11 @@ __metadata: languageName: node linkType: hard -"storybook@npm:8.4.5": - version: 8.4.5 - resolution: "storybook@npm:8.4.5" +"storybook@npm:8.5.0": + version: 8.5.0 + resolution: "storybook@npm:8.5.0" dependencies: - "@storybook/core": "npm:8.4.5" + "@storybook/core": "npm:8.5.0" peerDependencies: prettier: ^2 || ^3 peerDependenciesMeta: @@ -21848,7 +22060,7 @@ __metadata: getstorybook: ./bin/index.cjs sb: ./bin/index.cjs storybook: ./bin/index.cjs - checksum: 10c0/8dd216ea47ab8e76bb9cb24776999373b6d6cde061ff89db4e469e899e6b35b7f5882123e769eb6bf48457a995d0870a08f57a257afc2099161fbb6f6f098c4e + checksum: 10c0/d3941da2df06d1764888e056689b61a251e3b4106990e6de3a528c87bcfb5bdd4b53e7ba4355a763f1fbc6b2072342912d2e2d34545e2e7aec99e52306953eec languageName: node linkType: hard @@ -22468,6 +22680,20 @@ __metadata: languageName: node linkType: hard +"tinyrainbow@npm:^1.2.0": + version: 1.2.0 + resolution: "tinyrainbow@npm:1.2.0" + checksum: 10c0/7f78a4b997e5ba0f5ecb75e7ed786f30bab9063716e7dff24dd84013fb338802e43d176cb21ed12480561f5649a82184cf31efb296601a29d38145b1cdb4c192 + languageName: node + linkType: hard + +"tinyspy@npm:^3.0.0": + version: 3.0.2 + resolution: "tinyspy@npm:3.0.2" + checksum: 10c0/55ffad24e346622b59292e097c2ee30a63919d5acb7ceca87fc0d1c223090089890587b426e20054733f97a58f20af2c349fb7cc193697203868ab7ba00bcea0 + languageName: node + linkType: hard + "tldts-core@npm:^6.1.60": version: 6.1.60 resolution: "tldts-core@npm:6.1.60" @@ -23023,13 +23249,13 @@ __metadata: languageName: node linkType: hard -"typescript@npm:5.7.2": - version: 5.7.2 - resolution: "typescript@npm:5.7.2" +"typescript@npm:5.7.3": + version: 5.7.3 + resolution: "typescript@npm:5.7.3" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 10c0/a873118b5201b2ef332127ef5c63fb9d9c155e6fdbe211cbd9d8e65877283797cca76546bad742eea36ed7efbe3424a30376818f79c7318512064e8625d61622 + checksum: 10c0/b7580d716cf1824736cc6e628ab4cd8b51877408ba2be0869d2866da35ef8366dd6ae9eb9d0851470a39be17cbd61df1126f9e211d8799d764ea7431d5435afa languageName: node linkType: hard @@ -23053,13 +23279,13 @@ __metadata: languageName: node linkType: hard -"typescript@patch:typescript@npm%3A5.7.2#optional!builtin": - version: 5.7.2 - resolution: "typescript@patch:typescript@npm%3A5.7.2#optional!builtin::version=5.7.2&hash=5786d5" +"typescript@patch:typescript@npm%3A5.7.3#optional!builtin": + version: 5.7.3 + resolution: "typescript@patch:typescript@npm%3A5.7.3#optional!builtin::version=5.7.3&hash=5786d5" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 10c0/f3b8082c9d1d1629a215245c9087df56cb784f9fb6f27b5d55577a20e68afe2a889c040aacff6d27e35be165ecf9dca66e694c42eb9a50b3b2c451b36b5675cb + checksum: 10c0/6fd7e0ed3bf23a81246878c613423730c40e8bdbfec4c6e4d7bf1b847cbb39076e56ad5f50aa9d7ebd89877999abaee216002d3f2818885e41c907caaa192cc4 languageName: node linkType: hard @@ -23118,14 +23344,14 @@ __metadata: "@cypress/code-coverage": "npm:^3.10.0" "@eslint/js": "npm:^9.11.1" "@semantic-release/github": "npm:^11.0.0" - "@storybook/addon-a11y": "npm:8.4.5" - "@storybook/addon-essentials": "npm:8.4.5" - "@storybook/blocks": "npm:8.4.5" - "@storybook/manager-api": "npm:8.4.5" - "@storybook/preview-api": "npm:8.4.5" - "@storybook/react": "npm:8.4.5" - "@storybook/react-vite": "npm:8.4.5" - "@storybook/theming": "npm:8.4.5" + "@storybook/addon-a11y": "npm:8.5.0" + "@storybook/addon-essentials": "npm:8.5.0" + "@storybook/blocks": "npm:8.5.0" + "@storybook/manager-api": "npm:8.5.0" + "@storybook/preview-api": "npm:8.5.0" + "@storybook/react": "npm:8.5.0" + "@storybook/react-vite": "npm:8.5.0" + "@storybook/theming": "npm:8.5.0" "@tanstack/react-table": "npm:^8.20.6" "@testing-library/cypress": "npm:^10.0.0" "@types/eslint__js": "npm:^8.42.3" @@ -23170,9 +23396,9 @@ __metadata: react-dom: "npm:^19.0.0" remark-gfm: "npm:^4.0.0" rimraf: "npm:^6.0.0" - storybook: "npm:8.4.5" + storybook: "npm:8.5.0" tocbot: "npm:4.32.2" - typescript: "npm:5.7.2" + typescript: "npm:5.7.3" typescript-eslint: "npm:^8.8.0" vite: "npm:^6.0.0" vite-plugin-istanbul: "npm:^6.0.0" @@ -23928,6 +24154,22 @@ __metadata: languageName: node linkType: hard +"vitest-axe@npm:^0.1.0": + version: 0.1.0 + resolution: "vitest-axe@npm:0.1.0" + dependencies: + aria-query: "npm:^5.0.0" + axe-core: "npm:^4.4.2" + chalk: "npm:^5.0.1" + dom-accessibility-api: "npm:^0.5.14" + lodash-es: "npm:^4.17.21" + redent: "npm:^3.0.0" + peerDependencies: + vitest: ">=0.16.0" + checksum: 10c0/ad523ad3115f66b1605c3c0ea6a9932867c4273f8ad73b717afc5fe496159b5e11d50a8babf05ce4990c612558ee24579cf14258304ce6b32a77d15be9e98aa6 + languageName: node + linkType: hard + "vue-template-compiler@npm:^2.7.8": version: 2.7.16 resolution: "vue-template-compiler@npm:2.7.16" From f2e53d97780e12e383f939a7a87cfcb59933d3bf Mon Sep 17 00:00:00 2001 From: Lukas Harbarth Date: Thu, 30 Jan 2025 08:54:20 +0100 Subject: [PATCH 07/17] Update yarn.lock --- yarn.lock | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/yarn.lock b/yarn.lock index 8f515a1de24..86af4bb7421 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4806,6 +4806,18 @@ __metadata: languageName: node linkType: hard +"@tanstack/react-table@npm:^8.20.6": + version: 8.20.6 + resolution: "@tanstack/react-table@npm:8.20.6" + dependencies: + "@tanstack/table-core": "npm:8.20.5" + peerDependencies: + react: ">=16.8" + react-dom: ">=16.8" + checksum: 10c0/3213dc146f647fbd571f4e347007b969320819e588439b2ee95dd3a65efcbe30d097c24426dd82617041ed1e186182a5b303382bcebed5d61a1c6045a55c58d3 + languageName: node + linkType: hard + "@tanstack/react-virtual@npm:~3.11.0": version: 3.11.3 resolution: "@tanstack/react-virtual@npm:3.11.3" @@ -4818,6 +4830,13 @@ __metadata: languageName: node linkType: hard +"@tanstack/table-core@npm:8.20.5": + version: 8.20.5 + resolution: "@tanstack/table-core@npm:8.20.5" + checksum: 10c0/3c27b5debd61b6bd9bfbb40bfc7c5d5af90873ae1a566b20e3bf2d2f4f2e9a78061c081aacc5259a00e256f8df506ec250eb5472f5c01ff04baf9918b554982b + languageName: node + linkType: hard + "@tanstack/virtual-core@npm:3.11.3": version: 3.11.3 resolution: "@tanstack/virtual-core@npm:3.11.3" @@ -23478,6 +23497,7 @@ __metadata: "@storybook/react": "npm:8.5.0" "@storybook/react-vite": "npm:8.5.0" "@storybook/theming": "npm:8.5.0" + "@tanstack/react-table": "npm:^8.20.6" "@testing-library/cypress": "npm:^10.0.0" "@types/eslint__js": "npm:^8.42.3" "@types/jscodeshift": "npm:^0.12.0" From a060d4b5a29834acc7cc42d8b9c68276546b5182 Mon Sep 17 00:00:00 2001 From: Lukas Harbarth Date: Fri, 21 Feb 2025 10:21:22 +0100 Subject: [PATCH 08/17] Update yarn.lock --- yarn.lock | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/yarn.lock b/yarn.lock index 6399f369173..30d15c54144 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4927,6 +4927,18 @@ __metadata: languageName: node linkType: hard +"@tanstack/react-table@npm:^8.20.6": + version: 8.21.2 + resolution: "@tanstack/react-table@npm:8.21.2" + dependencies: + "@tanstack/table-core": "npm:8.21.2" + peerDependencies: + react: ">=16.8" + react-dom: ">=16.8" + checksum: 10c0/192392c7d945bf50a21a0904fdce07e4abb74bc6187bcd32ae843031d48e851a10899e0218443d742ee01c30ca1ff890a8677a5878b8904561ebc23479adfdf1 + languageName: node + linkType: hard + "@tanstack/react-virtual@npm:~3.13.0": version: 3.13.0 resolution: "@tanstack/react-virtual@npm:3.13.0" @@ -4939,6 +4951,13 @@ __metadata: languageName: node linkType: hard +"@tanstack/table-core@npm:8.21.2": + version: 8.21.2 + resolution: "@tanstack/table-core@npm:8.21.2" + checksum: 10c0/836326c6aef7e0b9b4566f9b2ade5ea73b90ce94f2b0cc35a3b3e1f44a9b3512d5507e4818c509824443b3d0488f2026df54e7a9f8f0cdccae6040347e6ff079 + languageName: node + linkType: hard + "@tanstack/virtual-core@npm:3.13.0": version: 3.13.0 resolution: "@tanstack/virtual-core@npm:3.13.0" @@ -23636,6 +23655,7 @@ __metadata: "@storybook/react": "npm:8.5.6" "@storybook/react-vite": "npm:8.5.6" "@storybook/theming": "npm:8.5.6" + "@tanstack/react-table": "npm:^8.20.6" "@testing-library/cypress": "npm:^10.0.0" "@types/eslint__js": "npm:^8.42.3" "@types/jscodeshift": "npm:^0.12.0" From 28a09f9c06a07b53608d8a31492f464a0025fc18 Mon Sep 17 00:00:00 2001 From: Lukas Harbarth Date: Fri, 21 Feb 2025 15:50:37 +0100 Subject: [PATCH 09/17] add table dep to main pkg --- package.json | 1 - packages/main/package.json | 1 + yarn.lock | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index a079a3314fc..e22a9c7c5df 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,6 @@ "@storybook/react": "8.5.6", "@storybook/react-vite": "8.5.6", "@storybook/theming": "8.5.6", - "@tanstack/react-table": "^8.20.6", "@ui5/webcomponents": "2.7.3", "@ui5/webcomponents-compat": "2.7.3", "@ui5/webcomponents-fiori": "2.7.3", diff --git a/packages/main/package.json b/packages/main/package.json index 4c7cbe1c1af..a48167f99f3 100644 --- a/packages/main/package.json +++ b/packages/main/package.json @@ -49,6 +49,7 @@ "watch:css": "yarn build:css --watch" }, "dependencies": { + "@tanstack/react-table": "^8.20.6", "@tanstack/react-virtual": "~3.13.0", "@ui5/webcomponents-react-base": "workspace:~", "clsx": "2.1.1", diff --git a/yarn.lock b/yarn.lock index 30d15c54144..355d3d16257 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6358,6 +6358,7 @@ __metadata: version: 0.0.0-use.local resolution: "@ui5/webcomponents-react@workspace:packages/main" dependencies: + "@tanstack/react-table": "npm:^8.20.6" "@tanstack/react-virtual": "npm:~3.13.0" "@ui5/webcomponents-react-base": "workspace:~" clsx: "npm:2.1.1" @@ -23655,7 +23656,6 @@ __metadata: "@storybook/react": "npm:8.5.6" "@storybook/react-vite": "npm:8.5.6" "@storybook/theming": "npm:8.5.6" - "@tanstack/react-table": "npm:^8.20.6" "@testing-library/cypress": "npm:^10.0.0" "@types/eslint__js": "npm:^8.42.3" "@types/jscodeshift": "npm:^0.12.0" From 0675e5811aa2b2cb6048cc16fb8a145162e86c90 Mon Sep 17 00:00:00 2001 From: Lukas Harbarth Date: Fri, 21 Feb 2025 16:35:35 +0100 Subject: [PATCH 10/17] Implement "Default" columns mode (spread to available space) --- .../AnalyticalTableV2.module.css | 4 + .../AnalyticalTableV2.stories.tsx | 100 +++++++------ .../features/exampleFeature.ts | 131 ++++++++++++++++++ .../components/AnalyticalTableV2/index.tsx | 68 +++++++-- .../AnalyticalTableV2/useColumnMode.ts | 125 +++++++++++++++++ .../utils/useTableContainerResizeObserver.ts | 13 +- 6 files changed, 384 insertions(+), 57 deletions(-) create mode 100644 packages/main/src/components/AnalyticalTableV2/features/exampleFeature.ts create mode 100644 packages/main/src/components/AnalyticalTableV2/useColumnMode.ts diff --git a/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.module.css b/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.module.css index 7aca0fb8961..09e02ee01d1 100644 --- a/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.module.css +++ b/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.module.css @@ -6,8 +6,12 @@ } .cell { + box-sizing: border-box; display: flex; background-color: var(--sapList_Background); + overflow: hidden; + /*todo: dev*/ + border-inline: solid 1px black; } /* ============================================================= */ diff --git a/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.stories.tsx b/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.stories.tsx index 6b9f07eea83..ab3cf5665f8 100644 --- a/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.stories.tsx +++ b/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.stories.tsx @@ -4,50 +4,70 @@ import type { ColumnDef } from '@tanstack/react-table'; import { Button } from '@ui5/webcomponents-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[] = [ - { header: 'Name', accessorKey: 'name' }, - { header: 'Age', accessorKey: 'age' }, - { header: 'Friend Name', accessorKey: 'friend.name' }, - { header: 'Friend Age', accessorKey: 'friend.age' }, { - header: 'Column Pinned', - id: 'c_pinned', - cell: ({ row }) => { - return 'Pinned'; - } + 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' } + ] }, { - header: 'Pin Row', - id: 'r_pinned', - size: 300, - cell: ({ row }) => { - return ( - <> - - - - - ); - } + id: 'G', + header: 'Pinnable', + columns: [ + { + header: 'Column Pinned', + id: 'c_pinned', + cell: ({ row }) => { + return 'Pinned'; + } + }, + { + header: 'Pin Row', + id: 'r_pinned', + size: 300, + cell: ({ row }) => { + return ( + <> + + + + + ); + } + } + ] } ]; @@ -55,7 +75,7 @@ const meta = { title: 'Data Display / AnalyticalTableV2', component: AnalyticalTableV2, args: { - data: dataLarge.map((item, index) => ({ ...item, friend: { ...item.friend, age: index } })), + data: dataLarge.map((item, index) => ({ ...item, friend: { ...item.friend, age: index } })).slice(0), columns, visibleRows: 5 }, diff --git a/packages/main/src/components/AnalyticalTableV2/features/exampleFeature.ts b/packages/main/src/components/AnalyticalTableV2/features/exampleFeature.ts new file mode 100644 index 00000000000..93ba3b82857 --- /dev/null +++ b/packages/main/src/components/AnalyticalTableV2/features/exampleFeature.ts @@ -0,0 +1,131 @@ +/* eslint-disable */ +// @ts-nocheck +// todo: dev + +import type { OnChangeFn, RowData, Table, TableFeature, Updater } from '@tanstack/react-table'; +import { makeStateUpdater } from '@tanstack/react-table'; +import { functionalUpdate } from '@tanstack/react-table'; +import { useEffect } from 'react'; + +// Use declaration merging to add our new feature APIs and state types to TanStack Table's existing types. +declare module '@tanstack/react-table' { + //merge our new feature's state with the existing table state + interface TableState extends DensityTableState {} + //merge our new feature's options with the existing table options + interface TableOptionsResolved extends DensityOptions {} + //merge our new feature's instance APIs with the existing table instance APIs + interface Table extends DensityInstance {} + // if you need to add cell instance APIs... + // interface Cell extends DensityCell + // if you need to add row instance APIs... + // interface Row extends DensityRow + // if you need to add column instance APIs... + // interface Column extends DensityColumn + // if you need to add header instance APIs... + // interface Header extends DensityHeader + + // Note: declaration merging on `ColumnDef` is not possible because it is a type, not an interface. + // But you can still use declaration merging on `ColumnDef.meta` +} + +export type DensityState = 'sm' | 'md' | 'lg'; +export interface DensityTableState { + density: DensityState; +} +// define types for our new feature's table options +export interface DensityOptions { + enableDensity?: boolean; + onDensityChange?: OnChangeFn; +} + +// Define types for our new feature's table APIs +export interface DensityInstance { + setDensity: (updater: Updater) => void; + toggleDensity: (value?: DensityState) => void; +} + +export const DensityFeature: TableFeature = { + // define the new feature's initial state + getInitialState: (state): DensityTableState => { + return { + density: 'md', + ...state + }; + }, + + // define the new feature's default options + getDefaultOptions: (table: Table): DensityOptions => { + return { + enableDensity: true, + onDensityChange: makeStateUpdater('density', table) + } as DensityOptions; + }, + // if you need to add a default column definition... + // getDefaultColumnDef: (): Partial> => { + // return { meta: {} } //use meta instead of directly adding to the columnDef to avoid typescript stuff that's hard to workaround + // }, + + // define the new feature's table instance methods + createTable: (table: Table): void => { + table.setDensity = (updater) => { + const safeUpdater: Updater = (old) => { + const newState = functionalUpdate(updater, old); + return newState; + }; + return table.options.onDensityChange?.(safeUpdater); + }; + table.toggleDensity = (value) => { + table.setDensity((old) => { + if (value) return value; + return old === 'lg' ? 'md' : old === 'md' ? 'sm' : 'lg'; //cycle through the 3 options + }); + }; + } + + // if you need to add row instance APIs... + // createRow: (row, table): void => {}, + // if you need to add cell instance APIs... + // createCell: (cell, column, row, table): void => {}, + // if you need to add column instance APIs... + // createColumn: (column, table): void => {}, + // if you need to add header instance APIs... + // createHeader: (header, table): void => {}, +}; + +export const ColumnModesFeature: TableFeature = { + // define the new feature's initial state + getInitialState: (state): any => { + return { ...state, tableWidth: 0 }; + }, + + // define the new feature's default options + getDefaultOptions: (table: Table): any => { + return { + columnMode: 'Default' + }; + }, + // if you need to add a default column definition... + // getDefaultColumnDef: (): Partial> => { + // return { meta: {} } //use meta instead of directly adding to the columnDef to avoid typescript stuff that's hard to workaround + // }, + + // define the new feature's table instance methods + createTable: (table: Table): void => { + const state = table.getState(); + // console.log(table); + }, + + // if you need to add row instance APIs... + // createRow: (row, table): void => {}, + // if you need to add cell instance APIs... + // createCell: (cell, column, row, table): void => {}, + // if you need to add column instance APIs... + createColumn: (column, table): void => { + console.log(column); + // console.log(column); + // column.columnDef.size = 2000; + // console.log(column, table.getState()); + } + // if you need to add header instance APIs... + // createHeader: (header, table): void => {}, +}; diff --git a/packages/main/src/components/AnalyticalTableV2/index.tsx b/packages/main/src/components/AnalyticalTableV2/index.tsx index fbfe093ebd5..71a6a9d3c01 100644 --- a/packages/main/src/components/AnalyticalTableV2/index.tsx +++ b/packages/main/src/components/AnalyticalTableV2/index.tsx @@ -2,10 +2,12 @@ import { getCoreRowModel, useReactTable } from '@tanstack/react-table'; import { CssSizeVariables, useStylesheet } from '@ui5/webcomponents-react-base'; import { clsx } from 'clsx'; import type { CSSProperties, ReactElement } from 'react'; -import { useRef } from 'react'; +import { useRef, useState } from 'react'; import { classNames, styleData } from './AnalyticalTableV2.module.css.js'; import { Cell } from './core/Cell.js'; import { Row } from './core/Row.js'; +import { DensityFeature, ColumnModesFeature } from './features/exampleFeature.js'; +import { useColumnWidths } from './useColumnMode.js'; import { useRowVirtualizer } from './useRowVirtualizer.js'; import { useTableContainerResizeObserver } from './utils/useTableContainerResizeObserver.js'; @@ -14,6 +16,12 @@ interface AnalyticalTableV2Props { columns?: any[]; rowHeight?: number; visibleRows?: number; + // todo: fka scaleWidthMode + columnMode?: string; + + //todo: check if this should be controllable, if so add respective checks otherwise the table-option won't do anything + enableRowPinning?: boolean; + enableColumnPinning?: boolean; } interface CSSPropertiesWithVars extends CSSProperties { @@ -27,31 +35,60 @@ const ROW_HEIGHT_VAR = 'var(--_ui5WcrAnalyticalTableControlledRowHeight)'; //todo forwardRef or React19? --> prob forwardRef function AnalyticalTableV2(props: AnalyticalTableV2Props): ReactElement { - const { columns, data, rowHeight, visibleRows = 15 } = props; + const { columns, data, rowHeight, visibleRows = 15, enableRowPinning, enableColumnPinning, columnMode } = props; useStylesheet(styleData, AnalyticalTableV2.displayName); const tableContainerRef = useRef(null); - const { tableWidth: _tableWidth, horizontalScrollbarHeight } = useTableContainerResizeObserver(tableContainerRef); - + const { tableWidth, horizontalScrollbarHeight, verticalScrollbarWidth } = + useTableContainerResizeObserver(tableContainerRef); + const [columnSizing, setColumnSizing] = useState({}); + //const setColumnSizing = useColumnWidths(tableWidth, reactTable.getAllLeafColumns().map((item) => item.columnDef) const reactTable = useReactTable({ + _features: [DensityFeature, ColumnModesFeature], data, columns, getCoreRowModel: getCoreRowModel(), //todo: remove debugTable: true, //todo: optional - enableColumnPinning: true, - initialState: { - columnPinning: { - left: ['c_pinned'], - right: ['friend_age'] - }, - rowPinning: { - bottom: ['0', '1'], - top: ['499', '498'] - } + // enableColumnPinning: false, + // enableRowPinning: false, + state: { + columnSizing, + //todo: add types & clarify how to inject types (declare module '@tanstack/react-table' is probably not the best approach - probably we have to cast a lot...) + //ColumnModesFeature + tableWidth, + + //DensityFeature + density: 'md' }, - enableRowPinning: true + // initialState: { + // columnPinning: { + // left: ['c_pinned'], + // right: ['friend_age'] + // }, + // rowPinning: { + // bottom: ['0', '1'], + // top: ['499', '498'] + // } + // } + // column sizing + defaultColumn: { + size: 0, + minSize: 60 + }, + //ColumnModesFeature + columnMode, + onColumnSizingChange: setColumnSizing }); + console.log(enableRowPinning, enableColumnPinning); + + useColumnWidths( + tableWidth, + //todo: refactor to use getAllLeafColumns directly, or use the `columns` array? + reactTable.getAllLeafColumns().map((item) => item.columnDef), + setColumnSizing, + verticalScrollbarWidth + ); const headerGroups = reactTable.getHeaderGroups(); const topRows = reactTable.getTopRows(); @@ -68,6 +105,7 @@ function AnalyticalTableV2(props: AnalyticalTableV2Props): ReactElement[], verticalScrollbarWidth) { + // columns with external size + const fixed = []; + const dynamic = []; + let fixedTotal = 0; + + // separate fixed and dynamic columns + for (const col of columns) { + const minSize = col.minSize ?? 0; + const maxSize = col.maxSize; + // external `size` defined + if (col.size !== 0) { + let width = col.size; + if (width < minSize) { + width = minSize; + } + if (width > maxSize) { + width = maxSize; + } + fixedTotal += width; + fixed.push({ col, width }); + } else { + dynamic.push({ col, width: 0 }); + } + } + + // Determine remaining width for dynamic columns + const remaining = tableWidth - fixedTotal - verticalScrollbarWidth; + + // Calc total min-width required by dynamic columns + let totalFlexibleMin = 0; + for (const { col } of dynamic) { + totalFlexibleMin += col.minSize ?? 0; + } + + if (remaining < totalFlexibleMin) { + // Not enough space - assign each dynamic column its `minSize` + for (const dc of dynamic) { + dc.width = dc.col.minSize ?? 0; + } + } else if (dynamic.length) { + // grant same space for each dynamic column + const initialShare = remaining / dynamic.length; + for (const dc of dynamic) { + const minSize = dc.col.minSize ?? 0; + const maxSize = dc.col.maxSize; + let width = initialShare; + if (width < minSize) { + width = minSize; + } + if (width > maxSize) { + width = maxSize; + } + dc.width = width; + } + + // Calc assigned width and remaining space + let assigned = 0; + for (const { width } of dynamic) { + assigned += width; + } + let remainingSpace = remaining - assigned; + + // Share remaining space among columns that can grow + while (remainingSpace > 0) { + let expandableCount = 0; + for (const { col, width } of dynamic) { + if (width < col.maxSize) { + expandableCount++; + } + } + if (expandableCount === 0) { + break; + } + const extra = remainingSpace / expandableCount; + let used = 0; + for (const dc of dynamic) { + const { maxSize } = dc.col; + if (dc.width < maxSize) { + const potential = dc.width + extra; + if (potential > maxSize) { + used += maxSize - dc.width; + dc.width = maxSize; + } else { + dc.width = potential; + used += extra; + } + } + } + remainingSpace -= used; + if (used === 0) { + break; + } + } + } + + const result = {}; + for (const { col, width } of [...fixed, ...dynamic]) { + const key = col.id ?? col.accessorKey; + result[key] = width; + } + return result; +} + +//todo types +//todo implement other modes (pass different calc function - check what can be reused) +//todo debounce? +export function useColumnWidths( + tableWidth: number, + columnDefs: ColumnDef[], + setColumnSizing, + verticalScrollbarWidth +) { + useEffect(() => { + if (!tableWidth) { + return; + } + + setColumnSizing(calculateDefaultColumnWidths(tableWidth, columnDefs, verticalScrollbarWidth)); + //todo: check deps + }, [tableWidth, columnDefs.length, verticalScrollbarWidth]); +} diff --git a/packages/main/src/components/AnalyticalTableV2/utils/useTableContainerResizeObserver.ts b/packages/main/src/components/AnalyticalTableV2/utils/useTableContainerResizeObserver.ts index ed76e94bbca..e17f67b4898 100644 --- a/packages/main/src/components/AnalyticalTableV2/utils/useTableContainerResizeObserver.ts +++ b/packages/main/src/components/AnalyticalTableV2/utils/useTableContainerResizeObserver.ts @@ -4,6 +4,7 @@ import { useState, useEffect } from 'react'; export const useTableContainerResizeObserver = (tableContainerRef: RefObject) => { const [tableWidth, setTableWidth] = useState(0); const [horizontalScrollbarHeight, setHorizontalScrollbarHeight] = useState(0); + const [verticalScrollbarWidth, setVerticalScrollbarWidth] = useState(0); useEffect(() => { const tableContainer = tableContainerRef.current; @@ -14,13 +15,21 @@ export const useTableContainerResizeObserver = (tableContainerRef: RefObject contentBoxHeight) { setHorizontalScrollbarHeight(borderBoxHeight - contentBoxHeight); } else { setHorizontalScrollbarHeight(0); } - const borderBoxWidth = borderBoxSize[0].inlineSize; + if (borderBoxWidth > contentBoxWidth) { + setVerticalScrollbarWidth(borderBoxWidth - contentBoxWidth); + } else { + setVerticalScrollbarWidth(0); + } + setTableWidth(borderBoxWidth); } } @@ -37,5 +46,5 @@ export const useTableContainerResizeObserver = (tableContainerRef: RefObject Date: Wed, 26 Feb 2025 17:23:05 +0100 Subject: [PATCH 11/17] add `aria-rowindex` --- .../AnalyticalTableV2.stories.tsx | 2 +- .../AnalyticalTableV2/core/Cell.tsx | 3 +- .../components/AnalyticalTableV2/core/Row.tsx | 5 ++- .../features/exampleFeature.ts | 2 +- .../components/AnalyticalTableV2/index.tsx | 42 +++++++++++-------- 5 files changed, 32 insertions(+), 22 deletions(-) diff --git a/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.stories.tsx b/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.stories.tsx index ab3cf5665f8..d4b194bd983 100644 --- a/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.stories.tsx +++ b/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.stories.tsx @@ -75,7 +75,7 @@ const meta = { title: 'Data Display / AnalyticalTableV2', component: AnalyticalTableV2, args: { - data: dataLarge.map((item, index) => ({ ...item, friend: { ...item.friend, age: index } })).slice(0), + data: dataLarge.map((item, index) => ({ ...item, friend: { ...item.friend, age: index } })).slice(0, 1), columns, visibleRows: 5 }, diff --git a/packages/main/src/components/AnalyticalTableV2/core/Cell.tsx b/packages/main/src/components/AnalyticalTableV2/core/Cell.tsx index 785285a6c22..d07c1cb1219 100644 --- a/packages/main/src/components/AnalyticalTableV2/core/Cell.tsx +++ b/packages/main/src/components/AnalyticalTableV2/core/Cell.tsx @@ -35,6 +35,7 @@ interface CellProps { export function Cell(props: CellProps) { const { style = {}, role, cell, renderable } = props; + const cellContext = cell.getContext(); return (
(props: CellProps) { }} className={classNames.cell} > - {flexRender(renderable, cell.getContext())} + {flexRender(renderable, cellContext)}
); } diff --git a/packages/main/src/components/AnalyticalTableV2/core/Row.tsx b/packages/main/src/components/AnalyticalTableV2/core/Row.tsx index a959703ad70..a1afcd6e696 100644 --- a/packages/main/src/components/AnalyticalTableV2/core/Row.tsx +++ b/packages/main/src/components/AnalyticalTableV2/core/Row.tsx @@ -5,12 +5,13 @@ import { classNames } from '../AnalyticalTableV2.module.css.js'; interface RowProps extends CommonProps { //todo children type children: any; + startIndex: number; } export function Row(props: RowProps) { - const { children, className, ...rest } = props; + const { children, className, startIndex, ...rest } = props; return ( -
+
{children}
); diff --git a/packages/main/src/components/AnalyticalTableV2/features/exampleFeature.ts b/packages/main/src/components/AnalyticalTableV2/features/exampleFeature.ts index 93ba3b82857..2d2f065908a 100644 --- a/packages/main/src/components/AnalyticalTableV2/features/exampleFeature.ts +++ b/packages/main/src/components/AnalyticalTableV2/features/exampleFeature.ts @@ -121,7 +121,7 @@ export const ColumnModesFeature: TableFeature = { // createCell: (cell, column, row, table): void => {}, // if you need to add column instance APIs... createColumn: (column, table): void => { - console.log(column); + // console.log(column); // console.log(column); // column.columnDef.size = 2000; // console.log(column, table.getState()); diff --git a/packages/main/src/components/AnalyticalTableV2/index.tsx b/packages/main/src/components/AnalyticalTableV2/index.tsx index 71a6a9d3c01..eaa2ecd3e26 100644 --- a/packages/main/src/components/AnalyticalTableV2/index.tsx +++ b/packages/main/src/components/AnalyticalTableV2/index.tsx @@ -61,16 +61,16 @@ function AnalyticalTableV2(props: AnalyticalTableV2Props): ReactElement(rowHeight, tableContainerRef, { count: centerRows.length }); // const { rows } = reactTable.getRowModel(); - return ( <>
- {headerGroups.map((headerGroup) => { + {headerGroups.map((headerGroup, index) => { return ( {headerGroup.headers.map((header) => { return ( @@ -142,9 +142,13 @@ function AnalyticalTableV2(props: AnalyticalTableV2Props): ReactElement {topRows.length > 0 && (
- {topRows.map((row) => { + {topRows.map((row, index) => { return ( - + {row.getVisibleCells().map((cell) => { return ( {bottomRows.length > 0 && (
- {bottomRows.map((row) => { + {bottomRows.map((row, index) => { return ( - + {row.getVisibleCells().map((cell) => { return ( Date: Thu, 27 Feb 2025 10:49:04 +0100 Subject: [PATCH 12/17] add `aria-rowcount` --- .../AnalyticalTableV2/AnalyticalTableV2.stories.tsx | 2 +- packages/main/src/components/AnalyticalTableV2/index.tsx | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.stories.tsx b/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.stories.tsx index d4b194bd983..ab3cf5665f8 100644 --- a/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.stories.tsx +++ b/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.stories.tsx @@ -75,7 +75,7 @@ const meta = { title: 'Data Display / AnalyticalTableV2', component: AnalyticalTableV2, args: { - data: dataLarge.map((item, index) => ({ ...item, friend: { ...item.friend, age: index } })).slice(0, 1), + data: dataLarge.map((item, index) => ({ ...item, friend: { ...item.friend, age: index } })).slice(0), columns, visibleRows: 5 }, diff --git a/packages/main/src/components/AnalyticalTableV2/index.tsx b/packages/main/src/components/AnalyticalTableV2/index.tsx index eaa2ecd3e26..107edf65870 100644 --- a/packages/main/src/components/AnalyticalTableV2/index.tsx +++ b/packages/main/src/components/AnalyticalTableV2/index.tsx @@ -82,9 +82,9 @@ function AnalyticalTableV2(props: AnalyticalTableV2Props): ReactElement item.columnDef), setColumnSizing, verticalScrollbarWidth @@ -94,6 +94,7 @@ function AnalyticalTableV2(props: AnalyticalTableV2Props): ReactElement(rowHeight, tableContainerRef, { count: centerRows.length }); @@ -115,7 +116,7 @@ function AnalyticalTableV2(props: AnalyticalTableV2Props): ReactElement -
+
{headerGroups.map((headerGroup, index) => { return ( From 0d93c09ec92f4db205bf4326d73c5d11e52ad174 Mon Sep 17 00:00:00 2001 From: Lukas Harbarth Date: Thu, 27 Feb 2025 15:28:18 +0100 Subject: [PATCH 13/17] basic keyboard navigation --- .../AnalyticalTableV2.module.css | 4 +- .../AnalyticalTableV2.stories.tsx | 7 ++- .../AnalyticalTableV2/core/Cell.tsx | 8 ++- .../components/AnalyticalTableV2/index.tsx | 35 +++++++++--- .../useKeyboardNavigation.ts | 57 +++++++++++++++++++ 5 files changed, 95 insertions(+), 16 deletions(-) create mode 100644 packages/main/src/components/AnalyticalTableV2/useKeyboardNavigation.ts diff --git a/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.module.css b/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.module.css index 09e02ee01d1..6b2e39c5c90 100644 --- a/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.module.css +++ b/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.module.css @@ -28,10 +28,10 @@ } /* ============================================================= */ -/* Table Body Container */ +/* Table */ /* ============================================================= */ -.tableBodyContainer { +.table { /*todo check if we really require grid here*/ display: grid; } diff --git a/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.stories.tsx b/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.stories.tsx index ab3cf5665f8..9284bfedd3d 100644 --- a/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.stories.tsx +++ b/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.stories.tsx @@ -1,7 +1,7 @@ import dataLarge from '@sb/mockData/Friends500.json'; import type { Meta, StoryObj } from '@storybook/react'; import type { ColumnDef } from '@tanstack/react-table'; -import { Button } from '@ui5/webcomponents-react'; +import { Button, Input } from '@ui5/webcomponents-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 @@ -66,7 +66,8 @@ const columns: ColumnDef[] = [ ); } - } + }, + { header: 'Input', cell: () => , id: 'input' } ] } ]; @@ -75,7 +76,7 @@ const meta = { title: 'Data Display / AnalyticalTableV2', component: AnalyticalTableV2, args: { - data: dataLarge.map((item, index) => ({ ...item, friend: { ...item.friend, age: index } })).slice(0), + data: dataLarge.map((item, index) => ({ ...item, friend: { ...item.friend, age: index } })).slice(0, 15), columns, visibleRows: 5 }, diff --git a/packages/main/src/components/AnalyticalTableV2/core/Cell.tsx b/packages/main/src/components/AnalyticalTableV2/core/Cell.tsx index d07c1cb1219..9faf7127c1f 100644 --- a/packages/main/src/components/AnalyticalTableV2/core/Cell.tsx +++ b/packages/main/src/components/AnalyticalTableV2/core/Cell.tsx @@ -31,12 +31,13 @@ interface CellProps { cell: CoreCell | CoreHeader; //todo type renderable: any; + startIndex: number; + isFirstFocusableCell?: boolean; } export function Cell(props: CellProps) { - const { style = {}, role, cell, renderable } = props; + const { style = {}, role, cell, renderable, startIndex, isFirstFocusableCell } = props; const cellContext = cell.getContext(); - return (
(props: CellProps) { ...style }} className={classNames.cell} + aria-colindex={startIndex + 1} + data-cell={true} + tabIndex={isFirstFocusableCell ? 0 : undefined} > {flexRender(renderable, cellContext)}
diff --git a/packages/main/src/components/AnalyticalTableV2/index.tsx b/packages/main/src/components/AnalyticalTableV2/index.tsx index 107edf65870..611e24c3134 100644 --- a/packages/main/src/components/AnalyticalTableV2/index.tsx +++ b/packages/main/src/components/AnalyticalTableV2/index.tsx @@ -1,5 +1,5 @@ import { getCoreRowModel, useReactTable } from '@tanstack/react-table'; -import { CssSizeVariables, useStylesheet } from '@ui5/webcomponents-react-base'; +import { CssSizeVariables, useIsRTL, useStylesheet } from '@ui5/webcomponents-react-base'; import { clsx } from 'clsx'; import type { CSSProperties, ReactElement } from 'react'; import { useRef, useState } from 'react'; @@ -8,6 +8,7 @@ import { Cell } from './core/Cell.js'; import { Row } from './core/Row.js'; import { DensityFeature, ColumnModesFeature } from './features/exampleFeature.js'; import { useColumnWidths } from './useColumnMode.js'; +import { handleKeyboardNavigation } from './useKeyboardNavigation.js'; import { useRowVirtualizer } from './useRowVirtualizer.js'; import { useTableContainerResizeObserver } from './utils/useTableContainerResizeObserver.js'; @@ -38,6 +39,7 @@ function AnalyticalTableV2(props: AnalyticalTableV2Props): ReactElement(null); + const isRTL = useIsRTL(tableContainerRef); const { tableWidth, horizontalScrollbarHeight, verticalScrollbarWidth } = useTableContainerResizeObserver(tableContainerRef); const [columnSizing, setColumnSizing] = useState({}); @@ -49,6 +51,7 @@ function AnalyticalTableV2(props: AnalyticalTableV2Props): ReactElement(rowHeight, tableContainerRef, { count: centerRows.length }); @@ -116,17 +121,23 @@ function AnalyticalTableV2(props: AnalyticalTableV2Props): ReactElement -
+
handleKeyboardNavigation(e, totalRowCount, visibleLeafColumns.length, isRTL)} + >
- {headerGroups.map((headerGroup, index) => { + {headerGroups.map((headerGroup, groupIndex) => { return ( - {headerGroup.headers.map((header) => { + {headerGroup.headers.map((header, index) => { return ( ); })} @@ -150,13 +164,14 @@ function AnalyticalTableV2(props: AnalyticalTableV2Props): ReactElement - {row.getVisibleCells().map((cell) => { + {row.getVisibleCells().map((cell, index) => { return ( ); @@ -186,7 +201,7 @@ function AnalyticalTableV2(props: AnalyticalTableV2Props): ReactElement - {row.getVisibleCells().map((cell) => { + {row.getVisibleCells().map((cell, index) => { return ( ); })} @@ -210,7 +226,7 @@ function AnalyticalTableV2(props: AnalyticalTableV2Props): ReactElement - {row.getVisibleCells().map((cell) => { + {row.getVisibleCells().map((cell, index) => { return ( ); })} diff --git a/packages/main/src/components/AnalyticalTableV2/useKeyboardNavigation.ts b/packages/main/src/components/AnalyticalTableV2/useKeyboardNavigation.ts new file mode 100644 index 00000000000..0f3ba846ff1 --- /dev/null +++ b/packages/main/src/components/AnalyticalTableV2/useKeyboardNavigation.ts @@ -0,0 +1,57 @@ +import type { KeyboardEvent } from 'react'; +// todo: rename file +// todo: nav only works when tabbing, not when clicking cell or interactive content inside of it +// todo: verify what the intended behavior of PageUp & PageDown is + +// using currying for now, as this way code splitting is easier +export const handleKeyboardNavigation = ( + e: KeyboardEvent, + totalRowCount: number, + visibleColumnsCount: number, + isRTL: boolean +) => { + const target = e.target as HTMLElement; + if (target.role !== 'columnheader' && target.role !== 'gridcell') { + return; + } + + const dir = isRTL ? -1 : 1; + // IMPORTANT: Since we're relying on `aria-rowindex` and `aria-colindex` here, the index starts at 1. + const rowIndex = parseInt(target.parentElement.ariaRowIndex); + const colIndex = parseInt(target.ariaColIndex); + let newRowIndex = rowIndex; + let newColIndex = colIndex; + switch (e.key) { + case 'ArrowDown': + newRowIndex = Math.min(rowIndex + 1, totalRowCount); + break; + case 'ArrowUp': + newRowIndex = Math.max(rowIndex - 1, 1); + break; + case 'ArrowRight': + newColIndex = Math.min(colIndex + dir, visibleColumnsCount); + break; + case 'ArrowLeft': + newColIndex = Math.max(colIndex - dir, 1); + break; + case 'Home': + newColIndex = 1; + break; + case 'End': + newColIndex = visibleColumnsCount; + break; + default: + return; + } + e.preventDefault(); + + const focusCell: HTMLDivElement = e.currentTarget.querySelector( + `[aria-rowindex="${newRowIndex}"] > [data-cell][aria-colindex="${newColIndex}"]` + ); + + if (focusCell) { + target.removeAttribute('tabindex'); + focusCell.tabIndex = 0; + focusCell.focus(); + } +}; From 1c61ec1ad0477989761a7c97139136bf5e4fc0a0 Mon Sep 17 00:00:00 2001 From: Lukas Harbarth Date: Thu, 17 Apr 2025 11:20:33 +0200 Subject: [PATCH 14/17] update react-table dep --- packages/main/package.json | 2 +- packages/main/src/components/AnalyticalTableV2/index.tsx | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/main/package.json b/packages/main/package.json index 3acc3c9bb28..0357cf391d9 100644 --- a/packages/main/package.json +++ b/packages/main/package.json @@ -49,7 +49,7 @@ "watch:css": "yarn build:css --watch" }, "dependencies": { - "@tanstack/react-table": "^8.20.6", + "@tanstack/react-table": "^8.21.3", "@tanstack/react-virtual": "~3.13.0", "@ui5/webcomponents-react-base": "workspace:~", "clsx": "2.1.1", diff --git a/packages/main/src/components/AnalyticalTableV2/index.tsx b/packages/main/src/components/AnalyticalTableV2/index.tsx index 611e24c3134..d5d593acca7 100644 --- a/packages/main/src/components/AnalyticalTableV2/index.tsx +++ b/packages/main/src/components/AnalyticalTableV2/index.tsx @@ -34,7 +34,7 @@ interface CSSPropertiesWithVars extends CSSProperties { const ROW_HEIGHT_VAR = 'var(--_ui5WcrAnalyticalTableControlledRowHeight)'; -//todo forwardRef or React19? --> prob forwardRef +//todo forwardRef or React19 prop? --> prob forwardRef function AnalyticalTableV2(props: AnalyticalTableV2Props): ReactElement { const { columns, data, rowHeight, visibleRows = 15, enableRowPinning, enableColumnPinning, columnMode } = props; useStylesheet(styleData, AnalyticalTableV2.displayName); @@ -48,6 +48,7 @@ function AnalyticalTableV2(props: AnalyticalTableV2Props): ReactElement Date: Thu, 17 Apr 2025 12:09:37 +0200 Subject: [PATCH 15/17] use AT calculation for "Dynamic" col mode --- .../AnalyticalTableV2.stories.tsx | 3 +- .../components/AnalyticalTableV2/index.tsx | 3 +- .../AnalyticalTableV2/useColumnMode.ts | 121 ++++++++++++------ yarn.lock | 20 +-- 4 files changed, 96 insertions(+), 51 deletions(-) diff --git a/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.stories.tsx b/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.stories.tsx index 9284bfedd3d..e23dfc427d4 100644 --- a/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.stories.tsx +++ b/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.stories.tsx @@ -12,7 +12,7 @@ const columns: ColumnDef[] = [ header: 'Person', id: 'A', columns: [ - { header: 'Name', accessorKey: 'name', id: 'B' }, + { header: 'Name', accessorKey: 'name', id: 'B', minSize: 1000 }, { header: 'Age', accessorKey: 'age', id: 'C' } ] }, @@ -29,6 +29,7 @@ const columns: ColumnDef[] = [ header: 'Pinnable', columns: [ { + maxSize: 100, header: 'Column Pinned', id: 'c_pinned', cell: ({ row }) => { diff --git a/packages/main/src/components/AnalyticalTableV2/index.tsx b/packages/main/src/components/AnalyticalTableV2/index.tsx index d5d593acca7..a55a296e7af 100644 --- a/packages/main/src/components/AnalyticalTableV2/index.tsx +++ b/packages/main/src/components/AnalyticalTableV2/index.tsx @@ -77,7 +77,8 @@ function AnalyticalTableV2(props: AnalyticalTableV2Props): ReactElement[], verticalScrollbarWidth) { - // columns with external size +//todo: share function between AT & ATV2 +function calculateDefaultColumnWidths(tableWidth: number, columns: ColumnDef[], verticalScrollbarWidth: number) { + // Columns w/ external size property const fixed = []; + // Columns w/o external size property const dynamic = []; let fixedTotal = 0; - // separate fixed and dynamic columns + // Separate fixed and dynamic columns for (const col of columns) { const minSize = col.minSize ?? 0; - const maxSize = col.maxSize; - // external `size` defined - if (col.size !== 0) { - let width = col.size; - if (width < minSize) { - width = minSize; + const maxSize = col.maxSize ?? Infinity; + + // External `size` defined + if (col.size !== undefined) { + let size = col.size; + if (size < minSize) { + size = minSize; } - if (width > maxSize) { - width = maxSize; + if (size > maxSize) { + size = maxSize; } - fixedTotal += width; - fixed.push({ col, width }); + fixedTotal += size; + fixed.push({ col, size }); } else { - dynamic.push({ col, width: 0 }); + dynamic.push({ col, size: 0 }); } } - // Determine remaining width for dynamic columns + // Determine remaining size for dynamic columns const remaining = tableWidth - fixedTotal - verticalScrollbarWidth; // Calc total min-width required by dynamic columns @@ -39,36 +42,43 @@ function calculateDefaultColumnWidths(tableWidth: number, columns: ColumnDef maxSize) { - width = maxSize; + if (size > maxSize) { + size = maxSize; } - dc.width = width; + dc.size = size; } - // Calc assigned width and remaining space + // Calc assigned size and remaining space let assigned = 0; - for (const { width } of dynamic) { - assigned += width; + for (const { size } of dynamic) { + assigned += size ?? 0; } + + /** + * - negative: table overflows + * - positive: table has white-space between last column and borderInlineEnd + */ let remainingSpace = remaining - assigned; - // Share remaining space among columns that can grow + // Grow or shrink columns that are still dynamic + + // Grow columns while (remainingSpace > 0) { let expandableCount = 0; - for (const { col, width } of dynamic) { - if (width < col.maxSize) { + for (const { col, size } of dynamic) { + if (size < (col.maxSize ?? Infinity)) { expandableCount++; } } @@ -78,14 +88,14 @@ function calculateDefaultColumnWidths(tableWidth: number, columns: ColumnDef maxSize) { - used += maxSize - dc.width; - dc.width = maxSize; + used += maxSize - dc.size; + dc.size = maxSize; } else { - dc.width = potential; + dc.size = potential; used += extra; } } @@ -95,12 +105,46 @@ function calculateDefaultColumnWidths(tableWidth: number, columns: ColumnDef min) { + shrinkableCount++; + } + } + if (shrinkableCount === 0) { + break; + } + const reduction = Math.abs(remainingSpace) / shrinkableCount; + let used = 0; + for (const dc of dynamic) { + const min = dc.col.minSize ?? 0; + if (dc.size > min) { + const potential = dc.size - reduction; + if (potential < min) { + used += dc.size - min; + dc.size = min; + } else { + dc.size = potential; + used += reduction; + } + } + } + remainingSpace += used; + if (used === 0) { + break; + } + } } const result = {}; - for (const { col, width } of [...fixed, ...dynamic]) { + for (const { col, size } of [...fixed, ...dynamic]) { + // todo: accessorKey sufficient here? const key = col.id ?? col.accessorKey; - result[key] = width; + result[key] = size; } return result; } @@ -118,7 +162,6 @@ export function useColumnWidths( if (!tableWidth) { return; } - setColumnSizing(calculateDefaultColumnWidths(tableWidth, columnDefs, verticalScrollbarWidth)); //todo: check deps }, [tableWidth, columnDefs.length, verticalScrollbarWidth]); diff --git a/yarn.lock b/yarn.lock index 7091db6066a..5f6f0bdac37 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4981,15 +4981,15 @@ __metadata: languageName: node linkType: hard -"@tanstack/react-table@npm:^8.20.6": - version: 8.21.2 - resolution: "@tanstack/react-table@npm:8.21.2" +"@tanstack/react-table@npm:^8.21.3": + version: 8.21.3 + resolution: "@tanstack/react-table@npm:8.21.3" dependencies: - "@tanstack/table-core": "npm:8.21.2" + "@tanstack/table-core": "npm:8.21.3" peerDependencies: react: ">=16.8" react-dom: ">=16.8" - checksum: 10c0/192392c7d945bf50a21a0904fdce07e4abb74bc6187bcd32ae843031d48e851a10899e0218443d742ee01c30ca1ff890a8677a5878b8904561ebc23479adfdf1 + checksum: 10c0/85d1d0fcb690ecc011f68a5a61c96f82142e31a0270dcf9cbc699a6f36715b1653fe6ff1518302a6d08b7093351fc4cabefd055a7db3cd8ac01e068956b0f944 languageName: node linkType: hard @@ -5005,10 +5005,10 @@ __metadata: languageName: node linkType: hard -"@tanstack/table-core@npm:8.21.2": - version: 8.21.2 - resolution: "@tanstack/table-core@npm:8.21.2" - checksum: 10c0/836326c6aef7e0b9b4566f9b2ade5ea73b90ce94f2b0cc35a3b3e1f44a9b3512d5507e4818c509824443b3d0488f2026df54e7a9f8f0cdccae6040347e6ff079 +"@tanstack/table-core@npm:8.21.3": + version: 8.21.3 + resolution: "@tanstack/table-core@npm:8.21.3" + checksum: 10c0/40e3560e6d55e07cc047024aa7f83bd47a9323d21920d4adabba8071fd2d21230c48460b26cedf392588f8265b9edc133abb1b0d6d0adf4dae0970032900a8c9 languageName: node linkType: hard @@ -6435,7 +6435,7 @@ __metadata: version: 0.0.0-use.local resolution: "@ui5/webcomponents-react@workspace:packages/main" dependencies: - "@tanstack/react-table": "npm:^8.20.6" + "@tanstack/react-table": "npm:^8.21.3" "@tanstack/react-virtual": "npm:~3.13.0" "@ui5/webcomponents-react-base": "workspace:~" clsx: "npm:2.1.1" From c7f08815f8fe14bc640709a57f76cc0099d06a32 Mon Sep 17 00:00:00 2001 From: Lukas Harbarth Date: Thu, 17 Apr 2025 15:45:45 +0200 Subject: [PATCH 16/17] add basic sort --- .../AnalyticalTableV2.module.css | 6 ++ .../AnalyticalTableV2.stories.tsx | 9 +- .../AnalyticalTableV2/core/Cell.tsx | 53 +++++++++--- .../AnalyticalTableV2/core/ColumnPopover.tsx | 86 +++++++++++++++++++ .../components/AnalyticalTableV2/index.tsx | 33 +++++-- .../AnalyticalTableV2/types/index.ts | 0 .../AnalyticalTableV2/useSorting.tsx | 28 ++++++ 7 files changed, 193 insertions(+), 22 deletions(-) create mode 100644 packages/main/src/components/AnalyticalTableV2/core/ColumnPopover.tsx create mode 100644 packages/main/src/components/AnalyticalTableV2/types/index.ts create mode 100644 packages/main/src/components/AnalyticalTableV2/useSorting.tsx diff --git a/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.module.css b/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.module.css index 6b2e39c5c90..a0821b65819 100644 --- a/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.module.css +++ b/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.module.css @@ -93,6 +93,12 @@ background-color: var(--sapList_HeaderBackground); } +.headerInteractive { + cursor: pointer; + /* todo:remove*/ + background: lightgrey; +} + /* ============================================================= */ /* Body */ /* ============================================================= */ diff --git a/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.stories.tsx b/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.stories.tsx index e23dfc427d4..bc941676fe1 100644 --- a/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.stories.tsx +++ b/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.stories.tsx @@ -2,6 +2,7 @@ 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 { 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 @@ -12,7 +13,7 @@ const columns: ColumnDef[] = [ header: 'Person', id: 'A', columns: [ - { header: 'Name', accessorKey: 'name', id: 'B', minSize: 1000 }, + { header: 'Name', accessorKey: 'name', id: 'B' }, { header: 'Age', accessorKey: 'age', id: 'C' } ] }, @@ -77,7 +78,7 @@ const meta = { title: 'Data Display / AnalyticalTableV2', component: AnalyticalTableV2, args: { - data: dataLarge.map((item, index) => ({ ...item, friend: { ...item.friend, age: index } })).slice(0, 15), + data: dataLarge.map((item, index) => ({ ...item, friend: { ...item.friend, age: index } })).slice(0), columns, visibleRows: 5 }, @@ -88,10 +89,12 @@ type Story = StoryObj; export const Default: Story = { render(args) { + const [sortable, toggleSortable] = useReducer((prev) => !prev, false); return ( <>
- + + ); } diff --git a/packages/main/src/components/AnalyticalTableV2/core/Cell.tsx b/packages/main/src/components/AnalyticalTableV2/core/Cell.tsx index 9faf7127c1f..acfb35c8c6e 100644 --- a/packages/main/src/components/AnalyticalTableV2/core/Cell.tsx +++ b/packages/main/src/components/AnalyticalTableV2/core/Cell.tsx @@ -1,8 +1,12 @@ 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): CSSProperties => { const isPinned = column.getIsPinned(); const isLastLeftPinnedColumn = isPinned === 'left' && column.getIsLastColumn('left'); @@ -33,25 +37,46 @@ interface CellProps { renderable: any; startIndex: number; isFirstFocusableCell?: boolean; + isSortable?: boolean; } +//todo: create own component for header cells or handle this via props? export function Cell(props: CellProps) { - const { style = {}, role, cell, renderable, startIndex, isFirstFocusableCell } = props; + const { style = {}, role, cell, renderable, startIndex, isFirstFocusableCell, isSortable, ...rest } = props; const cellContext = cell.getContext(); + const isInteractive = isSortable; + const openerId = `${useId()}-opener`; + + const [popoverOpen, setPopoverOpen] = useState(false); + + const openPopover = (e) => { + setPopoverOpen(true); + }; + return ( -
- {flexRender(renderable, cellContext)} -
+ <> +
+ {flexRender(renderable, cellContext)} +
+ {/*`id` as opener is simpler than Ref, because we can't add a ref directly as prop (React18)*/} + {popoverOpen && ( + + )} + ); } diff --git a/packages/main/src/components/AnalyticalTableV2/core/ColumnPopover.tsx b/packages/main/src/components/AnalyticalTableV2/core/ColumnPopover.tsx new file mode 100644 index 00000000000..5da3c3e2ed8 --- /dev/null +++ b/packages/main/src/components/AnalyticalTableV2/core/ColumnPopover.tsx @@ -0,0 +1,86 @@ +import type { Column } from '@tanstack/react-table'; +import iconDecline from '@ui5/webcomponents-icons/dist/decline.js'; +import iconSortAscending from '@ui5/webcomponents-icons/dist/sort-ascending.js'; +import iconSortDescending from '@ui5/webcomponents-icons/dist/sort-descending.js'; +import { useI18nBundle } from '@ui5/webcomponents-react-base'; +import type { Dispatch, SetStateAction } from 'react'; +import { forwardRef } from 'react'; +import { CLEAR_SORTING, FILTER, GROUP, SORT_ASCENDING, SORT_DESCENDING, UNGROUP } from '../../../i18n/i18n-defaults.js'; +import type { ListPropTypes } from '../../../webComponents/List/index.js'; +import { List } from '../../../webComponents/List/index.js'; +import { ListItemStandard } from '../../../webComponents/ListItemStandard/index.js'; +import type { PopoverDomRef } from '../../../webComponents/Popover/index.js'; +import { Popover } from '../../../webComponents/Popover/index.js'; + +interface ColumnPopoverProps { + isSortable?: boolean; + openerId: string; + //todo: type + column: Column; + setOpen: Dispatch>; +} + +//todo: check if forward ref is still required +export const ColumnPopover = forwardRef((props, ref) => { + const { isSortable, openerId, setOpen, column } = props; + const i18nBundle = useI18nBundle('@ui5/webcomponents-react'); + const clearSortingText = i18nBundle.getText(CLEAR_SORTING); + const sortAscendingText = i18nBundle.getText(SORT_ASCENDING); + const sortDescendingText = i18nBundle.getText(SORT_DESCENDING); + const groupText = i18nBundle.getText(GROUP); + const ungroupText = i18nBundle.getText(UNGROUP); + const filterText = i18nBundle.getText(FILTER); + + const handleClose = () => { + setOpen(false); + }; + + const handleSelectionChange: ListPropTypes['onSelectionChange'] = (e) => { + const { type } = e.detail.targetItem.dataset; + + switch (type) { + case 'asc': + //todo: multi sort? + column.toggleSorting(false); + break; + case 'desc': + column.toggleSorting(true); + break; + case 'clear': + column.clearSorting(); + break; + } + handleClose(); + }; + + const isSorted = column.getIsSorted(); + + return ( + + + {isSorted === 'asc' && ( + + {clearSortingText} + + )} + {isSortable && isSorted !== 'asc' && ( + + {sortAscendingText} + + )} + {isSortable && isSorted !== 'desc' && ( + + {sortDescendingText} + + )} + {isSorted === 'desc' && ( + + {clearSortingText} + + )} + + + ); +}); + +ColumnPopover.displayName = 'ColumnPopover'; diff --git a/packages/main/src/components/AnalyticalTableV2/index.tsx b/packages/main/src/components/AnalyticalTableV2/index.tsx index a55a296e7af..90e2a1625b8 100644 --- a/packages/main/src/components/AnalyticalTableV2/index.tsx +++ b/packages/main/src/components/AnalyticalTableV2/index.tsx @@ -6,10 +6,11 @@ import { useRef, useState } from 'react'; import { classNames, styleData } from './AnalyticalTableV2.module.css.js'; import { Cell } from './core/Cell.js'; import { Row } from './core/Row.js'; -import { DensityFeature, ColumnModesFeature } from './features/exampleFeature.js'; +import { DensityFeature } from './features/exampleFeature.js'; import { useColumnWidths } from './useColumnMode.js'; import { handleKeyboardNavigation } from './useKeyboardNavigation.js'; import { useRowVirtualizer } from './useRowVirtualizer.js'; +import { useSorting } from './useSorting.js'; import { useTableContainerResizeObserver } from './utils/useTableContainerResizeObserver.js'; interface AnalyticalTableV2Props { @@ -19,6 +20,7 @@ interface AnalyticalTableV2Props { visibleRows?: number; // todo: fka scaleWidthMode columnMode?: string; + sortable?: boolean; //todo: check if this should be controllable, if so add respective checks otherwise the table-option won't do anything enableRowPinning?: boolean; @@ -36,20 +38,35 @@ const ROW_HEIGHT_VAR = 'var(--_ui5WcrAnalyticalTableControlledRowHeight)'; //todo forwardRef or React19 prop? --> prob forwardRef function AnalyticalTableV2(props: AnalyticalTableV2Props): ReactElement { - const { columns, data, rowHeight, visibleRows = 15, enableRowPinning, enableColumnPinning, columnMode } = props; + const { + columns, + data, + rowHeight, + visibleRows = 15, + enableRowPinning, + enableColumnPinning, + columnMode, + sortable + } = props; useStylesheet(styleData, AnalyticalTableV2.displayName); const tableContainerRef = useRef(null); const isRTL = useIsRTL(tableContainerRef); const { tableWidth, horizontalScrollbarHeight, verticalScrollbarWidth } = useTableContainerResizeObserver(tableContainerRef); const [columnSizing, setColumnSizing] = useState({}); + + const [sortingOptions, sortingState] = useSorting(sortable); + + console.log(sortingOptions, sortingState); + //const setColumnSizing = useColumnWidths(tableWidth, reactTable.getAllLeafColumns().map((item) => item.columnDef) const reactTable = useReactTable({ - _features: [DensityFeature, ColumnModesFeature], + _features: [DensityFeature /*ColumnModesFeature*/], data, columns, //todo: check feasibility to use only row models that are implementing features used by the implementation getCoreRowModel: getCoreRowModel(), + ...sortingOptions, //todo: remove debugTable: true, columnResizeDirection: isRTL ? 'rtl' : 'ltr', @@ -63,7 +80,8 @@ function AnalyticalTableV2(props: AnalyticalTableV2Props): ReactElement {headerGroup.headers.map((header, index) => { + const isSortable = sortable && header.column.getCanSort(); return ( ); })} diff --git a/packages/main/src/components/AnalyticalTableV2/types/index.ts b/packages/main/src/components/AnalyticalTableV2/types/index.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/main/src/components/AnalyticalTableV2/useSorting.tsx b/packages/main/src/components/AnalyticalTableV2/useSorting.tsx new file mode 100644 index 00000000000..8d1c8450071 --- /dev/null +++ b/packages/main/src/components/AnalyticalTableV2/useSorting.tsx @@ -0,0 +1,28 @@ +import type { RowModel, SortingState, Table } from '@tanstack/react-table'; +import { getSortedRowModel } from '@tanstack/react-table'; +import type { Dispatch, SetStateAction } from 'react'; +import { useState } from 'react'; + +interface SortingTableOptions { + getSortedRowModel: (table: Table) => () => RowModel; + onSortingChange: Dispatch>; +} + +interface SortingTableState { + sorting: SortingState; +} + +type EmptyObject = Record; + +//todo: overload or conditional return type --> overload - only use conditional return if complex generic is required +export function useSorting(sortable: true): [SortingTableOptions, SortingTableState]; +export function useSorting(sortable: false | undefined): [EmptyObject, EmptyObject]; + +export function useSorting(sortable?: boolean): [SortingTableOptions, SortingTableState] | [EmptyObject, EmptyObject] { + const [sorting, setSorting] = useState([]); + + if (!sortable) { + return [{}, {}]; + } + return [{ getSortedRowModel: getSortedRowModel(), onSortingChange: setSorting }, { sorting }]; +} From 4bfbb558a6d6a6094e0b016fa88e27efd2c52dd5 Mon Sep 17 00:00:00 2001 From: Lukas Harbarth Date: Tue, 29 Apr 2025 12:17:25 +0200 Subject: [PATCH 17/17] Add selection feature --- .../AnalyticalTableV2.module.css | 11 ++- .../AnalyticalTableV2.stories.tsx | 29 +++++- .../AnalyticalTableV2/core/Cell.tsx | 21 ++++- .../AnalyticalTableV2/factories/index.ts | 55 +++++++++++ .../AnalyticalTableV2/factories/rowProps.ts | 58 ++++++++++++ .../features/SelectionFeature.ts | 32 +++++++ .../components/AnalyticalTableV2/index.tsx | 92 ++++++++++++++----- .../AnalyticalTableV2/types/index.ts | 1 + .../AnalyticalTableV2/useRowSelection.tsx | 79 ++++++++++++++++ .../AnalyticalTableV2/useSorting.tsx | 20 +++- 10 files changed, 361 insertions(+), 37 deletions(-) create mode 100644 packages/main/src/components/AnalyticalTableV2/factories/index.ts create mode 100644 packages/main/src/components/AnalyticalTableV2/factories/rowProps.ts create mode 100644 packages/main/src/components/AnalyticalTableV2/features/SelectionFeature.ts create mode 100644 packages/main/src/components/AnalyticalTableV2/useRowSelection.tsx diff --git a/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.module.css b/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.module.css index a0821b65819..84cd059e409 100644 --- a/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.module.css +++ b/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.module.css @@ -8,7 +8,6 @@ .cell { box-sizing: border-box; display: flex; - background-color: var(--sapList_Background); overflow: hidden; /*todo: dev*/ border-inline: solid 1px black; @@ -76,9 +75,19 @@ /* ============================================================= */ .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); + } } /* ============================================================= */ diff --git a/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.stories.tsx b/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.stories.tsx index bc941676fe1..441b6239fa1 100644 --- a/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.stories.tsx +++ b/packages/main/src/components/AnalyticalTableV2/AnalyticalTableV2.stories.tsx @@ -2,7 +2,7 @@ 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 { useReducer } from '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 @@ -74,13 +74,32 @@ const columns: ColumnDef[] = [ } ]; +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: dataLarge.map((item, index) => ({ ...item, friend: { ...item.friend, age: index } })).slice(0), + data: data100k, columns, - visibleRows: 5 + visibleRows: 5, + selectionMode: 'Single' }, argTypes: { data: { control: { disable: true } }, columns: { control: { disable: true } } } } satisfies Meta; @@ -89,12 +108,14 @@ type Story = StoryObj; export const Default: Story = { render(args) { - const [sortable, toggleSortable] = useReducer((prev) => !prev, false); + const [sortable, toggleSortable] = useReducer((prev) => !prev, true); return ( <>
+ {/**/} + {/**/} ); } diff --git a/packages/main/src/components/AnalyticalTableV2/core/Cell.tsx b/packages/main/src/components/AnalyticalTableV2/core/Cell.tsx index acfb35c8c6e..c16d91fdb42 100644 --- a/packages/main/src/components/AnalyticalTableV2/core/Cell.tsx +++ b/packages/main/src/components/AnalyticalTableV2/core/Cell.tsx @@ -38,18 +38,31 @@ interface CellProps { 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(props: CellProps) { - const { style = {}, role, cell, renderable, startIndex, isFirstFocusableCell, isSortable, ...rest } = props; + 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 = (e) => { + const openPopover = () => { setPopoverOpen(true); }; @@ -65,10 +78,12 @@ export function Cell(props: CellProps) { }} className={clsx(classNames.cell, isInteractive && classNames.headerInteractive)} aria-colindex={startIndex + 1} - data-cell={true} + 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)}
diff --git a/packages/main/src/components/AnalyticalTableV2/factories/index.ts b/packages/main/src/components/AnalyticalTableV2/factories/index.ts new file mode 100644 index 00000000000..75d64f7d381 --- /dev/null +++ b/packages/main/src/components/AnalyticalTableV2/factories/index.ts @@ -0,0 +1,55 @@ +import type { Row, RowData } from '@tanstack/react-table'; +import type { HTMLAttributes } from 'react'; +import type { FeaturesList } from '../types/index.js'; +import type { FeatureRowProps } from './rowProps.js'; +import { rowProps } from './rowProps.js'; + +type RowProps = Partial>; + +/** + * Creates an object of (merged) React props by features. + */ +export function createRowProps(features: FeaturesList, row: Row): RowProps { + const propsList: HTMLAttributes[] = features + .map((feature) => rowProps[feature]?.(row)) + .filter(Boolean); + + if (!propsList.length) { + return {}; + } + + if (propsList.length === 1) { + return propsList[0]; + } + + const mergedProps: HTMLAttributes = {}; + const classNames: string[] = []; + + for (const props of propsList) { + for (const prop of Object.keys(props)) { + const next = props[prop]; + const prev = mergedProps[prop]; + if (typeof prev === 'function' && typeof next === 'function') { + // merge handlers of identical event + mergedProps[prop] = (e) => { + prev(e); + next(e); + }; + } else if (typeof next === 'function') { + // single handler + mergedProps[prop] = next; + //todo: extend this for other props if required + } else if (prop === 'className' && typeof next === 'string') { + // add className to merge later + classNames.push(next); + } else { + mergedProps[prop] = next; + } + } + } + + if (classNames.length) { + mergedProps.className = classNames.join(' '); + } + return mergedProps; +} diff --git a/packages/main/src/components/AnalyticalTableV2/factories/rowProps.ts b/packages/main/src/components/AnalyticalTableV2/factories/rowProps.ts new file mode 100644 index 00000000000..229b1059dfe --- /dev/null +++ b/packages/main/src/components/AnalyticalTableV2/factories/rowProps.ts @@ -0,0 +1,58 @@ +import type { Row, RowData } from '@tanstack/react-table'; +import { clsx } from 'clsx'; +import type { KeyboardEventHandler, MouseEvent, KeyboardEvent, HTMLAttributes } from 'react'; +import { classNames } from './../AnalyticalTableV2.module.css.js'; + +interface SelectionProps extends Pick, 'className' | 'aria-selected' | 'onClick'> { + /** + * ENTER press + */ + onKeyDown: KeyboardEventHandler; + /** + * SPACE release (default prevented) + */ + onKeyUp: KeyboardEventHandler; +} +export interface FeatureRowProps { + selection: (row: Row) => SelectionProps; +} + +function selectionHandler(e: MouseEvent | KeyboardEvent, row: Row) { + if (e.currentTarget.querySelector('[data-selectable-cell]')) { + //todo: check what is better for our use case + row.getToggleSelectedHandler()(e); + // row.toggleSelected() + } +} + +export const rowProps: FeatureRowProps = { + selection: (row) => { + const isSelected = row.getIsSelected?.() ?? false; + return { + className: clsx(classNames.selectable, isSelected && classNames.selected), + 'aria-selected': `${isSelected}`, + onClick: (e) => { + selectionHandler(e, row); + }, + onKeyDown: (e) => { + if (e.key === 'Enter') { + selectionHandler(e, row); + } + }, + onKeyUp: (e) => { + if (e.code === 'Space') { + selectionHandler(e, row); + e.preventDefault(); + } + } + }; + }, + //todo: remove + //@ts-expect-error: will be removed + test: () => ({ + className: 'testClassName', + onClick: () => console.log('test'), + onKeyDown: () => console.log('test'), + onKeyUp: () => console.log('test') + }) +}; diff --git a/packages/main/src/components/AnalyticalTableV2/features/SelectionFeature.ts b/packages/main/src/components/AnalyticalTableV2/features/SelectionFeature.ts new file mode 100644 index 00000000000..8d3221f1554 --- /dev/null +++ b/packages/main/src/components/AnalyticalTableV2/features/SelectionFeature.ts @@ -0,0 +1,32 @@ +//todo remove +/* eslint-disable */ +//@ts-nocheck + +import type { TableFeature, Row, Updater, RowData } from '@tanstack/react-table'; +import { KeyboardEvent } from 'react'; + +//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 +export const SelectionFeature: TableFeature = { + createRow: (row: Row, table) => { + row.selectionBehavior = table.options.selectionBehavior; + + if (table.options.enableRowSelection) { + row.getRowProps = () => ({ + onClick: (e) => { + if (table.options.enableRowSelection) { + if (row.selectionBehavior !== 'RowSelector') { + console.log(e); + row.toggleSelected!(); + } + } + }, + onKeyDown: (e: KeyboardEvent) => { + if (e.key === 'Enter' && table.options.enableRowSelection) { + row.toggleSelected!(); + } + }, + 'data-selection-behavior': row.selectionBehavior + }); + } + } +}; diff --git a/packages/main/src/components/AnalyticalTableV2/index.tsx b/packages/main/src/components/AnalyticalTableV2/index.tsx index 90e2a1625b8..62516429a0c 100644 --- a/packages/main/src/components/AnalyticalTableV2/index.tsx +++ b/packages/main/src/components/AnalyticalTableV2/index.tsx @@ -2,13 +2,16 @@ import { getCoreRowModel, useReactTable } from '@tanstack/react-table'; import { CssSizeVariables, useIsRTL, useStylesheet } from '@ui5/webcomponents-react-base'; import { clsx } from 'clsx'; import type { CSSProperties, ReactElement } from 'react'; -import { useRef, useState } from 'react'; -import { classNames, styleData } from './AnalyticalTableV2.module.css.js'; +import { useMemo, useRef, useState } from 'react'; +import { classNames, content } from './AnalyticalTableV2.module.css.js'; import { Cell } from './core/Cell.js'; import { Row } from './core/Row.js'; +import { createRowProps } from './factories/index.js'; import { DensityFeature } from './features/exampleFeature.js'; +import type { FeaturesList } from './types/index.js'; import { useColumnWidths } from './useColumnMode.js'; import { handleKeyboardNavigation } from './useKeyboardNavigation.js'; +import { useRowSelection } from './useRowSelection.js'; import { useRowVirtualizer } from './useRowVirtualizer.js'; import { useSorting } from './useSorting.js'; import { useTableContainerResizeObserver } from './utils/useTableContainerResizeObserver.js'; @@ -25,6 +28,10 @@ interface AnalyticalTableV2Props { //todo: check if this should be controllable, if so add respective checks otherwise the table-option won't do anything enableRowPinning?: boolean; enableColumnPinning?: boolean; + + //todo: enum + selectionMode?: 'None' | 'Single' | 'Multiple'; + selectionBehavior?: 'Row' | 'RowOnly' | 'RowSelector'; } interface CSSPropertiesWithVars extends CSSProperties { @@ -46,53 +53,51 @@ function AnalyticalTableV2(props: AnalyticalTableV2Props): ReactElement(null); const isRTL = useIsRTL(tableContainerRef); const { tableWidth, horizontalScrollbarHeight, verticalScrollbarWidth } = useTableContainerResizeObserver(tableContainerRef); const [columnSizing, setColumnSizing] = useState({}); + //todo: refactor overload + //@ts-expect-error: fix type later const [sortingOptions, sortingState] = useSorting(sortable); - - console.log(sortingOptions, sortingState); + const [selectionOptions, selectionState] = useRowSelection(selectionMode, selectionBehavior, columns); //const setColumnSizing = useColumnWidths(tableWidth, reactTable.getAllLeafColumns().map((item) => item.columnDef) const reactTable = useReactTable({ - _features: [DensityFeature /*ColumnModesFeature*/], + _features: [DensityFeature /*SelectionFeature*/], data, columns, //todo: check feasibility to use only row models that are implementing features used by the implementation getCoreRowModel: getCoreRowModel(), ...sortingOptions, + ...selectionOptions, //todo: remove - debugTable: true, + // debugTable: true, columnResizeDirection: isRTL ? 'rtl' : 'ltr', - //todo: optional - // enableColumnPinning: false, - // enableRowPinning: false, + //todo: `false` doesn't disable pinning --> use `.getCanPin()` + enableColumnPinning, + enableRowPinning, state: { columnSizing, //todo: add types & clarify how to inject types (declare module '@tanstack/react-table' is probably not the best approach - probably we have to cast a lot...) //ColumnModesFeature + //todo: type + //@ts-expect-error: fix type later tableWidth, //DensityFeature density: 'md', - ...sortingState - }, - initialState: { - columnPinning: { - left: ['c_pinned'], - right: ['F'] - } - // rowPinning: { - // bottom: ['0', '1'], - // top: ['499', '498'] - // } + ...sortingState, + ...selectionState }, + initialState: {}, // column sizing defaultColumn: { // need to overwrite default size for dynamic column widths @@ -103,7 +108,14 @@ function AnalyticalTableV2(props: AnalyticalTableV2Props): ReactElement [selectionMode !== 'None' ? 'selection' : undefined, 'test'].filter(Boolean), + [selectionMode] + ); + const renderSelectionCell = selectionMode !== 'None' && selectionBehavior !== 'RowOnly'; + // console.log(enableRowPinning, enableColumnPinning); //todo: refactor to use getAllLeafColumns directly, or use the `columns` array? useColumnWidths( @@ -159,6 +171,8 @@ function AnalyticalTableV2(props: AnalyticalTableV2Props): ReactElement {headerGroup.headers.map((header, index) => { const isSortable = sortable && header.column.getCanSort(); + //todo outsource to factory? + const isSelectionCell = renderSelectionCell && header.id === '_ui5wcr_selection_col'; return ( ); })} @@ -183,13 +198,20 @@ function AnalyticalTableV2(props: AnalyticalTableV2Props): ReactElement 0 && (
{topRows.map((row, index) => { + const featureProps = createRowProps(activeFeatures, row); return ( {row.getVisibleCells().map((cell, index) => { + //todo outsource to factory? + const isSelectionCell = renderSelectionCell && cell.id === '_ui5wcr_selection_col'; + const isSelectableCell = + selectionMode !== 'None' && selectionBehavior === 'RowSelector' ? isSelectionCell : true; + return ( ); @@ -216,6 +240,7 @@ function AnalyticalTableV2(props: AnalyticalTableV2Props): ReactElement {rowVirtualizer.getVirtualItems().map((virtualRow) => { const row = centerRows[virtualRow.index]; + const { className, ...featureProps } = createRowProps(activeFeatures, row); return ( {row.getVisibleCells().map((cell, index) => { + //todo outsource to factory? + const isSelectionCell = renderSelectionCell && cell.id === '_ui5wcr_selection_col'; + const isSelectableCell = + selectionMode !== 'None' && selectionBehavior === 'RowSelector' ? isSelectionCell : true; + return ( ); })} @@ -245,13 +280,20 @@ function AnalyticalTableV2(props: AnalyticalTableV2Props): ReactElement 0 && (
{bottomRows.map((row, index) => { + const featureProps = createRowProps(activeFeatures, row); return ( {row.getVisibleCells().map((cell, index) => { + //todo outsource to factory? + const isSelectionCell = renderSelectionCell && cell.id === '_ui5wcr_selection_col'; + const isSelectableCell = + selectionMode !== 'None' && selectionBehavior === 'RowSelector' ? isSelectionCell : true; + return ( ); })} diff --git a/packages/main/src/components/AnalyticalTableV2/types/index.ts b/packages/main/src/components/AnalyticalTableV2/types/index.ts index e69de29bb2d..f7fd1e9fb24 100644 --- a/packages/main/src/components/AnalyticalTableV2/types/index.ts +++ b/packages/main/src/components/AnalyticalTableV2/types/index.ts @@ -0,0 +1 @@ +export type FeaturesList = string[]; diff --git a/packages/main/src/components/AnalyticalTableV2/useRowSelection.tsx b/packages/main/src/components/AnalyticalTableV2/useRowSelection.tsx new file mode 100644 index 00000000000..559087a9023 --- /dev/null +++ b/packages/main/src/components/AnalyticalTableV2/useRowSelection.tsx @@ -0,0 +1,79 @@ +import type { CellContext, ColumnDef, HeaderContext } from '@tanstack/react-table'; +import { useMemo } from 'react'; +import { CheckBox } from '../../webComponents/CheckBox/index.js'; +import type { AnalyticalTableV2Props } from './index.js'; + +interface SelectionTableOptions { + enableRowSelection: boolean; + enableMultiRowSelection: boolean; + selectionBehavior: AnalyticalTableV2Props['selectionBehavior']; + columns?: ColumnDef[]; +} + +export interface SelectionState { + columnPinning: { + left: ['_ui5wcr_selection_col']; + }; +} + +const SelectionHeader = ({ table }: HeaderContext) => { + if (!table.options.enableMultiRowSelection) { + return null; + } + const isSomeSelected = table.getIsSomeRowsSelected(); + return ; +}; + +const SelectionCell = ({ row, table }: CellContext) => { + if (!table.options.enableMultiRowSelection) { + return null; + } + const someSelected = row.getIsSomeSelected(); + return ( + + ); +}; + +//todo types +const selectionColumn: ColumnDef = { + id: '_ui5wcr_selection_col', + // todo: centralize defaults + minSize: 44, + size: 44, + maxSize: 44, + // todo: how to handle multiple headers? + header: SelectionHeader, + cell: SelectionCell +}; + +//todo type +export function useRowSelection( + mode: AnalyticalTableV2Props['selectionMode'], + behavior: AnalyticalTableV2Props['selectionBehavior'], + userColumns: AnalyticalTableV2Props['columns'] +): [SelectionTableOptions, SelectionState] { + const selectionTableOptions = useMemo(() => { + // todo: fully control selection state? (onRowSelectionChange) + const options: SelectionTableOptions = { + enableRowSelection: mode !== 'None', + enableMultiRowSelection: mode === 'Multiple', + selectionBehavior: behavior + }; + if (mode !== 'None' && behavior !== 'RowOnly') { + options.columns = [selectionColumn as ColumnDef, ...userColumns]; + } + return options; + }, [mode, userColumns, behavior]); + + const selectionState: SelectionState = { + columnPinning: { + left: ['_ui5wcr_selection_col'] + } + }; + + return [selectionTableOptions, selectionState]; +} diff --git a/packages/main/src/components/AnalyticalTableV2/useSorting.tsx b/packages/main/src/components/AnalyticalTableV2/useSorting.tsx index 8d1c8450071..8d8d3441a57 100644 --- a/packages/main/src/components/AnalyticalTableV2/useSorting.tsx +++ b/packages/main/src/components/AnalyticalTableV2/useSorting.tsx @@ -1,9 +1,10 @@ import type { RowModel, SortingState, Table } from '@tanstack/react-table'; import { getSortedRowModel } from '@tanstack/react-table'; import type { Dispatch, SetStateAction } from 'react'; -import { useState } from 'react'; +import { useMemo, useState } from 'react'; interface SortingTableOptions { + //todo: type getSortedRowModel: (table: Table) => () => RowModel; onSortingChange: Dispatch>; } @@ -21,8 +22,17 @@ export function useSorting(sortable: false | undefined): [EmptyObject, EmptyObje export function useSorting(sortable?: boolean): [SortingTableOptions, SortingTableState] | [EmptyObject, EmptyObject] { const [sorting, setSorting] = useState([]); - if (!sortable) { - return [{}, {}]; - } - return [{ getSortedRowModel: getSortedRowModel(), onSortingChange: setSorting }, { sorting }]; + return useMemo(() => { + if (!sortable) { + return [{}, {}] as [EmptyObject, EmptyObject]; + } + + return [ + { + getSortedRowModel: getSortedRowModel(), + onSortingChange: setSorting + }, + { sorting } + ] as [SortingTableOptions, SortingTableState]; + }, [sortable, sorting]); }