From ff5a27366f9c27512899e6fd34ee7ab8bfe4c59e Mon Sep 17 00:00:00 2001 From: PapeThePope Date: Sat, 16 May 2026 11:35:34 +0200 Subject: [PATCH 1/2] feat(data-grid): support inline html cells --- .../data-grid/cell-handlers/html-cell.tsx | 40 +++++++- .../src/components/data-grid/data-grid.css | 6 ++ .../components/data-grid/data-grid.spec.ts | 95 +++++++++++++++++++ .../src/components/data-grid/data-grid.tsx | 4 +- .../stories/components/data-grid/data-grid.md | 2 + .../components/data-grid/data-grid_de.md | 2 + 6 files changed, 144 insertions(+), 5 deletions(-) create mode 100644 packages/components/src/components/data-grid/data-grid.spec.ts diff --git a/packages/components/src/components/data-grid/cell-handlers/html-cell.tsx b/packages/components/src/components/data-grid/cell-handlers/html-cell.tsx index f77e71ce8e..535d783ea9 100644 --- a/packages/components/src/components/data-grid/cell-handlers/html-cell.tsx +++ b/packages/components/src/components/data-grid/cell-handlers/html-cell.tsx @@ -16,11 +16,45 @@ import { Cell } from './cell-interface'; export const HTMLCell: Cell = { defaults: {}, - getLongestContent({ rows, columnIndex }) { - // Skip check as content width is always the same + getLongestContent({ rows, columnIndex, field }) { + if (field.display === 'inline') { + let maxLength = 0; + let longestContent; + rows.forEach((row) => { + const content = row[columnIndex]; + const length = content?.textContent?.length || 0; + if (length > maxLength) { + longestContent = content; + maxLength = length; + } + }); + return longestContent; + } + + // Skip check as nested HTML content width is always the same toggle button. return rows[0][columnIndex]; }, - render: ({ content, component, localization }) => { + render: ({ field, content, component, localization }) => { + if (field.display === 'inline') { + return ( + content && ( +
{ + if (el) { + let child = el.lastElementChild; + while (child) { + el.removeChild(child); + child = el.lastElementChild; + } + el.appendChild(content); + } + }} + >
+ ) + ); + } + const getAriaLabel = () => { if (localization?.expand && localization?.collapse) { return content.isExpanded diff --git a/packages/components/src/components/data-grid/data-grid.css b/packages/components/src/components/data-grid/data-grid.css index 5584eb27e4..dc6025a5f1 100644 --- a/packages/components/src/components/data-grid/data-grid.css +++ b/packages/components/src/components/data-grid/data-grid.css @@ -308,6 +308,12 @@ width: auto; } +.tbody__html-cell--inline { + display: inline-flex; + align-items: center; + max-width: 100%; +} + .tbody__nested { white-space: nowrap; padding: 0px; diff --git a/packages/components/src/components/data-grid/data-grid.spec.ts b/packages/components/src/components/data-grid/data-grid.spec.ts new file mode 100644 index 0000000000..f8cc9f7d46 --- /dev/null +++ b/packages/components/src/components/data-grid/data-grid.spec.ts @@ -0,0 +1,95 @@ +/** + * @license + * Scale https://github.com/telekom/scale + * + * Copyright (c) 2021 Egor Kirpichev and contributors, Deutsche Telekom AG + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +import { newSpecPage } from '@stencil/core/testing'; +import { DataGrid } from './data-grid'; +import { HTMLCell } from './cell-handlers/html-cell'; + +class ResizeObserverMock { + observe() {} + unobserve() {} +} + +describe('DataGrid', () => { + beforeEach(() => { + (globalThis as any).ResizeObserver = ResizeObserverMock; + }); + + it('renders html cell content inline when display is inline', async () => { + const inlineButton = document.createElement('button'); + inlineButton.textContent = 'Open details'; + + const page = await newSpecPage({ + components: [DataGrid], + html: '', + }); + + page.root.fields = [ + { + type: 'html', + display: 'inline', + label: 'Action', + width: 120, + }, + ]; + page.root.rows = [[inlineButton]]; + + await page.waitForChanges(); + + expect(page.root.shadowRoot.querySelector('.tbody__nested')).toBeNull(); + expect( + page.root.shadowRoot.querySelector('.tbody__cell button').textContent + ).toBe('Open details'); + }); + + it('keeps html cell content nested by default', async () => { + const nestedContent = document.createElement('div'); + nestedContent.textContent = 'Nested details'; + + const page = await newSpecPage({ + components: [DataGrid], + html: '', + }); + + page.root.fields = [ + { + type: 'html', + label: 'Details', + width: 96, + }, + ]; + page.root.rows = [[nestedContent]]; + + await page.waitForChanges(); + + expect( + page.root.shadowRoot.querySelector('.tbody__cell scale-button') + ).toBeTruthy(); + expect( + page.root.shadowRoot.querySelector('.tbody__nested div').textContent + ).toBe('Nested details'); + }); + + it('uses the longest inline html content for auto width checks', () => { + const shortButton = document.createElement('button'); + shortButton.textContent = 'Open'; + const longButton = document.createElement('button'); + longButton.textContent = 'Open ticket details'; + + const longestContent = HTMLCell.getLongestContent({ + rows: [[shortButton], [longButton]], + columnIndex: 0, + field: { display: 'inline' }, + }); + + expect(longestContent).toBe(longButton); + }); +}); diff --git a/packages/components/src/components/data-grid/data-grid.tsx b/packages/components/src/components/data-grid/data-grid.tsx index 52fde0cfc7..2cd6202953 100644 --- a/packages/components/src/components/data-grid/data-grid.tsx +++ b/packages/components/src/components/data-grid/data-grid.tsx @@ -1187,8 +1187,8 @@ export class DataGrid { if (!visible) { return; } - // Add rows nested tables to array - if (field.type === 'html') { + // Add non-inline HTML cells to the nested content row. + if (field.type === 'html' && field.display !== 'inline') { if (!cellContent) { return this.renderTableCell( field, diff --git a/packages/storybook-vue/stories/components/data-grid/data-grid.md b/packages/storybook-vue/stories/components/data-grid/data-grid.md index 50b0d2a844..f134eed8e3 100644 --- a/packages/storybook-vue/stories/components/data-grid/data-grid.md +++ b/packages/storybook-vue/stories/components/data-grid/data-grid.md @@ -66,6 +66,8 @@ To aid readability, you can highlight the rows when hovering over them. If you fill in the HTML slot in a table row, it will add an expand icon at the end of the row. If users click on the expand icon, it will display the HTML content of this slot. +If you want to render an `HTMLElement` directly inside a data grid cell instead, use the `html` cell type with `display: "inline"` in the field configuration. The default behavior stays expandable nested content. + #### Pagination (8) With the help of pagination, users can move through the entire data set in a deliberate way. diff --git a/packages/storybook-vue/stories/components/data-grid/data-grid_de.md b/packages/storybook-vue/stories/components/data-grid/data-grid_de.md index 6265a349cf..311212b0a0 100644 --- a/packages/storybook-vue/stories/components/data-grid/data-grid_de.md +++ b/packages/storybook-vue/stories/components/data-grid/data-grid_de.md @@ -59,6 +59,8 @@ Der Inhalt einer Zeile bildet eine Dateneinheit und unterscheidet sich sowohl in Füllst du den HTML-Slot in einer Tabellenzeile aus, wird am Ende der Zeile ein Expand-Icon hinzugefügt. Klicken/tippen Nutzer\*innen auf das Expand-Icon, blendet sich der HTML-Inhalt dieses Slots ein. +Wenn du ein `HTMLElement` stattdessen direkt in einer Data-Grid-Zelle darstellen möchtest, verwende den `html`-Zelltyp mit `display: "inline"` in der Feldkonfiguration. Das Standardverhalten bleibt aufklappbarer Nested Content. + #### Pagination (8) Mit Hilfe der Pagination bewegen sich Nutzer\*innen gezielt durch den gesamten Datensatz. From 7c7bc6dcca3add5d10bd66eafb2a2080525a1154 Mon Sep 17 00:00:00 2001 From: PapeThePope Date: Sat, 16 May 2026 11:45:53 +0200 Subject: [PATCH 2/2] fix(data-grid): handle empty inline html content --- .../data-grid/cell-handlers/html-cell.tsx | 4 ++-- .../src/components/data-grid/data-grid.spec.ts | 13 +++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/components/src/components/data-grid/cell-handlers/html-cell.tsx b/packages/components/src/components/data-grid/cell-handlers/html-cell.tsx index 535d783ea9..30a55f8b45 100644 --- a/packages/components/src/components/data-grid/cell-handlers/html-cell.tsx +++ b/packages/components/src/components/data-grid/cell-handlers/html-cell.tsx @@ -18,12 +18,12 @@ export const HTMLCell: Cell = { defaults: {}, getLongestContent({ rows, columnIndex, field }) { if (field.display === 'inline') { - let maxLength = 0; + let maxLength = -1; let longestContent; rows.forEach((row) => { const content = row[columnIndex]; const length = content?.textContent?.length || 0; - if (length > maxLength) { + if (content && length > maxLength) { longestContent = content; maxLength = length; } diff --git a/packages/components/src/components/data-grid/data-grid.spec.ts b/packages/components/src/components/data-grid/data-grid.spec.ts index f8cc9f7d46..2060279544 100644 --- a/packages/components/src/components/data-grid/data-grid.spec.ts +++ b/packages/components/src/components/data-grid/data-grid.spec.ts @@ -92,4 +92,17 @@ describe('DataGrid', () => { expect(longestContent).toBe(longButton); }); + + it('uses inline html content with empty text for auto width checks', () => { + const iconOnlyButton = document.createElement('button'); + iconOnlyButton.setAttribute('aria-label', 'Open details'); + + const longestContent = HTMLCell.getLongestContent({ + rows: [[iconOnlyButton]], + columnIndex: 0, + field: { display: 'inline' }, + }); + + expect(longestContent).toBe(iconOnlyButton); + }); });