diff --git a/vuu-ui/cypress.config.ts b/vuu-ui/cypress.config.ts index d0e74de288..47eb6a9863 100644 --- a/vuu-ui/cypress.config.ts +++ b/vuu-ui/cypress.config.ts @@ -33,6 +33,7 @@ export default defineConfig({ viewportHeight: 1024, video: false, component: { + scrollBehavior: false, setupNodeEvents(on, config) { // installCoverageTask(on, config); //Setting up a log task to allow logging to the console during an axe test because console.log() does not work directly in a test diff --git a/vuu-ui/packages/vuu-table/src/Row.tsx b/vuu-ui/packages/vuu-table/src/Row.tsx index 5a2aa70056..e7ff5fd12a 100644 --- a/vuu-ui/packages/vuu-table/src/Row.tsx +++ b/vuu-ui/packages/vuu-table/src/Row.tsx @@ -14,23 +14,11 @@ import { RowSelected, } from "@finos/vuu-utils"; import cx from "clsx"; -import { CSSProperties, memo, MouseEvent, useCallback, useEffect } from "react"; +import { CSSProperties, memo, MouseEvent, useCallback } from "react"; import { TableCell, TableGroupCell } from "./table-cell"; import "./Row.css"; -// const cellStyle = { background: "green", width: 150 }; -// const MyCell = () => { -// useEffect(() => { -// console.log("MyCell mounted"); -// return () => { -// console.log("MyCell unmounted"); -// }; -// }, []); - -// return
; -// }; - export interface RowProps { className?: string; columnMap: ColumnMap; diff --git a/vuu-ui/packages/vuu-table/src/__tests__/__component__/Table.cy.tsx b/vuu-ui/packages/vuu-table/src/__tests__/__component__/Table.cy.tsx index a733530f32..a3b9a018c5 100644 --- a/vuu-ui/packages/vuu-table/src/__tests__/__component__/Table.cy.tsx +++ b/vuu-ui/packages/vuu-table/src/__tests__/__component__/Table.cy.tsx @@ -1,18 +1,41 @@ import React from "react"; // TODO try and get TS path alias working to avoid relative paths like this -import { Instruments } from "../../../../../showcase/src/examples/Table/SIMUL.examples"; +import { SimulTable } from "../../../../../showcase/src/examples/Table/SIMUL.examples"; +import { TestTable } from "../../../../../showcase/src/examples/Table/Table.examples"; +import { assertRenderedRows } from "./table-test-utils"; + +const withAriaIndex = (index: number) => ({ + name: (_: string, el: Element) => el.ariaRowIndex === `${index}`, +}); describe("WHEN it initially renders", () => { + const RENDER_BUFFER = 5; + const ROW_COUNT = 1000; + const tableConfig = { + renderBufferSize: RENDER_BUFFER, + headerHeight: 25, + height: 625, + rowCount: ROW_COUNT, + rowHeight: 20, + width: 1000, + }; + it("THEN expected classname is present", () => { cy.mount( - ); const container = cy.findByTestId("table"); container.should("have.class", "vuuTable"); }); + + it("THEN expected number of rows are present, with buffered rows, all with correct aria index", () => { + cy.mount(); + assertRenderedRows({ from: 0, to: 30 }, RENDER_BUFFER, ROW_COUNT); + }); }); diff --git a/vuu-ui/packages/vuu-table/src/__tests__/__component__/Table.scrolling.cy.tsx b/vuu-ui/packages/vuu-table/src/__tests__/__component__/Table.scrolling.cy.tsx index a06e1ab775..c520a6b93b 100644 --- a/vuu-ui/packages/vuu-table/src/__tests__/__component__/Table.scrolling.cy.tsx +++ b/vuu-ui/packages/vuu-table/src/__tests__/__component__/Table.scrolling.cy.tsx @@ -1,31 +1,184 @@ -import React from "react"; // TODO try and get TS path alias working to avoid relative paths like this import { TestTable } from "../../../../../showcase/src/examples/Table/Table.examples"; +import { assertRenderedRows, withAriaIndex } from "./table-test-utils"; -const withAriaIndex = (index: number) => ({ - name: (_: string, el: HTMLElement) => el.ariaRowIndex === `${index}`, -}); +describe("Table scrolling and keyboard navigation", () => { + const RENDER_BUFFER = 5; + const ROW_COUNT = 1000; + const tableConfig = { + renderBufferSize: RENDER_BUFFER, + headerHeight: 25, + height: 625, + rowCount: ROW_COUNT, + rowHeight: 20, + width: 1000, + }; + describe("Page Keys", () => { + describe("WHEN first cell is focussed and page down pressed", () => { + it("THEN table scrolls down and next page of rows are rendered, first cell of new page is focussed", () => { + cy.mount(); + + // interestingly, realClick doesn't work here + cy.findByRole("cell", { name: "row 1" }).click(); + cy.findByRole("cell", { name: "row 1" }).should( + "have.attr", + "tabindex", + "0" + ); + cy.findByRole("cell", { name: "row 1" }).should("be.focused"); + cy.realPress("PageDown"); + + cy.findByRole("row", withAriaIndex(25)).should("not.exist"); + cy.findByRole("row", withAriaIndex(26)).should("exist"); + + cy.get(".vuuTable-contentContainer") + .then((el) => el[0].scrollTop) + .should("equal", 600); + + // row 31 should be top row in viewport + cy.findByRole("row", withAriaIndex(31)).should( + "have.css", + "transform", + "matrix(1, 0, 0, 1, 0, 600)" + ); + + cy.findByRole("cell", { name: "row 31" }).should( + "have.attr", + "tabindex", + "0" + ); + cy.findByRole("cell", { name: "row 31" }).should("be.focused"); + }); + + describe("AND WHEN page up is then pressed", () => { + it("THEN table is back to original state, and first cell is once again focussed", () => { + cy.mount(); + + // interestingly, realClick doesn't work here + cy.findByRole("cell", { name: "row 1" }).click(); + + cy.realPress("PageDown"); + cy.wait(60); + cy.realPress("PageUp"); + + cy.findByRole("cell", { name: "row 1" }).should( + "have.attr", + "tabindex", + "0" + ); + cy.findByRole("cell", { name: "row 1" }).should("be.focused"); + + assertRenderedRows({ from: 0, to: 30 }, RENDER_BUFFER, ROW_COUNT); + }); + }); + }); + }); + + describe("Home / End Keys", () => { + describe("WHEN topmost rows are in viewport, first cell is focussed and Home key pressed ", () => { + it("THEN nothing changes", () => { + cy.mount(); + // interestingly, realClick doesn't work here + cy.findByRole("cell", { name: "row 1" }).click(); + cy.realPress("Home"); + cy.findByRole("cell", { name: "row 1" }).should( + "have.attr", + "tabindex", + "0" + ); + cy.findByRole("cell", { name: "row 1" }).should("be.focused"); + assertRenderedRows({ from: 0, to: 30 }, RENDER_BUFFER, ROW_COUNT); + }); + }); + describe("WHEN topmost rows are in viewport, cell in middle of viewport is focussed and Home key pressed ", () => { + it("THEN no scrolling, but focus moves to first cell", () => { + cy.mount(); + // interestingly, realClick doesn't work here + cy.findByRole("cell", { name: "row 5" }).click(); + cy.realPress("Home"); + cy.findByRole("cell", { name: "row 1" }).should( + "have.attr", + "tabindex", + "0" + ); + cy.findByRole("cell", { name: "row 1" }).should("be.focused"); + assertRenderedRows({ from: 0, to: 30 }, RENDER_BUFFER, ROW_COUNT); + }); + }); + + describe("WHEN topmost rows are in viewport, first cell is focussed and End key pressed ", () => { + it("THEN scrolls to end of data, last cell is focussed (same column)", () => { + cy.mount(); + // interestingly, realClick doesn't work here + cy.findByRole("cell", { name: "row 1" }).click(); + cy.realPress("End"); + cy.findByRole("cell", { name: "row 1,000" }).should( + "have.attr", + "tabindex", + "0" + ); + cy.findByRole("cell", { name: "row 1,000" }).should("be.focused"); + assertRenderedRows({ from: 970, to: 1000 }, RENDER_BUFFER, ROW_COUNT); + }); + }); + + describe("WHEN topmost rows are in viewport, cell mid viewport focussed and End key pressed ", () => { + it("THEN scrolls to end of data, last cell is focussed (same column)", () => { + cy.mount(); + // interestingly, realClick doesn't work here + cy.findByRole("cell", { name: "row 10" }).click(); + cy.realPress("End"); + cy.findByRole("cell", { name: "row 1,000" }).should( + "have.attr", + "tabindex", + "0" + ); + cy.findByRole("cell", { name: "row 1,000" }).should("be.focused"); + assertRenderedRows({ from: 970, to: 1000 }, RENDER_BUFFER, ROW_COUNT); + }); + }); + }); + + describe("Arrow Up / Down Keys", () => { + describe("WHEN topmost rows are in viewport, first cell is focussed and Down Arrow key pressed ", () => { + it("THEN no scrolling, focus moved down to next cell", () => { + cy.mount(); + // interestingly, realClick doesn't work here + cy.findByRole("cell", { name: "row 1" }).click(); + cy.realPress("ArrowDown"); + cy.findByRole("cell", { name: "row 2" }).should( + "have.attr", + "tabindex", + "0" + ); + cy.findByRole("cell", { name: "row 2" }).should("be.focused"); + assertRenderedRows({ from: 0, to: 30 }, RENDER_BUFFER, ROW_COUNT); + }); + }); + describe("WHEN topmost rows are in viewport, first cell in last row is focussed and Down Arrow key pressed ", () => { + it("THEN scroll down by 1 row, cell in bottom row has focus", () => { + cy.mount(); + // interestingly, realClick doesn't work here + cy.findByRole("cell", { name: "row 30" }).click(); + cy.realPress("ArrowDown"); + cy.findByRole("cell", { name: "row 31" }).should( + "have.attr", + "tabindex", + "0" + ); + cy.findByRole("cell", { name: "row 31" }).should("be.focused"); + assertRenderedRows({ from: 1, to: 31 }, RENDER_BUFFER, ROW_COUNT); + }); + }); + }); -describe("WHEN it initially renders", () => { - it("THEN expected number of rows are present, with buffered rows, all with correct aria index", () => { - cy.mount( - - ); - - // Note the Table Headers row is included in count - const container = cy.findAllByRole("row").should("have.length", 36); - cy.findByRole("row", withAriaIndex(0)).should("not.exist"); - cy.findByRole("row", withAriaIndex(1)).should("be.visible"); - cy.findByRole("row", withAriaIndex(30)).should("be.visible"); - cy.findByRole("row", withAriaIndex(31)).should("not.be.visible"); - cy.findByRole("row", withAriaIndex(35)).should("not.be.visible"); - cy.findByRole("row", withAriaIndex(36)).should("not.exist"); + describe("scrolling with Scrollbar", () => { + describe("WHEN scrolled down by a distance equating to 500 rows", () => { + it("THEN correct rows are within viewport", () => { + cy.mount(); + cy.get(".vuuTable-scrollbarContainer").scrollTo(0, 10000); + assertRenderedRows({ from: 500, to: 530 }, RENDER_BUFFER, ROW_COUNT); + }); + }); }); }); diff --git a/vuu-ui/packages/vuu-table/src/__tests__/__component__/table-test-utils.ts b/vuu-ui/packages/vuu-table/src/__tests__/__component__/table-test-utils.ts new file mode 100644 index 0000000000..e566d3068b --- /dev/null +++ b/vuu-ui/packages/vuu-table/src/__tests__/__component__/table-test-utils.ts @@ -0,0 +1,43 @@ +import { VuuRange } from "packages/vuu-protocol-types"; + +export const withAriaIndex = (index: number) => ({ + name: (_: string, el: Element) => el.ariaRowIndex === `${index}`, +}); + +export const assertRenderedRows = ( + { from, to }: VuuRange, + renderBufferSize: number, + totalRowCount: number +) => { + const leadingBufferedRows = from < renderBufferSize ? from : renderBufferSize; + const offsetFromEnd = totalRowCount - to; + const trailingBufferedRows = + offsetFromEnd < renderBufferSize + ? Math.min(0, offsetFromEnd) + : renderBufferSize; + const renderedRowCount = + to - from + leadingBufferedRows + trailingBufferedRows; + + // Note the Table Headers row is included in count, hence the + 1 + cy.findAllByRole("row").should("have.length", renderedRowCount + 1); + + // we use the aria index for locators, which is 1 based + const firstRenderedRow = from - leadingBufferedRows + 1; + const firstVisibleRow = from + 1; + const lastVisibleRow = to; + const lastRenderedRow = to + trailingBufferedRows; + + cy.findByRole("row", withAriaIndex(firstRenderedRow - 1)).should("not.exist"); + cy.findByRole("row", withAriaIndex(firstVisibleRow)).should("be.visible"); + cy.findByRole("row", withAriaIndex(lastVisibleRow)).should("be.visible"); + + if (trailingBufferedRows > 0) { + cy.findByRole("row", withAriaIndex(lastVisibleRow + 1)).should( + "not.be.visible" + ); + cy.findByRole("row", withAriaIndex(lastRenderedRow)).should( + "not.be.visible" + ); + } + cy.findByRole("row", withAriaIndex(lastRenderedRow + 1)).should("not.exist"); +}; diff --git a/vuu-ui/packages/vuu-table/src/useDataSource.ts b/vuu-ui/packages/vuu-table/src/useDataSource.ts index 290199626f..90d6559e53 100644 --- a/vuu-ui/packages/vuu-table/src/useDataSource.ts +++ b/vuu-ui/packages/vuu-table/src/useDataSource.ts @@ -112,6 +112,7 @@ export const useDataSource = ({ const setRange = useCallback( (range: VuuRange) => { + console.log(`set Range ${range.from} ${range.to}`); if (!rangesAreSame(range, rangeRef.current)) { const fullRange = getFullRange(range, renderBufferSize); dataWindow.setRange(fullRange); diff --git a/vuu-ui/packages/vuu-table/src/useTableScroll.ts b/vuu-ui/packages/vuu-table/src/useTableScroll.ts index 4637ab16b2..455a7dee96 100644 --- a/vuu-ui/packages/vuu-table/src/useTableScroll.ts +++ b/vuu-ui/packages/vuu-table/src/useTableScroll.ts @@ -190,7 +190,7 @@ export const useTableScroll = ({ const firstRow = getRowAtPosition(scrollTop); if (firstRow !== firstRowRef.current) { firstRowRef.current = firstRow; - setRange({ from: firstRow, to: firstRow + viewportRowCount + 1 }); + setRange({ from: firstRow, to: firstRow + viewportRowCount }); } }, [getRowAtPosition, onVerticalScroll, setRange, viewportRowCount] @@ -368,7 +368,7 @@ export const useTableScroll = ({ onVerticalScrollInSitu?.(offset); const firstRow = firstRowRef.current + offset; firstRowRef.current = firstRow; - setRange({ from: firstRow, to: firstRow + viewportRowCount + 1 }); + setRange({ from: firstRow, to: firstRow + viewportRowCount }); } else { const scrollBy = direction === "down" ? appliedPageSize : -appliedPageSize; diff --git a/vuu-ui/showcase/src/examples/Table/SIMUL.examples.tsx b/vuu-ui/showcase/src/examples/Table/SIMUL.examples.tsx index f49d811732..7c83ef83f5 100644 --- a/vuu-ui/showcase/src/examples/Table/SIMUL.examples.tsx +++ b/vuu-ui/showcase/src/examples/Table/SIMUL.examples.tsx @@ -56,10 +56,11 @@ const getDefaultColumnConfig = ( } }; -const SimulTable = ({ +export const SimulTable = ({ getDefaultColumnConfig, + height = 625, renderBufferSize = 0, - tableName, + tableName = "instruments", ...props }: Partial & { getDefaultColumnConfig?: DefaultColumnConfiguration; @@ -96,6 +97,7 @@ const SimulTable = ({ ); }; - -export const Instruments = () => ; -Instruments.displaySequence = displaySequence++; +SimulTable.displaySequence = displaySequence++; export const InstrumentsExtended = () => ( );