diff --git a/package-lock.json b/package-lock.json index 06ad5f7e6..2d4bb6cc3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3454,6 +3454,33 @@ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/@tanstack/react-virtual": { + "version": "3.13.18", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.18.tgz", + "integrity": "sha512-dZkhyfahpvlaV0rIKnvQiVoWPyURppl6w4m9IwMDpuIjcJ1sD9YGWrt0wISvgU7ewACXx2Ct46WPgI6qAD4v6A==", + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.13.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.13.18", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.18.tgz", + "integrity": "sha512-Mx86Hqu1k39icq2Zusq+Ey2J6dDWTjDvEv43PJtRCoEYTLyfaPnxIQ6iy7YAOK0NV/qOEmZQ/uCufrppZxTgcg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@testing-library/dom": { "version": "10.4.1", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", @@ -12956,6 +12983,7 @@ "version": "0.0.1", "license": "MIT", "dependencies": { + "@tanstack/react-virtual": "^3.13.12", "@vitejs/plugin-react": "^5.1.3", "clsx": "^2.1.1", "hast-util-to-jsx-runtime": "^2.3.6", diff --git a/packages/pxweb2-ui/package.json b/packages/pxweb2-ui/package.json index 373297634..93bbdbf62 100644 --- a/packages/pxweb2-ui/package.json +++ b/packages/pxweb2-ui/package.json @@ -16,6 +16,7 @@ "author": "", "license": "MIT", "dependencies": { + "@tanstack/react-virtual": "^3.13.12", "@vitejs/plugin-react": "^5.1.3", "clsx": "^2.1.1", "hast-util-to-jsx-runtime": "^2.3.6", diff --git a/packages/pxweb2-ui/src/lib/components/Table/Table.module.scss b/packages/pxweb2-ui/src/lib/components/Table/Table.module.scss index 40b0a785d..868b567e2 100644 --- a/packages/pxweb2-ui/src/lib/components/Table/Table.module.scss +++ b/packages/pxweb2-ui/src/lib/components/Table/Table.module.scss @@ -255,4 +255,25 @@ background: var(--px-color-surface-moderate); border: none; } + + .virtualPaddingCell { + padding: 0; + border: none; + background: none; + } +} + +.virtualizedWrapper { + max-height: 70vh; + overflow-x: auto; + overflow-y: auto; +} + +.virtualizedWrapperUseParentScroll { + max-height: none; +} + +.virtualizedTable { + width: max-content; + min-width: 100%; } diff --git a/packages/pxweb2-ui/src/lib/components/Table/Table.tsx b/packages/pxweb2-ui/src/lib/components/Table/Table.tsx index 37a77140c..1c8b92747 100644 --- a/packages/pxweb2-ui/src/lib/components/Table/Table.tsx +++ b/packages/pxweb2-ui/src/lib/components/Table/Table.tsx @@ -1,5 +1,6 @@ -import { memo, useMemo } from 'react'; +import { memo, useEffect, useMemo, useRef, useState } from 'react'; import cl from 'clsx'; +import { useVirtualizer } from '@tanstack/react-virtual'; import classes from './Table.module.scss'; import { PxTable } from '../../shared-types/pxTable'; @@ -12,6 +13,7 @@ import { Variable } from '../../shared-types/variable'; export interface TableProps { readonly pxtable: PxTable; readonly isMobile: boolean; + readonly getVerticalScrollElement?: () => HTMLElement | null; readonly className?: string; } @@ -32,6 +34,7 @@ interface CreateRowParams { stubIteration: number; table: PxTable; tableMeta: columnRowMeta; + columnWindow: ColumnRenderWindow; stubDataCellCodes: DataCellCodes; headingDataCellCodes: DataCellCodes[]; tableRows: React.JSX.Element[]; @@ -43,6 +46,7 @@ interface CreateRowMobileParams { rowSpan: number; table: PxTable; tableMeta: columnRowMeta; + columnWindow: ColumnRenderWindow; stubDataCellCodes: DataCellCodes; headingDataCellCodes: DataCellCodes[]; tableRows: React.JSX.Element[]; @@ -56,20 +60,114 @@ interface CreateRowMobileParams { */ type DataCellCodes = DataCellMeta[]; +interface ColumnRenderWindow { + start: number; + end: number; + leftPadding: number; + rightPadding: number; +} + +const TABLE_COLUMN_ESTIMATE_WIDTH_PX = 88; +const HEADER_CELL_HORIZONTAL_PADDING_PX = 24; +const HEADER_CELL_VERTICAL_PADDING_PX = 16; +const HEADER_ROW_LINE_HEIGHT_PX = 24; +const AVERAGE_CHARACTER_WIDTH_PX = 8; +const HEADER_BASE_TEXT_LENGTH = 10; +const HEADER_ADDITIONAL_WIDTH_PER_CHAR_PX = 4; +const HEADER_MAX_WIDTH_PX = 280; +const HEADER_MAX_LINE_COUNT = 25; + +function trimValueLabel(valueLabel: string): string { + return valueLabel.trim(); +} + +function calculateHeaderCellWidthPx(valueLabelLength: number): number { + const extraCharacterCount = Math.max( + 0, + valueLabelLength - HEADER_BASE_TEXT_LENGTH, + ); + + return Math.min( + HEADER_MAX_WIDTH_PX, + TABLE_COLUMN_ESTIMATE_WIDTH_PX + + extraCharacterCount * HEADER_ADDITIONAL_WIDTH_PER_CHAR_PX, + ); +} + export const Table = memo(function Table({ pxtable, isMobile, + getVerticalScrollElement, className = '', }: TableProps) { + const scrollContainerRef = useRef(null); + const [verticalScrollElement, setVerticalScrollElement] = + useState(null); + const [tableScrollMargin, setTableScrollMargin] = useState(0); const cssClasses = className.length > 0 ? ' ' + className : ''; - const tableMeta: columnRowMeta = calculateRowAndColumnMeta(pxtable); + useEffect(() => { + // Use outer container scroll if it is provided, otherwise use the table container scroll + const updateVerticalScrollElement = () => { + if (getVerticalScrollElement) { + setVerticalScrollElement(getVerticalScrollElement()); + } else { + setVerticalScrollElement(null); + } + }; + + updateVerticalScrollElement(); + globalThis.addEventListener('resize', updateVerticalScrollElement); + + return () => { + globalThis.removeEventListener('resize', updateVerticalScrollElement); + }; + }, [getVerticalScrollElement]); + + useEffect(() => { + if (!verticalScrollElement || !scrollContainerRef.current) { + setTableScrollMargin(0); + return; + } + + const updateTableScrollMargin = () => { + if (!scrollContainerRef.current) { + return; + } + + const tableTop = scrollContainerRef.current.getBoundingClientRect().top; + const containerTop = verticalScrollElement.getBoundingClientRect().top; + const margin = tableTop - containerTop + verticalScrollElement.scrollTop; + setTableScrollMargin(Math.max(0, margin)); + }; + + updateTableScrollMargin(); + globalThis.addEventListener('resize', updateTableScrollMargin); + + const resizeObserver = + typeof ResizeObserver === 'undefined' + ? null + : new ResizeObserver(() => { + updateTableScrollMargin(); + }); + + if (resizeObserver && scrollContainerRef.current) { + resizeObserver.observe(scrollContainerRef.current); + resizeObserver.observe(verticalScrollElement); + } + + return () => { + globalThis.removeEventListener('resize', updateTableScrollMargin); + resizeObserver?.disconnect(); + }; + }, [verticalScrollElement]); + + const tableMeta: columnRowMeta = useMemo( + () => calculateRowAndColumnMeta(pxtable), + [pxtable], + ); const tableColumnSize: number = tableMeta.columns - tableMeta.columnOffset; - const headingDataCellCodes = useMemo( - () => new Array(tableColumnSize), - [tableColumnSize], - ); // Contains header variable and value codes for each column in the table // Find the contents variable const contentsVariable = pxtable.metadata.variables.find( @@ -81,68 +179,192 @@ export const Table = memo(function Table({ contentVarIndex = pxtable.data.variableOrder.indexOf(contentsVariable.id); } - const contentsVariableDecimals = Object.fromEntries( - pxtable.metadata.variables - .filter((variable) => variable.type === 'ContentsVariable') - .flatMap((variable) => - variable.values.map((value) => [ - value.code, - { decimals: value.contentInfo?.decimals ?? 6 }, - ]), + const contentsVariableDecimals = useMemo( + () => + Object.fromEntries( + pxtable.metadata.variables + .filter((variable) => variable.type === 'ContentsVariable') + .flatMap((variable) => + variable.values.map((value) => [ + value.code, + { decimals: value.contentInfo?.decimals ?? 6 }, + ]), + ), ), + [pxtable.metadata.variables], ); - // Create empty metadata structure for the dimensions in the header. - // This structure will be filled with metadata when the header is created. + const shouldVirtualizeColumns = !isMobile && tableColumnSize > 60; + const headingRowHeights = useMemo( + () => calculateHeadingRowHeights(pxtable, tableMeta), + [pxtable, tableMeta], + ); - // Loop through all columns in the table. i is the column index - for (let i = 0; i < tableColumnSize; i++) { - const dataCellCodes: DataCellCodes = new Array( - pxtable.heading.length, - ); + const columnVirtualizer = useVirtualizer({ + horizontal: true, + count: tableColumnSize, + getScrollElement: () => scrollContainerRef.current, + estimateSize: () => TABLE_COLUMN_ESTIMATE_WIDTH_PX, + overscan: 8, + }); - // Loop through all header variables. j is the header variable index - for (let j = 0; j < pxtable.heading.length; j++) { - const dataCellMeta: DataCellMeta = { - varId: '', - valCode: '', - valLabel: '', - varPos: 0, - htmlId: '', - }; - dataCellCodes[j] = dataCellMeta; // add empty object + const virtualColumns = shouldVirtualizeColumns + ? columnVirtualizer.getVirtualItems() + : []; + const firstVirtualColumn = virtualColumns[0]; + const lastVirtualColumn = virtualColumns.at(-1); + + const columnWindow: ColumnRenderWindow = useMemo( + () => + shouldVirtualizeColumns + ? { + start: firstVirtualColumn?.index ?? 0, + end: lastVirtualColumn + ? lastVirtualColumn.index + 1 + : tableColumnSize, + leftPadding: firstVirtualColumn?.start ?? 0, + rightPadding: lastVirtualColumn + ? columnVirtualizer.getTotalSize() - lastVirtualColumn.end + : 0, + } + : { + start: 0, + end: tableColumnSize, + leftPadding: 0, + rightPadding: 0, + }, + [ + shouldVirtualizeColumns, + firstVirtualColumn, + lastVirtualColumn, + tableColumnSize, + columnVirtualizer, + ], + ); + + const { headingRows, bodyRows } = useMemo(() => { + const headingDataCellCodes = new Array(tableColumnSize); + + for (let i = 0; i < tableColumnSize; i++) { + const dataCellCodes: DataCellCodes = new Array( + pxtable.heading.length, + ); + + for (let j = 0; j < pxtable.heading.length; j++) { + dataCellCodes[j] = { + varId: '', + valCode: '', + valLabel: '', + varPos: 0, + htmlId: '', + }; + } + headingDataCellCodes[i] = dataCellCodes; } - headingDataCellCodes[i] = dataCellCodes; - } + + return { + headingRows: createHeading( + pxtable, + tableMeta, + headingDataCellCodes, + columnWindow, + headingRowHeights, + ), + bodyRows: createRows( + pxtable, + tableMeta, + headingDataCellCodes, + columnWindow, + isMobile, + contentVarIndex, + contentsVariableDecimals, + ), + }; + }, [ + tableColumnSize, + pxtable, + tableMeta, + columnWindow, + headingRowHeights, + isMobile, + contentVarIndex, + contentsVariableDecimals, + ]); + + const shouldVirtualize = bodyRows.length > 100; + const rowVirtualizer = useVirtualizer({ + count: bodyRows.length, + getScrollElement: () => verticalScrollElement ?? scrollContainerRef.current, + scrollMargin: tableScrollMargin, + estimateSize: () => (isMobile ? 44 : 36), + overscan: 12, + }); + + const virtualRows = shouldVirtualize ? rowVirtualizer.getVirtualItems() : []; + const firstVirtualRow = virtualRows[0]; + const lastVirtualRow = virtualRows.at(-1); + const topPaddingHeight = firstVirtualRow + ? Math.max(0, firstVirtualRow.start - tableScrollMargin) + : 0; + const bottomPaddingHeight = lastVirtualRow + ? rowVirtualizer.getTotalSize() - lastVirtualRow.end + : 0; + const visibleRowStart = firstVirtualRow?.index ?? 0; + const visibleRowEnd = lastVirtualRow ? lastVirtualRow.index + 1 : 0; + const visibleBodyRows = shouldVirtualize + ? bodyRows.slice(visibleRowStart, visibleRowEnd) + : bodyRows; + + const renderedColumnCount = + tableMeta.columnOffset + + (columnWindow.end - columnWindow.start) + + (columnWindow.leftPadding > 0 ? 1 : 0) + + (columnWindow.rightPadding > 0 ? 1 : 0); return ( - - {createHeading(pxtable, tableMeta, headingDataCellCodes)} - - {useMemo( - () => - createRows( - pxtable, - tableMeta, - headingDataCellCodes, - isMobile, - contentVarIndex, - contentsVariableDecimals, - ), - [ - pxtable, - tableMeta, - headingDataCellCodes, - isMobile, - contentVarIndex, - contentsVariableDecimals, - ], - )} - -
+ + {headingRows} + + {shouldVirtualize && topPaddingHeight > 0 && ( + + + )} + + {visibleBodyRows} + + {shouldVirtualize && bottomPaddingHeight > 0 && ( + + + )} + +
+
+
+ ); }); @@ -158,6 +380,8 @@ export function createHeading( table: PxTable, tableMeta: columnRowMeta, headingDataCellCodes: DataCellCodes[], + columnWindow: ColumnRenderWindow, + headingRowHeights: number[], ): React.JSX.Element[] { // Number of times to add all values for a variable, default to 1 for first header row let repetitionsCurrentHeaderLevel = 1; @@ -188,6 +412,21 @@ export function createHeading( idxHeadingLevel < table.heading.length; idxHeadingLevel++ ) { + const headingRowHeight = headingRowHeights[idxHeadingLevel]; + + if (columnWindow.leftPadding > 0) { + headerRow.push( + , + ); + } + // Set the column span for the header cells for the current row columnSpan = columnSpan / table.heading[idxHeadingLevel].values.length; const variable = table.heading[idxHeadingLevel]; @@ -200,6 +439,9 @@ export function createHeading( ) { // loop trough all the values for the header variable for (let i = 0; i < variable.values.length; i++) { + const valueLabel = variable.values[i].label; + const spanStart = columnIndex; + const spanEnd = columnIndex + columnSpan; const htmlId: string = 'H' + idxHeadingLevel + @@ -207,27 +449,42 @@ export function createHeading( variable.values[i].code + '.I' + idxRepetitionCurrentHeadingLevel; - headerRow.push( - - {variable.values[i].label} - , - ); + const visibleSpanStart = Math.max(spanStart, columnWindow.start); + const visibleSpanEnd = Math.min(spanEnd, columnWindow.end); + const visibleSpan = visibleSpanEnd - visibleSpanStart; + + if (visibleSpan > 0) { + headerRow.push( + + {valueLabel} + , + ); + } + // Repeat for the number of columns in the column span for (let j = 0; j < columnSpan; j++) { // Fill the metadata structure for the dimensions of the header cells @@ -243,6 +500,19 @@ export function createHeading( } } + if (columnWindow.rightPadding > 0) { + headerRow.push( + , + ); + } + headerRows.push({headerRow}); // Set repetiton for the next header variable @@ -254,6 +524,46 @@ export function createHeading( return headerRows; } +export function calculateHeadingRowHeights( + table: PxTable, + tableMeta: columnRowMeta, +): number[] { + let columnSpan = tableMeta.columns - tableMeta.columnOffset; + + return table.heading.map((variable) => { + columnSpan = columnSpan / variable.values.length; + + const estimatedCellWidthPx = Math.max( + TABLE_COLUMN_ESTIMATE_WIDTH_PX, + columnSpan * TABLE_COLUMN_ESTIMATE_WIDTH_PX, + ); + const longestLabelLength = + tableMeta.longestValueTextByVariableId[variable.id] ?? 0; + const adjustedEstimatedCellWidthPx = Math.max( + estimatedCellWidthPx, + calculateHeaderCellWidthPx(longestLabelLength), + ); + const headerCellTextWidthPx = Math.max( + 1, + adjustedEstimatedCellWidthPx - HEADER_CELL_HORIZONTAL_PADDING_PX, + ); + const estimatedCharsPerLine = Math.max( + 1, + Math.floor(headerCellTextWidthPx / AVERAGE_CHARACTER_WIDTH_PX), + ); + + let lineCount = Math.max( + 1, + Math.ceil(longestLabelLength / estimatedCharsPerLine), + ); + lineCount = Math.min(lineCount, HEADER_MAX_LINE_COUNT); + + return ( + lineCount * HEADER_ROW_LINE_HEIGHT_PX + HEADER_CELL_VERTICAL_PADDING_PX + ); + }); +} + /** * Creates an array of React.JSX elements representing the rows of a table. * @param table The PxWeb table. @@ -265,6 +575,7 @@ export function createRows( table: PxTable, tableMeta: columnRowMeta, headingDataCellCodes: DataCellCodes[], + columnWindow: ColumnRenderWindow, isMobile: boolean, contentVarIndex: number, contentsVariableDecimals?: Record, @@ -278,6 +589,7 @@ export function createRows( rowSpan: tableMeta.rows - tableMeta.rowOffset, table, tableMeta, + columnWindow, stubDataCellCodes: stubDatacellCodes, headingDataCellCodes, tableRows, @@ -292,6 +604,7 @@ export function createRows( stubIteration: 0, table, tableMeta, + columnWindow, stubDataCellCodes: stubDatacellCodes, headingDataCellCodes, tableRows, @@ -303,7 +616,7 @@ export function createRows( const tableRow: React.JSX.Element[] = []; fillData( table, - tableMeta, + columnWindow, stubDatacellCodes, headingDataCellCodes, tableRow, @@ -339,6 +652,7 @@ function createRowDesktop({ stubIteration, table, tableMeta, + columnWindow, stubDataCellCodes, headingDataCellCodes, tableRows, @@ -354,6 +668,7 @@ function createRowDesktop({ // Loop through all the values in the stub variable for (const val of table.stub[stubIndex].values) { + const trimmedValueLabel = trimValueLabel(val.label); if (stubIndex === 0) { stubIteration++; } @@ -361,7 +676,7 @@ function createRowDesktop({ const cellMeta: DataCellMeta = { varId: variable.id, valCode: val.code, - valLabel: val.label, + valLabel: trimmedValueLabel, varPos: table.data.variableOrder.indexOf(variable.id), htmlId: 'R.' + stubIndex + val.code + '.I' + stubIteration, }; @@ -377,20 +692,20 @@ function createRowDesktop({ scope="row" aria-label={ variable.type === VartypeEnum.TIME_VARIABLE - ? `${variable.label} ${val.label}` + ? `${variable.label} ${trimmedValueLabel}` : undefined } className={cl(classes.stub, classes[`stub-${stubIndex}`])} key={getNewKey()} > - {val.label} + {trimmedValueLabel} , ); // If there are more stub variables that need to add headers to this row if (table.stub.length > stubIndex + 1) { // make the rest of this row empty - fillEmpty(tableMeta, tableRow); + fillEmpty(tableRow, columnWindow); tableRows.push( - {val.label} + {trimmedValueLabel} , ); fillData( table, - tableMeta, + columnWindow, stubDataCellCodes, headingDataCellCodes, tableRow, @@ -598,18 +917,38 @@ function createRowMobile({ * @param tableRow - The array of React.JSX.Element representing the row of the table. */ function fillEmpty( - tableMeta: columnRowMeta, tableRow: React.JSX.Element[], + columnWindow: ColumnRenderWindow, ): void { const emptyText = ''; - // Loop through cells that need to be added to the row - const maxCols = tableMeta.columns - tableMeta.columnOffset; + if (columnWindow.leftPadding > 0) { + tableRow.push( + + {emptyText} + , + ); + } - // Loop through all data columns in the table - for (let i = 0; i < maxCols; i++) { + for (let i = columnWindow.start; i < columnWindow.end; i++) { tableRow.push({emptyText}); } + + if (columnWindow.rightPadding > 0) { + tableRow.push( + + {emptyText} + , + ); + } } /* @@ -623,17 +962,22 @@ function fillEmpty( */ function fillData( table: PxTable, - tableMeta: columnRowMeta, + columnWindow: ColumnRenderWindow, stubDataCellCodes: DataCellCodes, headingDataCellCodes: DataCellCodes[], tableRow: React.JSX.Element[], ): void { - // Loop through cells that need to be added to the row - const maxCols = tableMeta.columns - tableMeta.columnOffset; - - // Loop through all data columns in the table + if (columnWindow.leftPadding > 0) { + tableRow.push( + , + ); + } - for (let i = 0; i < maxCols; i++) { + for (let i = columnWindow.start; i < columnWindow.end; i++) { // Merge the metadata structure for the dimensions of the stub and header cells const dataCellCodes = stubDataCellCodes.concat(headingDataCellCodes[i]); const datacellIds: string[] = dataCellCodes.map((obj) => obj.htmlId); @@ -659,7 +1003,18 @@ function fillData( , ); } + + if (columnWindow.rightPadding > 0) { + tableRow.push( + , + ); + } } + /** * Creates repeated mobile headers for a table and appends them to the provided table rows. * @@ -743,6 +1098,7 @@ function createSecondLastMobileHeader( tableRows: React.JSX.Element[], uniqueIdCounter: { idCounter: number }, ): void { + const trimmedValueLabel = trimValueLabel(val.label); // second last level let tableRowSecondLastHeader: React.JSX.Element[] = []; let tempid = @@ -755,13 +1111,13 @@ function createSecondLastMobileHeader( scope="col" aria-label={ variable.type === VartypeEnum.TIME_VARIABLE - ? `${variable.label} ${val.label}` + ? `${variable.label} ${trimmedValueLabel}` : undefined } className={cl(classes.stub, classes[`stub-${stubIndex}`])} key={getNewKey()} > - {val.label} + {trimmedValueLabel} , ); diff --git a/packages/pxweb2-ui/src/lib/components/Table/columnRowMeta.ts b/packages/pxweb2-ui/src/lib/components/Table/columnRowMeta.ts index 7367c7da1..e33a2c388 100644 --- a/packages/pxweb2-ui/src/lib/components/Table/columnRowMeta.ts +++ b/packages/pxweb2-ui/src/lib/components/Table/columnRowMeta.ts @@ -20,6 +20,10 @@ export type columnRowMeta = { * The number of rows that contain headers */ rowOffset: number; + /** + * The longest value label for each variable, keyed by variable id. + */ + longestValueTextByVariableId: Record; }; /** @@ -38,13 +42,29 @@ export function calculateRowAndColumnMeta(pxtable: PxTable): columnRowMeta { let rowCount = 1; const columnOffset = 1; const rowOffset = pxtable.heading.length; + const longestValueTextByVariableId: Record = {}; + + for (const headingVariable of pxtable.heading) { + columnCount *= headingVariable.values.length; + } - for (let i = 0; i < pxtable.heading.length; i++) { - columnCount *= pxtable.heading[i].values.length; + for (const stubVariable of pxtable.stub) { + rowCount *= stubVariable.values.length; } - for (let i = 0; i < pxtable.stub.length; i++) { - rowCount *= pxtable.stub[i].values.length; + const allVariables = [...pxtable.heading, ...pxtable.stub]; + for (const variable of allVariables) { + let longestValueText = ''; + + for (const value of variable.values) { + const valueLabel = value.label; + + if (valueLabel.length > longestValueText.length) { + longestValueText = valueLabel; + } + } + + longestValueTextByVariableId[variable.id] = longestValueText.length; } rowCount += pxtable.heading.length; @@ -55,5 +75,6 @@ export function calculateRowAndColumnMeta(pxtable: PxTable): columnRowMeta { columns: columnCount, columnOffset: columnOffset, rowOffset: rowOffset, + longestValueTextByVariableId: longestValueTextByVariableId, }; } diff --git a/packages/pxweb2/src/app/components/NavigationDrawer/Drawers/DrawerEdit.tsx b/packages/pxweb2/src/app/components/NavigationDrawer/Drawers/DrawerEdit.tsx index 95930f4e4..728241e6e 100644 --- a/packages/pxweb2/src/app/components/NavigationDrawer/Drawers/DrawerEdit.tsx +++ b/packages/pxweb2/src/app/components/NavigationDrawer/Drawers/DrawerEdit.tsx @@ -40,7 +40,7 @@ function PivotButton({ setIsFadingTable(true); setAnnounceOnNextChange(true); setLoadingPivotType(pivotType); - await new Promise((resolve) => setTimeout(resolve, 1000)); // Allow spinner to render + // await new Promise((resolve) => setTimeout(resolve, 1000)); // Allow spinner to render try { await Promise.resolve(pivot(pivotType)); } finally { diff --git a/packages/pxweb2/src/app/components/Presentation/Presentation.tsx b/packages/pxweb2/src/app/components/Presentation/Presentation.tsx index 3f48d6014..eef8ffb21 100644 --- a/packages/pxweb2/src/app/components/Presentation/Presentation.tsx +++ b/packages/pxweb2/src/app/components/Presentation/Presentation.tsx @@ -1,6 +1,12 @@ import cl from 'clsx'; import { useTranslation } from 'react-i18next'; -import React, { useRef, useEffect, useState, useLayoutEffect } from 'react'; +import React, { + useRef, + useEffect, + useState, + useLayoutEffect, + useCallback, +} from 'react'; import isEqual from 'lodash/isEqual'; import classes from './Presentation.module.scss'; @@ -20,12 +26,25 @@ type propsType = { }; const MemoizedTable = React.memo( - ({ pxtable, isMobile }: { pxtable: PxTable; isMobile: boolean }) => ( - + ({ + pxtable, + isMobile, + getVerticalScrollElement, + }: { + pxtable: PxTable; + isMobile: boolean; + getVerticalScrollElement?: () => HTMLElement | null; + }) => ( +
), (prevProps, nextProps) => isEqual(prevProps.pxtable, nextProps.pxtable) && - prevProps.isMobile === nextProps.isMobile, + prevProps.isMobile === nextProps.isMobile && + prevProps.getVerticalScrollElement === nextProps.getVerticalScrollElement, ); export function Presentation({ selectedTabId, @@ -46,6 +65,12 @@ export function Presentation({ selectedVBValues, } = variables; const tableId: string = selectedTabId; + const getVerticalScrollElement = useCallback((): HTMLElement | null => { + if (!scrollRef || typeof scrollRef === 'function') { + return null; + } + return scrollRef.current; + }, [scrollRef]); const [isMissingMandatoryVariables, setIsMissingMandatoryVariables] = useState(false); const [initialRun, setInitialRun] = useState(true); @@ -270,7 +295,11 @@ export function Presentation({ ref={gradientContainerRef} >
- +
)} @@ -282,7 +311,11 @@ export function Presentation({ ref={gradientContainerRef} >
- +
)}