Skip to content
Closed
26 changes: 13 additions & 13 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
"devDependencies": {
"@babel/core": "^7.26.9",
"@babel/preset-env": "^7.26.9",
"@playwright/test": "^1.50.1",
"@playwright/test": "^1.60.0",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-terser": "^0.4.4",
"@types/node": "^22.13.5",
Expand Down
1 change: 1 addition & 0 deletions src/js/core/CoreFeature.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export default class CoreFeature{
//////////////// Layout /////////////////
//////////////////////////////////////////

/** @returns {("fitData" | "fitDataFill" | "fitDataTable" | "fitDataStretch" | "fitColumns")} */
layoutMode(){
return this.table.modules.layout.getMode();
}
Expand Down
16 changes: 13 additions & 3 deletions src/js/core/tools/Popup.js
Original file line number Diff line number Diff line change
Expand Up @@ -203,13 +203,23 @@ export default class Popup extends CoreFeature{
case "bottom":
this.element.style.top = (parseInt(this.element.style.top) - this.element.offsetHeight - parentEl.offsetHeight - 1) + "px";
break;

default:
this.element.style.top = (parseInt(this.element.style.top) - this.element.offsetHeight + parentEl.offsetHeight + 1) + "px";
}

}else{
this.element.style.height = offsetHeight + "px";
let menuHeight = this.element.offsetHeight;
if(menuHeight > offsetHeight){
this.element.style.top = "0px";
this.element.style.height = offsetHeight + "px";
}else{
let newTop = y - menuHeight;
if(newTop < 0){
newTop = offsetHeight - menuHeight;
}
this.element.style.top = newTop + "px";
}
}
}
}
Expand Down
4 changes: 3 additions & 1 deletion src/js/modules/GroupRows/Group.js
Original file line number Diff line number Diff line change
Expand Up @@ -592,7 +592,9 @@ export default class Group{

reinitializeHeight(){}

calcHeight(){}
calcHeight(){
this.outerHeight = this.element.offsetHeight;
}

setCellHeight(){}

Expand Down
20 changes: 9 additions & 11 deletions src/js/modules/GroupRows/GroupRows.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export default class GroupRows extends Module{
this.subscribe("rows-sample", this.rowSample.bind(this));

this.subscribe("render-virtual-fill", this.virtualRenderFill.bind(this));
this.subscribe("table-layout", this.virtualRenderFill.bind(this));

this.registerDisplayHandler(this.displayHandler, 20);

Expand Down Expand Up @@ -191,17 +192,14 @@ export default class GroupRows extends Module{
}

virtualRenderFill(){
var el = this.table.rowManager.tableElement;
var rows = this.table.rowManager.getVisibleRows();

if(this.table.options.groupBy){
rows = rows.filter((row) => {
return row.type !== "group";
});

el.style.minWidth = !rows.length ? this.table.columnManager.getWidth() + "px" : "";
}else{
return rows;
const layout = this.layoutMode();
if (
layout === "fitDataFill"
|| layout === "fitDataStretch"
|| layout === "fitColumns"
) {
this.table.rowManager.tableElement.style.minWidth =
this.table.columnManager.getWidth() + "px";
}
}

Expand Down
55 changes: 55 additions & 0 deletions test/e2e/group-scroll.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<title>Tabulator Group Scroll Test</title>
<link rel="stylesheet" href="../../dist/css/tabulator.min.css" />
<script src="../../dist/js/tabulator.js"></script>
<style>
body {
padding: 20px;
font-family: Arial, sans-serif;
}
#test-table {
width: 600px;
}
</style>
</head>
<body>
<h1>Tabulator Group Scroll Test</h1>
<div id="test-table"></div>

<script>
function generateData() {
const groupCount = 1000;
const rowsPerGroup = 3;
const data = [];
let id = 1;
for (let g = 0; g < groupCount; g++) {
for (let r = 0; r < rowsPerGroup; r++) {
data.push({
id: id++,
category: "Group " + g,
name: "Row " + g + "-" + r,
value: Math.round(Math.random() * 1000),
});
}
}
return data;
}

window.testTable = new Tabulator("#test-table", {
data: generateData(),
columns: [
{ title: "Name", field: "name" },
{ title: "Category", field: "category" },
{ title: "Value", field: "value", sorter: "number" },
],
height: "300px",
layout: "fitColumns",
groupBy: "category",
groupStartOpen: false,
});
</script>
</body>
</html>
90 changes: 90 additions & 0 deletions test/e2e/group-scroll.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { test, expect } from "@playwright/test";
import { join } from "path";

const holderSel = ".tabulator-tableholder";

// - #4219 (cannot scroll to the bottom)
// - #4525 (expanding a group resets the scroll position)
test.describe("Grouped table virtual scrolling (#4219, #4525)", () => {
test.beforeEach(async ({ page }) => {
await page.goto(`file://${join(__dirname, "group-scroll.html")}`);
await page.waitForSelector(".tabulator-row");
// position the pointer over the table so wheel events scroll it
await page.locator(holderSel).hover();
});

async function atBottom(page) {
return page.evaluate((sel) => {
const holder = document.querySelector(sel);
return holder.scrollTop >= holder.scrollHeight - holder.clientHeight - 1;
}, holderSel);
}

test("scrolls to the bottom through many collapsed groups (#4219)", async ({
page,
}) => {
// Scroll down with the mouse wheel until we reach the bottom or give up.
for (let i = 0; i < 250; i++) {
if (await atBottom(page)) {
break;
}
await page.mouse.wheel(0, 300);
await page.waitForTimeout(12);
}

const result = await page.evaluate((sel) => {
const holder = document.querySelector(sel);
return {
scrollTop: holder.scrollTop,
maxScroll: holder.scrollHeight - holder.clientHeight,
};
}, holderSel);

// Before the fix the view snapped back to the top after a few hundred
// pixels and never reached the bottom of a ~25,000px tall table.
expect(result.scrollTop).toBeGreaterThan(result.maxScroll * 0.9);
});

test("expanding a group below the fold keeps the scroll position (#4525)", async ({
page,
}) => {
// Wheel into the middle of the table, away from the top.
for (let i = 0; i < 45; i++) {
await page.mouse.wheel(0, 300);
await page.waitForTimeout(12);
}
await page.waitForTimeout(150);

const result = await page.evaluate((sel) => {
const holder = document.querySelector(sel);
const before = holder.scrollTop;

const groups = window.testTable.getGroups();
groups[Math.floor(groups.length / 2) + 2].toggle();

return new Promise((resolve) => {
setTimeout(() => resolve({ before, after: holder.scrollTop }), 200);
});
}, holderSel);

// Before the fix expanding a group reset the scroll position to 0.
expect(result.before).toBeGreaterThan(0);
expect(result.after).toBeGreaterThan(result.before * 0.8);
});

test("should not shrink group rows and their content horizontally when scrolling down (#4877)", async ({ page }) => {
const borderOffset = 2;

await page.evaluate(() => window.testTable.getGroups()[0].toggle());
await page.waitForTimeout(200);

await page.mouse.wheel(0, 400);
await page.waitForTimeout(200);

const [tableWidth, groupWidth] = await page.evaluate(() => [
window.testTable.element.offsetWidth,
window.testTable.getGroups()[18].getElement().offsetWidth,
]);
expect(groupWidth).toBe(tableWidth - borderOffset);
});
});
61 changes: 61 additions & 0 deletions test/e2e/menu.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<title>Tabulator Menu Test</title>
<link rel="stylesheet" href="../../dist/css/tabulator.min.css" />
<script src="../../dist/js/tabulator.js"></script>
<style>
html, body {
margin: 0;
padding: 0;
height: 100%;
overflow: hidden;
font-family: Arial, sans-serif;
}
#test-table {
width: 100vw;
height: 100vh;
}
</style>
</head>
<body>
<div id="test-table"></div>

<script>
document.addEventListener("DOMContentLoaded", function () {
const testData = [];
for (let i = 1; i <= 50; i++) {
testData.push({
id: i,
name: "Person " + i,
age: 20 + (i % 50),
gender: i % 2 ? "Male" : "Female",
});
}

const columns = [
{ title: "ID", field: "id", sorter: "number" },
{ title: "Name", field: "name", sorter: "string" },
{ title: "Age", field: "age", sorter: "number" },
{ title: "Gender", field: "gender", sorter: "string" },
];

const rowContextMenu = [];
for (let i = 1; i <= 12; i++) {
rowContextMenu.push({
label: "Long menu item number " + i,
action: function () {},
});
}

window.testTable = new Tabulator("#test-table", {
data: testData,
columns: columns,
layout: "fitColumns",
rowContextMenu: rowContextMenu,
});
});
</script>
</body>
</html>
Loading
Loading