From 2cdc3e6de3d30a438994c907659ab3494c817509 Mon Sep 17 00:00:00 2001 From: Joshua Hawkins Date: Wed, 5 Aug 2020 12:27:20 -0400 Subject: [PATCH 1/9] Update mouse area selections to object x0, x1, y0, y1 format --- examples/area_mouse_selection.md | 42 ++++++++++++++++---------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/examples/area_mouse_selection.md b/examples/area_mouse_selection.md index d887458e..eccade00 100644 --- a/examples/area_mouse_selection.md +++ b/examples/area_mouse_selection.md @@ -73,14 +73,18 @@ const getMousedownListener = (table) => (event) => { ``` The `EventListener` returned for `"mouseover"` first checks that a valid `CURRENT_MOUSEDOWN_COORDINATES` is set and then reapplies the cell selection with the `event.target`'s coordinates - as the final coordinate pair - rendering the current potential selection. + used to calculate the `potentialSelection`. ```javascript const getMouseoverListener = (table) => (event) => { - if (CURRENT_MOUSEDOWN_COORDINATES.x !== undefined) { + if (CURRENT_MOUSEDOWN_COORDINATES && CURRENT_MOUSEDOWN_COORDINATES.x !== undefined) { const meta = table.getMeta(event.target); if (meta && meta.x !== undefined && meta.y !== undefined) { - const overCoord = {x: meta.x, y: meta.y}; - const potentialSelection = [CURRENT_MOUSEDOWN_COORDINATES, overCoord]; + const potentialSelection = { + x0: Math.min(meta.x, CURRENT_MOUSEDOWN_COORDINATES.x), + x1: Math.max(meta.x, CURRENT_MOUSEDOWN_COORDINATES.x), + y0: Math.min(meta.y, CURRENT_MOUSEDOWN_COORDINATES.y), + y1: Math.max(meta.y, CURRENT_MOUSEDOWN_COORDINATES.y), + }; reapplyMouseAreaSelections(table, MOUSE_SELECTED_AREAS.concat([potentialSelection])); } } @@ -92,9 +96,14 @@ With our `MOUSE_SELECTED_AREAS` up to date, we will reapply the selection then c ```javascript const getMouseupListener = (table) => (event) => { const meta = table.getMeta(event.target); - if (CURRENT_MOUSEDOWN_COORDINATES.x !== undefined && meta.x !== undefined && meta.y !== undefined) { - const upCoord = {x: meta.x, y: meta.y}; - MOUSE_SELECTED_AREAS.push([CURRENT_MOUSEDOWN_COORDINATES, upCoord]); + if (CURRENT_MOUSEDOWN_COORDINATES && CURRENT_MOUSEDOWN_COORDINATES.x !== undefined && meta.x !== undefined && meta.y !== undefined) { + const selection = { + x0: Math.min(meta.x, CURRENT_MOUSEDOWN_COORDINATES.x), + x1: Math.max(meta.x, CURRENT_MOUSEDOWN_COORDINATES.x), + y0: Math.min(meta.y, CURRENT_MOUSEDOWN_COORDINATES.y), + y1: Math.max(meta.y, CURRENT_MOUSEDOWN_COORDINATES.y), + }; + MOUSE_SELECTED_AREAS.push(selection); reapplyMouseAreaSelections(table); } CURRENT_MOUSEDOWN_COORDINATES = {}; @@ -113,27 +122,18 @@ const reapplyMouseAreaSelections = (table, areaSelections = MOUSE_SELECTED_AREAS } for (const as of areaSelections) { - applyMouseAreaSelection(table, as[0], as[1]); + applyMouseAreaSelection(table, as); } }; ``` Much like our `MetaData` `object`, we will use `x0` and `y0` to describe the upper left corner and `x1` and `y1` for the lower right corner in the body of `applyMouseAreaSelection()`. -We need to select the `min()` `.x` between both up and down coordinates for our `x0` in -case the user made their selection in reverse - applying similar logic for defining -`x1`, `y0` and `y1`. Then we can iterate through the `td`s in the `table` adding -the `MOUSE_SELECTED_AREA_CLASS` if the `td`'s metadata falls within the rectangular region -defined by those coordinates. +We can iterate through the `td`s in the `table` adding the `MOUSE_SELECTED_AREA_CLASS` +if the `td`'s metadata falls within the rectangular region defined by those coordinates. ```javascript - -const applyMouseAreaSelection = (table, mousedownCoord, mouseupCoord) => { +const applyMouseAreaSelection = (table, {x0, x1, y0, y1}) => { const tds = table.querySelectorAll("tbody td"); - if (mousedownCoord.x !== undefined && mousedownCoord.y !== undefined && mouseupCoord.x !== undefined && mouseupCoord.y !== undefined) { - const x0 = Math.min(mousedownCoord.x, mouseupCoord.x); - const x1 = Math.max(mousedownCoord.x, mouseupCoord.x); - const y0 = Math.min(mousedownCoord.y, mouseupCoord.y); - const y1 = Math.max(mousedownCoord.y, mouseupCoord.y); - + if (x0 !== undefined && y0 !== undefined && x1 !== undefined && y1 !== undefined) { for (const td of tds) { const meta = table.getMeta(td); if (x0 <= meta.x && meta.x <= x1) { From 5f08a3b0df5e09a67095540576d3847c2fe5fa80 Mon Sep 17 00:00:00 2001 From: Joshua Hawkins Date: Wed, 5 Aug 2020 15:55:52 -0400 Subject: [PATCH 2/9] Update area clipboard example to match area mouse selection changes --- examples/area_clipboard.md | 28 +++++++++------------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/examples/area_clipboard.md b/examples/area_clipboard.md index 41b4222c..dd9b0e88 100644 --- a/examples/area_clipboard.md +++ b/examples/area_clipboard.md @@ -78,6 +78,10 @@ const zip = (arr, ...arrs) => arr.map((val, i) => arrs.reduce((a, arr) => [...a, const transpose = (m) => m[0].map((x, i) => m.map((x) => x[i])); +const getSelectedAreas = () => { + return MOUSE_SELECTED_AREAS; +}; + const addAreaClipboard = (table, dl, write) => { const areaClipboardSelectionData = () => { const data = AREA_CLIPBOARD_COPY_SELECTIONS.map(({x0, x1, y0, y1}) => dl(x0, y0, x1 + 1, y1 + 1).data); @@ -85,19 +89,7 @@ const addAreaClipboard = (table, dl, write) => { }; const setAreaClipboardSelections = () => { - AREA_CLIPBOARD_COPY_SELECTIONS = MOUSE_SELECTED_AREAS.map(([p1, p2]) => { - const x0 = Math.min(p1.x, p2.x); - const x1 = Math.max(p1.x, p2.x); - const y0 = Math.min(p1.y, p2.y); - const y1 = Math.max(p1.y, p2.y); - - return { - x0, - x1, - y0, - y1, - }; - }); + AREA_CLIPBOARD_COPY_SELECTIONS = getSelectedAreas(); }; const copy = async () => { @@ -125,10 +117,7 @@ const addAreaClipboard = (table, dl, write) => { }; const _paste = (data) => - zip(MOUSE_SELECTED_AREAS, data).map(([area, data]) => { - const x0 = Math.min(area[0].x, area[1].x); - const y0 = Math.min(area[0].y, area[1].y); - + zip(getSelectedAreas(), data).map(([{x0, y0}, data]) => { if (data) { const x1 = x0 + data[0].length - 1; const y1 = y0 + data.length - 1; @@ -143,15 +132,16 @@ const addAreaClipboard = (table, dl, write) => { }); const paste = async () => { + console.error("paste"); AREA_CLIPBOARD_PASTE_SELECTIONS = []; const parsedData = await parseClipboardTextExcel(); const useLocalData = eqArray(parsedData, AREA_CLIPBOARD_COPIED_DATA[0]); if (!parsedData || useLocalData) { - const data = Array.from(Array(MOUSE_SELECTED_AREAS.length).keys()).flatMap(() => AREA_CLIPBOARD_COPIED_DATA); + const data = Array.from(Array(getSelectedAreas().length).keys()).flatMap(() => AREA_CLIPBOARD_COPIED_DATA); _paste(data); } else { - const data = Array.from(Array(MOUSE_SELECTED_AREAS.length).keys()).map(() => parsedData); + const data = Array.from(Array(getSelectedAreas().length).keys()).map(() => parsedData); _paste(data); } await table.draw(); From e413e1f3e7a435cc7a3b3e92aa9d6e92638cadc1 Mon Sep 17 00:00:00 2001 From: Joshua Hawkins Date: Fri, 7 Aug 2020 15:27:20 -0400 Subject: [PATCH 3/9] Refactor tests for clarity and speed --- examples/area_clipboard.md | 1 - test/examples/column_mouse_selection.test.js | 195 -------------- .../selecting_column_headers.test.js | 75 ++++++ .../selecting_one_column.test.js | 48 ++++ .../selecting_one_column_range.test.js | 49 ++++ .../selecting_two_columns.test.js | 64 +++++ .../splitting_one_column_range.test.js | 56 ++++ .../row_column_area_selection.test.js | 157 ----------- .../area_selection.test.js | 36 +++ .../column_selection.test.js | 44 +++ .../row_selection.test.js | 46 ++++ test/examples/row_mouse_selection.test.js | 252 ------------------ .../selecting_grouped_row_headers.test.js | 97 +++++++ .../selecting_one_row.test.js | 46 ++++ .../selecting_one_row_range.test.js | 45 ++++ .../selecting_row_headers.test.js | 59 ++++ .../selecting_two_rows.test.js | 64 +++++ .../splitting_one_row_range.test.js | 51 ++++ 18 files changed, 780 insertions(+), 605 deletions(-) delete mode 100644 test/examples/column_mouse_selection.test.js create mode 100644 test/examples/column_mouse_selection/selecting_column_headers.test.js create mode 100644 test/examples/column_mouse_selection/selecting_one_column.test.js create mode 100644 test/examples/column_mouse_selection/selecting_one_column_range.test.js create mode 100644 test/examples/column_mouse_selection/selecting_two_columns.test.js create mode 100644 test/examples/column_mouse_selection/splitting_one_column_range.test.js delete mode 100644 test/examples/row_column_area_selection.test.js create mode 100644 test/examples/row_column_area_selection/area_selection.test.js create mode 100644 test/examples/row_column_area_selection/column_selection.test.js create mode 100644 test/examples/row_column_area_selection/row_selection.test.js delete mode 100644 test/examples/row_mouse_selection.test.js create mode 100644 test/examples/row_mouse_selection/selecting_grouped_row_headers.test.js create mode 100644 test/examples/row_mouse_selection/selecting_one_row.test.js create mode 100644 test/examples/row_mouse_selection/selecting_one_row_range.test.js create mode 100644 test/examples/row_mouse_selection/selecting_row_headers.test.js create mode 100644 test/examples/row_mouse_selection/selecting_two_rows.test.js create mode 100644 test/examples/row_mouse_selection/splitting_one_row_range.test.js diff --git a/examples/area_clipboard.md b/examples/area_clipboard.md index dd9b0e88..3e981238 100644 --- a/examples/area_clipboard.md +++ b/examples/area_clipboard.md @@ -132,7 +132,6 @@ const addAreaClipboard = (table, dl, write) => { }); const paste = async () => { - console.error("paste"); AREA_CLIPBOARD_PASTE_SELECTIONS = []; const parsedData = await parseClipboardTextExcel(); const useLocalData = eqArray(parsedData, AREA_CLIPBOARD_COPIED_DATA[0]); diff --git a/test/examples/column_mouse_selection.test.js b/test/examples/column_mouse_selection.test.js deleted file mode 100644 index 9cb18903..00000000 --- a/test/examples/column_mouse_selection.test.js +++ /dev/null @@ -1,195 +0,0 @@ -/****************************************************************************** - * - * 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. The full license can be found in the LICENSE - * file. - * - */ - -describe("column_mouse_selection.html", () => { - const selectedColumns = async () => { - const selectedCells = await page.$$("regular-table thead th.mouse-selected-column"); - const selectedValues = []; - for (const td of selectedCells) { - selectedValues.push(await page.evaluate((td) => td.innerHTML.trim().split(" ").slice(0, 2).join(" "), td)); - } - return selectedValues; - }; - - beforeAll(async () => { - await page.setViewport({width: 2500, height: 2500}); - await page.goto("http://localhost:8081/dist/examples/column_mouse_selection.html"); - await page.waitFor("regular-table table tbody tr td"); - }); - - describe("initial view", () => { - test("includes no selection", async () => { - expect(await selectedColumns()).toEqual([]); - }); - }); - - describe("column selection", () => { - describe("selecting the origin header", () => { - test("includes no selection", async () => { - const ths = await page.$$("regular-table thead th"); - - await page.evaluate(async (th) => { - const event = new MouseEvent("click", {bubbles: true}); - th.dispatchEvent(event); - }, ths[0]); - - expect(await selectedColumns()).toEqual([]); - }); - }); - - describe("selecting the first column group", () => { - let ths; - beforeEach(async () => { - ths = await page.$$("regular-table thead th"); - }); - - test("includes the group and the rows", async () => { - await page.evaluate(async (th) => { - const event = new MouseEvent("click", {bubbles: true}); - th.dispatchEvent(event); - }, ths[1]); - - expect(await selectedColumns()).toEqual(["Group 0", "Column 0", "Column 1", "Column 2", "Column 3", "Column 4", "Column 5", "Column 6", "Column 7", "Column 8", "Column 9"]); - }); - - test("splitting the group with ctrl", async () => { - expect(await selectedColumns()).toEqual(["Group 0", "Column 0", "Column 1", "Column 2", "Column 3", "Column 4", "Column 5", "Column 6", "Column 7", "Column 8", "Column 9"]); - - await page.evaluate(async (th) => { - const event = new MouseEvent("click", {bubbles: true, ctrlKey: true}); - th.dispatchEvent(event); - }, ths[9]); - - expect(await selectedColumns()).toEqual(["Column 0", "Column 1", "Column 3", "Column 4", "Column 5", "Column 6", "Column 7", "Column 8", "Column 9"]); - }); - }); - - describe("selecting one column", () => { - beforeEach(async () => { - await page.goto("http://localhost:8081/dist/examples/column_mouse_selection.html"); - await page.waitFor("regular-table table tbody tr td"); - - const ths = await page.$$("regular-table thead tr:nth-of-type(2) th"); - - await page.evaluate(async (th) => { - const event = new MouseEvent("click", {bubbles: true}); - th.dispatchEvent(event); - }, ths[4]); - }); - - test("selects the cells", async () => { - const selectedCells = await page.$$("regular-table tbody tr td.mouse-selected-column"); - const selectedValues = []; - for (const td of selectedCells) { - selectedValues.push(await page.evaluate((td) => td.innerHTML.trim().split(" ").slice(0, 2).join(" "), td)); - } - expect(selectedValues.length).toEqual(131); - }); - - test("includes the column", async () => { - expect(await selectedColumns()).toEqual(["Column 2"]); - }); - }); - - describe("selecting a column range", () => { - let ths; - - beforeEach(async () => { - await page.goto("http://localhost:8081/dist/examples/column_mouse_selection.html"); - await page.waitFor("regular-table table tbody tr td"); - ths = await page.$$("regular-table thead th"); - }); - - test("selects the columns' headers and cells", async () => { - await page.evaluate(async (th) => { - const event = new MouseEvent("click", {bubbles: true, shiftKey: true}); - th.dispatchEvent(event); - }, ths[9]); - - await page.evaluate(async (th) => { - const event = new MouseEvent("click", {bubbles: true, shiftKey: true}); - th.dispatchEvent(event); - }, ths[11]); - - await page.waitFor("regular-table td.mouse-selected-column"); - expect(await selectedColumns()).toEqual(["Column 2", "Column 3", "Column 4"]); - }); - }); - - describe("splitting a row range", () => { - let ths; - beforeEach(async () => { - await page.goto("http://localhost:8081/dist/examples/column_mouse_selection.html"); - await page.waitFor("regular-table table tbody tr td"); - ths = await page.$$("regular-table thead th"); - }); - - test("selects the columns' headers and cells", async () => { - await page.evaluate(async (th) => { - const event = new MouseEvent("click", {bubbles: true, shiftKey: true}); - th.dispatchEvent(event); - }, ths[17]); - - await page.evaluate(async (th) => { - const event = new MouseEvent("click", {bubbles: true, shiftKey: true}); - th.dispatchEvent(event); - }, ths[13]); - - await page.waitFor("regular-table td.mouse-selected-column"); - expect(await selectedColumns()).toEqual(["Column 6", "Column 7", "Column 8", "Column 9", "Column 10"]); - await page.evaluate(async (th) => { - const event = new MouseEvent("click", {bubbles: true, ctrlKey: true}); - th.dispatchEvent(event); - }, ths[15]); - - await page.waitFor("regular-table td.mouse-selected-column"); - expect(await selectedColumns()).toEqual(["Column 6", "Column 7", "Column 9", "Column 10"]); - }); - }); - - describe("selecting two columns", () => { - describe("without CTRL pressed", () => { - test("includes only the most recent selection", async () => { - const ths = await page.$$("regular-table thead tr:nth-of-type(2) th"); - - await page.evaluate(async (th) => { - const event = new MouseEvent("click", {bubbles: true}); - th.dispatchEvent(event); - }, ths[3]); - - await page.evaluate(async (th) => { - const event = new MouseEvent("click", {bubbles: true, ctrlKey: false}); - th.dispatchEvent(event); - }, ths[5]); - - expect(await selectedColumns()).toEqual(["Column 3"]); - }); - }); - - describe("with CTRL pressed", () => { - test("includes both columns", async () => { - const ths = await page.$$("regular-table thead tr:nth-of-type(2) th"); - - await page.evaluate(async (th) => { - const event = new MouseEvent("click", {bubbles: true}); - th.dispatchEvent(event); - }, ths[3]); - - await page.evaluate(async (th) => { - const event = new MouseEvent("click", {bubbles: true, ctrlKey: true}); - th.dispatchEvent(event); - }, ths[5]); - - expect(await selectedColumns()).toEqual(["Column 1", "Column 3"]); - }); - }); - }); - }); -}); diff --git a/test/examples/column_mouse_selection/selecting_column_headers.test.js b/test/examples/column_mouse_selection/selecting_column_headers.test.js new file mode 100644 index 00000000..8f3b3df4 --- /dev/null +++ b/test/examples/column_mouse_selection/selecting_column_headers.test.js @@ -0,0 +1,75 @@ +/****************************************************************************** + * + * 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. The full license can be found in the LICENSE + * file. + * + */ + +describe("column_mouse_selection.html", () => { + const selectedColumns = async () => { + const selectedCells = await page.$$("regular-table thead th.mouse-selected-column"); + const selectedValues = []; + for (const td of selectedCells) { + selectedValues.push(await page.evaluate((td) => td.innerHTML.trim().split(" ").slice(0, 2).join(" "), td)); + } + return selectedValues; + }; + + beforeAll(async () => { + await page.setViewport({width: 2500, height: 2500}); + await page.goto("http://localhost:8081/dist/examples/column_mouse_selection.html"); + await page.waitFor("regular-table table tbody tr td"); + }); + + describe("initial view", () => { + test("includes no selection", async () => { + expect(await selectedColumns()).toEqual([]); + }); + }); + + describe("column selection", () => { + describe("selecting the origin header", () => { + test("includes no selection", async () => { + const ths = await page.$$("regular-table thead th"); + + await page.evaluate(async (th) => { + const event = new MouseEvent("click", {bubbles: true}); + th.dispatchEvent(event); + }, ths[0]); + + expect(await selectedColumns()).toEqual([]); + }); + }); + + describe("selecting the first column group", () => { + let ths; + + beforeAll(async () => { + ths = await page.$$("regular-table thead th"); + }); + + test("includes the group and the columns", async () => { + await page.evaluate(async (th) => { + const event = new MouseEvent("click", {bubbles: true}); + th.dispatchEvent(event); + }, ths[1]); + + expect(await selectedColumns()).toEqual(["Group 0", "Column 0", "Column 1", "Column 2", "Column 3", "Column 4", "Column 5", "Column 6", "Column 7", "Column 8", "Column 9"]); + }); + + test("splitting the group with ctrl", async () => { + expect(await selectedColumns()).toEqual(["Group 0", "Column 0", "Column 1", "Column 2", "Column 3", "Column 4", "Column 5", "Column 6", "Column 7", "Column 8", "Column 9"]); + + await page.evaluate(async (th) => { + const event = new MouseEvent("click", {bubbles: true, ctrlKey: true}); + th.dispatchEvent(event); + }, ths[9]); + + expect(await selectedColumns()).toEqual(["Column 0", "Column 1", "Column 3", "Column 4", "Column 5", "Column 6", "Column 7", "Column 8", "Column 9"]); + }); + }); + }); +}); diff --git a/test/examples/column_mouse_selection/selecting_one_column.test.js b/test/examples/column_mouse_selection/selecting_one_column.test.js new file mode 100644 index 00000000..19040258 --- /dev/null +++ b/test/examples/column_mouse_selection/selecting_one_column.test.js @@ -0,0 +1,48 @@ +/****************************************************************************** + * + * 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. The full license can be found in the LICENSE + * file. + * + */ + +describe("column_mouse_selection.html", () => { + const selectedColumns = async () => { + const selectedCells = await page.$$("regular-table thead th.mouse-selected-column"); + const selectedValues = []; + for (const td of selectedCells) { + selectedValues.push(await page.evaluate((td) => td.innerHTML.trim().split(" ").slice(0, 2).join(" "), td)); + } + return selectedValues; + }; + + beforeAll(async () => { + await page.setViewport({width: 2500, height: 2500}); + await page.goto("http://localhost:8081/dist/examples/column_mouse_selection.html"); + await page.waitFor("regular-table table tbody tr td"); + }); + + describe("selecting one column", () => { + beforeAll(async () => { + const ths = await page.$$("regular-table thead tr:nth-of-type(2) th"); + + await page.evaluate(async (th) => { + const event = new MouseEvent("click", {bubbles: true}); + th.dispatchEvent(event); + }, ths[4]); + }); + + test("selects the cells", async () => { + expect(await selectedColumns()).toEqual(["Column 2"]); + + const selectedCells = await page.$$("regular-table tbody tr td.mouse-selected-column"); + const selectedValues = []; + for (const td of selectedCells) { + selectedValues.push(await page.evaluate((td) => td.innerHTML.trim().split(" ").slice(0, 2).join(" "), td)); + } + expect(selectedValues.length).toEqual(131); + }); + }); +}); diff --git a/test/examples/column_mouse_selection/selecting_one_column_range.test.js b/test/examples/column_mouse_selection/selecting_one_column_range.test.js new file mode 100644 index 00000000..3f76b990 --- /dev/null +++ b/test/examples/column_mouse_selection/selecting_one_column_range.test.js @@ -0,0 +1,49 @@ +/****************************************************************************** + * + * 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. The full license can be found in the LICENSE + * file. + * + */ + +describe("column_mouse_selection.html", () => { + const selectedColumns = async () => { + const selectedCells = await page.$$("regular-table thead th.mouse-selected-column"); + const selectedValues = []; + for (const td of selectedCells) { + selectedValues.push(await page.evaluate((td) => td.innerHTML.trim().split(" ").slice(0, 2).join(" "), td)); + } + return selectedValues; + }; + + beforeAll(async () => { + await page.setViewport({width: 2500, height: 2500}); + await page.goto("http://localhost:8081/dist/examples/column_mouse_selection.html"); + await page.waitFor("regular-table table tbody tr td"); + }); + + describe("selecting a column range", () => { + let ths; + + beforeAll(async () => { + ths = await page.$$("regular-table thead th"); + }); + + test("selects the columns' headers and cells", async () => { + await page.evaluate(async (th) => { + const event = new MouseEvent("click", {bubbles: true, shiftKey: true}); + th.dispatchEvent(event); + }, ths[9]); + + await page.evaluate(async (th) => { + const event = new MouseEvent("click", {bubbles: true, shiftKey: true}); + th.dispatchEvent(event); + }, ths[11]); + + await page.waitFor("regular-table td.mouse-selected-column"); + expect(await selectedColumns()).toEqual(["Column 2", "Column 3", "Column 4"]); + }); + }); +}); diff --git a/test/examples/column_mouse_selection/selecting_two_columns.test.js b/test/examples/column_mouse_selection/selecting_two_columns.test.js new file mode 100644 index 00000000..27545e2f --- /dev/null +++ b/test/examples/column_mouse_selection/selecting_two_columns.test.js @@ -0,0 +1,64 @@ +/****************************************************************************** + * + * 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. The full license can be found in the LICENSE + * file. + * + */ + +describe("column_mouse_selection.html", () => { + const selectedColumns = async () => { + const selectedCells = await page.$$("regular-table thead th.mouse-selected-column"); + const selectedValues = []; + for (const td of selectedCells) { + selectedValues.push(await page.evaluate((td) => td.innerHTML.trim().split(" ").slice(0, 2).join(" "), td)); + } + return selectedValues; + }; + + beforeAll(async () => { + await page.setViewport({width: 2500, height: 2500}); + await page.goto("http://localhost:8081/dist/examples/column_mouse_selection.html"); + await page.waitFor("regular-table table tbody tr td"); + }); + + describe("selecting two columns", () => { + describe("without CTRL pressed", () => { + test("includes only the most recent selection", async () => { + const ths = await page.$$("regular-table thead tr:nth-of-type(2) th"); + + await page.evaluate(async (th) => { + const event = new MouseEvent("click", {bubbles: true}); + th.dispatchEvent(event); + }, ths[3]); + + await page.evaluate(async (th) => { + const event = new MouseEvent("click", {bubbles: true, ctrlKey: false}); + th.dispatchEvent(event); + }, ths[5]); + + expect(await selectedColumns()).toEqual(["Column 3"]); + }); + }); + + describe("with CTRL pressed", () => { + test("includes both columns", async () => { + const ths = await page.$$("regular-table thead tr:nth-of-type(2) th"); + + await page.evaluate(async (th) => { + const event = new MouseEvent("click", {bubbles: true}); + th.dispatchEvent(event); + }, ths[3]); + + await page.evaluate(async (th) => { + const event = new MouseEvent("click", {bubbles: true, ctrlKey: true}); + th.dispatchEvent(event); + }, ths[5]); + + expect(await selectedColumns()).toEqual(["Column 1", "Column 3"]); + }); + }); + }); +}); diff --git a/test/examples/column_mouse_selection/splitting_one_column_range.test.js b/test/examples/column_mouse_selection/splitting_one_column_range.test.js new file mode 100644 index 00000000..92d7f561 --- /dev/null +++ b/test/examples/column_mouse_selection/splitting_one_column_range.test.js @@ -0,0 +1,56 @@ +/****************************************************************************** + * + * 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. The full license can be found in the LICENSE + * file. + * + */ + +describe("column_mouse_selection.html", () => { + const selectedColumns = async () => { + const selectedCells = await page.$$("regular-table thead th.mouse-selected-column"); + const selectedValues = []; + for (const td of selectedCells) { + selectedValues.push(await page.evaluate((td) => td.innerHTML.trim().split(" ").slice(0, 2).join(" "), td)); + } + return selectedValues; + }; + + beforeAll(async () => { + await page.setViewport({width: 2500, height: 2500}); + await page.goto("http://localhost:8081/dist/examples/column_mouse_selection.html"); + await page.waitFor("regular-table table tbody tr td"); + }); + + describe("splitting a column range", () => { + let ths; + + beforeAll(async () => { + ths = await page.$$("regular-table thead th"); + }); + + test("selects the columns' headers and cells", async () => { + await page.evaluate(async (th) => { + const event = new MouseEvent("click", {bubbles: true, shiftKey: true}); + th.dispatchEvent(event); + }, ths[17]); + + await page.evaluate(async (th) => { + const event = new MouseEvent("click", {bubbles: true, shiftKey: true}); + th.dispatchEvent(event); + }, ths[13]); + + await page.waitFor("regular-table td.mouse-selected-column"); + expect(await selectedColumns()).toEqual(["Column 6", "Column 7", "Column 8", "Column 9", "Column 10"]); + await page.evaluate(async (th) => { + const event = new MouseEvent("click", {bubbles: true, ctrlKey: true}); + th.dispatchEvent(event); + }, ths[15]); + + await page.waitFor("regular-table td.mouse-selected-column"); + expect(await selectedColumns()).toEqual(["Column 6", "Column 7", "Column 9", "Column 10"]); + }); + }); +}); diff --git a/test/examples/row_column_area_selection.test.js b/test/examples/row_column_area_selection.test.js deleted file mode 100644 index 7f81ca79..00000000 --- a/test/examples/row_column_area_selection.test.js +++ /dev/null @@ -1,157 +0,0 @@ -/****************************************************************************** - * - * 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. The full license can be found in the LICENSE - * file. - * - */ - -describe("row_column_area_selection.html", () => { - const selectedCellValues = async () => { - const selectedCells = await page.$$("regular-table tbody tr td.mouse-selected-area"); - const selectedValues = []; - for (const td of selectedCells) { - selectedValues.push(await page.evaluate((td) => td.innerHTML, td)); - } - return selectedValues; - }; - - const selectedColumns = async () => { - const selectedCells = await page.$$("regular-table thead th.mouse-selected-column"); - const selectedValues = []; - for (const td of selectedCells) { - selectedValues.push(await page.evaluate((td) => td.innerHTML.trim().split(" ").slice(0, 2).join(" "), td)); - } - return selectedValues; - }; - - const selectedRows = async () => { - const selectedCells = await page.$$("regular-table tbody tr th.mouse-selected-row"); - const selectedValues = []; - for (const td of selectedCells) { - selectedValues.push(await page.evaluate((td) => td.innerHTML, td)); - } - return selectedValues; - }; - - beforeEach(async () => { - await page.setViewport({width: 2500, height: 2500}); - await page.goto("http://localhost:8081/dist/examples/row_column_area_selection.html"); - await page.waitFor("regular-table table tbody tr td"); - }); - - describe("initial view", () => { - test("includes no selection", async () => { - expect(await selectedCellValues()).toEqual([]); - expect(await selectedRows()).toEqual([]); - expect(await selectedColumns()).toEqual([]); - }); - }); - - describe("selecting one cell", () => { - test("includes one selection", async () => { - const tds = await page.$$("regular-table tbody tr td:nth-of-type(1)"); - - await page.evaluate(async (td) => { - const event = new MouseEvent("mousedown", {bubbles: true}); - td.dispatchEvent(event); - }, tds[0]); - - await page.evaluate(async (td) => { - const event = new MouseEvent("mouseup", {bubbles: true}); - td.dispatchEvent(event); - }, tds[0]); - - const selectedCells = await page.$$("regular-table tbody tr td.mouse-selected-area"); - expect(selectedCells.length).toEqual(1); - }); - }); - - describe("selecting one column", () => { - beforeEach(async () => { - const ths = await page.$$("regular-table thead tr:nth-of-type(2) th"); - - await page.evaluate(async (th) => { - const event = new MouseEvent("click", {bubbles: true}); - th.dispatchEvent(event); - }, ths[4]); - }); - - test("selects the cells", async () => { - const selectedCells = await page.$$("regular-table tbody tr td.mouse-selected-column"); - const selectedValues = []; - for (const td of selectedCells) { - selectedValues.push(await page.evaluate((td) => td.innerHTML.trim().split(" ").slice(0, 2).join(" "), td)); - } - expect(selectedValues.length > 0).toEqual(true); - }); - - test("includes the column", async () => { - expect(await selectedColumns()).toEqual(["Column 2"]); - }); - }); - - describe("selecting one row", () => { - beforeEach(async () => { - const ths = await page.$$("regular-table tbody tr th:nth-of-type(2)"); - - await page.evaluate(async (th) => { - const event = new MouseEvent("click", {bubbles: true}); - th.dispatchEvent(event); - }, ths[0]); - }); - - test("selects the cells", async () => { - const selectedCells = await page.$$("regular-table tbody tr td.mouse-selected-row"); - const selectedValues = []; - for (const td of selectedCells) { - selectedValues.push(await page.evaluate((td) => td.innerHTML.trim().split(" ").slice(0, 2).join(" "), td)); - } - expect(selectedValues.length > 0).toEqual(true); - }); - - test("selects the row", async () => { - expect(await selectedRows()).toEqual(["Row 0"]); - }); - }); - - describe("selecting two rows", () => { - describe("without CTRL pressed", () => { - test("includes only the most recent selection", async () => { - const ths = await page.$$("regular-table tbody tr th"); - - await page.evaluate(async (th) => { - const event = new MouseEvent("click", {bubbles: true}); - th.dispatchEvent(event); - }, ths[3]); - - await page.evaluate(async (th) => { - const event = new MouseEvent("click", {bubbles: true, ctrlKey: false}); - th.dispatchEvent(event); - }, ths[5]); - - expect(await selectedRows()).toEqual(["Row 4"]); - }); - }); - - describe("with CTRL pressed", () => { - test("includes the rows", async () => { - const ths = await page.$$("regular-table tbody tr th"); - - await page.evaluate(async (th) => { - const event = new MouseEvent("click", {bubbles: true}); - th.dispatchEvent(event); - }, ths[3]); - - await page.evaluate(async (th) => { - const event = new MouseEvent("click", {bubbles: true, ctrlKey: true}); - th.dispatchEvent(event); - }, ths[5]); - - expect(await selectedRows()).toEqual(["Row 2", "Row 4"]); - }); - }); - }); -}); diff --git a/test/examples/row_column_area_selection/area_selection.test.js b/test/examples/row_column_area_selection/area_selection.test.js new file mode 100644 index 00000000..600caabe --- /dev/null +++ b/test/examples/row_column_area_selection/area_selection.test.js @@ -0,0 +1,36 @@ +/****************************************************************************** + * + * 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. The full license can be found in the LICENSE + * file. + * + */ + +describe("row_column_area_selection.html", () => { + beforeEach(async () => { + await page.setViewport({width: 2500, height: 2500}); + await page.goto("http://localhost:8081/dist/examples/row_column_area_selection.html"); + await page.waitFor("regular-table table tbody tr td"); + }); + + describe("selecting one cell", () => { + test("includes one selection", async () => { + const tds = await page.$$("regular-table tbody tr td:nth-of-type(1)"); + + await page.evaluate(async (td) => { + const event = new MouseEvent("mousedown", {bubbles: true}); + td.dispatchEvent(event); + }, tds[0]); + + await page.evaluate(async (td) => { + const event = new MouseEvent("mouseup", {bubbles: true}); + td.dispatchEvent(event); + }, tds[0]); + + const selectedCells = await page.$$("regular-table tbody tr td.mouse-selected-area"); + expect(selectedCells.length).toEqual(1); + }); + }); +}); diff --git a/test/examples/row_column_area_selection/column_selection.test.js b/test/examples/row_column_area_selection/column_selection.test.js new file mode 100644 index 00000000..35222771 --- /dev/null +++ b/test/examples/row_column_area_selection/column_selection.test.js @@ -0,0 +1,44 @@ +/****************************************************************************** + * + * 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. The full license can be found in the LICENSE + * file. + * + */ + +describe("row_column_area_selection.html", () => { + const selectedColumns = async () => { + const selectedCells = await page.$$("regular-table thead th.mouse-selected-column"); + const selectedValues = []; + for (const td of selectedCells) { + selectedValues.push(await page.evaluate((td) => td.innerHTML.trim().split(" ").slice(0, 2).join(" "), td)); + } + return selectedValues; + }; + + beforeEach(async () => { + await page.setViewport({width: 2500, height: 2500}); + await page.goto("http://localhost:8081/dist/examples/row_column_area_selection.html"); + await page.waitFor("regular-table table tbody tr td"); + }); + + describe("selecting one column", () => { + test("selects the cells", async () => { + const ths = await page.$$("regular-table thead tr:nth-of-type(2) th"); + + await page.evaluate(async (th) => { + const event = new MouseEvent("click", {bubbles: true}); + th.dispatchEvent(event); + }, ths[4]); + const selectedCells = await page.$$("regular-table tbody tr td.mouse-selected-column"); + const selectedValues = []; + for (const td of selectedCells) { + selectedValues.push(await page.evaluate((td) => td.innerHTML.trim().split(" ").slice(0, 2).join(" "), td)); + } + expect(selectedValues.length > 0).toEqual(true); + expect(await selectedColumns()).toEqual(["Column 2"]); + }); + }); +}); diff --git a/test/examples/row_column_area_selection/row_selection.test.js b/test/examples/row_column_area_selection/row_selection.test.js new file mode 100644 index 00000000..7e5b75a4 --- /dev/null +++ b/test/examples/row_column_area_selection/row_selection.test.js @@ -0,0 +1,46 @@ +/****************************************************************************** + * + * 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. The full license can be found in the LICENSE + * file. + * + */ + +describe("row_column_area_selection.html", () => { + const selectedRows = async () => { + const selectedCells = await page.$$("regular-table tbody tr th.mouse-selected-row"); + const selectedValues = []; + for (const td of selectedCells) { + selectedValues.push(await page.evaluate((td) => td.innerHTML, td)); + } + return selectedValues; + }; + + let ths; + + beforeEach(async () => { + await page.setViewport({width: 2500, height: 2500}); + await page.goto("http://localhost:8081/dist/examples/row_column_area_selection.html"); + await page.waitFor("regular-table table tbody tr td"); + ths = await page.$$("regular-table tbody tr th:nth-of-type(2)"); + }); + + describe("selecting one row", () => { + test("selects the cells", async () => { + await page.evaluate(async (th) => { + const event = new MouseEvent("click", {bubbles: true}); + th.dispatchEvent(event); + }, ths[0]); + + const selectedCells = await page.$$("regular-table tbody tr td.mouse-selected-row"); + const selectedValues = []; + for (const td of selectedCells) { + selectedValues.push(await page.evaluate((td) => td.innerHTML.trim().split(" ").slice(0, 2).join(" "), td)); + } + expect(selectedValues.length > 0).toEqual(true); + expect(await selectedRows()).toEqual(["Row 0"]); + }); + }); +}); diff --git a/test/examples/row_mouse_selection.test.js b/test/examples/row_mouse_selection.test.js deleted file mode 100644 index f91a1bf1..00000000 --- a/test/examples/row_mouse_selection.test.js +++ /dev/null @@ -1,252 +0,0 @@ -/****************************************************************************** - * - * 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. The full license can be found in the LICENSE - * file. - * - */ - -describe("row_mouse_selection.html", () => { - const selectedRows = async () => { - const selectedCells = await page.$$("regular-table tbody tr th.mouse-selected-row"); - const selectedValues = []; - for (const td of selectedCells) { - selectedValues.push(await page.evaluate((td) => td.innerHTML, td)); - } - return selectedValues; - }; - - beforeAll(async () => { - await page.setViewport({width: 2500, height: 2500}); - await page.goto("http://localhost:8081/dist/examples/row_mouse_selection.html"); - await page.waitFor("regular-table table tbody tr td"); - }); - - describe("initial view", () => { - test("includes no selection", async () => { - expect(await selectedRows()).toEqual([]); - }); - }); - - describe("row selection", () => { - describe("selecting one row group", () => { - test("includes the group and the rows", async () => { - const groupHeader0 = await page.$("regular-table tbody tr th:nth-of-type(1)"); - - await page.evaluate(async (th) => { - const event = new MouseEvent("click", {bubbles: true}); - th.dispatchEvent(event); - }, groupHeader0); - - expect(await selectedRows()).toEqual(["Group 0", "Row 0", "Row 1", "Row 2", "Row 3", "Row 4", "Row 5", "Row 6", "Row 7", "Row 8", "Row 9"]); - }); - - test("splitting the group with ctrl", async () => { - expect(await selectedRows()).toEqual(["Group 0", "Row 0", "Row 1", "Row 2", "Row 3", "Row 4", "Row 5", "Row 6", "Row 7", "Row 8", "Row 9"]); - - const rowHeader3 = await page.$("regular-table tbody tr:nth-of-type(4) th"); - await page.evaluate(async (th) => { - const event = new MouseEvent("click", {bubbles: true, ctrlKey: true}); - th.dispatchEvent(event); - }, rowHeader3); - - expect(await selectedRows()).toEqual(["Row 0", "Row 1", "Row 2", "Row 4", "Row 5", "Row 6", "Row 7", "Row 8", "Row 9"]); - }); - }); - - describe("selecting one row", () => { - beforeEach(async () => { - await page.goto("http://localhost:8081/dist/examples/row_mouse_selection.html"); - await page.waitFor("regular-table table tbody tr td"); - }); - - test("selects the row header and cells then deselects", async () => { - const rowHeader1 = await page.$("regular-table tbody tr:nth-of-type(2) th"); - await page.evaluate(async (th) => { - const event = new MouseEvent("click", {bubbles: true}); - th.dispatchEvent(event); - }, rowHeader1); - await page.waitFor("regular-table td.mouse-selected-row"); - const selectedCells = await page.$$("regular-table tbody tr td.mouse-selected-row"); - expect(await selectedRows()).toEqual(["Row 1"]); - expect(selectedCells.length > 0).toEqual(true); - - await page.evaluate(async (th) => { - const event = new MouseEvent("click", {bubbles: true, ctrlKey: true}); - th.dispatchEvent(event); - }, rowHeader1); - expect(await selectedRows()).toEqual([]); - }); - }); - - describe("selecting a row range", () => { - beforeEach(async () => { - await page.goto("http://localhost:8081/dist/examples/row_mouse_selection.html"); - await page.waitFor("regular-table table tbody tr td"); - }); - - test("selects the rows' headers and cells", async () => { - const rowHeader1 = await page.$("regular-table tbody tr:nth-of-type(2) th"); - await page.evaluate(async (th) => { - const event = new MouseEvent("click", {bubbles: true, shiftKey: true}); - th.dispatchEvent(event); - }, rowHeader1); - - const rowHeader3 = await page.$("regular-table tbody tr:nth-of-type(4) th"); - await page.evaluate(async (th) => { - const event = new MouseEvent("click", {bubbles: true, shiftKey: true}); - th.dispatchEvent(event); - }, rowHeader3); - - await page.waitFor("regular-table td.mouse-selected-row"); - expect(await selectedRows()).toEqual(["Row 1", "Row 2", "Row 3"]); - }); - }); - - describe("selecting a group range", () => { - beforeEach(async () => { - await page.goto("http://localhost:8081/dist/examples/row_mouse_selection.html"); - await page.waitFor("regular-table table tbody tr td"); - }); - - describe("both selections are group headers", () => { - test("selects the groups' headers, rows' headers and cells", async () => { - const groupHeader0 = await page.$("regular-table tbody tr th:nth-of-type(1)"); - - await page.evaluate(async (th) => { - const event = new MouseEvent("click", {bubbles: true}); - th.dispatchEvent(event); - }, groupHeader0); - - const ths = await page.$$("regular-table tbody th"); - const groupHeader10 = ths[11]; - await page.evaluate(async (th) => { - const event = new MouseEvent("click", {bubbles: true, shiftKey: true}); - th.dispatchEvent(event); - }, groupHeader10); - - await page.waitFor("regular-table td.mouse-selected-row"); - expect(await selectedRows()).toEqual([ - "Group 0", - "Row 0", - "Row 1", - "Row 2", - "Row 3", - "Row 4", - "Row 5", - "Row 6", - "Row 7", - "Row 8", - "Row 9", - "Group 10", - "Row 10", - "Row 11", - "Row 12", - "Row 13", - "Row 14", - "Row 15", - "Row 16", - "Row 17", - "Row 18", - "Row 19", - ]); - }); - }); - - describe("second selection is a row header", () => { - test("selects the rows' headers and cells", async () => { - const groupHeader0 = await page.$("regular-table tbody tr th:nth-of-type(1)"); - - await page.evaluate(async (th) => { - const event = new MouseEvent("click", {bubbles: true}); - th.dispatchEvent(event); - }, groupHeader0); - - const rowHeader11 = await page.$("regular-table tbody tr:nth-of-type(12) th"); - await page.evaluate(async (th) => { - const event = new MouseEvent("click", {bubbles: true, shiftKey: true}); - th.dispatchEvent(event); - }, rowHeader11); - - await page.waitFor("regular-table td.mouse-selected-row"); - expect(await selectedRows()).toEqual(["Row 0", "Row 1", "Row 2", "Row 3", "Row 4", "Row 5", "Row 6", "Row 7", "Row 8", "Row 9", "Row 10", "Row 11"]); - }); - }); - }); - - describe("splitting a row range", () => { - beforeEach(async () => { - await page.goto("http://localhost:8081/dist/examples/row_mouse_selection.html"); - await page.waitFor("regular-table table tbody tr td"); - }); - - test("selects the rows' headers and cells", async () => { - const rowHeader1 = await page.$("regular-table tbody tr:nth-of-type(2) th"); - await page.evaluate(async (th) => { - const event = new MouseEvent("click", {bubbles: true, shiftKey: true}); - th.dispatchEvent(event); - }, rowHeader1); - - const rowHeader3 = await page.$("regular-table tbody tr:nth-of-type(4) th"); - await page.evaluate(async (th) => { - const event = new MouseEvent("click", {bubbles: true, shiftKey: true}); - th.dispatchEvent(event); - }, rowHeader3); - - const rowHeader2 = await page.$("regular-table tbody tr:nth-of-type(3) th"); - await page.evaluate(async (th) => { - const event = new MouseEvent("click", {bubbles: true, ctrlKey: true}); - th.dispatchEvent(event); - }, rowHeader2); - - await page.waitFor("regular-table td.mouse-selected-row"); - expect(await selectedRows()).toEqual(["Row 1", "Row 3"]); - }); - }); - - describe("selecting two rows", () => { - beforeAll(async () => { - await page.goto("http://localhost:8081/dist/examples/row_mouse_selection.html"); - await page.waitFor("regular-table table tbody tr td"); - }); - - describe("without CTRL pressed", () => { - test("includes only the most recent selection", async () => { - const ths = await page.$$("regular-table tbody tr th"); - - await page.evaluate(async (th) => { - const event = new MouseEvent("click", {bubbles: true}); - th.dispatchEvent(event); - }, ths[3]); - - await page.evaluate(async (th) => { - const event = new MouseEvent("click", {bubbles: true, ctrlKey: false}); - th.dispatchEvent(event); - }, ths[5]); - - expect(await selectedRows()).toEqual(["Row 4"]); - }); - }); - - describe("with CTRL pressed", () => { - test("includes the rows", async () => { - const ths = await page.$$("regular-table tbody tr th"); - - await page.evaluate(async (th) => { - const event = new MouseEvent("click", {bubbles: true}); - th.dispatchEvent(event); - }, ths[3]); - - await page.evaluate(async (th) => { - const event = new MouseEvent("click", {bubbles: true, ctrlKey: true}); - th.dispatchEvent(event); - }, ths[5]); - - expect(await selectedRows()).toEqual(["Row 2", "Row 4"]); - }); - }); - }); - }); -}); diff --git a/test/examples/row_mouse_selection/selecting_grouped_row_headers.test.js b/test/examples/row_mouse_selection/selecting_grouped_row_headers.test.js new file mode 100644 index 00000000..c7d8565a --- /dev/null +++ b/test/examples/row_mouse_selection/selecting_grouped_row_headers.test.js @@ -0,0 +1,97 @@ +/****************************************************************************** + * + * 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. The full license can be found in the LICENSE + * file. + * + */ + +describe("row_mouse_selection.html", () => { + const selectedRows = async () => { + const selectedCells = await page.$$("regular-table tbody tr th.mouse-selected-row"); + const selectedValues = []; + for (const td of selectedCells) { + selectedValues.push(await page.evaluate((td) => td.innerHTML, td)); + } + return selectedValues; + }; + + beforeAll(async () => { + await page.setViewport({width: 2500, height: 2500}); + await page.goto("http://localhost:8081/dist/examples/row_mouse_selection.html"); + await page.waitFor("regular-table table tbody tr td"); + }); + + describe("selecting a group range", () => { + describe("both selections are group headers", () => { + test("selects the groups' headers, rows' headers and cells", async () => { + const groupHeader0 = await page.$("regular-table tbody tr th:nth-of-type(1)"); + + await page.evaluate(async (th) => { + const event = new MouseEvent("click", {bubbles: true}); + th.dispatchEvent(event); + }, groupHeader0); + + const ths = await page.$$("regular-table tbody th"); + const groupHeader10 = ths[11]; + await page.evaluate(async (th) => { + const event = new MouseEvent("click", {bubbles: true, shiftKey: true}); + th.dispatchEvent(event); + }, groupHeader10); + + await page.waitFor("regular-table td.mouse-selected-row"); + expect(await selectedRows()).toEqual([ + "Group 0", + "Row 0", + "Row 1", + "Row 2", + "Row 3", + "Row 4", + "Row 5", + "Row 6", + "Row 7", + "Row 8", + "Row 9", + "Group 10", + "Row 10", + "Row 11", + "Row 12", + "Row 13", + "Row 14", + "Row 15", + "Row 16", + "Row 17", + "Row 18", + "Row 19", + ]); + + await page.evaluate(async (th) => { + const event = new MouseEvent("click", {bubbles: true}); + th.dispatchEvent(event); + }, ths[8]); + }); + }); + + describe("second selection is a row header", () => { + test("selects the rows' headers and cells", async () => { + const groupHeader0 = await page.$("regular-table tbody tr th:nth-of-type(1)"); + + await page.evaluate(async (th) => { + const event = new MouseEvent("click", {bubbles: true}); + th.dispatchEvent(event); + }, groupHeader0); + + const rowHeader11 = await page.$("regular-table tbody tr:nth-of-type(12) th"); + await page.evaluate(async (th) => { + const event = new MouseEvent("click", {bubbles: true, shiftKey: true}); + th.dispatchEvent(event); + }, rowHeader11); + + await page.waitFor("regular-table td.mouse-selected-row"); + expect(await selectedRows()).toEqual(["Row 0", "Row 1", "Row 2", "Row 3", "Row 4", "Row 5", "Row 6", "Row 7", "Row 8", "Row 9", "Row 10", "Row 11"]); + }); + }); + }); +}); diff --git a/test/examples/row_mouse_selection/selecting_one_row.test.js b/test/examples/row_mouse_selection/selecting_one_row.test.js new file mode 100644 index 00000000..951bcf26 --- /dev/null +++ b/test/examples/row_mouse_selection/selecting_one_row.test.js @@ -0,0 +1,46 @@ +/****************************************************************************** + * + * 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. The full license can be found in the LICENSE + * file. + * + */ + +describe("row_mouse_selection.html", () => { + const selectedRows = async () => { + const selectedCells = await page.$$("regular-table tbody tr th.mouse-selected-row"); + const selectedValues = []; + for (const td of selectedCells) { + selectedValues.push(await page.evaluate((td) => td.innerHTML, td)); + } + return selectedValues; + }; + + beforeAll(async () => { + await page.setViewport({width: 2500, height: 2500}); + await page.goto("http://localhost:8081/dist/examples/row_mouse_selection.html"); + await page.waitFor("regular-table table tbody tr td"); + }); + + describe("selecting one row", () => { + test("selects the row header and cells then deselects", async () => { + const rowHeader1 = await page.$("regular-table tbody tr:nth-of-type(2) th"); + await page.evaluate(async (th) => { + const event = new MouseEvent("click", {bubbles: true}); + th.dispatchEvent(event); + }, rowHeader1); + await page.waitFor("regular-table td.mouse-selected-row"); + const selectedCells = await page.$$("regular-table tbody tr td.mouse-selected-row"); + expect(await selectedRows()).toEqual(["Row 1"]); + expect(selectedCells.length > 0).toEqual(true); + + await page.evaluate(async (th) => { + const event = new MouseEvent("click", {bubbles: true, ctrlKey: true}); + th.dispatchEvent(event); + }, rowHeader1); + expect(await selectedRows()).toEqual([]); + }); + }); +}); diff --git a/test/examples/row_mouse_selection/selecting_one_row_range.test.js b/test/examples/row_mouse_selection/selecting_one_row_range.test.js new file mode 100644 index 00000000..afe78dc5 --- /dev/null +++ b/test/examples/row_mouse_selection/selecting_one_row_range.test.js @@ -0,0 +1,45 @@ +/****************************************************************************** + * + * 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. The full license can be found in the LICENSE + * file. + * + */ + +describe("row_mouse_selection.html", () => { + const selectedRows = async () => { + const selectedCells = await page.$$("regular-table tbody tr th.mouse-selected-row"); + const selectedValues = []; + for (const td of selectedCells) { + selectedValues.push(await page.evaluate((td) => td.innerHTML, td)); + } + return selectedValues; + }; + + beforeAll(async () => { + await page.setViewport({width: 2500, height: 2500}); + await page.goto("http://localhost:8081/dist/examples/row_mouse_selection.html"); + await page.waitFor("regular-table table tbody tr td"); + }); + + describe("selecting a row range", () => { + test("selects the rows' headers and cells", async () => { + const rowHeader1 = await page.$("regular-table tbody tr:nth-of-type(2) th"); + await page.evaluate(async (th) => { + const event = new MouseEvent("click", {bubbles: true, shiftKey: true}); + th.dispatchEvent(event); + }, rowHeader1); + + const rowHeader3 = await page.$("regular-table tbody tr:nth-of-type(4) th"); + await page.evaluate(async (th) => { + const event = new MouseEvent("click", {bubbles: true, shiftKey: true}); + th.dispatchEvent(event); + }, rowHeader3); + + await page.waitFor("regular-table td.mouse-selected-row"); + expect(await selectedRows()).toEqual(["Row 1", "Row 2", "Row 3"]); + }); + }); +}); diff --git a/test/examples/row_mouse_selection/selecting_row_headers.test.js b/test/examples/row_mouse_selection/selecting_row_headers.test.js new file mode 100644 index 00000000..15455529 --- /dev/null +++ b/test/examples/row_mouse_selection/selecting_row_headers.test.js @@ -0,0 +1,59 @@ +/****************************************************************************** + * + * 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. The full license can be found in the LICENSE + * file. + * + */ + +describe("row_mouse_selection.html", () => { + const selectedRows = async () => { + const selectedCells = await page.$$("regular-table tbody tr th.mouse-selected-row"); + const selectedValues = []; + for (const td of selectedCells) { + selectedValues.push(await page.evaluate((td) => td.innerHTML, td)); + } + return selectedValues; + }; + + beforeAll(async () => { + await page.setViewport({width: 2500, height: 2500}); + await page.goto("http://localhost:8081/dist/examples/row_mouse_selection.html"); + await page.waitFor("regular-table table tbody tr td"); + }); + + describe("initial view", () => { + test("includes no selection", async () => { + expect(await selectedRows()).toEqual([]); + }); + }); + + describe("row selection", () => { + describe("selecting one row group", () => { + test("includes the group and the rows", async () => { + const groupHeader0 = await page.$("regular-table tbody tr th:nth-of-type(1)"); + + await page.evaluate(async (th) => { + const event = new MouseEvent("click", {bubbles: true}); + th.dispatchEvent(event); + }, groupHeader0); + + expect(await selectedRows()).toEqual(["Group 0", "Row 0", "Row 1", "Row 2", "Row 3", "Row 4", "Row 5", "Row 6", "Row 7", "Row 8", "Row 9"]); + }); + + test("splitting the group with ctrl", async () => { + expect(await selectedRows()).toEqual(["Group 0", "Row 0", "Row 1", "Row 2", "Row 3", "Row 4", "Row 5", "Row 6", "Row 7", "Row 8", "Row 9"]); + + const rowHeader3 = await page.$("regular-table tbody tr:nth-of-type(4) th"); + await page.evaluate(async (th) => { + const event = new MouseEvent("click", {bubbles: true, ctrlKey: true}); + th.dispatchEvent(event); + }, rowHeader3); + + expect(await selectedRows()).toEqual(["Row 0", "Row 1", "Row 2", "Row 4", "Row 5", "Row 6", "Row 7", "Row 8", "Row 9"]); + }); + }); + }); +}); diff --git a/test/examples/row_mouse_selection/selecting_two_rows.test.js b/test/examples/row_mouse_selection/selecting_two_rows.test.js new file mode 100644 index 00000000..baa447e2 --- /dev/null +++ b/test/examples/row_mouse_selection/selecting_two_rows.test.js @@ -0,0 +1,64 @@ +/****************************************************************************** + * + * 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. The full license can be found in the LICENSE + * file. + * + */ + +describe("row_mouse_selection.html", () => { + const selectedRows = async () => { + const selectedCells = await page.$$("regular-table tbody tr th.mouse-selected-row"); + const selectedValues = []; + for (const td of selectedCells) { + selectedValues.push(await page.evaluate((td) => td.innerHTML, td)); + } + return selectedValues; + }; + + beforeAll(async () => { + await page.setViewport({width: 2500, height: 2500}); + await page.goto("http://localhost:8081/dist/examples/row_mouse_selection.html"); + await page.waitFor("regular-table table tbody tr td"); + }); + + describe("selecting two rows", () => { + describe("without CTRL pressed", () => { + test("includes only the most recent selection", async () => { + const ths = await page.$$("regular-table tbody tr th"); + + await page.evaluate(async (th) => { + const event = new MouseEvent("click", {bubbles: true}); + th.dispatchEvent(event); + }, ths[3]); + + await page.evaluate(async (th) => { + const event = new MouseEvent("click", {bubbles: true, ctrlKey: false}); + th.dispatchEvent(event); + }, ths[5]); + + expect(await selectedRows()).toEqual(["Row 4"]); + }); + }); + + describe("with CTRL pressed", () => { + test("includes the rows", async () => { + const ths = await page.$$("regular-table tbody tr th"); + + await page.evaluate(async (th) => { + const event = new MouseEvent("click", {bubbles: true}); + th.dispatchEvent(event); + }, ths[3]); + + await page.evaluate(async (th) => { + const event = new MouseEvent("click", {bubbles: true, ctrlKey: true}); + th.dispatchEvent(event); + }, ths[5]); + + expect(await selectedRows()).toEqual(["Row 2", "Row 4"]); + }); + }); + }); +}); diff --git a/test/examples/row_mouse_selection/splitting_one_row_range.test.js b/test/examples/row_mouse_selection/splitting_one_row_range.test.js new file mode 100644 index 00000000..0d76f119 --- /dev/null +++ b/test/examples/row_mouse_selection/splitting_one_row_range.test.js @@ -0,0 +1,51 @@ +/****************************************************************************** + * + * 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. The full license can be found in the LICENSE + * file. + * + */ + +describe("row_mouse_selection.html", () => { + const selectedRows = async () => { + const selectedCells = await page.$$("regular-table tbody tr th.mouse-selected-row"); + const selectedValues = []; + for (const td of selectedCells) { + selectedValues.push(await page.evaluate((td) => td.innerHTML, td)); + } + return selectedValues; + }; + + beforeAll(async () => { + await page.setViewport({width: 2500, height: 2500}); + await page.goto("http://localhost:8081/dist/examples/row_mouse_selection.html"); + await page.waitFor("regular-table table tbody tr td"); + }); + + describe("splitting a row range", () => { + test("selects the rows' headers and cells", async () => { + const rowHeader1 = await page.$("regular-table tbody tr:nth-of-type(2) th"); + await page.evaluate(async (th) => { + const event = new MouseEvent("click", {bubbles: true, shiftKey: true}); + th.dispatchEvent(event); + }, rowHeader1); + + const rowHeader3 = await page.$("regular-table tbody tr:nth-of-type(4) th"); + await page.evaluate(async (th) => { + const event = new MouseEvent("click", {bubbles: true, shiftKey: true}); + th.dispatchEvent(event); + }, rowHeader3); + + const rowHeader2 = await page.$("regular-table tbody tr:nth-of-type(3) th"); + await page.evaluate(async (th) => { + const event = new MouseEvent("click", {bubbles: true, ctrlKey: true}); + th.dispatchEvent(event); + }, rowHeader2); + + await page.waitFor("regular-table td.mouse-selected-row"); + expect(await selectedRows()).toEqual(["Row 1", "Row 3"]); + }); + }); +}); From a230cfbc0cbd3662791b25fc49d60db9b9de7dec Mon Sep 17 00:00:00 2001 From: Joshua Hawkins Date: Mon, 10 Aug 2020 16:56:48 -0400 Subject: [PATCH 4/9] Create standalone keyboard nav example --- examples/keyboard_navigation.md | 290 ++++++++++++++++++++++++++++++++ 1 file changed, 290 insertions(+) create mode 100644 examples/keyboard_navigation.md diff --git a/examples/keyboard_navigation.md b/examples/keyboard_navigation.md new file mode 100644 index 00000000..a4cbc177 --- /dev/null +++ b/examples/keyboard_navigation.md @@ -0,0 +1,290 @@ +# Clipboard + +... + +```html + +``` +## Styling + +... + +```css +regular-table tbody tr th { + user-select: none; +} +regular-table thead tr th { + user-select: none; +} + +regular-table tbody tr td.single-cell-selected { + background-color: rgb(255, 0, 0, 0.25); /* red */ +} + +td { + outline: none; + border-right: 1px solid #eee; + border-bottom: 1px solid #eee; + min-width: 22px; +} +``` +... + +```javascript +const SINGLE_CELL_SELECTED_CLASS = "single-cell-selected"; +const KEYBOARD_SELECTED_AREA_CLASS = "keyboard-selected-area"; + +let KEYBOARD_SELECTED_AREA = {}; +const SELECTED_POSITION = {x: 0, y: 0}; + +const reapplyKeyboardArea = (table) => { + const tds = table.querySelectorAll("td"); + + for (const td of tds) { + const meta = table.getMeta(td); + + if (KEYBOARD_SELECTED_AREA.x0 <= meta.x && meta.x <= KEYBOARD_SELECTED_AREA.x1 && KEYBOARD_SELECTED_AREA.y0 <= meta.y && meta.y <= KEYBOARD_SELECTED_AREA.y1) { + td.classList.add(KEYBOARD_SELECTED_AREA_CLASS); + } else { + td.classList.remove(KEYBOARD_SELECTED_AREA_CLASS); + } + } +}; + +const updateFocus = (table, selectAll, editable = true) => { + function selectElementContents(el) { + var range = document.createRange(); + range.selectNodeContents(el); + var sel = window.getSelection(); + sel.removeAllRanges(); + sel.addRange(range); + } + + const tds = table.querySelectorAll("td"); + + for (const td of tds) { + td.setAttribute("contenteditable", editable); + const meta = table.getMeta(td); + + if (meta.x === SELECTED_POSITION.x && meta.y === SELECTED_POSITION.y) { + td.classList.add(SINGLE_CELL_SELECTED_CLASS); + td.focus(); + if (selectAll) { + selectElementContents(td); + } + } else { + td.classList.remove(SINGLE_CELL_SELECTED_CLASS); + } + } + reapplyKeyboardArea(table); +}; + +const getSelection = (table) => { + return table.querySelector(`td.${SINGLE_CELL_SELECTED_CLASS}`); +}; + +const addKeyboardNavigation = (table, write, editable = true) => { + table.addEventListener("scroll", () => { + const meta = table.getMeta(document.activeElement); + if (meta) { + write(meta.x, meta.y, document.activeElement.textContent); + } else { + console.error("no meta for ", document.activeElement.textContent); + } + }); + + table.addEventListener("focusout", (event) => { + const meta = table.getMeta(event.target); + if (meta) { + write(meta.x, meta.y, event.target.textContent); + } else { + console.error("no meta for ", event.target); + } + }); + + const SCROLL_AHEAD = 4; + + function moveSelection(active_cell, dx, dy) { + const meta = table.getMeta(active_cell); + const areaSelection = { + x0: KEYBOARD_SELECTED_AREA.x0 || SELECTED_POSITION.x, + x1: KEYBOARD_SELECTED_AREA.x1 || SELECTED_POSITION.x, + y0: KEYBOARD_SELECTED_AREA.y0 || SELECTED_POSITION.y, + y1: KEYBOARD_SELECTED_AREA.y1 || SELECTED_POSITION.y, + }; + + if (dx !== 0) { + if (meta.x + dx < NUM_COLUMNS && 0 <= meta.x + dx) { + const x = meta.x + dx; + areaSelection.x0 = Math.min(areaSelection.x0, areaSelection.x1, x); + areaSelection.x1 = Math.max(areaSelection.x0, areaSelection.x1, x); + SELECTED_POSITION.x = x; + } + if (meta.x1 <= SELECTED_POSITION.x + SCROLL_AHEAD) { + table.scrollToCell(meta.x0 + 2, meta.y0, NUM_COLUMNS, NUM_ROWS); + } else if (SELECTED_POSITION.x - SCROLL_AHEAD < meta.x0) { + if (0 < meta.x0 - 1) { + table.scrollToCell(meta.x0 - 1, meta.y0, NUM_COLUMNS, NUM_ROWS); + } else { + table.scrollToCell(0, meta.y0, NUM_COLUMNS, NUM_ROWS); + } + } + } + + if (dy !== 0) { + if (meta.y + dy < NUM_ROWS && 0 <= meta.y + dy) { + const y = meta.y + dy; + areaSelection.y0 = Math.min(areaSelection.y0, areaSelection.y1, y); + areaSelection.y1 = Math.max(areaSelection.y0, areaSelection.y1, y); + SELECTED_POSITION.y = y; + } + if (meta.y1 <= SELECTED_POSITION.y + SCROLL_AHEAD) { + table.scrollToCell(meta.x0, meta.y0 + 1, NUM_COLUMNS, NUM_ROWS); + } else if (SELECTED_POSITION.y - SCROLL_AHEAD + 2 < meta.y0) { + if (0 < meta.y0 - 1) { + table.scrollToCell(meta.x0, meta.y0 - 1, NUM_COLUMNS, NUM_ROWS); + } else { + table.scrollToCell(meta.x0, 0, NUM_COLUMNS, NUM_ROWS); + } + } + } + updateFocus(table, true, editable); + return areaSelection; + } + + table.addEventListener("keypress", (event) => { + const target = getSelection(table); + if (event.keyCode === 13) { + event.preventDefault(); + if (event.shiftKey) { + moveSelection(target, 0, -1); + event.preventDefault(); + event.stopPropagation(); + } else { + moveSelection(target, 0, 1); + event.preventDefault(); + event.stopPropagation(); + } + } + }); + + table.addEventListener("keydown", (event) => { + const target = getSelection(table); + let area; + + switch (event.keyCode) { + // tab + case 9: + event.preventDefault(); + if (event.shiftKey) { + moveSelection(target, -1, 0); + event.preventDefault(); + event.stopPropagation(); + } else { + moveSelection(target, 1, 0); + event.preventDefault(); + event.stopPropagation(); + } + break; + // left arrow + case 37: + area = moveSelection(target, -1, 0); + event.preventDefault(); + event.stopPropagation(); + break; + // up arrow + case 38: + area = moveSelection(target, 0, -1); + event.preventDefault(); + event.stopPropagation(); + break; + // right arrow + case 39: + area = moveSelection(target, 1, 0); + event.preventDefault(); + event.stopPropagation(); + break; + // down arrow + case 40: + area = moveSelection(target, 0, 1); + event.preventDefault(); + event.stopPropagation(); + break; + } + if (event.shiftKey && area) { + KEYBOARD_SELECTED_AREA = area; + updateFocus(table, true, editable); + } else if (!event.metaKey && !event.ctrlKey) { + if (area) { + MOUSE_SELECTED_AREAS = []; + AREA_CLIPBOARD_COPY_SELECTIONS = []; + AREA_CLIPBOARD_PASTE_SELECTIONS = []; + AREA_CLIPBOARD_COPIED_DATA = []; + table.draw(); + } + + KEYBOARD_SELECTED_AREA = {}; + reapplyKeyboardArea(table); + } + }); + + const makeSingleSelect = (event) => { + const meta = table.getMeta(event.target); + if (meta) { + SELECTED_POSITION.x = meta.x; + SELECTED_POSITION.y = meta.y; + updateFocus(table, false, editable); + } + }; + + table.addEventListener("click", makeSingleSelect); + table.addEventListener("mousedown", makeSingleSelect); + + addKeyboardNavigationStyleListener(table); + return table; +}; + +const addKeyboardNavigationStyleListener = (table) => { + table.addStyleListener(() => { + updateFocus(table, false, false); + }); +}; + +function generateDataListener(num_rows, num_columns) { + const allData = range(0, num_columns, (x) => range(0, num_rows, (y) => `${x}, ${y}`)); + return function dl(x0, y0, x1, y1) { + return { + num_rows, + num_columns, + data: allData.slice(x0, x1).map((col) => col.slice(y0, y1)), + allData, + }; + }; +} + +window.addEventListener("load", () => { + const table = window.keyboardNavigationRegularTable; + if (table) { + const dl = generateDataListener(50, 50); + + addKeyboardNavigation(table); + table.setDataListener(dl); + table.draw(); + } +}); +``` + +## Appendix (Dependencies) + +Our Libraries. + +```html + + +``` + +The `two_billion_rows` example for the its `DataListener`. + +```html + +``` From 46334637a2f19ef0da128b6cfd5e889b2abc75d9 Mon Sep 17 00:00:00 2001 From: Joshua Hawkins Date: Tue, 11 Aug 2020 10:54:46 -0400 Subject: [PATCH 5/9] Add tests and refactor --- .../keyboard_navigation.test.js | 188 ++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 test/examples/keyboard_navigation/keyboard_navigation.test.js diff --git a/test/examples/keyboard_navigation/keyboard_navigation.test.js b/test/examples/keyboard_navigation/keyboard_navigation.test.js new file mode 100644 index 00000000..220199e8 --- /dev/null +++ b/test/examples/keyboard_navigation/keyboard_navigation.test.js @@ -0,0 +1,188 @@ +/****************************************************************************** + * + * 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. The full license can be found in the LICENSE + * file. + * + */ + +describe("keyboard_navigation.html", () => { + beforeAll(async () => { + await page.setViewport({width: 150, height: 150}); + }); + + describe("Navigating with the arrow keys", () => { + const sayHello = async (table) => { + await page.evaluate(async (table) => { + const target = document.activeElement; + target.textContent = "Hello, World!"; + const event = document.createEvent("HTMLEvents"); + event.initEvent("keypress", false, true); + event.ctrlKey = true; + event.keyCode = 13; + table.dispatchEvent(event); + }, table); + }; + + const keypressReturn = async (table, times = 1) => { + Array.from(Array(times)).forEach(async () => { + await page.evaluate(async (table) => { + const event = document.createEvent("HTMLEvents"); + event.initEvent("keypress", false, true); + event.ctrlKey = true; + event.keyCode = 13; + table.dispatchEvent(event); + }, table); + }); + }; + + const keydownLeftArrow = async (table, times = 1) => { + Array.from(Array(times)).forEach(async () => { + await page.evaluate(async (table) => { + const event = document.createEvent("HTMLEvents"); + event.initEvent("keydown", false, true); + event.keyCode = 37; + table.dispatchEvent(event); + }, table); + }); + }; + + const keydownUpArrow = async (table, times = 1) => { + Array.from(Array(times)).forEach(async () => { + await page.evaluate(async (table) => { + const event = document.createEvent("HTMLEvents"); + event.initEvent("keydown", false, true); + event.keyCode = 38; + table.dispatchEvent(event); + }, table); + }); + }; + + const keydownRightArrow = async (table, times = 1) => { + Array.from(Array(times)).forEach(async () => { + await page.evaluate(async (table) => { + const event = document.createEvent("HTMLEvents"); + event.initEvent("keydown", false, true); + event.keyCode = 39; + table.dispatchEvent(event); + }, table); + }); + }; + + const keydownDownArrow = async (table, times = 1) => { + Array.from(Array(times)).forEach(async () => { + await page.evaluate(async (table) => { + const event = document.createEvent("HTMLEvents"); + event.initEvent("keydown", false, true); + event.keyCode = 40; + table.dispatchEvent(event); + }, table); + }); + }; + + beforeEach(async () => { + await page.goto("http://localhost:8081/dist/examples/keyboard_navigation.html"); + await page.waitFor("regular-table table tbody tr td"); + }); + + test("initializes with focus on (0,0)", async () => { + const table = await page.$("regular-table"); + await sayHello(table); + const tr = await page.$$("regular-table tbody tr:nth-of-type(1) td"); + const cell_values = []; + for (const td of tr) { + cell_values.push(await page.evaluate((td) => td.innerHTML, td)); + } + expect(cell_values).toEqual(["Hello, World!", "", "", "", "", ""]); + }); + + xtest("scrolls as right arrow is down", async () => { + const table = await page.$("regular-table"); + keydownRightArrow(table, 5); + await sayHello(table); + + const tds = await page.$$("regular-table tbody tr:nth-of-type(1) td"); + const cells = []; + for (const td of tds) { + cells.push(await page.evaluate((td) => td.innerHTML, td)); + } + expect(cells).toEqual(["", "", "", "", "", "Hello, World!"]); + + const ths = await page.$$("regular-table tbody tr:nth-of-type(1) th"); + const th_value = await page.evaluate((th) => th.innerHTML, ths[0]); + expect(th_value).toEqual("0"); + }); + + xtest("scrolls as down arrow is down", async () => { + const table = await page.$("regular-table"); + keydownDownArrow(table, 5); + await sayHello(table); + + const tds = await page.$$("regular-table tbody tr:nth-of-type(3) td"); + const cells = []; + for (const td of tds) { + cells.push(await page.evaluate((td) => td.innerHTML, td)); + } + expect(cells).toEqual(["Hello, World!", "", ""]); + + const ths = await page.$$("regular-table tbody tr:nth-of-type(3) th"); + const th = await page.evaluate((th) => th.innerHTML, ths[0]); + expect(th).toEqual("5"); + }); + + xtest("scrolls down and back up", async () => { + const table = await page.$("regular-table"); + keydownDownArrow(table, 5); + keydownUpArrow(table, 2); + await sayHello(table); + + const tds = await page.$$("regular-table tbody tr:nth-of-type(3) td"); + const cells = []; + for (const td of tds) { + cells.push(await page.evaluate((td) => td.innerHTML, td)); + } + expect(cells).toEqual(["Hello, World!", "", ""]); + + const ths = await page.$$("regular-table tbody tr:nth-of-type(3) th"); + const th = await page.evaluate((th) => th.innerHTML, ths[0]); + expect(th).toEqual("3"); + }); + + xtest("scrolls right and back left", async () => { + const table = await page.$("regular-table"); + keydownRightArrow(table, 10); + keydownLeftArrow(table, 5); + await sayHello(table); + + const tds = await page.$$("regular-table tbody tr:nth-of-type(1) td"); + const cells = []; + for (const td of tds) { + cells.push(await page.evaluate((td) => td.innerHTML, td)); + } + expect(cells).toEqual(["", "", "", "", "", "Hello, World!"]); + + const ths = await page.$$("regular-table tbody tr:nth-of-type(1) th"); + const th = await page.evaluate((th) => th.innerHTML, ths[0]); + expect(th).toEqual("0"); + }); + + xtest("scrolls as return is pressed", async () => { + const table = await page.$("regular-table"); + keypressReturn(table, 5); + await sayHello(table); + + const tds = await page.$$("regular-table tbody tr:nth-of-type(3) td"); + const cells = []; + for (const td of tds) { + cells.push(await page.evaluate((td) => td.innerHTML, td)); + } + expect(cells).toEqual(["Hello, World!", "", ""]); + + const ths = await page.$$("regular-table tbody tr:nth-of-type(3) th"); + const th = await page.evaluate((th) => th.innerHTML, ths[0]); + expect(th).toEqual("5"); + }); + }); +}); From d3e581edc61dd6423bcd9cacba0754fb16cd1af1 Mon Sep 17 00:00:00 2001 From: Joshua Hawkins Date: Tue, 11 Aug 2020 13:14:53 -0400 Subject: [PATCH 6/9] Add standalone key nav tests --- examples/keyboard_navigation.md | 94 ++++-- .../keyboard_area_selection.test.js | 66 +++++ .../keyboard_navigation.test.js | 276 ++++++++++-------- .../keyboard_navigation_scrolling.test.js | 161 ++++++++++ 4 files changed, 443 insertions(+), 154 deletions(-) create mode 100644 test/examples/keyboard_navigation/keyboard_area_selection.test.js create mode 100644 test/examples/keyboard_navigation/keyboard_navigation_scrolling.test.js diff --git a/examples/keyboard_navigation.md b/examples/keyboard_navigation.md index a4cbc177..29660ec1 100644 --- a/examples/keyboard_navigation.md +++ b/examples/keyboard_navigation.md @@ -1,4 +1,4 @@ -# Clipboard +# Keyboard Navigation ... @@ -17,10 +17,18 @@ regular-table thead tr th { user-select: none; } +regular-table tbody tr td.keyboard-selected-area { + background-color: rgb(0, 0, 255, 0.15); /* blue */ +} + regular-table tbody tr td.single-cell-selected { background-color: rgb(255, 0, 0, 0.25); /* red */ } +regular-table tbody tr td.single-cell-selected.keyboard-selected-area { + background-color: rgb(128, 0, 128, 0.33); /* violet */ +} + td { outline: none; border-right: 1px solid #eee; @@ -51,7 +59,7 @@ const reapplyKeyboardArea = (table) => { } }; -const updateFocus = (table, selectAll, editable = true) => { +const updateFocus = (table, selectAll) => { function selectElementContents(el) { var range = document.createRange(); range.selectNodeContents(el); @@ -63,7 +71,7 @@ const updateFocus = (table, selectAll, editable = true) => { const tds = table.querySelectorAll("td"); for (const td of tds) { - td.setAttribute("contenteditable", editable); + td.setAttribute("contenteditable", true); const meta = table.getMeta(td); if (meta.x === SELECTED_POSITION.x && meta.y === SELECTED_POSITION.y) { @@ -83,68 +91,75 @@ const getSelection = (table) => { return table.querySelector(`td.${SINGLE_CELL_SELECTED_CLASS}`); }; -const addKeyboardNavigation = (table, write, editable = true) => { +const addKeyboardNavigation = (table, dl, write, editable = true) => { table.addEventListener("scroll", () => { const meta = table.getMeta(document.activeElement); - if (meta) { + if (meta && write) { write(meta.x, meta.y, document.activeElement.textContent); } else { - console.error("no meta for ", document.activeElement.textContent); } }); table.addEventListener("focusout", (event) => { const meta = table.getMeta(event.target); - if (meta) { + if (meta && write) { write(meta.x, meta.y, event.target.textContent); } else { - console.error("no meta for ", event.target); } }); const SCROLL_AHEAD = 4; function moveSelection(active_cell, dx, dy) { + const numCols = dl().num_columns; + const numRows = dl().num_rows; + const meta = table.getMeta(active_cell); + + const x0 = typeof KEYBOARD_SELECTED_AREA.x0 === "number" ? KEYBOARD_SELECTED_AREA.x0 : SELECTED_POSITION.x; + const x1 = typeof KEYBOARD_SELECTED_AREA.x1 === "number" ? KEYBOARD_SELECTED_AREA.x1 : SELECTED_POSITION.x; + const y0 = typeof KEYBOARD_SELECTED_AREA.y0 === "number" ? KEYBOARD_SELECTED_AREA.y0 : SELECTED_POSITION.y; + const y1 = typeof KEYBOARD_SELECTED_AREA.y1 === "number" ? KEYBOARD_SELECTED_AREA.y1 : SELECTED_POSITION.y; + const areaSelection = { - x0: KEYBOARD_SELECTED_AREA.x0 || SELECTED_POSITION.x, - x1: KEYBOARD_SELECTED_AREA.x1 || SELECTED_POSITION.x, - y0: KEYBOARD_SELECTED_AREA.y0 || SELECTED_POSITION.y, - y1: KEYBOARD_SELECTED_AREA.y1 || SELECTED_POSITION.y, + x0, + x1, + y0, + y1, }; if (dx !== 0) { - if (meta.x + dx < NUM_COLUMNS && 0 <= meta.x + dx) { + if (meta.x + dx < numCols && 0 <= meta.x + dx) { const x = meta.x + dx; areaSelection.x0 = Math.min(areaSelection.x0, areaSelection.x1, x); areaSelection.x1 = Math.max(areaSelection.x0, areaSelection.x1, x); SELECTED_POSITION.x = x; } if (meta.x1 <= SELECTED_POSITION.x + SCROLL_AHEAD) { - table.scrollToCell(meta.x0 + 2, meta.y0, NUM_COLUMNS, NUM_ROWS); + table.scrollToCell(meta.x0 + 2, meta.y0, numCols, numRows); } else if (SELECTED_POSITION.x - SCROLL_AHEAD < meta.x0) { if (0 < meta.x0 - 1) { - table.scrollToCell(meta.x0 - 1, meta.y0, NUM_COLUMNS, NUM_ROWS); + table.scrollToCell(meta.x0 - 1, meta.y0, numCols, numRows); } else { - table.scrollToCell(0, meta.y0, NUM_COLUMNS, NUM_ROWS); + table.scrollToCell(0, meta.y0, numCols, numRows); } } } if (dy !== 0) { - if (meta.y + dy < NUM_ROWS && 0 <= meta.y + dy) { + if (meta.y + dy < numRows && 0 <= meta.y + dy) { const y = meta.y + dy; areaSelection.y0 = Math.min(areaSelection.y0, areaSelection.y1, y); areaSelection.y1 = Math.max(areaSelection.y0, areaSelection.y1, y); SELECTED_POSITION.y = y; } if (meta.y1 <= SELECTED_POSITION.y + SCROLL_AHEAD) { - table.scrollToCell(meta.x0, meta.y0 + 1, NUM_COLUMNS, NUM_ROWS); + table.scrollToCell(meta.x0, meta.y0 + 1, numCols, numRows); } else if (SELECTED_POSITION.y - SCROLL_AHEAD + 2 < meta.y0) { if (0 < meta.y0 - 1) { - table.scrollToCell(meta.x0, meta.y0 - 1, NUM_COLUMNS, NUM_ROWS); + table.scrollToCell(meta.x0, meta.y0 - 1, numCols, numRows); } else { - table.scrollToCell(meta.x0, 0, NUM_COLUMNS, NUM_ROWS); + table.scrollToCell(meta.x0, 0, numCols, numRows); } } } @@ -154,6 +169,7 @@ const addKeyboardNavigation = (table, write, editable = true) => { table.addEventListener("keypress", (event) => { const target = getSelection(table); + if (event.keyCode === 13) { event.preventDefault(); if (event.shiftKey) { @@ -170,8 +186,8 @@ const addKeyboardNavigation = (table, write, editable = true) => { table.addEventListener("keydown", (event) => { const target = getSelection(table); - let area; + let area; switch (event.keyCode) { // tab case 9: @@ -220,7 +236,7 @@ const addKeyboardNavigation = (table, write, editable = true) => { AREA_CLIPBOARD_COPY_SELECTIONS = []; AREA_CLIPBOARD_PASTE_SELECTIONS = []; AREA_CLIPBOARD_COPIED_DATA = []; - table.draw(); + // table.draw(); } KEYBOARD_SELECTED_AREA = {}; @@ -246,16 +262,39 @@ const addKeyboardNavigation = (table, write, editable = true) => { const addKeyboardNavigationStyleListener = (table) => { table.addStyleListener(() => { - updateFocus(table, false, false); + updateFocus(table, false, true); }); }; function generateDataListener(num_rows, num_columns) { - const allData = range(0, num_columns, (x) => range(0, num_rows, (y) => `${x}, ${y}`)); - return function dl(x0, y0, x1, y1) { + function to_column_name(i, letter) { + return Array(i).fill(letter).join(""); + } + + function generate_column_names() { + const nums = Array.from(Array(26)); + const alphabet = nums.map((val, i) => String.fromCharCode(i + 65)); + let caps = [], + i = 1; + while (caps.length < num_columns) { + caps = caps.concat(alphabet.map((letter) => to_column_name(i, letter))); + i++; + } + return caps; + } + + const column_names = generate_column_names(); + + const allData = Array(num_columns) + .fill() + .map(() => Array(num_rows).fill()); + + return function dl(x0 = 0, y0 = 0, x1 = 0, y1 = 0) { return { num_rows, num_columns, + row_headers: Array.from(Array(Math.ceil(y1) - y0).keys()).map((y) => [`${y + y0}`]), + column_headers: column_names.slice(x0, x1).map((x) => [x]), data: allData.slice(x0, x1).map((col) => col.slice(y0, y1)), allData, }; @@ -266,8 +305,11 @@ window.addEventListener("load", () => { const table = window.keyboardNavigationRegularTable; if (table) { const dl = generateDataListener(50, 50); + const write = (x, y, value) => { + dl().allData[x][y] = value; + }; - addKeyboardNavigation(table); + addKeyboardNavigation(table, dl, write, true); table.setDataListener(dl); table.draw(); } diff --git a/test/examples/keyboard_navigation/keyboard_area_selection.test.js b/test/examples/keyboard_navigation/keyboard_area_selection.test.js new file mode 100644 index 00000000..c882e06c --- /dev/null +++ b/test/examples/keyboard_navigation/keyboard_area_selection.test.js @@ -0,0 +1,66 @@ +/****************************************************************************** + * + * 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. The full license can be found in the LICENSE + * file. + * + */ + +describe("keyboard_navigation.html", () => { + const keydownRightArrow = async (table, times = 1, shiftKey = false) => { + Array.from(Array(times)).forEach(async () => { + await page.evaluate( + async (table, shiftKey) => { + const event = document.createEvent("HTMLEvents"); + event.initEvent("keydown", false, true); + event.keyCode = 39; + if (shiftKey) { + event.shiftKey = true; + } + table.dispatchEvent(event); + }, + table, + shiftKey + ); + }); + }; + + const keydownDownArrow = async (table, times = 1, shiftKey = false) => { + Array.from(Array(times)).forEach(async () => { + await page.evaluate( + async (table, shiftKey) => { + const event = document.createEvent("HTMLEvents"); + event.initEvent("keydown", false, true); + event.keyCode = 40; + if (shiftKey) { + event.shiftKey = true; + } + table.dispatchEvent(event); + }, + table, + shiftKey + ); + }); + }; + + let table; + + beforeAll(async () => { + await page.goto("http://localhost:8081/dist/examples/keyboard_navigation.html"); + await page.setViewport({width: 150, height: 150}); + await page.waitFor("regular-table table tbody tr td"); + table = await page.$("regular-table"); + }); + + describe("Selecting an area with the keyboard", () => { + test("adds an area selection", async () => { + await keydownRightArrow(table, 2, true); + await keydownDownArrow(table, 2, true); + + const tds = await page.$$("regular-table tbody tr td.keyboard-selected-area"); + expect(tds.length).toEqual(9); + }); + }); +}); diff --git a/test/examples/keyboard_navigation/keyboard_navigation.test.js b/test/examples/keyboard_navigation/keyboard_navigation.test.js index 220199e8..d358c909 100644 --- a/test/examples/keyboard_navigation/keyboard_navigation.test.js +++ b/test/examples/keyboard_navigation/keyboard_navigation.test.js @@ -9,180 +9,200 @@ */ describe("keyboard_navigation.html", () => { - beforeAll(async () => { - await page.setViewport({width: 150, height: 150}); - }); - - describe("Navigating with the arrow keys", () => { - const sayHello = async (table) => { - await page.evaluate(async (table) => { + const writeByReturn = async (table, text) => { + await page.evaluate( + async (table, text) => { const target = document.activeElement; - target.textContent = "Hello, World!"; + target.textContent = text; const event = document.createEvent("HTMLEvents"); event.initEvent("keypress", false, true); event.ctrlKey = true; event.keyCode = 13; table.dispatchEvent(event); - }, table); - }; - - const keypressReturn = async (table, times = 1) => { - Array.from(Array(times)).forEach(async () => { - await page.evaluate(async (table) => { + }, + table, + text + ); + }; + + const keypressReturn = async (table, times = 1, shiftKey) => { + Array.from(Array(times)).forEach(async () => { + await page.evaluate( + async (table, shiftKey) => { const event = document.createEvent("HTMLEvents"); event.initEvent("keypress", false, true); event.ctrlKey = true; + if (shiftKey) { + event.shiftKey = true; + } event.keyCode = 13; table.dispatchEvent(event); - }, table); - }); - }; + }, + table, + shiftKey + ); + }); + }; - const keydownLeftArrow = async (table, times = 1) => { - Array.from(Array(times)).forEach(async () => { - await page.evaluate(async (table) => { + const keydownTab = async (table, times = 1, shiftKey = false) => { + Array.from(Array(times)).forEach(async () => { + await page.evaluate( + async (table, shiftKey) => { const event = document.createEvent("HTMLEvents"); event.initEvent("keydown", false, true); - event.keyCode = 37; + event.keyCode = 9; + if (shiftKey) { + event.shiftKey = true; + } table.dispatchEvent(event); - }, table); - }); - }; + }, + table, + shiftKey + ); + }); + }; - const keydownUpArrow = async (table, times = 1) => { - Array.from(Array(times)).forEach(async () => { - await page.evaluate(async (table) => { - const event = document.createEvent("HTMLEvents"); - event.initEvent("keydown", false, true); - event.keyCode = 38; - table.dispatchEvent(event); - }, table); - }); - }; + const keydownLeftArrow = async (table, times = 1) => { + Array.from(Array(times)).forEach(async () => { + await page.evaluate(async (table) => { + const event = document.createEvent("HTMLEvents"); + event.initEvent("keydown", false, true); + event.keyCode = 37; + table.dispatchEvent(event); + }, table); + }); + }; - const keydownRightArrow = async (table, times = 1) => { - Array.from(Array(times)).forEach(async () => { - await page.evaluate(async (table) => { - const event = document.createEvent("HTMLEvents"); - event.initEvent("keydown", false, true); - event.keyCode = 39; - table.dispatchEvent(event); - }, table); - }); - }; + const keydownUpArrow = async (table, times = 1) => { + Array.from(Array(times)).forEach(async () => { + await page.evaluate(async (table) => { + const event = document.createEvent("HTMLEvents"); + event.initEvent("keydown", false, true); + event.keyCode = 38; + table.dispatchEvent(event); + }, table); + }); + }; - const keydownDownArrow = async (table, times = 1) => { - Array.from(Array(times)).forEach(async () => { - await page.evaluate(async (table) => { - const event = document.createEvent("HTMLEvents"); - event.initEvent("keydown", false, true); - event.keyCode = 40; - table.dispatchEvent(event); - }, table); - }); - }; + const keydownRightArrow = async (table, times = 1) => { + Array.from(Array(times)).forEach(async () => { + await page.evaluate(async (table) => { + const event = document.createEvent("HTMLEvents"); + event.initEvent("keydown", false, true); + event.keyCode = 39; + table.dispatchEvent(event); + }, table); + }); + }; - beforeEach(async () => { - await page.goto("http://localhost:8081/dist/examples/keyboard_navigation.html"); - await page.waitFor("regular-table table tbody tr td"); + const keydownDownArrow = async (table, times = 1) => { + Array.from(Array(times)).forEach(async () => { + await page.evaluate(async (table) => { + const event = document.createEvent("HTMLEvents"); + event.initEvent("keydown", false, true); + event.keyCode = 40; + table.dispatchEvent(event); + }, table); }); + }; + + beforeAll(async () => { + await page.goto("http://localhost:8081/dist/examples/keyboard_navigation.html"); + await page.setViewport({width: 500, height: 500}); + await page.waitFor("regular-table table tbody tr td"); + }); + describe("Navigating with the keyboard", () => { test("initializes with focus on (0,0)", async () => { const table = await page.$("regular-table"); - await sayHello(table); + await writeByReturn(table, "1. test origin"); const tr = await page.$$("regular-table tbody tr:nth-of-type(1) td"); - const cell_values = []; - for (const td of tr) { - cell_values.push(await page.evaluate((td) => td.innerHTML, td)); - } - expect(cell_values).toEqual(["Hello, World!", "", "", "", "", ""]); + const cellValue = await page.evaluate((td) => td.innerHTML, tr[0]); + expect(cellValue).toEqual("1. test origin"); }); - xtest("scrolls as right arrow is down", async () => { + test("moves as right arrow is down", async () => { const table = await page.$("regular-table"); keydownRightArrow(table, 5); - await sayHello(table); + await writeByReturn(table, "2. right arrow"); - const tds = await page.$$("regular-table tbody tr:nth-of-type(1) td"); - const cells = []; - for (const td of tds) { - cells.push(await page.evaluate((td) => td.innerHTML, td)); - } - expect(cells).toEqual(["", "", "", "", "", "Hello, World!"]); + const tds = await page.$$("regular-table tbody tr:nth-of-type(2) td"); + const cellValue = await page.evaluate((td) => td.innerHTML, tds[5]); + expect(cellValue).toEqual("2. right arrow"); const ths = await page.$$("regular-table tbody tr:nth-of-type(1) th"); - const th_value = await page.evaluate((th) => th.innerHTML, ths[0]); - expect(th_value).toEqual("0"); + const thValue = await page.evaluate((th) => th.innerHTML, ths[0]); + expect(thValue).toEqual("0"); }); - xtest("scrolls as down arrow is down", async () => { + test("moves as down arrow is down", async () => { const table = await page.$("regular-table"); keydownDownArrow(table, 5); - await sayHello(table); - - const tds = await page.$$("regular-table tbody tr:nth-of-type(3) td"); - const cells = []; - for (const td of tds) { - cells.push(await page.evaluate((td) => td.innerHTML, td)); - } - expect(cells).toEqual(["Hello, World!", "", ""]); - - const ths = await page.$$("regular-table tbody tr:nth-of-type(3) th"); - const th = await page.evaluate((th) => th.innerHTML, ths[0]); - expect(th).toEqual("5"); + await writeByReturn(table, "3. down arrow"); + + const tds = await page.$$("regular-table tbody tr:nth-of-type(8) td"); + const cellValue = await page.evaluate((td) => td.innerHTML, tds[5]); + expect(cellValue).toEqual("3. down arrow"); }); - xtest("scrolls down and back up", async () => { + test("moves as up arrow is down", async () => { const table = await page.$("regular-table"); - keydownDownArrow(table, 5); - keydownUpArrow(table, 2); - await sayHello(table); - - const tds = await page.$$("regular-table tbody tr:nth-of-type(3) td"); - const cells = []; - for (const td of tds) { - cells.push(await page.evaluate((td) => td.innerHTML, td)); - } - expect(cells).toEqual(["Hello, World!", "", ""]); - - const ths = await page.$$("regular-table tbody tr:nth-of-type(3) th"); - const th = await page.evaluate((th) => th.innerHTML, ths[0]); - expect(th).toEqual("3"); + keydownUpArrow(table, 5); + await writeByReturn(table, "4. up arrow"); + + const tds = await page.$$("regular-table tbody tr:nth-of-type(4) td"); + const cellValue = await page.evaluate((td) => td.innerHTML, tds[5]); + expect(cellValue).toEqual("4. up arrow"); }); - xtest("scrolls right and back left", async () => { + test("moves as left arrow is down", async () => { const table = await page.$("regular-table"); - keydownRightArrow(table, 10); - keydownLeftArrow(table, 5); - await sayHello(table); + keydownLeftArrow(table, 3); + await writeByReturn(table, "5. left arrow"); - const tds = await page.$$("regular-table tbody tr:nth-of-type(1) td"); - const cells = []; - for (const td of tds) { - cells.push(await page.evaluate((td) => td.innerHTML, td)); - } - expect(cells).toEqual(["", "", "", "", "", "Hello, World!"]); + const tds = await page.$$("regular-table tbody tr:nth-of-type(5) td"); + const cellValue = await page.evaluate((td) => td.innerHTML, tds[2]); + expect(cellValue).toEqual("5. left arrow"); + }); - const ths = await page.$$("regular-table tbody tr:nth-of-type(1) th"); - const th = await page.evaluate((th) => th.innerHTML, ths[0]); - expect(th).toEqual("0"); + test("moves as return is pressed", async () => { + const table = await page.$("regular-table"); + keypressReturn(table); + await writeByReturn(table, "6. return"); + + const tds = await page.$$("regular-table tbody tr:nth-of-type(7) td"); + const cellValue = await page.evaluate((td) => td.innerHTML, tds[2]); + expect(cellValue).toEqual("6. return"); }); - xtest("scrolls as return is pressed", async () => { + test("moves as tab is down", async () => { const table = await page.$("regular-table"); - keypressReturn(table, 5); - await sayHello(table); - - const tds = await page.$$("regular-table tbody tr:nth-of-type(3) td"); - const cells = []; - for (const td of tds) { - cells.push(await page.evaluate((td) => td.innerHTML, td)); - } - expect(cells).toEqual(["Hello, World!", "", ""]); - - const ths = await page.$$("regular-table tbody tr:nth-of-type(3) th"); - const th = await page.evaluate((th) => th.innerHTML, ths[0]); - expect(th).toEqual("5"); + keydownTab(table); + await writeByReturn(table, "7. tab"); + + const tds = await page.$$("regular-table tbody tr:nth-of-type(8) td"); + const cellValue = await page.evaluate((td) => td.innerHTML, tds[3]); + expect(cellValue).toEqual("7. tab"); + }); + + test("moves as shift tab is down", async () => { + const table = await page.$("regular-table"); + keydownTab(table, 1, true); + await writeByReturn(table, "8. shift tab"); + + const tds = await page.$$("regular-table tbody tr:nth-of-type(9) td"); + const cellValue = await page.evaluate((td) => td.innerHTML, tds[2]); + expect(cellValue).toEqual("8. shift tab"); + }); + + test("moves as shift return is pressed", async () => { + const table = await page.$("regular-table"); + keypressReturn(table, 2, true); + await writeByReturn(table, "9. shift return"); + + const tds = await page.$$("regular-table tbody tr:nth-of-type(8) td"); + const cellValue = await page.evaluate((td) => td.innerHTML, tds[2]); + expect(cellValue).toEqual("9. shift return"); }); }); }); diff --git a/test/examples/keyboard_navigation/keyboard_navigation_scrolling.test.js b/test/examples/keyboard_navigation/keyboard_navigation_scrolling.test.js new file mode 100644 index 00000000..5064edc7 --- /dev/null +++ b/test/examples/keyboard_navigation/keyboard_navigation_scrolling.test.js @@ -0,0 +1,161 @@ +/****************************************************************************** + * + * 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. The full license can be found in the LICENSE + * file. + * + */ + +describe("keyboard_navigation.html", () => { + const writeByReturn = async (table, text) => { + await page.evaluate( + async (table, text) => { + const target = document.activeElement; + target.textContent = text; + const event = document.createEvent("HTMLEvents"); + event.initEvent("keypress", false, true); + event.ctrlKey = true; + event.keyCode = 13; + table.dispatchEvent(event); + }, + table, + text + ); + }; + + const keydownLeftArrow = async (table, times = 1) => { + Array.from(Array(times)).forEach(async () => { + await page.evaluate(async (table) => { + const event = document.createEvent("HTMLEvents"); + event.initEvent("keydown", false, true); + event.keyCode = 37; + table.dispatchEvent(event); + }, table); + }); + }; + + const keydownUpArrow = async (table, times = 1) => { + Array.from(Array(times)).forEach(async () => { + await page.evaluate(async (table) => { + const event = document.createEvent("HTMLEvents"); + event.initEvent("keydown", false, true); + event.keyCode = 38; + table.dispatchEvent(event); + }, table); + }); + }; + + const keydownRightArrow = async (table, times = 1) => { + Array.from(Array(times)).forEach(async () => { + await page.evaluate(async (table) => { + const event = document.createEvent("HTMLEvents"); + event.initEvent("keydown", false, true); + event.keyCode = 39; + table.dispatchEvent(event); + }, table); + }); + }; + + const keydownDownArrow = async (table, times = 1) => { + Array.from(Array(times)).forEach(async () => { + await page.evaluate(async (table) => { + const event = document.createEvent("HTMLEvents"); + event.initEvent("keydown", false, true); + event.keyCode = 40; + table.dispatchEvent(event); + }, table); + }); + }; + + let table; + + beforeAll(async () => { + await page.goto("http://localhost:8081/dist/examples/keyboard_navigation.html"); + await page.setViewport({width: 150, height: 150}); + await page.waitFor("regular-table table tbody tr td"); + table = await page.$("regular-table"); + }); + + describe("Scrolling with the arrow keys", () => { + test("scrolls as right arrow is down", async () => { + const text = "1. scrolling right"; + + keydownRightArrow(table, 5); + await writeByReturn(table, text); + + const tds = await page.$$("regular-table tbody tr:nth-of-type(1) td"); + + const rowThs = await page.$$("regular-table tbody tr:nth-of-type(1) th"); + const rowHeaderValue = await page.evaluate((th) => th.innerHTML, rowThs[0]); + expect(rowHeaderValue).toEqual("0"); + + const colThs = await page.$$("regular-table thead th"); + const colThValues = []; + for (const td of colThs) { + colThValues.push(await page.evaluate((td) => td.textContent.trim(), td)); + } + expect(colThValues).toEqual(["", "C", "D", "E", "F"]); + + const cellValue = await page.evaluate((td) => td.innerHTML, tds[tds.length - 1]); + expect(cellValue).toEqual(text); + }); + + xtest("scrolls as down arrow is down", async () => { + const text = "2. scrolling down"; + + keydownDownArrow(table, 5); + await writeByReturn(table, text); + + const rowThs = await page.$$("regular-table tbody tr:nth-of-type(1) th"); + const rowHeaderValue = await page.evaluate((th) => th.innerHTML, rowThs[0]); + expect(rowHeaderValue).toEqual("4"); + + const tds = await page.$$("regular-table tbody tr:nth-of-type(3) td"); + const cellValue = await page.evaluate((td) => td.innerHTML, tds[tds.length - 1]); + + expect(cellValue).toEqual(text); + }); + + xtest("scrolls as left arrow is down", async () => { + const text = "3. scrolling left"; + + keydownLeftArrow(table, 5); + await writeByReturn(table, text); + + const colThs = await page.$$("regular-table thead th"); + + const colThValues = []; + for (const td of colThs) { + colThValues.push(await page.evaluate((td) => td.textContent.trim(), td)); + } + expect(colThValues).toEqual(["", "A", "B"]); + + const tds = await page.$$("regular-table tbody tr:nth-of-type(3) td"); + const cellValue = await page.evaluate((td) => td.innerHTML, tds[0]); + + expect(cellValue).toEqual(text); + }); + + xtest("scrolls as up arrow is down", async () => { + const text = "4. scrolling up"; + + keydownUpArrow(table, 10); + await writeByReturn(table, text); + + const colThs = await page.$$("regular-table thead th"); + + const colThValues = []; + for (const td of colThs) { + colThValues.push(await page.evaluate((td) => td.textContent.trim(), td)); + } + expect(colThValues).toEqual(["", "A", "B"]); + + const tds = await page.$$("regular-table tbody tr:nth-of-type(1) td"); + const cellValue = await page.evaluate((td) => td.innerHTML, tds[0]); + + expect(cellValue).toEqual(text); + }); + }); +}); From f59b6bb215a902bc22af965ca737143833e81136 Mon Sep 17 00:00:00 2001 From: Joshua Hawkins Date: Thu, 13 Aug 2020 10:22:25 -0400 Subject: [PATCH 7/9] Refactor --- examples/keyboard_navigation.md | 2 +- .../keyboard_navigation/keyboard_navigation_scrolling.test.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/keyboard_navigation.md b/examples/keyboard_navigation.md index 29660ec1..3c3e6b21 100644 --- a/examples/keyboard_navigation.md +++ b/examples/keyboard_navigation.md @@ -88,7 +88,7 @@ const updateFocus = (table, selectAll) => { }; const getSelection = (table) => { - return table.querySelector(`td.${SINGLE_CELL_SELECTED_CLASS}`); + return document.activeElement || table.querySelector(`td.${SINGLE_CELL_SELECTED_CLASS}`); }; const addKeyboardNavigation = (table, dl, write, editable = true) => { diff --git a/test/examples/keyboard_navigation/keyboard_navigation_scrolling.test.js b/test/examples/keyboard_navigation/keyboard_navigation_scrolling.test.js index 5064edc7..494fee2b 100644 --- a/test/examples/keyboard_navigation/keyboard_navigation_scrolling.test.js +++ b/test/examples/keyboard_navigation/keyboard_navigation_scrolling.test.js @@ -72,8 +72,8 @@ describe("keyboard_navigation.html", () => { let table; beforeAll(async () => { - await page.goto("http://localhost:8081/dist/examples/keyboard_navigation.html"); await page.setViewport({width: 150, height: 150}); + await page.goto("http://localhost:8081/dist/examples/keyboard_navigation.html"); await page.waitFor("regular-table table tbody tr td"); table = await page.$("regular-table"); }); From b1f969a6f55f346f982b797d7880c8bbcf9da0d5 Mon Sep 17 00:00:00 2001 From: Joshua Hawkins Date: Tue, 18 Aug 2020 09:15:15 -0400 Subject: [PATCH 8/9] Update area clipboard --- examples/area_clipboard.md | 414 ++++++++++++++++++++++--------------- 1 file changed, 249 insertions(+), 165 deletions(-) diff --git a/examples/area_clipboard.md b/examples/area_clipboard.md index 3e981238..aae0ce84 100644 --- a/examples/area_clipboard.md +++ b/examples/area_clipboard.md @@ -1,14 +1,207 @@ -# Clipboard +# Area Clipboard Interactions -... +This example adds clipboard edit interactions to the area selection behavior applied to a +[``](https://github.com/jpmorganchase/regular-table), allowing the user +to select groups of cells then copy, paste and cut. +First we'll add a `` to the page with an `id` accessible on the window +object. ```html - + ``` -## Styling +## Extending Area Selection +Now we'll need to make the area selection behavior available by including the `area_mouse_selection` example ... +```html + +``` +and we'll also need a quick helper `function` to `getSelectedAreas()`. +```javascript +function getSelectedAreas() { + return MOUSE_SELECTED_AREAS; +} +``` +## `addAreaClipboardInteractions()` +We'll create a single function to add the clipboard behavior to the `` +passed in as the first argument. Additionally, it will take the `DataListener` and a +`function` to `write` edits to the `DataModel`. Its direct responsibilities include +adding `EventListener`s and the clipboard interaction's `StyleListener`. + +Our `EventListener` will dispatch to each of the behaviors for copy, paste and cut +as expected - we'll define those next. +```javascript +const addAreaClipboardInteractions = (table, dl, write) => { + const keyListener = (event) => { + const meta = table.getMeta(event.target); + switch (event.keyCode) { + // C + case 67: + if (event.metaKey || event.ctrlKey) { + areaClipboardCopy(table, dl); + break; + } + // V + case 86: + if (event.metaKey || event.ctrlKey) { + areaClipboardPaste(table, write); + break; + } + // X + case 88: + if (event.metaKey || event.ctrlKey) { + areaClipboardCut(table, dl, write); + break; + } + } + }; + + table.addEventListener("keydown", keyListener); + addAreaClipboardInteractionsStyleListener(table, dl); + return table; +}; +``` +For our `areaClipboardCopy()`, we'll need to keep track of the `AREA_CLIPBOARD_COPY_SELECTIONS`. +Each will represent a rectangular selection using familiar attributes `x0` as the upper left and `y1` as the lower right. +```javascript +let AREA_CLIPBOARD_COPY_SELECTIONS = []; +``` +We'll also keep track of the data we get from the `DataListener` for the areas selected +mapping over the selections then transposing the collection. +```javascript +let AREA_CLIPBOARD_COPIED_DATA = []; + +const areaClipboardCopyData = (dl) => { + const transpose = (m) => m[0].map((x, i) => m.map((x) => x[i])); + const data = AREA_CLIPBOARD_COPY_SELECTIONS.map(({x0, x1, y0, y1}) => dl(x0, y0, x1 + 1, y1 + 1).data); + return data.map(transpose); +}; +``` +So we'll start our `function` by keeping track of the `AREA_CLIPBOARD_COPY_SELECTIONS` +and `AREA_CLIPBOARD_COPIED_DATA`. +Next, we can generate the corresponding `textSelections` by splitting the lines with `\t` +and selections with `\n`. + +Finally, we write our generated text to the `navigator.clipboard` and update the +styling. Don't worry, we'll define `updateAreaClipboardInteractionsStyle()` later. +```javascript +const areaClipboardCopy = async (table, dl) => { + AREA_CLIPBOARD_COPY_SELECTIONS = getSelectedAreas(); + AREA_CLIPBOARD_COPIED_DATA = areaClipboardCopyData(dl); + + const textSelections = AREA_CLIPBOARD_COPIED_DATA.map((area) => { + return area.map((row) => row.join("\t")).join("\n"); + }); + + try { + await navigator.clipboard.writeText(textSelections[0]); + updateAreaClipboardInteractionsStyle(table); + } catch (e) { + console.error("Failed to writeText to navigator.clipboard.", e); + } +}; +``` +For our `areaClipboardCut()`, we can simply call `areaClipboardCopy()` prior to +overwriting the data in the `DataModel` with `undefined`, clearing the selected region. + +Then we call `draw()` to ensure the data in the `table` reflects the cut. +```javascript +const areaClipboardCut = async (table, dl, write) => { + await areaClipboardCopy(table, dl); + for (const {x0, x1, y0, y1} of AREA_CLIPBOARD_COPY_SELECTIONS) { + for (var x = x0; x < x1 + 1; x++) { + for (var y = y0; y < y1 + 1; y++) { + write(x, y, undefined); + } + } + } + table.draw(); +}; +``` +Our implementation of `areaClipboardPaste()` is a bit tricky as we'd like to cover a +couple of use cases. -... +If the user is copying multiple selections from our `table` and there are multiple +areas selected in our `table` to paste to, then we'd like to paste the first copied +selection to the first paste selection and the second copied selection to the second +paste selection and so on... +In the event that the end user is copying from a different spreadsheet, we'll need +to parse the text on the `clipboard` and attempt to `write` the parsed content to our +`table`. + +If the `parsedData` is unusable, we can only try to write the `AREA_CLIPBOARD_COPIED_DATA`. +We'll also `useLocalData` if it matches the what's been parsed from the `clipboard`. +Otherwise, we know that the `data` on the `clipboard` came from outside of +``, and we should map it to each of the selected areas we're pasting to. + +We'll want to keep track of the `AREA_CLIPBOARD_PASTE_SELECTIONS` to style the areas +we paste to using the same structure as `AREA_CLIPBOARD_COPY_SELECTIONS`. +```javascript +let AREA_CLIPBOARD_PASTE_SELECTIONS = []; +``` +We'll then duplicate the `data` collection to ensure we can paste into all of the +currently selected areas and zip the collections - pairing the currently selected areas +with copied data. We can then iterate through the zipped collection writing the data to +for each of the selections and calculating the pasted areas' dimensions. + +Finally, we call `draw()` to force the `table` to update the `date` shown. +```javascript +const areaClipboardPaste = async (table, write) => { + const zip = (arr, ...arrs) => arr.map((val, i) => arrs.reduce((a, arr) => [...a, arr[i]], [val])); + + const parsedData = await tryParseAreaClipboardText(); + const useLocalData = eqArray(parsedData, AREA_CLIPBOARD_COPIED_DATA[0]); + + let data = []; + if (!parsedData || useLocalData) { + data = Array.from(Array(getSelectedAreas().length).keys()).flatMap(() => AREA_CLIPBOARD_COPIED_DATA); + } else { + data = Array.from(Array(getSelectedAreas().length).keys()).map(() => parsedData); + } + + AREA_CLIPBOARD_PASTE_SELECTIONS = zip(getSelectedAreas(), data).map(([{x0, y0}, data]) => { + data.map((row, ridx) => { + row.map((value, cidx) => { + write(x0 + cidx, y0 + ridx, value); + }); + }); + + const x1 = x0 + data[0].length - 1; + const y1 = y0 + data.length - 1; + return {x0, y0, x1, y1}; + }); + + await table.draw(); +}; +``` +Our implementation of `tryParseAreaClipboardText()` takes the current `clipboard` +text and attempts to map it assuming that it's formatted similar to content copied +from a spreadsheet. +```javascript +async function tryParseAreaClipboardText() { + try { + const text = await navigator.clipboard.readText(); + const rows = text.split(/\r\n|\n|\r/); + return rows.length > 0 ? rows.map((r) => r.split("\t")) : r; + } catch (e) { + console.error("Failed to readText from navigator.clipboard.", e); + } +} +``` +We'll also need a quick `function` to compare our `Array`s of data for `areaClipboardPaste()`. +```javascript +function eqArray(a1, a2) { + if (!Array.isArray(a1) || !Array.isArray(a2) || a1.length !== a2.length) return false; + for (var i = 0; i < a1.length; i++) { + const eqArrays = Array.isArray(a1[i]) && Array.isArray(a2[i]) && eqArray(a1[i], a2[i]); + if (!eqArrays && a1[i] !== a2[i]) { + return false; + } + } + return true; +} +``` +## Styling +Similar to other examples like `row_column_area_selection`, we will use a primary color scheme to show the selected areas and their overlap with areas that are being copied or have been pasted to. ```css regular-table tbody tr td.mouse-selected-area { background-color: rgb(255, 0, 0, 0.25); /* red */ @@ -32,7 +225,7 @@ regular-table tbody tr td.mouse-selected-area.clipboard-copy-selected-area.clipb background-color: rgb(183, 65, 14, 0.33); /* rust */ } ``` -Lets turn off the `user-select` style for this example too. +Lets turn off the `user-select` style for this example too ```css regular-table tbody tr td { user-select: none; @@ -43,7 +236,9 @@ regular-table tbody tr th { regular-table thead tr th { user-select: none; } - +``` +And outline the `td`s. +```css td { outline: none; border-right: 1px solid #eee; @@ -51,167 +246,48 @@ td { min-width: 22px; } ``` -## Adding the Behaviors -```html - -``` - -... - +## `StyleListener` +We'll need to add a `StyleListener` to `add` or `remove` the classes for copy and paste. ```javascript -let AREA_CLIPBOARD_COPY_SELECTIONS = []; -let AREA_CLIPBOARD_PASTE_SELECTIONS = []; -let AREA_CLIPBOARD_COPIED_DATA = []; - -const eqArray = (a1, a2) => { - if (!Array.isArray(a1) || !Array.isArray(a2) || a1.length !== a2.length) return false; - for (var i = 0; i < a1.length; i++) { - const eqArrays = Array.isArray(a1[i]) && Array.isArray(a2[i]) && eqArray(a1[i], a2[i]); - if (!eqArrays && a1[i] !== a2[i]) { - return false; - } - } - return true; -}; - -const zip = (arr, ...arrs) => arr.map((val, i) => arrs.reduce((a, arr) => [...a, arr[i]], [val])); - -const transpose = (m) => m[0].map((x, i) => m.map((x) => x[i])); - -const getSelectedAreas = () => { - return MOUSE_SELECTED_AREAS; -}; - -const addAreaClipboard = (table, dl, write) => { - const areaClipboardSelectionData = () => { - const data = AREA_CLIPBOARD_COPY_SELECTIONS.map(({x0, x1, y0, y1}) => dl(x0, y0, x1 + 1, y1 + 1).data); - return data.map(transpose); - }; - - const setAreaClipboardSelections = () => { - AREA_CLIPBOARD_COPY_SELECTIONS = getSelectedAreas(); - }; - - const copy = async () => { - setAreaClipboardSelections(); - AREA_CLIPBOARD_COPIED_DATA = areaClipboardSelectionData(); - const textSelections = AREA_CLIPBOARD_COPIED_DATA.map((area) => { - return area.map((row) => row.join("\t")).join("\n"); - }); - try { - await navigator.clipboard.writeText(textSelections[0]); - } catch (e) { - console.error("failed"); - } - table.draw(); - }; - - const parseClipboardTextExcel = async () => { - try { - const text = await navigator.clipboard.readText(); - const rows = text.split(/\r\n|\n|\r/); - return rows.length > 0 ? rows.map((r) => r.split("\t")) : r; - } catch (e) { - console.error("failed"); - } - }; - - const _paste = (data) => - zip(getSelectedAreas(), data).map(([{x0, y0}, data]) => { - if (data) { - const x1 = x0 + data[0].length - 1; - const y1 = y0 + data.length - 1; - - data.map((row, ridx) => { - row.map((value, cidx) => { - write(x0 + cidx, y0 + ridx, value); - }); - }); - AREA_CLIPBOARD_PASTE_SELECTIONS.push({x0, y0, x1, y1}); - } - }); - - const paste = async () => { - AREA_CLIPBOARD_PASTE_SELECTIONS = []; - const parsedData = await parseClipboardTextExcel(); - const useLocalData = eqArray(parsedData, AREA_CLIPBOARD_COPIED_DATA[0]); - - if (!parsedData || useLocalData) { - const data = Array.from(Array(getSelectedAreas().length).keys()).flatMap(() => AREA_CLIPBOARD_COPIED_DATA); - _paste(data); - } else { - const data = Array.from(Array(getSelectedAreas().length).keys()).map(() => parsedData); - _paste(data); - } - await table.draw(); - }; - - const cut = async () => { - await copy(); - for (const {x0, x1, y0, y1} of AREA_CLIPBOARD_COPY_SELECTIONS) { - for (var x = x0; x < x1 + 1; x++) { - for (var y = y0; y < y1 + 1; y++) { - write(x, y, undefined); - } - } - } - table.draw(); - }; - - const keyListener = (event) => { - const meta = table.getMeta(event.target); - switch (event.keyCode) { - // C - case 67: - if (event.metaKey || event.ctrlKey) { - copy(); - } - break; - // V - case 86: - if (event.metaKey || event.ctrlKey) { - paste(); - } - break; - // X - case 88: - if (event.metaKey || event.ctrlKey) { - cut(); - } - break; - } - }; - - table.addEventListener("keydown", keyListener); - addAreaClipboardSelectionStyleListener(table, dl); - return table; -}; - const AREA_CLIPBOARD_COPY_SELECTED_CLASS = "clipboard-copy-selected-area"; const AREA_CLIPBOARD_PASTE_SELECTED_CLASS = "clipboard-paste-selected-area"; -const addAreaClipboardSelectionStyleListener = (table) => { - table.addStyleListener(() => { - const tds = table.querySelectorAll("tbody td"); - for (const td of tds) { - const meta = table.getMeta(td); - td.classList.remove(AREA_CLIPBOARD_COPY_SELECTED_CLASS); - td.classList.remove(AREA_CLIPBOARD_PASTE_SELECTED_CLASS); +const addAreaClipboardInteractionsStyleListener = (table) => { + table.addStyleListener(() => updateAreaClipboardInteractionsStyle(table)); +}; +``` +We'll make the logic for updating the `classList` available via a `function` that +can be called outside of the `StyleListener`, so that we can invoke it without forcing a +`draw()` on the `table`. + +Basically, `updateAreaClipboardInteractionsStyle()` iterates through the `td`s on the +screen removing the `AREA_CLIPBOARD_COPY_SELECTED_CLASS` and `AREA_CLIPBOARD_PASTE_SELECTED_CLASS` +from each and then checks to see if the `MetaData` shows that it intersects a copied +or pasted selection, reapplying the classes on a match. +```javascript +const updateAreaClipboardInteractionsStyle = (table) => { + const tds = table.querySelectorAll("tbody td"); + for (const td of tds) { + const meta = table.getMeta(td); + td.classList.remove(AREA_CLIPBOARD_COPY_SELECTED_CLASS); + td.classList.remove(AREA_CLIPBOARD_PASTE_SELECTED_CLASS); - const copyMatch = AREA_CLIPBOARD_COPY_SELECTIONS.find(({x0, x1, y0, y1}) => x0 <= meta.x && meta.x <= x1 && y0 <= meta.y && meta.y <= y1); + const copyMatch = AREA_CLIPBOARD_COPY_SELECTIONS.find(({x0, x1, y0, y1}) => x0 <= meta.x && meta.x <= x1 && y0 <= meta.y && meta.y <= y1); - const pasteMatch = AREA_CLIPBOARD_PASTE_SELECTIONS.find(({x0, x1, y0, y1}) => x0 <= meta.x && meta.x <= x1 && y0 <= meta.y && meta.y <= y1); + const pasteMatch = AREA_CLIPBOARD_PASTE_SELECTIONS.find(({x0, x1, y0, y1}) => x0 <= meta.x && meta.x <= x1 && y0 <= meta.y && meta.y <= y1); - if (!!copyMatch) { - td.classList.add(AREA_CLIPBOARD_COPY_SELECTED_CLASS); - } - if (!!pasteMatch) { - td.classList.add(AREA_CLIPBOARD_PASTE_SELECTED_CLASS); - } + if (!!copyMatch) { + td.classList.add(AREA_CLIPBOARD_COPY_SELECTED_CLASS); } - }); + if (!!pasteMatch) { + td.classList.add(AREA_CLIPBOARD_PASTE_SELECTED_CLASS); + } + } }; - +``` +## Our `DataListener` +Our `DataListener` generator uses some borrowed code from `two_billion_rows` and extends the `return`ed `object` with `allData` - exposed to enable our `write` `function`. +```javascript function generateDataListener(num_rows, num_columns) { const allData = range(0, num_columns, (x) => range(0, num_rows, (y) => `${x}, ${y}`)); return function dl(x0, y0, x1, y1) { @@ -223,19 +299,27 @@ function generateDataListener(num_rows, num_columns) { }; }; } - +``` +## On `"load"` +We will wire it all up on `"load"` by checking that the `table` exists on the `window` +then creating and setting our `DataListener`. We need to make sure that we +`addAreaMouseSelection()` and create a `write()` `function` for use in +`addAreaClipboardInteractions()`, and then we can `addAreaClipboardInteractions()` and kick +off a `draw()`. +```javascript window.addEventListener("load", () => { - const table = window.clipboardCopyPasteAreaRegularTable; + const table = window.areaClipboardInteractionsRegularTable; if (table) { const dl = generateDataListener(50, 50); + table.setDataListener(dl); + + addAreaMouseSelection(table); const write = (x, y, value) => { dl().allData[x][y] = value; }; - addAreaMouseSelection(table); - addAreaClipboard(table, dl, write); - table.setDataListener(dl); + addAreaClipboardInteractions(table, dl, write); table.draw(); } }); @@ -250,7 +334,7 @@ Our Libraries. ``` -The `two_billion_rows` example for the its `DataListener`. +The `two_billion_rows` example for the its helper `function`s. ```html From e663b96a43ad6c5d08e307a6987118b749fd3dec Mon Sep 17 00:00:00 2001 From: Joshua Hawkins Date: Tue, 18 Aug 2020 14:57:33 -0400 Subject: [PATCH 9/9] Move keyboard nav example to another PR --- examples/keyboard_navigation.md | 332 ------------------ .../keyboard_area_selection.test.js | 66 ---- .../keyboard_navigation.test.js | 208 ----------- .../keyboard_navigation_scrolling.test.js | 161 --------- 4 files changed, 767 deletions(-) delete mode 100644 examples/keyboard_navigation.md delete mode 100644 test/examples/keyboard_navigation/keyboard_area_selection.test.js delete mode 100644 test/examples/keyboard_navigation/keyboard_navigation.test.js delete mode 100644 test/examples/keyboard_navigation/keyboard_navigation_scrolling.test.js diff --git a/examples/keyboard_navigation.md b/examples/keyboard_navigation.md deleted file mode 100644 index 3c3e6b21..00000000 --- a/examples/keyboard_navigation.md +++ /dev/null @@ -1,332 +0,0 @@ -# Keyboard Navigation - -... - -```html - -``` -## Styling - -... - -```css -regular-table tbody tr th { - user-select: none; -} -regular-table thead tr th { - user-select: none; -} - -regular-table tbody tr td.keyboard-selected-area { - background-color: rgb(0, 0, 255, 0.15); /* blue */ -} - -regular-table tbody tr td.single-cell-selected { - background-color: rgb(255, 0, 0, 0.25); /* red */ -} - -regular-table tbody tr td.single-cell-selected.keyboard-selected-area { - background-color: rgb(128, 0, 128, 0.33); /* violet */ -} - -td { - outline: none; - border-right: 1px solid #eee; - border-bottom: 1px solid #eee; - min-width: 22px; -} -``` -... - -```javascript -const SINGLE_CELL_SELECTED_CLASS = "single-cell-selected"; -const KEYBOARD_SELECTED_AREA_CLASS = "keyboard-selected-area"; - -let KEYBOARD_SELECTED_AREA = {}; -const SELECTED_POSITION = {x: 0, y: 0}; - -const reapplyKeyboardArea = (table) => { - const tds = table.querySelectorAll("td"); - - for (const td of tds) { - const meta = table.getMeta(td); - - if (KEYBOARD_SELECTED_AREA.x0 <= meta.x && meta.x <= KEYBOARD_SELECTED_AREA.x1 && KEYBOARD_SELECTED_AREA.y0 <= meta.y && meta.y <= KEYBOARD_SELECTED_AREA.y1) { - td.classList.add(KEYBOARD_SELECTED_AREA_CLASS); - } else { - td.classList.remove(KEYBOARD_SELECTED_AREA_CLASS); - } - } -}; - -const updateFocus = (table, selectAll) => { - function selectElementContents(el) { - var range = document.createRange(); - range.selectNodeContents(el); - var sel = window.getSelection(); - sel.removeAllRanges(); - sel.addRange(range); - } - - const tds = table.querySelectorAll("td"); - - for (const td of tds) { - td.setAttribute("contenteditable", true); - const meta = table.getMeta(td); - - if (meta.x === SELECTED_POSITION.x && meta.y === SELECTED_POSITION.y) { - td.classList.add(SINGLE_CELL_SELECTED_CLASS); - td.focus(); - if (selectAll) { - selectElementContents(td); - } - } else { - td.classList.remove(SINGLE_CELL_SELECTED_CLASS); - } - } - reapplyKeyboardArea(table); -}; - -const getSelection = (table) => { - return document.activeElement || table.querySelector(`td.${SINGLE_CELL_SELECTED_CLASS}`); -}; - -const addKeyboardNavigation = (table, dl, write, editable = true) => { - table.addEventListener("scroll", () => { - const meta = table.getMeta(document.activeElement); - if (meta && write) { - write(meta.x, meta.y, document.activeElement.textContent); - } else { - } - }); - - table.addEventListener("focusout", (event) => { - const meta = table.getMeta(event.target); - if (meta && write) { - write(meta.x, meta.y, event.target.textContent); - } else { - } - }); - - const SCROLL_AHEAD = 4; - - function moveSelection(active_cell, dx, dy) { - const numCols = dl().num_columns; - const numRows = dl().num_rows; - - const meta = table.getMeta(active_cell); - - const x0 = typeof KEYBOARD_SELECTED_AREA.x0 === "number" ? KEYBOARD_SELECTED_AREA.x0 : SELECTED_POSITION.x; - const x1 = typeof KEYBOARD_SELECTED_AREA.x1 === "number" ? KEYBOARD_SELECTED_AREA.x1 : SELECTED_POSITION.x; - const y0 = typeof KEYBOARD_SELECTED_AREA.y0 === "number" ? KEYBOARD_SELECTED_AREA.y0 : SELECTED_POSITION.y; - const y1 = typeof KEYBOARD_SELECTED_AREA.y1 === "number" ? KEYBOARD_SELECTED_AREA.y1 : SELECTED_POSITION.y; - - const areaSelection = { - x0, - x1, - y0, - y1, - }; - - if (dx !== 0) { - if (meta.x + dx < numCols && 0 <= meta.x + dx) { - const x = meta.x + dx; - areaSelection.x0 = Math.min(areaSelection.x0, areaSelection.x1, x); - areaSelection.x1 = Math.max(areaSelection.x0, areaSelection.x1, x); - SELECTED_POSITION.x = x; - } - if (meta.x1 <= SELECTED_POSITION.x + SCROLL_AHEAD) { - table.scrollToCell(meta.x0 + 2, meta.y0, numCols, numRows); - } else if (SELECTED_POSITION.x - SCROLL_AHEAD < meta.x0) { - if (0 < meta.x0 - 1) { - table.scrollToCell(meta.x0 - 1, meta.y0, numCols, numRows); - } else { - table.scrollToCell(0, meta.y0, numCols, numRows); - } - } - } - - if (dy !== 0) { - if (meta.y + dy < numRows && 0 <= meta.y + dy) { - const y = meta.y + dy; - areaSelection.y0 = Math.min(areaSelection.y0, areaSelection.y1, y); - areaSelection.y1 = Math.max(areaSelection.y0, areaSelection.y1, y); - SELECTED_POSITION.y = y; - } - if (meta.y1 <= SELECTED_POSITION.y + SCROLL_AHEAD) { - table.scrollToCell(meta.x0, meta.y0 + 1, numCols, numRows); - } else if (SELECTED_POSITION.y - SCROLL_AHEAD + 2 < meta.y0) { - if (0 < meta.y0 - 1) { - table.scrollToCell(meta.x0, meta.y0 - 1, numCols, numRows); - } else { - table.scrollToCell(meta.x0, 0, numCols, numRows); - } - } - } - updateFocus(table, true, editable); - return areaSelection; - } - - table.addEventListener("keypress", (event) => { - const target = getSelection(table); - - if (event.keyCode === 13) { - event.preventDefault(); - if (event.shiftKey) { - moveSelection(target, 0, -1); - event.preventDefault(); - event.stopPropagation(); - } else { - moveSelection(target, 0, 1); - event.preventDefault(); - event.stopPropagation(); - } - } - }); - - table.addEventListener("keydown", (event) => { - const target = getSelection(table); - - let area; - switch (event.keyCode) { - // tab - case 9: - event.preventDefault(); - if (event.shiftKey) { - moveSelection(target, -1, 0); - event.preventDefault(); - event.stopPropagation(); - } else { - moveSelection(target, 1, 0); - event.preventDefault(); - event.stopPropagation(); - } - break; - // left arrow - case 37: - area = moveSelection(target, -1, 0); - event.preventDefault(); - event.stopPropagation(); - break; - // up arrow - case 38: - area = moveSelection(target, 0, -1); - event.preventDefault(); - event.stopPropagation(); - break; - // right arrow - case 39: - area = moveSelection(target, 1, 0); - event.preventDefault(); - event.stopPropagation(); - break; - // down arrow - case 40: - area = moveSelection(target, 0, 1); - event.preventDefault(); - event.stopPropagation(); - break; - } - if (event.shiftKey && area) { - KEYBOARD_SELECTED_AREA = area; - updateFocus(table, true, editable); - } else if (!event.metaKey && !event.ctrlKey) { - if (area) { - MOUSE_SELECTED_AREAS = []; - AREA_CLIPBOARD_COPY_SELECTIONS = []; - AREA_CLIPBOARD_PASTE_SELECTIONS = []; - AREA_CLIPBOARD_COPIED_DATA = []; - // table.draw(); - } - - KEYBOARD_SELECTED_AREA = {}; - reapplyKeyboardArea(table); - } - }); - - const makeSingleSelect = (event) => { - const meta = table.getMeta(event.target); - if (meta) { - SELECTED_POSITION.x = meta.x; - SELECTED_POSITION.y = meta.y; - updateFocus(table, false, editable); - } - }; - - table.addEventListener("click", makeSingleSelect); - table.addEventListener("mousedown", makeSingleSelect); - - addKeyboardNavigationStyleListener(table); - return table; -}; - -const addKeyboardNavigationStyleListener = (table) => { - table.addStyleListener(() => { - updateFocus(table, false, true); - }); -}; - -function generateDataListener(num_rows, num_columns) { - function to_column_name(i, letter) { - return Array(i).fill(letter).join(""); - } - - function generate_column_names() { - const nums = Array.from(Array(26)); - const alphabet = nums.map((val, i) => String.fromCharCode(i + 65)); - let caps = [], - i = 1; - while (caps.length < num_columns) { - caps = caps.concat(alphabet.map((letter) => to_column_name(i, letter))); - i++; - } - return caps; - } - - const column_names = generate_column_names(); - - const allData = Array(num_columns) - .fill() - .map(() => Array(num_rows).fill()); - - return function dl(x0 = 0, y0 = 0, x1 = 0, y1 = 0) { - return { - num_rows, - num_columns, - row_headers: Array.from(Array(Math.ceil(y1) - y0).keys()).map((y) => [`${y + y0}`]), - column_headers: column_names.slice(x0, x1).map((x) => [x]), - data: allData.slice(x0, x1).map((col) => col.slice(y0, y1)), - allData, - }; - }; -} - -window.addEventListener("load", () => { - const table = window.keyboardNavigationRegularTable; - if (table) { - const dl = generateDataListener(50, 50); - const write = (x, y, value) => { - dl().allData[x][y] = value; - }; - - addKeyboardNavigation(table, dl, write, true); - table.setDataListener(dl); - table.draw(); - } -}); -``` - -## Appendix (Dependencies) - -Our Libraries. - -```html - - -``` - -The `two_billion_rows` example for the its `DataListener`. - -```html - -``` diff --git a/test/examples/keyboard_navigation/keyboard_area_selection.test.js b/test/examples/keyboard_navigation/keyboard_area_selection.test.js deleted file mode 100644 index c882e06c..00000000 --- a/test/examples/keyboard_navigation/keyboard_area_selection.test.js +++ /dev/null @@ -1,66 +0,0 @@ -/****************************************************************************** - * - * 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. The full license can be found in the LICENSE - * file. - * - */ - -describe("keyboard_navigation.html", () => { - const keydownRightArrow = async (table, times = 1, shiftKey = false) => { - Array.from(Array(times)).forEach(async () => { - await page.evaluate( - async (table, shiftKey) => { - const event = document.createEvent("HTMLEvents"); - event.initEvent("keydown", false, true); - event.keyCode = 39; - if (shiftKey) { - event.shiftKey = true; - } - table.dispatchEvent(event); - }, - table, - shiftKey - ); - }); - }; - - const keydownDownArrow = async (table, times = 1, shiftKey = false) => { - Array.from(Array(times)).forEach(async () => { - await page.evaluate( - async (table, shiftKey) => { - const event = document.createEvent("HTMLEvents"); - event.initEvent("keydown", false, true); - event.keyCode = 40; - if (shiftKey) { - event.shiftKey = true; - } - table.dispatchEvent(event); - }, - table, - shiftKey - ); - }); - }; - - let table; - - beforeAll(async () => { - await page.goto("http://localhost:8081/dist/examples/keyboard_navigation.html"); - await page.setViewport({width: 150, height: 150}); - await page.waitFor("regular-table table tbody tr td"); - table = await page.$("regular-table"); - }); - - describe("Selecting an area with the keyboard", () => { - test("adds an area selection", async () => { - await keydownRightArrow(table, 2, true); - await keydownDownArrow(table, 2, true); - - const tds = await page.$$("regular-table tbody tr td.keyboard-selected-area"); - expect(tds.length).toEqual(9); - }); - }); -}); diff --git a/test/examples/keyboard_navigation/keyboard_navigation.test.js b/test/examples/keyboard_navigation/keyboard_navigation.test.js deleted file mode 100644 index d358c909..00000000 --- a/test/examples/keyboard_navigation/keyboard_navigation.test.js +++ /dev/null @@ -1,208 +0,0 @@ -/****************************************************************************** - * - * 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. The full license can be found in the LICENSE - * file. - * - */ - -describe("keyboard_navigation.html", () => { - const writeByReturn = async (table, text) => { - await page.evaluate( - async (table, text) => { - const target = document.activeElement; - target.textContent = text; - const event = document.createEvent("HTMLEvents"); - event.initEvent("keypress", false, true); - event.ctrlKey = true; - event.keyCode = 13; - table.dispatchEvent(event); - }, - table, - text - ); - }; - - const keypressReturn = async (table, times = 1, shiftKey) => { - Array.from(Array(times)).forEach(async () => { - await page.evaluate( - async (table, shiftKey) => { - const event = document.createEvent("HTMLEvents"); - event.initEvent("keypress", false, true); - event.ctrlKey = true; - if (shiftKey) { - event.shiftKey = true; - } - event.keyCode = 13; - table.dispatchEvent(event); - }, - table, - shiftKey - ); - }); - }; - - const keydownTab = async (table, times = 1, shiftKey = false) => { - Array.from(Array(times)).forEach(async () => { - await page.evaluate( - async (table, shiftKey) => { - const event = document.createEvent("HTMLEvents"); - event.initEvent("keydown", false, true); - event.keyCode = 9; - if (shiftKey) { - event.shiftKey = true; - } - table.dispatchEvent(event); - }, - table, - shiftKey - ); - }); - }; - - const keydownLeftArrow = async (table, times = 1) => { - Array.from(Array(times)).forEach(async () => { - await page.evaluate(async (table) => { - const event = document.createEvent("HTMLEvents"); - event.initEvent("keydown", false, true); - event.keyCode = 37; - table.dispatchEvent(event); - }, table); - }); - }; - - const keydownUpArrow = async (table, times = 1) => { - Array.from(Array(times)).forEach(async () => { - await page.evaluate(async (table) => { - const event = document.createEvent("HTMLEvents"); - event.initEvent("keydown", false, true); - event.keyCode = 38; - table.dispatchEvent(event); - }, table); - }); - }; - - const keydownRightArrow = async (table, times = 1) => { - Array.from(Array(times)).forEach(async () => { - await page.evaluate(async (table) => { - const event = document.createEvent("HTMLEvents"); - event.initEvent("keydown", false, true); - event.keyCode = 39; - table.dispatchEvent(event); - }, table); - }); - }; - - const keydownDownArrow = async (table, times = 1) => { - Array.from(Array(times)).forEach(async () => { - await page.evaluate(async (table) => { - const event = document.createEvent("HTMLEvents"); - event.initEvent("keydown", false, true); - event.keyCode = 40; - table.dispatchEvent(event); - }, table); - }); - }; - - beforeAll(async () => { - await page.goto("http://localhost:8081/dist/examples/keyboard_navigation.html"); - await page.setViewport({width: 500, height: 500}); - await page.waitFor("regular-table table tbody tr td"); - }); - - describe("Navigating with the keyboard", () => { - test("initializes with focus on (0,0)", async () => { - const table = await page.$("regular-table"); - await writeByReturn(table, "1. test origin"); - const tr = await page.$$("regular-table tbody tr:nth-of-type(1) td"); - const cellValue = await page.evaluate((td) => td.innerHTML, tr[0]); - expect(cellValue).toEqual("1. test origin"); - }); - - test("moves as right arrow is down", async () => { - const table = await page.$("regular-table"); - keydownRightArrow(table, 5); - await writeByReturn(table, "2. right arrow"); - - const tds = await page.$$("regular-table tbody tr:nth-of-type(2) td"); - const cellValue = await page.evaluate((td) => td.innerHTML, tds[5]); - expect(cellValue).toEqual("2. right arrow"); - - const ths = await page.$$("regular-table tbody tr:nth-of-type(1) th"); - const thValue = await page.evaluate((th) => th.innerHTML, ths[0]); - expect(thValue).toEqual("0"); - }); - - test("moves as down arrow is down", async () => { - const table = await page.$("regular-table"); - keydownDownArrow(table, 5); - await writeByReturn(table, "3. down arrow"); - - const tds = await page.$$("regular-table tbody tr:nth-of-type(8) td"); - const cellValue = await page.evaluate((td) => td.innerHTML, tds[5]); - expect(cellValue).toEqual("3. down arrow"); - }); - - test("moves as up arrow is down", async () => { - const table = await page.$("regular-table"); - keydownUpArrow(table, 5); - await writeByReturn(table, "4. up arrow"); - - const tds = await page.$$("regular-table tbody tr:nth-of-type(4) td"); - const cellValue = await page.evaluate((td) => td.innerHTML, tds[5]); - expect(cellValue).toEqual("4. up arrow"); - }); - - test("moves as left arrow is down", async () => { - const table = await page.$("regular-table"); - keydownLeftArrow(table, 3); - await writeByReturn(table, "5. left arrow"); - - const tds = await page.$$("regular-table tbody tr:nth-of-type(5) td"); - const cellValue = await page.evaluate((td) => td.innerHTML, tds[2]); - expect(cellValue).toEqual("5. left arrow"); - }); - - test("moves as return is pressed", async () => { - const table = await page.$("regular-table"); - keypressReturn(table); - await writeByReturn(table, "6. return"); - - const tds = await page.$$("regular-table tbody tr:nth-of-type(7) td"); - const cellValue = await page.evaluate((td) => td.innerHTML, tds[2]); - expect(cellValue).toEqual("6. return"); - }); - - test("moves as tab is down", async () => { - const table = await page.$("regular-table"); - keydownTab(table); - await writeByReturn(table, "7. tab"); - - const tds = await page.$$("regular-table tbody tr:nth-of-type(8) td"); - const cellValue = await page.evaluate((td) => td.innerHTML, tds[3]); - expect(cellValue).toEqual("7. tab"); - }); - - test("moves as shift tab is down", async () => { - const table = await page.$("regular-table"); - keydownTab(table, 1, true); - await writeByReturn(table, "8. shift tab"); - - const tds = await page.$$("regular-table tbody tr:nth-of-type(9) td"); - const cellValue = await page.evaluate((td) => td.innerHTML, tds[2]); - expect(cellValue).toEqual("8. shift tab"); - }); - - test("moves as shift return is pressed", async () => { - const table = await page.$("regular-table"); - keypressReturn(table, 2, true); - await writeByReturn(table, "9. shift return"); - - const tds = await page.$$("regular-table tbody tr:nth-of-type(8) td"); - const cellValue = await page.evaluate((td) => td.innerHTML, tds[2]); - expect(cellValue).toEqual("9. shift return"); - }); - }); -}); diff --git a/test/examples/keyboard_navigation/keyboard_navigation_scrolling.test.js b/test/examples/keyboard_navigation/keyboard_navigation_scrolling.test.js deleted file mode 100644 index 494fee2b..00000000 --- a/test/examples/keyboard_navigation/keyboard_navigation_scrolling.test.js +++ /dev/null @@ -1,161 +0,0 @@ -/****************************************************************************** - * - * 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. The full license can be found in the LICENSE - * file. - * - */ - -describe("keyboard_navigation.html", () => { - const writeByReturn = async (table, text) => { - await page.evaluate( - async (table, text) => { - const target = document.activeElement; - target.textContent = text; - const event = document.createEvent("HTMLEvents"); - event.initEvent("keypress", false, true); - event.ctrlKey = true; - event.keyCode = 13; - table.dispatchEvent(event); - }, - table, - text - ); - }; - - const keydownLeftArrow = async (table, times = 1) => { - Array.from(Array(times)).forEach(async () => { - await page.evaluate(async (table) => { - const event = document.createEvent("HTMLEvents"); - event.initEvent("keydown", false, true); - event.keyCode = 37; - table.dispatchEvent(event); - }, table); - }); - }; - - const keydownUpArrow = async (table, times = 1) => { - Array.from(Array(times)).forEach(async () => { - await page.evaluate(async (table) => { - const event = document.createEvent("HTMLEvents"); - event.initEvent("keydown", false, true); - event.keyCode = 38; - table.dispatchEvent(event); - }, table); - }); - }; - - const keydownRightArrow = async (table, times = 1) => { - Array.from(Array(times)).forEach(async () => { - await page.evaluate(async (table) => { - const event = document.createEvent("HTMLEvents"); - event.initEvent("keydown", false, true); - event.keyCode = 39; - table.dispatchEvent(event); - }, table); - }); - }; - - const keydownDownArrow = async (table, times = 1) => { - Array.from(Array(times)).forEach(async () => { - await page.evaluate(async (table) => { - const event = document.createEvent("HTMLEvents"); - event.initEvent("keydown", false, true); - event.keyCode = 40; - table.dispatchEvent(event); - }, table); - }); - }; - - let table; - - beforeAll(async () => { - await page.setViewport({width: 150, height: 150}); - await page.goto("http://localhost:8081/dist/examples/keyboard_navigation.html"); - await page.waitFor("regular-table table tbody tr td"); - table = await page.$("regular-table"); - }); - - describe("Scrolling with the arrow keys", () => { - test("scrolls as right arrow is down", async () => { - const text = "1. scrolling right"; - - keydownRightArrow(table, 5); - await writeByReturn(table, text); - - const tds = await page.$$("regular-table tbody tr:nth-of-type(1) td"); - - const rowThs = await page.$$("regular-table tbody tr:nth-of-type(1) th"); - const rowHeaderValue = await page.evaluate((th) => th.innerHTML, rowThs[0]); - expect(rowHeaderValue).toEqual("0"); - - const colThs = await page.$$("regular-table thead th"); - const colThValues = []; - for (const td of colThs) { - colThValues.push(await page.evaluate((td) => td.textContent.trim(), td)); - } - expect(colThValues).toEqual(["", "C", "D", "E", "F"]); - - const cellValue = await page.evaluate((td) => td.innerHTML, tds[tds.length - 1]); - expect(cellValue).toEqual(text); - }); - - xtest("scrolls as down arrow is down", async () => { - const text = "2. scrolling down"; - - keydownDownArrow(table, 5); - await writeByReturn(table, text); - - const rowThs = await page.$$("regular-table tbody tr:nth-of-type(1) th"); - const rowHeaderValue = await page.evaluate((th) => th.innerHTML, rowThs[0]); - expect(rowHeaderValue).toEqual("4"); - - const tds = await page.$$("regular-table tbody tr:nth-of-type(3) td"); - const cellValue = await page.evaluate((td) => td.innerHTML, tds[tds.length - 1]); - - expect(cellValue).toEqual(text); - }); - - xtest("scrolls as left arrow is down", async () => { - const text = "3. scrolling left"; - - keydownLeftArrow(table, 5); - await writeByReturn(table, text); - - const colThs = await page.$$("regular-table thead th"); - - const colThValues = []; - for (const td of colThs) { - colThValues.push(await page.evaluate((td) => td.textContent.trim(), td)); - } - expect(colThValues).toEqual(["", "A", "B"]); - - const tds = await page.$$("regular-table tbody tr:nth-of-type(3) td"); - const cellValue = await page.evaluate((td) => td.innerHTML, tds[0]); - - expect(cellValue).toEqual(text); - }); - - xtest("scrolls as up arrow is down", async () => { - const text = "4. scrolling up"; - - keydownUpArrow(table, 10); - await writeByReturn(table, text); - - const colThs = await page.$$("regular-table thead th"); - - const colThValues = []; - for (const td of colThs) { - colThValues.push(await page.evaluate((td) => td.textContent.trim(), td)); - } - expect(colThValues).toEqual(["", "A", "B"]); - - const tds = await page.$$("regular-table tbody tr:nth-of-type(1) td"); - const cellValue = await page.evaluate((td) => td.innerHTML, tds[0]); - - expect(cellValue).toEqual(text); - }); - }); -});