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 @@ -1319,3 +1319,64 @@ export const ColumnClassName: Story = {
);
},
};

const borderlessColumns: AnalyticalTableColumnDefinition[] = [
{ Header: 'Name', accessor: 'name' },
{ Header: 'Age', accessor: 'age' },
{ Header: 'Friend Name', accessor: 'friend.name' },
{ Header: 'Friend Age', accessor: 'friend.age' },
];

export const BorderlessContainer: Story = {
tags: ['custom'],
render() {
return (
<>
<style>{`
/* Removes only the outer container frame. The class is applied to the table's
root element, so it also covers the sibling vertical scrollbar. */
.at-borderless-container {
--_ui5wcr-AnalyticalTable-OuterBorderBlock: none;
--_ui5wcr-AnalyticalTable-OuterBorderInline: none;
--sapList_TableFooterBorder: transparent;
}
.at-borderless-container [data-component-name="AnalyticalTableContainer"]::after {
display: none;
}
`}</style>
<AnalyticalTable
className="at-borderless-container"
data={dataSmall}
columns={borderlessColumns}
selectionMode={AnalyticalTableSelectionMode.Multiple}
visibleRows={5}
/>
</>
);
},
};

export const NoVerticalCellBorders: Story = {
tags: ['custom'],
render() {
return (
<>
<style>{`
/* Removes the vertical dividers between columns (header + body) while keeping
the horizontal row separators. */
.at-no-vertical-cell-borders [role="columnheader"],
.at-no-vertical-cell-borders [data-column-id-cell] {
border-inline-end: none;
}
`}</style>
<AnalyticalTable
className="at-no-vertical-cell-borders"
data={dataSmall}
columns={borderlessColumns}
selectionMode={AnalyticalTableSelectionMode.Multiple}
visibleRows={5}
/>
</>
);
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -1053,4 +1053,40 @@ const columns = [
<AnalyticalTable data={data} columns={columns} />
```

## Custom Border Styles

> **⚠️ Not backed by SAP Design:** there are no design specifications for a fully borderless Grid/AnalyticalTable — for example the [`sap.ui.table.Table`](https://ui5.sap.com/#/entity/sap.ui.table.Table) always renders its borders and exposes no API to remove them. Customizing them is therefore your own responsibility and can lead to visual inconsistencies with other tables in the system. The recipes below also rely on internal CSS custom properties and `data-*` hooks that may change between releases. Use with caution.

There is no public prop for customizing the table's borders. The two opt-in recipes below apply a CSS class to the table — one removes the outer container frame, the other removes the vertical dividers between columns. Each class is applied to the table's root element.

### Borderless Container

Removes the outer container frame (top, inline-end and bottom borders). The class also covers the sibling vertical scrollbar; the cell and row borders are kept.

<Canvas sourceState="none" of={FeatureStories.BorderlessContainer} />

```css
.at-borderless-container {
--_ui5wcr-AnalyticalTable-OuterBorderBlock: none;
--_ui5wcr-AnalyticalTable-OuterBorderInline: none;
--sapList_TableFooterBorder: transparent;
}
.at-borderless-container [data-component-name='AnalyticalTableContainer']::after {
display: none;
}
```

### Table Without Vertical Cell Borders

Removes the vertical dividers between columns (header and body) while keeping the horizontal row separators.

<Canvas sourceState="none" of={FeatureStories.NoVerticalCellBorders} />

```css
.at-no-vertical-cell-borders [role='columnheader'],
.at-no-vertical-cell-borders [data-column-id-cell] {
border-inline-end: none;
}
```

<Footer />
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useCallback, useMemo, useRef, useState } from 'react';
import { AnalyticalTableSelectionMode } from '../../../enums/AnalyticalTableSelectionMode.js';
import { Panel } from '../../../webComponents/Panel/index.js';
import { Tab } from '../../../webComponents/Tab/index.js';
import { TabContainer } from '../../../webComponents/TabContainer/index.js';
Expand Down Expand Up @@ -211,3 +212,63 @@ export const ColumnsFitWidthHideRevealTestComp = () => {
</div>
);
};

const borderlessData = Array.from({ length: 20 }, (_, i) => ({
name: `Name-${i}`,
age: i,
friend: { name: `Friend-${i}`, age: i + 100 },
}));

const borderlessColumns: AnalyticalTableColumnDefinition[] = [
{ Header: 'Name', accessor: 'name' },
{ Header: 'Age', accessor: 'age' },
{ Header: 'Friend Name', accessor: 'friend.name' },
];

// A/B harness for the documented borderless recipes: `default` is the untouched baseline, `outer` removes the
// container frame, `vertical` removes the inter-column cell borders. 20 rows / visibleRows 5 → the custom vertical
// scrollbar renders.
export const BorderlessHarness = () => (
<>
<style>{`
.at-borderless-container {
--_ui5wcr-AnalyticalTable-OuterBorderBlock: none;
--_ui5wcr-AnalyticalTable-OuterBorderInline: none;
--sapList_TableFooterBorder: transparent;
}
.at-borderless-container [data-component-name="AnalyticalTableContainer"]::after {
display: none;
}
.at-no-vertical-cell-borders [role="columnheader"],
.at-no-vertical-cell-borders [data-column-id-cell] {
border-inline-end: none;
}
`}</style>
<div data-testid="default">
<AnalyticalTable
data={borderlessData}
columns={borderlessColumns}
selectionMode={AnalyticalTableSelectionMode.Multiple}
visibleRows={5}
/>
</div>
<div data-testid="outer">
<AnalyticalTable
className="at-borderless-container"
data={borderlessData}
columns={borderlessColumns}
selectionMode={AnalyticalTableSelectionMode.Multiple}
visibleRows={5}
/>
</div>
<div data-testid="vertical">
<AnalyticalTable
className="at-no-vertical-cell-borders"
data={borderlessData}
columns={borderlessColumns}
selectionMode={AnalyticalTableSelectionMode.Multiple}
visibleRows={5}
/>
</div>
</>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import type { Locator } from '@playwright/test';
import { expect, test } from '../../../../../../playwright/fixtures/gallery-fixtures.js';
import type { BorderlessHarness } from './AnalyticalTable.gallery.js';

const STORY = 'AnalyticalTable/BorderlessHarness';

const container = (scope: Locator) => scope.locator('[data-component-name="AnalyticalTableContainer"]');
const afterDisplay = (scope: Locator) => container(scope).evaluate((el) => getComputedStyle(el, '::after').display);
// "Age" is a middle column, so its body cell always has the inter-column divider (border-inline-end).
const cellDivider = (scope: Locator) =>
scope
.locator('[data-column-id-cell="age"]')
.first()
.evaluate((el) => getComputedStyle(el).borderInlineEndStyle);

// Two documented, class-based recipes, each asserted against the untouched baseline so the test can't pass on a
// no-op: `at-borderless-container` removes only the outer frame (keeping cell borders); `at-no-vertical-cell-borders`
// removes only the inter-column dividers (keeping the frame).
test.describe('AnalyticalTable borderless recipes', () => {
test('borderless container removes the outer ::after line but keeps cell dividers', async ({ mount, page }) => {
await mount<typeof BorderlessHarness>(STORY);
await expect(page.getByText('Name-0').first()).toBeVisible();

expect(await afterDisplay(page.getByTestId('outer'))).toBe('none');
expect(await afterDisplay(page.getByTestId('default'))).not.toBe('none');
// cell dividers must stay
expect(await cellDivider(page.getByTestId('outer'))).toBe('solid');
});

test('no-vertical-cell-borders removes the dividers but keeps the outer frame', async ({ mount, page }) => {
await mount<typeof BorderlessHarness>(STORY);
await expect(page.getByText('Name-0').first()).toBeVisible();

expect(await cellDivider(page.getByTestId('vertical'))).toBe('none');
expect(await cellDivider(page.getByTestId('default'))).toBe('solid');
// the outer frame must stay
expect(await afterDisplay(page.getByTestId('vertical'))).not.toBe('none');
});

test('the outer-border custom properties the recipe relies on still resolve to "none" in Horizon', async ({
mount,
page,
}) => {
await mount<typeof BorderlessHarness>(STORY);
await expect(page.getByText('Name-0').first()).toBeVisible();

// Read the theme-provided values off the untouched baseline (not the recipe), so this breaks if the vars are
// renamed/removed (getPropertyValue → '') or if Horizon stops defaulting them to `none`.
const vars = await container(page.getByTestId('default')).evaluate((el) => {
const cs = getComputedStyle(el);
return {
block: cs.getPropertyValue('--_ui5wcr-AnalyticalTable-OuterBorderBlock').trim(),
inline: cs.getPropertyValue('--_ui5wcr-AnalyticalTable-OuterBorderInline').trim(),
};
});
expect(vars.block).toBe('none');
expect(vars.inline).toBe('none');
});

test('borderless container makes the vertical scrollbar bottom border transparent', async ({ mount, page }) => {
await mount<typeof BorderlessHarness>(STORY);
await expect(page.getByText('Name-0').first()).toBeVisible();

const scrollbar = page.getByTestId('outer').locator('[data-component-name="AnalyticalTableVerticalScrollbar"]');
// The custom scrollbar only renders with native (non-overlay) scrollbars; skip the assertion otherwise.
if ((await scrollbar.count()) === 0) {
test.skip(true, 'custom vertical scrollbar not rendered (overlay scrollbars)');
}
const color = await scrollbar.evaluate((el) => getComputedStyle(el).borderBlockEndColor);
expect(color).toBe('rgba(0, 0, 0, 0)');
});
});
Loading