diff --git a/src/ts/table.ts b/src/ts/table.ts index 6fe64a4..c878dec 100644 --- a/src/ts/table.ts +++ b/src/ts/table.ts @@ -158,7 +158,7 @@ abstract class RegularTableViewModelBase { } view_state.row_height = view_state.row_height || cont_body.row_height; - const _virtual_x = row_headers[0].length; + const _virtual_x = row_headers_length; if (!preserve_width) { for (let i = 0; i < row_headers_length; i++) { @@ -840,7 +840,7 @@ export class RegularTableViewModel extends RegularTableViewModelBase { for (const rh of view_response.row_headers) { if (rh.length > maxLen) maxLen = rh.length; } - this._row_headers_length = maxLen; + this._row_headers_length = view_response.num_row_headers ?? maxLen; for (let i = 0; i < view_response.row_headers.length; i++) { view_response.row_headers[i].length = @@ -868,9 +868,13 @@ export class RegularTableViewModel extends RegularTableViewModelBase { let last_cells: CellTuple[] = []; let first_col = true; - if (view_response.row_headers?.length) { + const has_row_headers = + !!view_response.row_headers && + (view_response.row_headers.length > 0 || row_headers_length > 0); + + if (has_row_headers) { const row_header_result = this._drawRowHeaders( - view_response.row_headers, + view_response.row_headers!, row_headers_length, column_headers_length, container_height, diff --git a/tests/zeroRows.spec.js b/tests/zeroRows.spec.js new file mode 100644 index 0000000..83eaecc --- /dev/null +++ b/tests/zeroRows.spec.js @@ -0,0 +1,155 @@ +// ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +// ░░░░░░░░░░▄▀░█▀▄░█▀▀░█▀▀░█░█░█░░░█▀█░█▀▄░░░░░▀█▀░█▀█░█▀▄░█░░░█▀▀░▀▄░░░░░░░░░░ +// ░░░░░░░░░▀▄░░█▀▄░█▀▀░█░█░█░█░█░░░█▀█░█▀▄░▀▀▀░░█░░█▀█░█▀▄░█░░░█▀▀░░▄▀░░░░░░░░░ +// ░░░░░░░░░░░▀░▀░▀░▀▀▀░▀▀▀░▀▀▀░▀▀▀░▀░▀░▀░▀░░░░░░▀░░▀░▀░▀▀░░▀▀▀░▀▀▀░▀░░░░░░░░░░░ +// ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +// ┃ * Copyright (c) 2020, the Regular Table Authors. This file is part * ┃ +// ┃ * of the Regular Table library, distributed under the terms of the * ┃ +// ┃ * [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). * ┃ +// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +import { test, expect } from "@playwright/test"; + +const NUM_COLUMNS = 20; +const NUM_ROW_HEADERS = 2; + +/** + * Install a data listener whose row count can be toggled to zero via + * `window.__num_rows`. Row headers are deliberately narrow and column + * headers deliberately wide, so that a data column mis-keyed into a + * row-header size slot is visible as a large width change. + */ +async function install(page) { + await page.evaluate( + ({ NUM_COLUMNS, NUM_ROW_HEADERS }) => { + const range = (x0, x1, f) => + Array.from(Array(Math.max(0, x1 - x0)).keys()).map((x) => + f(x + x0), + ); + + window.__num_rows = 100; + const dataListener = async (x0, y0, x1, y1) => { + const num_rows = window.__num_rows; + y1 = Math.min(y1, num_rows); + return { + num_rows, + num_columns: NUM_COLUMNS, + num_row_headers: NUM_ROW_HEADERS, + num_column_headers: 1, + row_headers: range(y0, y1, (i) => [`${i}`, `${i}`]), + column_headers: range(x0, x1, (i) => [ + `A very long column header ${i}`, + ]), + data: range(x0, x1, (x) => + range(y0, y1, (y) => `${x * y}`), + ), + }; + }; + + const table = document.querySelector("regular-table"); + table.setDataListener(dataListener); + }, + { NUM_COLUMNS, NUM_ROW_HEADERS }, + ); +} + +async function redraw(page, num_rows) { + await page.evaluate(async (num_rows) => { + window.__num_rows = num_rows; + const table = document.querySelector("regular-table"); + await table.draw(); + await new Promise(requestAnimationFrame); + await new Promise(requestAnimationFrame); + }, num_rows); +} + +/** + * Snapshot the last `` row: size key, corner-ness and width of + * each ``. + */ +async function measure(page) { + return await page.evaluate(() => { + const table = document.querySelector("regular-table"); + const ths = Array.from( + document.querySelectorAll("regular-table thead tr:last-child th"), + ); + + return ths.map((th) => { + const meta = table.getMeta(th); + return { + size_key: meta.size_key, + x: meta.x, + corner: th.classList.contains("rt-group-corner"), + col_class: Array.from(th.classList).find((c) => + /^rt-col-\d+$/.test(c), + ), + width: th.getBoundingClientRect().width, + }; + }); + }); +} + +test.describe("Zero-row draws with row headers", () => { + test.beforeEach(async ({ page }) => { + await page.setViewportSize({ width: 600, height: 400 }); + await page.goto("/tests/api.html"); + await page.waitForSelector("regular-table table tbody tr td"); + await install(page); + await page.evaluate(async () => { + await document.fonts.ready; + }); + + await redraw(page, 100); + }); + + test("corner headers are drawn and data columns keep their size keys", async ({ + page, + }) => { + const before = await measure(page); + expect(before.slice(0, NUM_ROW_HEADERS).map((h) => h.size_key)).toEqual( + [0, 1], + ); + + expect(before[NUM_ROW_HEADERS].size_key).toBe(NUM_ROW_HEADERS); + + await redraw(page, 0); + expect(await page.locator("regular-table tbody tr").count()).toBe(0); + + const empty = await measure(page); + const corners = empty.slice(0, NUM_ROW_HEADERS); + expect(corners.every((h) => h.corner)).toBe(true); + expect(corners.map((h) => h.size_key)).toEqual([0, 1]); + expect(corners.map((h) => h.col_class)).toEqual([ + "rt-col-0", + "rt-col-1", + ]); + + const first_data = empty[NUM_ROW_HEADERS]; + expect(first_data.corner).toBe(false); + expect(first_data.x).toBe(0); + expect(first_data.size_key).toBe(NUM_ROW_HEADERS); + expect(first_data.col_class).toBe(`rt-col-${NUM_ROW_HEADERS}`); + }); + + test("row header widths survive a round trip through zero rows", async ({ + page, + }) => { + const before = await measure(page); + const before_widths = before + .slice(0, NUM_ROW_HEADERS) + .map((h) => h.width); + + await redraw(page, 0); + await redraw(page, 100); + + const after = await measure(page); + expect(after.slice(0, NUM_ROW_HEADERS).map((h) => h.size_key)).toEqual([ + 0, 1, + ]); + + expect(after.slice(0, NUM_ROW_HEADERS).map((h) => h.width)).toEqual( + before_widths, + ); + }); +});