Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,29 @@ export const ColumnHeaderContainer = forwardRef<HTMLDivElement, ColumnHeaderCont
ref={ref}
data-component-name="AnalyticalTableHeaderRow"
>
{/* Absolute layer so resizers don't take flex space; first child so the last ColumnHeader stays :last-child for the border selectors. */}
<div className={classNames.resizerLayer} style={{ width: `${totalSize}px` }} aria-hidden="true">
{columnVirtualizer.getVirtualItems().map((virtualColumn) => {
const column = headerGroup.headers[virtualColumn.index];
if (!column || !column.canResize || !column.getResizerProps) {
return null;
}
const isStickyStart = stickyStartSet.has(virtualColumn.index);
const isLastColumn = !column.disableResizing && virtualColumn.index + 1 === headerGroup.headers.length;
const resizerEdgeOffset = virtualColumn.start + virtualColumn.size - (isLastColumn ? 3 : 0);
const resizerProps = column.getResizerProps();
return (
<div
key={`resizer-${column.id}`}
{...resizerProps}
data-resizer
data-component-name="AnalyticalTableResizer"
className={clsx(classNames.resizer, isStickyStart && classNames.resizerSticky)}
style={{ '--_ui5wcr_AnalyticalTable_ResizerOffset': `${resizerEdgeOffset}px` } as CSSProperties}
/>
);
})}
</div>
{columnVirtualizer.getVirtualItems().map((virtualColumn, index) => {
const column = headerGroup.headers[virtualColumn.index];
if (!column) {
Expand Down Expand Up @@ -73,29 +96,6 @@ export const ColumnHeaderContainer = forwardRef<HTMLDivElement, ColumnHeaderCont
</ColumnHeader>
);
})}
{/* Resizers live in a separate absolute layer so they don't take up flex space. */}
<div className={classNames.resizerLayer} style={{ width: `${totalSize}px` }} aria-hidden="true">
{columnVirtualizer.getVirtualItems().map((virtualColumn) => {
const column = headerGroup.headers[virtualColumn.index];
if (!column || !column.canResize || !column.getResizerProps) {
return null;
}
const isStickyStart = stickyStartSet.has(virtualColumn.index);
const isLastColumn = !column.disableResizing && virtualColumn.index + 1 === headerGroup.headers.length;
const resizerEdgeOffset = virtualColumn.start + virtualColumn.size - (isLastColumn ? 3 : 0);
const resizerProps = column.getResizerProps();
return (
<div
key={`resizer-${column.id}`}
{...resizerProps}
data-resizer
data-component-name="AnalyticalTableResizer"
className={clsx(classNames.resizer, isStickyStart && classNames.resizerSticky)}
style={{ '--_ui5wcr_AnalyticalTable_ResizerOffset': `${resizerEdgeOffset}px` } as CSSProperties}
/>
);
})}
</div>
</div>
);
});
Expand Down
2 changes: 1 addition & 1 deletion packages/main/src/components/AnalyticalTable/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -925,7 +925,7 @@ const AnalyticalTable = forwardRef<AnalyticalTableDomRef, AnalyticalTablePropTyp
isRtl={isRtl}
columnVirtualizer={columnVirtualizer}
uniqueId={uniqueId}
showVerticalEndBorder={showVerticalEndBorder && !showVerticalScrollbar}
showVerticalEndBorder={showVerticalEndBorder}
classNames={classNames}
stickyStartIndices={stickyStartIndices}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ const scaleCols: AnalyticalTableColumnDefinition[] = [
{ Header: 'Friend Name', accessor: 'friend.name' },
];

// 3×150 = 450px total. In a ~440px container the columns fill the width (no end border); widening past 450px
// leaves empty space so the end border appears — the last column is 'friend.name' in both states.
const borderCols: AnalyticalTableColumnDefinition[] = [
{ Header: 'Name', accessor: 'name', width: 150 },
{ Header: 'Age', accessor: 'age', width: 150 },
{ Header: 'Friend Name', accessor: 'friend.name', width: 150 },
];

const disableStickyOn =
(accessor: string) =>
(col: AnalyticalTableColumnDefinition): AnalyticalTableColumnDefinition =>
Expand All @@ -33,6 +41,11 @@ const STORY = 'AnalyticalTable/StickyHarness';
const columnHeader = (page: Page, id: string) => page.locator(`[data-column-id="${id}"]`);
const stickyAncestor = (locator: Locator) => locator.locator('xpath=ancestor-or-self::*[@data-sticky-start]');
const boxWidth = async (locator: Locator) => (await locator.boundingBox()).width;
const borderInlineEnd = (locator: Locator) =>
locator.evaluate((el) => {
const cs = getComputedStyle(el);
return `${cs.borderInlineEndWidth} ${cs.borderInlineEndStyle} ${cs.borderInlineEndColor}`;
});
const listItem = (page: Page, text: string) => page.locator(`[ui5-li][text="${text}"]`);
const popover = (page: Page) => page.locator('[data-component-name="ATHeaderPopover"]');
const openHeaderPopover = (page: Page, text: string) => page.getByText(text, { exact: true }).click();
Expand Down Expand Up @@ -380,3 +393,42 @@ test.describe('AnalyticalTable', () => {
expect(firstRenderedColumnIndex).toBe(0);
});
});

test.describe('AnalyticalTable — last header cell end border', () => {
// Regression guard: the sticky feature added an absolute `.resizerLayer`. If it renders as the last child of the
// header row, `.thContainer:last-child` no longer matches the last column and the border logic breaks.
test('resizer layer is not the last child of the header row', async ({ mount, page }) => {
await mount<typeof StickyHarness>(STORY, { columns: borderCols, withHook: false });
await expect(columnHeader(page, 'friend.name')).toBeVisible();
const lastChildComponent = await page
.locator('[data-component-name="AnalyticalTableHeaderRow"]')
.first()
.evaluate((el) => el.lastElementChild?.getAttribute('data-component-name') ?? null);
expect(lastChildComponent).toMatch(/^ATHeaderContainer/);
});

// The last header cell must mirror the last body cell: no end border when the columns fill the table width,
// a border when they don't — the exact behavior the fix restores.
test('mirrors the body: no border when columns fill the width, border when they do not', async ({ mount, page }) => {
await mount<typeof StickyHarness>(STORY, {
columns: borderCols,
withHook: false,
resizable: true,
containerWidth: '440px', // 450px of columns → they fill the width, so no end border
narrowWidth: '440px',
wideWidth: '900px',
});
const lastHeader = columnHeader(page, 'friend.name');
const lastCell = page.locator('[data-column-id-cell="friend.name"]').first();
await expect(lastHeader).toBeVisible();

// Columns fill the width → last header matches the body (both use the transparent outer-cell border).
const headerFilled = await borderInlineEnd(lastHeader);
expect(headerFilled).toBe(await borderInlineEnd(lastCell));

// Widen so the columns no longer fill the width → an end border appears, still matching the body.
await page.getByTestId('set-wide').click();
await expect.poll(() => borderInlineEnd(lastHeader)).not.toBe(headerFilled);
expect(await borderInlineEnd(lastHeader)).toBe(await borderInlineEnd(lastCell));
});
});
Loading