Skip to content

Commit 4c393db

Browse files
authored
feat(AnalyticalTable): add scaleWidthModeOptions column option (#5188)
Closes #5189
1 parent a2bb5f6 commit 4c393db

File tree

5 files changed

+192
-65
lines changed

5 files changed

+192
-65
lines changed

packages/main/src/components/AnalyticalTable/AnalyticalTable.cy.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -887,7 +887,7 @@ describe('AnalyticalTable', () => {
887887
cy.get('[data-column-id="name"]').invoke('outerWidth').should('equal', 700);
888888

889889
cy.findByText('Custom maxWidth').click();
890-
cy.get('[data-column-id="name"]').invoke('outerWidth').should('equal', 5008);
890+
cy.get('[data-column-id="name"]').invoke('outerWidth').should('equal', 3824);
891891
});
892892

893893
it('Column Scaling: programatically change cols', () => {

packages/main/src/components/AnalyticalTable/AnayticalTable.jss.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,10 +239,13 @@ const styles = {
239239
},
240240
hiddenSmartColMeasure: {
241241
visibility: 'hidden',
242-
position: 'absolute',
242+
position: 'fixed',
243243
whiteSpace: 'nowrap',
244244
height: 0
245245
},
246+
hiddenSmartColMeasureHeader: {
247+
fontFamily: CustomThemingParameters.AnalyticalTableHeaderFontFamily
248+
},
246249
hiddenA11yText: {
247250
display: 'none'
248251
},

packages/main/src/components/AnalyticalTable/Recipes.mdx

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,4 +159,68 @@ Please note that the internal react-table is resetting its hidden state after hi
159159
/>
160160
```
161161

162+
## How to scale custom cells?
163+
164+
Scaling of custom cells is not supported when used with `scaleWidthMode: AnalyticalTableScaleWidthMode.Grow` or `scaleWidthMode: AnalyticalTableScaleWidthMode.Smart`, as we're using the text of the cell for our calculation.
165+
Since v1.22.0 you can use the column option `scaleWidthModeOptions` to pass a string for the internal width calculation of the header cell (`headerString`) and the body cells (`cellString`).
166+
167+
```jsx
168+
const columns = [
169+
// This column is automatically scaled because the header and body cells consists of text content only.
170+
{ Header: 'Text Content', accessor: 'name' },
171+
{
172+
accessor: 'age',
173+
// The header isn't included in the calculation of the column width, because it contains a custom component.
174+
Header: () => (
175+
<FlexBox direction={FlexBoxDirection.Column}>
176+
<Text wrapping={false}>Long header text in a vertical layout</Text>
177+
<Link>Click me!</Link>
178+
</FlexBox>
179+
),
180+
scaleWidthModeOptions: {
181+
headerString: 'Long header text in a vertical layout'
182+
}
183+
},
184+
{
185+
id: '1',
186+
// The body cells aren't included in the calculation of the column width, because they contain a custom component.
187+
Header: 'Custom cell',
188+
Cell: () => (
189+
<FlexBox direction={FlexBoxDirection.Column}>
190+
<Text wrapping={false}>Long body cell text in a vertical layout</Text>
191+
<Link>Click me!</Link>
192+
</FlexBox>
193+
),
194+
scaleWidthModeOptions: {
195+
cellString: 'Long body cell text in a vertical layout'
196+
}
197+
},
198+
{
199+
id: '2',
200+
// Neither the header nor the body cells are included in the calculation of the column width,
201+
// because they contain a custom component.
202+
Header: () => (
203+
<FlexBox direction={FlexBoxDirection.Column}>
204+
<Text wrapping={false}>Long header text in a vertical layout</Text>
205+
<Link>Click me!</Link>
206+
</FlexBox>
207+
),
208+
Cell: () => (
209+
<FlexBox direction={FlexBoxDirection.Column}>
210+
<Text wrapping={false}>Long body cell text in a vertical layout</Text>
211+
<Link>Click me!</Link>
212+
</FlexBox>
213+
),
214+
scaleWidthModeOptions: {
215+
headerString: 'Long header text in a vertical layout',
216+
cellString: 'Long body cell text in a vertical layout'
217+
}
218+
}
219+
];
220+
```
221+
222+
```jsx
223+
<AnalyticalTable data={props.data} columns={columns} scaleWidthMode={AnalyticalTableScaleWidthMode.Smart} />
224+
```
225+
162226
<Footer />

packages/main/src/components/AnalyticalTable/hooks/useDynamicColumnWidths.ts

Lines changed: 79 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,40 @@ import { DEFAULT_COLUMN_WIDTH } from '../defaults/Column/index.js';
44
import type { AnalyticalTableColumnDefinition } from '../index.js';
55

66
const ROW_SAMPLE_SIZE = 20;
7-
const DEFAULT_HEADER_NUM_CHAR = 10;
87
const MAX_WIDTH = 700;
98
const CELL_PADDING_PX = 18; /* padding left and right 0.5rem each (16px) + borders (1px) + buffer (1px) */
109

11-
// a function, which approximates header px sizes given a character length
12-
const approximateHeaderPxFromCharLength = (charLength) =>
13-
charLength < 15 ? Math.sqrt(charLength * 1500) : 8 * charLength;
14-
const approximateContentPxFromCharLength = (charLength) => 8 * charLength;
10+
function findLongestString(str1, str2) {
11+
if (typeof str1 !== 'string' || typeof str2 !== 'string') {
12+
return str1 || str2 || undefined;
13+
}
14+
15+
return str1.length > str2.length ? str1 : str2;
16+
}
17+
18+
function getContentPxAvg(rowSample, columnIdOrAccessor, uniqueId) {
19+
return (
20+
rowSample.reduce((acc, item) => {
21+
const dataPoint = item.values?.[columnIdOrAccessor];
22+
23+
let val = 0;
24+
if (dataPoint) {
25+
val = stringToPx(dataPoint, uniqueId) + CELL_PADDING_PX;
26+
}
27+
return acc + val;
28+
}, 0) / (rowSample.length || 1)
29+
);
30+
}
31+
32+
function stringToPx(dataPoint, id, isHeader = false) {
33+
const elementId = isHeader ? 'scaleModeHelperHeader' : 'scaleModeHelper';
34+
const ruler = document.getElementById(`${elementId}-${id}`);
35+
if (ruler) {
36+
ruler.innerHTML = `${dataPoint}`;
37+
return ruler.scrollWidth;
38+
}
39+
return 0;
40+
}
1541

1642
const columnsDeps = (
1743
deps,
@@ -52,14 +78,7 @@ interface IColumnMeta {
5278
headerPx: number;
5379
headerDefinesWidth?: boolean;
5480
}
55-
const stringToPx = (dataPoint, id) => {
56-
const ruler = document.getElementById(`smartScaleModeHelper-${id}`);
57-
if (ruler) {
58-
ruler.innerHTML = `${dataPoint}`;
59-
return ruler.offsetWidth;
60-
}
61-
return 0;
62-
};
81+
6382
const smartColumns = (columns: AnalyticalTableColumnDefinition[], instance, hiddenColumns) => {
6483
const { rows, state, webComponentsReactProperties } = instance;
6584
const rowSample = rows.slice(0, ROW_SAMPLE_SIZE);
@@ -84,22 +103,31 @@ const smartColumns = (columns: AnalyticalTableColumnDefinition[], instance, hidd
84103
return metadata;
85104
}
86105

87-
const contentPxAvg =
88-
rowSample.reduce((acc, item) => {
89-
const dataPoint = item.values?.[columnIdOrAccessor];
90-
let val = 0;
91-
if (dataPoint) {
92-
val = stringToPx(dataPoint, webComponentsReactProperties.uniqueId) + CELL_PADDING_PX;
93-
}
94-
return acc + val;
95-
}, 0) / (rowSample.length || 1);
106+
let headerPx, contentPxAvg;
96107

97-
metadata[columnIdOrAccessor] = {
98-
headerPx:
108+
if (column.scaleWidthModeOptions?.cellString) {
109+
contentPxAvg =
110+
stringToPx(column.scaleWidthModeOptions.cellString, webComponentsReactProperties.uniqueId) + CELL_PADDING_PX;
111+
} else {
112+
contentPxAvg = getContentPxAvg(rowSample, columnIdOrAccessor, webComponentsReactProperties.uniqueId);
113+
}
114+
115+
if (column.scaleWidthModeOptions?.headerString) {
116+
headerPx = Math.max(
117+
stringToPx(column.scaleWidthModeOptions.headerString, webComponentsReactProperties.uniqueId, true) +
118+
CELL_PADDING_PX,
119+
60
120+
);
121+
} else {
122+
headerPx =
99123
typeof column.Header === 'string'
100-
? Math.max(stringToPx(column.Header, webComponentsReactProperties.uniqueId) + CELL_PADDING_PX, 60)
101-
: 60,
102-
contentPxAvg: contentPxAvg
124+
? Math.max(stringToPx(column.Header, webComponentsReactProperties.uniqueId, true) + CELL_PADDING_PX, 60)
125+
: 60;
126+
}
127+
128+
metadata[columnIdOrAccessor] = {
129+
headerPx,
130+
contentPxAvg
103131
};
104132
return metadata;
105133
},
@@ -142,9 +170,10 @@ const smartColumns = (columns: AnalyticalTableColumnDefinition[], instance, hidd
142170
if (meta && !column.minWidth && !column.width && !meta.headerDefinesWidth) {
143171
let targetWidth;
144172
const { contentPxAvg, headerPx } = meta;
173+
145174
if (availableWidthPrio1 > 0) {
146175
const factor = contentPxAvg / totalContentPxAvgPrio1;
147-
targetWidth = Math.min(availableWidthPrio1 * factor, contentPxAvg);
176+
targetWidth = Math.max(Math.min(availableWidthPrio1 * factor, contentPxAvg), headerPx);
148177
availableWidthPrio2 -= targetWidth;
149178
}
150179
return {
@@ -183,7 +212,7 @@ const columns = (columns: AnalyticalTableColumnDefinition[], { instance }) => {
183212
}
184213
const { rows, state } = instance;
185214
const { hiddenColumns, tableClientWidth: totalWidth } = state;
186-
const { scaleWidthMode, loading } = instance.webComponentsReactProperties;
215+
const { scaleWidthMode, loading, uniqueId } = instance.webComponentsReactProperties;
187216

188217
if (columns.length === 0 || !totalWidth || !AnalyticalTableScaleWidthMode[scaleWidthMode]) {
189218
return columns;
@@ -296,52 +325,41 @@ const columns = (columns: AnalyticalTableColumnDefinition[], { instance }) => {
296325
const rowSample = rows.slice(0, ROW_SAMPLE_SIZE);
297326

298327
const columnMeta = visibleColumns.reduce((acc, column) => {
328+
const columnIdOrAccessor = (column.id ?? column.accessor) as string;
299329
if (
300330
column.id === '__ui5wcr__internal_selection_column' ||
301331
column.id === '__ui5wcr__internal_highlight_column' ||
302332
column.id === '__ui5wcr__internal_navigation_column'
303333
) {
304-
acc[column.id ?? column.accessor] = {
334+
acc[columnIdOrAccessor] = {
305335
minHeaderWidth: column.width,
306-
fullWidth: column.width,
307-
contentCharAvg: 0
336+
fullWidth: column.width
308337
};
309338
return acc;
310339
}
311340

312-
const headerLength = typeof column.Header === 'string' ? column.Header.length : DEFAULT_HEADER_NUM_CHAR;
313-
314-
// max character length
315-
const contentMaxCharLength = Math.max(
316-
headerLength,
317-
...rowSample.map((row) => {
318-
const dataPoint = row.values?.[column.id ?? column.accessor];
319-
if (dataPoint) {
320-
if (typeof dataPoint === 'string') return dataPoint.length;
321-
if (typeof dataPoint === 'number') return (dataPoint + '').length;
322-
}
323-
return 0;
324-
})
341+
const smartWidth = findLongestString(
342+
column.scaleWidthModeOptions?.headerString,
343+
column.scaleWidthModeOptions?.cellString
325344
);
326345

327-
// avg character length
328-
const contentCharAvg =
329-
rowSample.reduce((acc, item) => {
330-
const dataPoint = item.values?.[column.id ?? column.accessor];
331-
let val = 0;
332-
if (dataPoint) {
333-
if (typeof dataPoint === 'string') val = dataPoint.length;
334-
if (typeof dataPoint === 'number') val = (dataPoint + '').length;
335-
}
336-
return acc + val;
337-
}, 0) / rowSample.length;
346+
if (smartWidth) {
347+
const width = Math.max(stringToPx(smartWidth, uniqueId) + CELL_PADDING_PX, 60);
348+
acc[columnIdOrAccessor] = {
349+
minHeaderWidth: width,
350+
fullWidth: width
351+
};
352+
return acc;
353+
}
338354

339-
const minHeaderWidth = approximateHeaderPxFromCharLength(headerLength);
355+
const minHeaderWidth =
356+
typeof column.Header === 'string'
357+
? stringToPx(column.Header, uniqueId, true) + CELL_PADDING_PX
358+
: DEFAULT_COLUMN_WIDTH;
340359

341-
acc[column.id ?? column.accessor] = {
360+
acc[columnIdOrAccessor] = {
342361
minHeaderWidth,
343-
fullWidth: Math.max(minHeaderWidth, approximateContentPxFromCharLength(contentMaxCharLength)),
344-
contentCharAvg
362+
fullWidth: Math.max(minHeaderWidth, getContentPxAvg(rowSample, columnIdOrAccessor, uniqueId))
345363
};
346364
return acc;
347365
}, {});
@@ -367,6 +385,7 @@ const columns = (columns: AnalyticalTableColumnDefinition[], { instance }) => {
367385
return columns.map((column) => {
368386
const isColumnVisible = (column.isVisible ?? true) && !hiddenColumns.includes(column.id ?? column.accessor);
369387
const meta = columnMeta[column.id ?? (column.accessor as string)];
388+
370389
if (isColumnVisible && meta) {
371390
const { minHeaderWidth } = meta;
372391

@@ -378,7 +397,6 @@ const columns = (columns: AnalyticalTableColumnDefinition[], { instance }) => {
378397
minWidth: column.minWidth ?? minHeaderWidth
379398
};
380399
}
381-
382400
return column;
383401
});
384402
}

packages/main/src/components/AnalyticalTable/index.tsx

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,25 @@ import { TitleBar } from './TitleBar/index.js';
8989
import { getRowHeight, getSubRowsByString, tagNamesWhichShouldNotSelectARow } from './util/index.js';
9090
import { VerticalResizer } from './VerticalResizer.js';
9191

92+
interface ScaleWidthModeOptions {
93+
/**
94+
* Defines the string used for internal width calculation of custom header cells (e.g. `Header: () => <Link>Click me!</Link>`).
95+
*
96+
* You can find out more about it [here](https://sap.github.io/ui5-webcomponents-react/?path=/docs/data-display-analyticaltable-recipes--docs#how-to-scale-custom-cells).
97+
*
98+
* __Note:__ This property has no effect when used with `AnalyticalTableScaleWidthMode.Default`.
99+
*/
100+
headerString?: string;
101+
/**
102+
* Defines the string used for internal width calculation of the longest cell inside the body of the table (e.g. `Cell: () => <Link>Click me!</Link>`).
103+
*
104+
* You can find out more about it [here](https://sap.github.io/ui5-webcomponents-react/?path=/docs/data-display-analyticaltable-recipes--docs#how-to-scale-custom-cells).
105+
*
106+
* __Note:__ This property has no effect when used with `AnalyticalTableScaleWidthMode.Default`.
107+
*/
108+
cellString?: string;
109+
}
110+
92111
export interface AnalyticalTableColumnDefinition {
93112
// base properties
94113
/**
@@ -227,6 +246,16 @@ export interface AnalyticalTableColumnDefinition {
227246
* Vertical alignment of the cell.
228247
*/
229248
vAlign?: VerticalAlign;
249+
/**
250+
* Allows passing a custom string for the internal width calculation of custom cells for `scaleWidthMode` `Grow` and `Smart`.
251+
*
252+
* You can find out more about it here [here](https://sap.github.io/ui5-webcomponents-react/?path=/docs/data-display-analyticaltable-recipes--docs#how-to-scale-custom-cells).
253+
*
254+
* __Note:__ This property has no effect when used with `AnalyticalTableScaleWidthMode.Default`.
255+
*
256+
* @since 1.22.0
257+
*/
258+
scaleWidthModeOptions: ScaleWidthModeOptions;
230259

231260
// usePopIn
232261
/**
@@ -444,7 +473,7 @@ export interface AnalyticalTablePropTypes extends Omit<CommonProps, 'title'> {
444473
*
445474
* __Default:__ `"Default"`
446475
*
447-
* __Note:__ Custom cells with components instead of text as children are ignored by the `Smart` and `Grow` modes.
476+
* __Note:__ Custom cells with components instead of text as children are ignored by the `Smart` and `Grow` modes. To support them you can use the `scaleWidthModeOptions` column option.
448477
* __Note:__ For performance reasons, the `Smart` and `Grow` modes base their calculation for table cell width on a subset of column cells. If the first 20 cells of a column are significantly smaller than the rest of the column cells, the content may still not be fully displayed for all cells.
449478
*
450479
*/
@@ -1274,7 +1303,20 @@ const AnalyticalTable = forwardRef<AnalyticalTableDomRef, AnalyticalTablePropTyp
12741303
/>
12751304
)}
12761305
</div>
1277-
<Text aria-hidden="true" id={`smartScaleModeHelper-${uniqueId}`} className={classes.hiddenSmartColMeasure}>
1306+
<Text
1307+
aria-hidden="true"
1308+
id={`scaleModeHelper-${uniqueId}`}
1309+
className={classes.hiddenSmartColMeasure}
1310+
data-component-name="AnalyticalTableScaleModeHelper"
1311+
>
1312+
{''}
1313+
</Text>
1314+
<Text
1315+
aria-hidden="true"
1316+
id={`scaleModeHelperHeader-${uniqueId}`}
1317+
className={clsx(classes.hiddenSmartColMeasure, classes.hiddenSmartColMeasureHeader)}
1318+
data-component-name="AnalyticalTableScaleModeHelperHeader"
1319+
>
12781320
{''}
12791321
</Text>
12801322
</>

0 commit comments

Comments
 (0)