From 1cf35fd7ea8c0433d170864fe4485281b40036f9 Mon Sep 17 00:00:00 2001 From: edleeks87 Date: Tue, 11 Jul 2023 12:04:51 +0100 Subject: [PATCH 1/2] chore(flat-table-checkbox): add tags util to component --- .../flat-table-checkbox.component.tsx | 10 +++++++--- .../flat-table-checkbox.spec.tsx | 14 ++++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/components/flat-table/flat-table-checkbox/flat-table-checkbox.component.tsx b/src/components/flat-table/flat-table-checkbox/flat-table-checkbox.component.tsx index a007a9ee3d..714efc3334 100644 --- a/src/components/flat-table/flat-table-checkbox/flat-table-checkbox.component.tsx +++ b/src/components/flat-table/flat-table-checkbox/flat-table-checkbox.component.tsx @@ -2,7 +2,9 @@ import React, { useLayoutEffect, useRef } from "react"; import StyledFlatTableCheckbox from "./flat-table-checkbox.style"; import { Checkbox } from "../../checkbox"; import Events from "../../../__internal__/utils/helpers/events/events"; -import { TagProps } from "../../../__internal__/utils/helpers/tags"; +import tagComponent, { + TagProps, +} from "../../../__internal__/utils/helpers/tags"; export interface FlatTableCheckboxProps extends TagProps { /** Prop to polymorphically render either a 'th' or 'td' element */ @@ -84,9 +86,11 @@ export const FlatTableCheckbox = ({ className={reportCellWidth ? "isSticky" : undefined} leftPosition={leftPosition} rightPosition={rightPosition} - data-element={dataElement} as={as} - {...rest} + {...tagComponent("flat-table-checkbox", { + "data-element": dataElement, + ...rest, + })} > {selectable && ( ).mockImplementation( @@ -135,4 +136,17 @@ describe("FlatTableCheckbox", () => { expect(wrapper.find(Checkbox).exists()).toEqual(false); }); }); + + describe("data tags", () => { + it("has the expected attributes applied to the elements", () => { + const wrapper = render({ "data-element": "foo", "data-role": "bar" }); + + rootTagTest( + wrapper.find(StyledFlatTableCheckbox), + "flat-table-checkbox", + "foo", + "bar" + ); + }); + }); }); From ea27a57734ed75301a07d855fbd303a4a1686ce0 Mon Sep 17 00:00:00 2001 From: edleeks87 Date: Wed, 26 Jul 2023 16:54:34 +0100 Subject: [PATCH 2/2] feat(flat-table, flat-table-row): components can now be wrapped, subRows no longer need to be arrays Refactors `FlatTable` and sub-components to allow consumers greater flexibility with respect to wrapping the components by removing code that relied on iterating over children and cloning. Refactors `FlatTableRow` so that it is no longer required that an array is passed to the `subRows` prop. fix #6219 --- .../components/flat-table/flat-table.cy.tsx | 21 +- .../__internal__/build-position-map.ts | 18 + .../flat-table/__internal__/index.ts | 2 + .../use-calculate-sticky-cells.ts | 32 + .../flat-table-cell.component.tsx | 94 +- .../flat-table-cell/flat-table-cell.style.ts | 12 +- .../flat-table-checkbox.component.tsx | 51 +- .../flat-table-checkbox.style.ts | 7 +- .../flat-table-head.component.tsx | 65 +- .../flat-table-header.component.tsx | 52 +- .../flat-table-header.spec.tsx | 20 + .../flat-table-header.style.ts | 9 +- .../flat-table-row-header.component.tsx | 97 +-- .../flat-table-row-header.spec.tsx | 87 +- .../flat-table-row-header.style.ts | 14 +- .../__internal__/flat-table-row-context.tsx | 17 + .../flat-table-row-draggable.component.tsx | 15 +- .../__internal__/sub-row-provider.tsx | 39 + .../flat-table-row.component.tsx | 279 +++--- .../flat-table-row/flat-table-row.spec.tsx | 276 +++++- .../flat-table-row/flat-table-row.style.ts | 17 +- .../flat-table/flat-table-test.stories.tsx | 90 +- .../flat-table/flat-table.component.tsx | 41 +- src/components/flat-table/flat-table.spec.tsx | 812 +++++------------- .../flat-table/flat-table.stories.tsx | 6 + 25 files changed, 1095 insertions(+), 1078 deletions(-) create mode 100644 src/components/flat-table/__internal__/build-position-map.ts create mode 100644 src/components/flat-table/__internal__/index.ts create mode 100644 src/components/flat-table/__internal__/use-calculate-sticky-cells.ts create mode 100644 src/components/flat-table/flat-table-row/__internal__/flat-table-row-context.tsx create mode 100644 src/components/flat-table/flat-table-row/__internal__/sub-row-provider.tsx diff --git a/cypress/components/flat-table/flat-table.cy.tsx b/cypress/components/flat-table/flat-table.cy.tsx index 02d11e6751..f5ee33a889 100644 --- a/cypress/components/flat-table/flat-table.cy.tsx +++ b/cypress/components/flat-table/flat-table.cy.tsx @@ -7,7 +7,6 @@ import { import * as stories from "../../../src/components/flat-table/flat-table-test.stories"; import { FlatTableProps } from "../../../src/components/flat-table/flat-table.component"; import { FlatTableRowProps } from "../../../src/components/flat-table/flat-table-row/flat-table-row.component"; -import { FlatTableCellProps } from "../../../src/components/flat-table/flat-table-cell/flat-table-cell.component"; import Icon from "../../../src/components/icon"; import CypressMountWithProviders from "../../support/component-helper/cypress-mount"; import { getDataElementByValue, cyRoot } from "../../locators"; @@ -60,6 +59,7 @@ import { positionOfElement, getRotationAngle, } from "../../support/helper"; +import { FlatTableRowContextProps } from "../../../src/components/flat-table/flat-table-row/__internal__/flat-table-row-context"; const sizes = [ ["compact", "8px", "13px", 24], @@ -299,6 +299,21 @@ context("Tests for Flat Table component", () => { } }); + it("should render Flat Table with sticky header and multiple rows", () => { + CypressMountWithProviders( +
+ +
+ ); + + flatTableHeaderRowByPosition(0) + .find("th") + .should("have.css", "top", "0px"); + flatTableHeaderRowByPosition(1) + .find("th") + .should("have.css", "top", "40px"); + }); + it("should render Flat Table with sticky footer", () => { CypressMountWithProviders( @@ -2670,7 +2685,9 @@ context("Tests for Flat Table component", () => { }); it("should call onClick when first Flat Table column is sorted", () => { - const callback: FlatTableCellProps["onClick"] = cy.stub().as("onClick"); + const callback: FlatTableRowContextProps["onClick"] = cy + .stub() + .as("onClick"); CypressMountWithProviders( ); diff --git a/src/components/flat-table/__internal__/build-position-map.ts b/src/components/flat-table/__internal__/build-position-map.ts new file mode 100644 index 0000000000..dce78245c8 --- /dev/null +++ b/src/components/flat-table/__internal__/build-position-map.ts @@ -0,0 +1,18 @@ +export default ( + array: HTMLElement[], + propertyName: "offsetWidth" | "offsetHeight" +) => + array.reduce((acc: Record, _, index) => { + const currentId = array[index].getAttribute("id"); + if (currentId) { + if (index === 0) { + acc[currentId] = 0; + } else { + const previousId = array[index - 1].getAttribute("id"); + if (previousId) { + acc[currentId] = acc[previousId] + array[index - 1][propertyName]; + } + } + } + return acc; + }, {}); diff --git a/src/components/flat-table/__internal__/index.ts b/src/components/flat-table/__internal__/index.ts new file mode 100644 index 0000000000..3350f5173f --- /dev/null +++ b/src/components/flat-table/__internal__/index.ts @@ -0,0 +1,2 @@ +export { default as useCalculateStickyCells } from "./use-calculate-sticky-cells"; +export { default as buildPositionMap } from "./build-position-map"; diff --git a/src/components/flat-table/__internal__/use-calculate-sticky-cells.ts b/src/components/flat-table/__internal__/use-calculate-sticky-cells.ts new file mode 100644 index 0000000000..92eb3ff09e --- /dev/null +++ b/src/components/flat-table/__internal__/use-calculate-sticky-cells.ts @@ -0,0 +1,32 @@ +import { useContext } from "react"; +import FlatTableRowContext from "../flat-table-row/__internal__/flat-table-row-context"; + +export default (id: string) => { + const { + expandable, + firstCellId, + firstColumnExpandable, + leftPositions, + rightPositions, + onClick, + onKeyDown, + } = useContext(FlatTableRowContext); + + const leftPosition = leftPositions[id]; + const rightPosition = rightPositions[id]; + const makeCellSticky = + leftPosition !== undefined || rightPosition !== undefined; + const isFirstCell = id === firstCellId; + const isExpandableCell = expandable && isFirstCell && firstColumnExpandable; + + return { + expandable, + leftPosition, + rightPosition, + makeCellSticky, + onClick, + onKeyDown, + isFirstCell, + isExpandableCell, + }; +}; diff --git a/src/components/flat-table/flat-table-cell/flat-table-cell.component.tsx b/src/components/flat-table/flat-table-cell/flat-table-cell.component.tsx index 9e4afd6b9f..11251237be 100644 --- a/src/components/flat-table/flat-table-cell/flat-table-cell.component.tsx +++ b/src/components/flat-table/flat-table-cell/flat-table-cell.component.tsx @@ -1,10 +1,4 @@ -import React, { - useLayoutEffect, - useRef, - useState, - useEffect, - useContext, -} from "react"; +import React, { useRef, useState, useEffect, useContext } from "react"; import { PaddingProps } from "styled-system"; import { TableBorderSize, TableCellAlign } from ".."; @@ -15,6 +9,7 @@ import { import Icon from "../../icon"; import { FlatTableThemeContext } from "../flat-table.component"; import guid from "../../../__internal__/utils/helpers/guid"; +import useCalculateStickyCells from "../__internal__/use-calculate-sticky-cells"; export interface FlatTableCellProps extends PaddingProps { /** Content alignment */ @@ -35,102 +30,61 @@ export interface FlatTableCellProps extends PaddingProps { verticalBorder?: TableBorderSize; /** Sets the color of the right border */ verticalBorderColor?: string; - /** Sets an id string on the DOM element */ + /** Sets an id string on the element */ id?: string; - /** - * @private - * @ignore - */ - expandable?: boolean; - /** - * @private - * @ignore - */ - onClick?: () => void; - /** - * @private - * @ignore - */ - onKeyDown?: () => void; - /** - * @private - * @ignore - * Sets the left position when sticky column found - */ - leftPosition?: number; - /** - * @private - * @ignore - * Sets the right position when sticky column found - */ - rightPosition?: number; - /** - * @private - * @ignore - * Index of cell within row - */ - cellIndex?: number; - /** - * @private - * @ignore - * Callback to report the offsetWidth - */ - reportCellWidth?: (offset: number, index?: number) => void; } export const FlatTableCell = ({ align = "left", children, pl, - expandable = false, - onClick, - onKeyDown, - reportCellWidth, - cellIndex, - leftPosition, - rightPosition, width, truncate = false, title, colspan, rowspan, + id, ...rest }: FlatTableCellProps) => { const ref = useRef(null); - const id = useRef(guid()); + const internalId = useRef(id || guid()); const [tabIndex, setTabIndex] = useState(-1); const { selectedId } = useContext(FlatTableThemeContext); - - useLayoutEffect(() => { - if (ref.current && reportCellWidth) { - reportCellWidth(ref.current.offsetWidth, cellIndex); - } - }, [reportCellWidth, cellIndex]); + const { + leftPosition, + rightPosition, + expandable, + onClick, + onKeyDown, + isFirstCell, + isExpandableCell, + makeCellSticky, + } = useCalculateStickyCells(internalId.current); useEffect(() => { - setTabIndex(selectedId === id.current ? 0 : -1); - }, [selectedId]); + setTabIndex(isExpandableCell && selectedId === internalId.current ? 0 : -1); + }, [selectedId, isExpandableCell]); return ( - {expandable && ( + {expandable && isFirstCell && ( )} {children} diff --git a/src/components/flat-table/flat-table-cell/flat-table-cell.style.ts b/src/components/flat-table/flat-table-cell/flat-table-cell.style.ts index 6864199ffa..4a831ee8a9 100644 --- a/src/components/flat-table/flat-table-cell/flat-table-cell.style.ts +++ b/src/components/flat-table/flat-table-cell/flat-table-cell.style.ts @@ -13,17 +13,15 @@ const verticalBorderSizes = { interface StyledFlatTableCellProps extends Pick< FlatTableCellProps, - | "align" - | "leftPosition" - | "rightPosition" - | "expandable" - | "verticalBorder" - | "verticalBorderColor" + "align" | "verticalBorder" | "verticalBorderColor" >, PaddingProps { makeCellSticky: boolean; colWidth?: number; isTruncated: boolean; + leftPosition: number; + rightPosition: number; + expandable?: boolean; } const StyledFlatTableCell = styled.td` @@ -112,7 +110,7 @@ const StyledFlatTableCell = styled.td` `} `; -const StyledCellContent = styled.div>` +const StyledCellContent = styled.div<{ expandable?: boolean }>` ${({ expandable }) => expandable && css` diff --git a/src/components/flat-table/flat-table-checkbox/flat-table-checkbox.component.tsx b/src/components/flat-table/flat-table-checkbox/flat-table-checkbox.component.tsx index 714efc3334..0535756af7 100644 --- a/src/components/flat-table/flat-table-checkbox/flat-table-checkbox.component.tsx +++ b/src/components/flat-table/flat-table-checkbox/flat-table-checkbox.component.tsx @@ -1,10 +1,12 @@ -import React, { useLayoutEffect, useRef } from "react"; +import React, { useContext, useRef } from "react"; import StyledFlatTableCheckbox from "./flat-table-checkbox.style"; import { Checkbox } from "../../checkbox"; import Events from "../../../__internal__/utils/helpers/events/events"; import tagComponent, { TagProps, } from "../../../__internal__/utils/helpers/tags"; +import guid from "../../../__internal__/utils/helpers/guid"; +import FlatTableRowContext from "../flat-table-row/__internal__/flat-table-row-context"; export interface FlatTableCheckboxProps extends TagProps { /** Prop to polymorphically render either a 'th' or 'td' element */ @@ -19,30 +21,8 @@ export interface FlatTableCheckboxProps extends TagProps { onClick?: (ev: React.MouseEvent) => void; /** The id of the element that labels the input */ ariaLabelledBy?: string; - /** - * @private - * @ignore - * Sets the left position when sticky column found - */ - leftPosition?: number; - /** - * @private - * @ignore - * Sets the right position when sticky column found - */ - rightPosition?: number; - /** - * @private - * @ignore - * Index of cell within row - */ - cellIndex?: number; - /** - * @private - * @ignore - * Callback to report the offsetWidth - */ - reportCellWidth?: (offset: number, index?: number) => void; + /** Sets an id string on the element */ + id?: string; } export const FlatTableCheckbox = ({ @@ -51,20 +31,18 @@ export const FlatTableCheckbox = ({ onChange, selectable = true, onClick, - leftPosition, - rightPosition, - cellIndex, - reportCellWidth, ariaLabelledBy, + id, ...rest }: FlatTableCheckboxProps) => { const ref = useRef(null); + const internalId = useRef(id || guid()); + const { leftPositions, rightPositions } = useContext(FlatTableRowContext); - useLayoutEffect(() => { - if (ref.current && reportCellWidth) { - reportCellWidth(ref.current.offsetWidth, cellIndex); - } - }, [reportCellWidth, cellIndex]); + const leftPosition = leftPositions[internalId.current]; + const rightPosition = rightPositions[internalId.current]; + const makeCellSticky = + leftPosition !== undefined || rightPosition !== undefined; const dataElement = `flat-table-checkbox-${as === "td" ? "cell" : "header"}`; @@ -82,8 +60,8 @@ export const FlatTableCheckbox = ({ return ( {selectable && ( { + extends Pick { makeCellSticky: boolean; + leftPosition: number; + rightPosition: number; } const StyledFlatTableCheckbox = styled.td` diff --git a/src/components/flat-table/flat-table-head/flat-table-head.component.tsx b/src/components/flat-table/flat-table-head/flat-table-head.component.tsx index 415e7ae92d..a29787e316 100644 --- a/src/components/flat-table/flat-table-head/flat-table-head.component.tsx +++ b/src/components/flat-table/flat-table-head/flat-table-head.component.tsx @@ -1,57 +1,46 @@ import React, { useEffect, useState, useRef } from "react"; import StyledFlatTableHead from "./flat-table-head.style"; -import FlatTableRowHeader from "../flat-table-row-header"; -import { FlatTableRowProps } from "../flat-table-row"; +import { buildPositionMap } from "../__internal__"; export interface FlatTableHeadProps { /** Array of FlatTableRow. */ children: React.ReactNode; } -const getRefs = (length: number) => - Array.from({ length }, () => React.createRef()); +interface FlatTableHeadContextProps { + stickyOffsets: Record; +} + +export const FlatTableHeadContext = React.createContext( + { + stickyOffsets: {}, + } +); export const FlatTableHead = ({ children, ...rest }: FlatTableHeadProps) => { - const [rowHeights, setRowHeights] = useState([]); - const refs = useRef(getRefs(React.Children.count(children))); - let hasFlatTableRowHeader: boolean; + const ref = useRef(null); + const [stickyOffsets, setStickyOffsets] = useState>( + {} + ); useEffect(() => { - if (React.Children.count(children) > 1) { - setRowHeights(refs.current.map((ref) => ref.current?.clientHeight || 0)); + const headerRows = ref.current?.querySelectorAll("tr"); + + /* istanbul ignore else */ + if (headerRows) { + setStickyOffsets( + buildPositionMap(Array.from(headerRows), "offsetHeight") + ); + } else { + setStickyOffsets({}); } }, [children]); - if (React.Children.count(children) === 1) { - return {children}; - } - return ( - - {React.Children.map(children, (child, index) => { - /* Applies left border if preceding row has a FlatTableRowHeader and current one does not. - This is only needed when the preceding row has rowSpans applied, - as in any other use case the rows will all have FlatTableRowHeaders */ - const previousRowHasHeader = !!hasFlatTableRowHeader; - hasFlatTableRowHeader = - React.isValidElement(child) && - !!React.Children.toArray(child.props.children).find( - (c) => - React.isValidElement(c) && - (c.type as React.FunctionComponent).displayName === - FlatTableRowHeader.displayName - ); - return ( - React.isValidElement(child) && - React.cloneElement(child as React.ReactElement, { - stickyOffset: rowHeights - .slice(0, index) - .reduce((a: number, b: number) => a + b, 0), - ref: refs.current[index], - applyBorderLeft: previousRowHasHeader && !hasFlatTableRowHeader, - }) - ); - })} + + + {children} + ); }; diff --git a/src/components/flat-table/flat-table-header/flat-table-header.component.tsx b/src/components/flat-table/flat-table-header/flat-table-header.component.tsx index 90d91b5813..7c008ae514 100644 --- a/src/components/flat-table/flat-table-header/flat-table-header.component.tsx +++ b/src/components/flat-table/flat-table-header/flat-table-header.component.tsx @@ -1,9 +1,11 @@ -import React, { useLayoutEffect, useRef, useContext } from "react"; +import React, { useRef, useContext } from "react"; import { PaddingProps } from "styled-system"; import { TableBorderSize, TableCellAlign } from ".."; import StyledFlatTableHeader from "./flat-table-header.style"; import { FlatTableThemeContext } from "../flat-table.component"; +import guid from "../../../__internal__/utils/helpers/guid"; +import useCalculateStickyCells from "../__internal__/use-calculate-sticky-cells"; export interface FlatTableHeaderProps extends PaddingProps { /** Content alignment */ @@ -22,32 +24,8 @@ export interface FlatTableHeaderProps extends PaddingProps { verticalBorderColor?: string; /** Column width, pass a number to set a fixed width in pixels */ width?: number; - /** Sets an id string on the DOM element */ + /** Sets an id string on the element */ id?: string; - /** - * @private - * @ignore - * Sets the left position when sticky column found - */ - leftPosition?: number; - /** - * @private - * @ignore - * Sets the right position when sticky column found - */ - rightPosition?: number; - /** - * @private - * @ignore - * Index of cell within row - */ - cellIndex?: number; - /** - * @private - * @ignore - * Callback to report the offsetWidth - */ - reportCellWidth?: (offset: number, index?: number) => void; } export const FlatTableHeader = ({ @@ -58,28 +36,25 @@ export const FlatTableHeader = ({ width, py, px, - reportCellWidth, - cellIndex, - leftPosition, - rightPosition, + id, ...rest }: FlatTableHeaderProps) => { const ref = useRef(null); + const internalId = useRef(id || guid()); const { colorTheme } = useContext(FlatTableThemeContext); - - useLayoutEffect(() => { - if (ref.current && reportCellWidth) { - reportCellWidth(ref.current.offsetWidth, cellIndex); - } - }, [reportCellWidth, cellIndex]); + const { + leftPosition, + rightPosition, + makeCellSticky, + } = useCalculateStickyCells(internalId.current); return (
{children}
diff --git a/src/components/flat-table/flat-table-header/flat-table-header.spec.tsx b/src/components/flat-table/flat-table-header/flat-table-header.spec.tsx index d544c10694..8209fec042 100644 --- a/src/components/flat-table/flat-table-header/flat-table-header.spec.tsx +++ b/src/components/flat-table/flat-table-header/flat-table-header.spec.tsx @@ -141,4 +141,24 @@ describe("FlatTableHeader", () => { }); } ); + + describe("when colspan and rowSpan are passed", () => { + it("should be set on the underlying element", () => { + const wrapper = mount( + + + + Children + + + Children + + +
+ ); + + expect(wrapper.find(StyledFlatTableHeader).at(0).prop("colSpan")).toBe(2); + expect(wrapper.find(StyledFlatTableHeader).at(1).prop("rowSpan")).toBe(2); + }); + }); }); diff --git a/src/components/flat-table/flat-table-header/flat-table-header.style.ts b/src/components/flat-table/flat-table-header/flat-table-header.style.ts index 50bb602645..a75d59cc03 100644 --- a/src/components/flat-table/flat-table-header/flat-table-header.style.ts +++ b/src/components/flat-table/flat-table-header/flat-table-header.style.ts @@ -15,17 +15,14 @@ const verticalBorderSizes = { interface StyledFlatTableHeaderProps extends Pick< FlatTableHeaderProps, - | "align" - | "leftPosition" - | "rightPosition" - | "verticalBorder" - | "verticalBorderColor" - | "alternativeBgColor" + "align" | "verticalBorder" | "verticalBorderColor" | "alternativeBgColor" >, PaddingProps { makeCellSticky: boolean; colWidth?: number; colorTheme: FlatTableProps["colorTheme"]; + leftPosition: number; + rightPosition: number; } const StyledFlatTableHeader = styled.th` diff --git a/src/components/flat-table/flat-table-row-header/flat-table-row-header.component.tsx b/src/components/flat-table/flat-table-row-header/flat-table-row-header.component.tsx index ede36faca9..217e44b5aa 100644 --- a/src/components/flat-table/flat-table-row-header/flat-table-row-header.component.tsx +++ b/src/components/flat-table/flat-table-row-header/flat-table-row-header.component.tsx @@ -1,9 +1,9 @@ import React, { useCallback, - useEffect, useContext, useState, useRef, + useEffect, } from "react"; import { PaddingProps } from "styled-system"; import { TableBorderSize, TableCellAlign } from ".."; @@ -15,8 +15,12 @@ import { } from "./flat-table-row-header.style"; import { FlatTableThemeContext } from "../flat-table.component"; import guid from "../../../__internal__/utils/helpers/guid"; +import tagComponent, { + TagProps, +} from "../../../__internal__/utils/helpers/tags/tags"; +import useCalculateStickyCells from "../__internal__/use-calculate-sticky-cells"; -export interface FlatTableRowHeaderProps extends PaddingProps { +export interface FlatTableRowHeaderProps extends PaddingProps, TagProps { /** Content alignment */ align?: TableCellAlign; /** RowHeader content */ @@ -37,47 +41,8 @@ export interface FlatTableRowHeaderProps extends PaddingProps { colspan?: number | string; /** Number of rows that a header cell should span */ rowspan?: number | string; - /** Sets an id string on the DOM element */ + /** Sets an id string on the element */ id?: string; - /** - * @private - * @ignore - */ - expandable?: boolean; - /** - * @private - * @ignore - */ - onClick?: (ev: React.MouseEvent) => void; - /** - * @private - * @ignore - */ - onKeyDown?: (ev: React.KeyboardEvent) => void; - /** - * @private - * @ignore - * Sets the left position when sticky column found - */ - leftPosition?: number; - /** - * @private - * @ignore - * Sets the right position when sticky column found - */ - rightPosition?: number; - /** - * @private - * @ignore - * Index of cell within row - */ - cellIndex?: number; - /** - * @private - * @ignore - * Callback to report the offsetWidth - */ - reportCellWidth?: (offset: number, index?: number) => void; } export const FlatTableRowHeader = ({ @@ -86,39 +51,48 @@ export const FlatTableRowHeader = ({ width, py, px, - expandable = false, - onClick, - onKeyDown, - leftPosition, - rightPosition, truncate, title, stickyAlignment = "left", colspan, rowspan, + id, ...rest }: FlatTableRowHeaderProps) => { - const id = useRef(guid()); + const internalId = useRef(id || guid()); const [tabIndex, setTabIndex] = useState(-1); const { selectedId } = useContext(FlatTableThemeContext); + const { + leftPosition, + rightPosition, + expandable, + onClick, + onKeyDown, + isFirstCell, + isExpandableCell, + } = useCalculateStickyCells(internalId.current); + + useEffect(() => { + setTabIndex(isExpandableCell && selectedId === internalId.current ? 0 : -1); + }, [selectedId, isExpandableCell]); + const handleOnClick = useCallback( (ev: React.MouseEvent) => { - if (expandable && onClick) onClick(ev); + if (isExpandableCell && onClick) onClick(ev); }, - [expandable, onClick] + [isExpandableCell, onClick] ); + const handleOnKeyDown = useCallback( (ev: React.KeyboardEvent) => { - if (expandable && onKeyDown) onKeyDown(ev); + if (isExpandableCell && onKeyDown) { + onKeyDown(ev); + } }, - [expandable, onKeyDown] + [isExpandableCell, onKeyDown] ); - useEffect(() => { - setTabIndex(selectedId === id.current ? 0 : -1); - }, [selectedId]); - return ( - {expandable && ( + {expandable && isFirstCell && ( )} {children} diff --git a/src/components/flat-table/flat-table-row-header/flat-table-row-header.spec.tsx b/src/components/flat-table/flat-table-row-header/flat-table-row-header.spec.tsx index 02619ecbe6..3f55d82552 100644 --- a/src/components/flat-table/flat-table-row-header/flat-table-row-header.spec.tsx +++ b/src/components/flat-table/flat-table-row-header/flat-table-row-header.spec.tsx @@ -10,6 +10,7 @@ import { testStyledSystemPadding, } from "../../../__spec_helper__/test-utils"; import StyledIcon from "../../icon/icon.style"; +import FlatTableRowContext from "../flat-table-row/__internal__/flat-table-row-context"; describe("FlatTableRowHeader", () => { testStyledSystemPadding( @@ -106,13 +107,22 @@ describe("FlatTableRowHeader", () => { }); }); - describe("when expandable prop is true", () => { + describe("when expandable", () => { it("should render an arrow icon", () => { const wrapper = mount( - + + +
@@ -128,7 +138,18 @@ describe("FlatTableRowHeader", () => { - + + +
@@ -147,7 +168,18 @@ describe("FlatTableRowHeader", () => { - + + +
@@ -168,7 +200,16 @@ describe("FlatTableRowHeader", () => { - + + +
@@ -187,7 +228,16 @@ describe("FlatTableRowHeader", () => { - + + +
@@ -248,6 +298,31 @@ describe("FlatTableRowHeader", () => { }); }); + describe("stickyAlignment", () => { + it.each(["left", "right"])( + "sets the data-sticky-align attribute to %s", + (stickyAlignment) => { + const element = mount( + + + + + Foo + + + +
+ ) + .find(StyledFlatTableRowHeader) + .getDOMNode(); + + expect(element.getAttribute("data-sticky-align")).toEqual( + stickyAlignment + ); + } + ); + }); + describe.each([ ["small", "1px", "left"], ["small", "1px", "right"], diff --git a/src/components/flat-table/flat-table-row-header/flat-table-row-header.style.ts b/src/components/flat-table/flat-table-row-header/flat-table-row-header.style.ts index 6454762500..fe653dd8e2 100644 --- a/src/components/flat-table/flat-table-row-header/flat-table-row-header.style.ts +++ b/src/components/flat-table/flat-table-row-header/flat-table-row-header.style.ts @@ -11,7 +11,19 @@ const verticalBorderSizes = { large: "4px", }; -const StyledFlatTableRowHeader = styled.th` +const StyledFlatTableRowHeader = styled.th.attrs( + ({ + stickyAlignment, + }: { + stickyAlignment: FlatTableRowHeaderProps["stickyAlignment"]; + }) => ({ "data-sticky-align": stickyAlignment }) +)< + FlatTableRowHeaderProps & { + expandable?: boolean; + leftPosition?: number; + rightPosition?: number; + } +>` ${({ align, theme, diff --git a/src/components/flat-table/flat-table-row/__internal__/flat-table-row-context.tsx b/src/components/flat-table/flat-table-row/__internal__/flat-table-row-context.tsx new file mode 100644 index 0000000000..5437582b26 --- /dev/null +++ b/src/components/flat-table/flat-table-row/__internal__/flat-table-row-context.tsx @@ -0,0 +1,17 @@ +import React, { createContext } from "react"; + +export interface FlatTableRowContextProps { + expandable?: boolean; + onClick?: (ev?: React.MouseEvent) => void; + onKeyDown?: (ev: React.KeyboardEvent) => void; + firstCellId: string | null; + leftPositions: Record; + rightPositions: Record; + firstColumnExpandable?: boolean; +} + +export default createContext({ + firstCellId: "", + leftPositions: {}, + rightPositions: {}, +}); diff --git a/src/components/flat-table/flat-table-row/__internal__/flat-table-row-draggable.component.tsx b/src/components/flat-table/flat-table-row/__internal__/flat-table-row-draggable.component.tsx index 0ade4302a8..b322c82ff1 100644 --- a/src/components/flat-table/flat-table-row/__internal__/flat-table-row-draggable.component.tsx +++ b/src/components/flat-table/flat-table-row/__internal__/flat-table-row-draggable.component.tsx @@ -12,6 +12,8 @@ export interface FlatTableRowDraggableProps { moveItem?: (id?: number | string, index?: number) => void; /** item is draggable */ draggable?: boolean; + /** ref for row element */ + rowRef?: React.ForwardedRef; } interface DragItem { @@ -24,6 +26,7 @@ export const FlatTableRowDraggable = ({ id, findItem, moveItem, + rowRef, }: FlatTableRowDraggableProps) => { const originalIndex = Number(findItem?.(id).index); @@ -57,7 +60,17 @@ export const FlatTableRowDraggable = ({ key: originalIndex, id, isDragging, - ref: (node: HTMLElement) => drag(drop(node)), + ref: (node: HTMLTableRowElement) => { + drag(drop(node)); + /* istanbul ignore else */ + if (rowRef) { + if (typeof rowRef === "function") { + rowRef(node); + } else { + rowRef.current = node; + } + } + }, }); }; diff --git a/src/components/flat-table/flat-table-row/__internal__/sub-row-provider.tsx b/src/components/flat-table/flat-table-row/__internal__/sub-row-provider.tsx new file mode 100644 index 0000000000..b5cd04eda3 --- /dev/null +++ b/src/components/flat-table/flat-table-row/__internal__/sub-row-provider.tsx @@ -0,0 +1,39 @@ +import React, { createContext, useCallback, useState } from "react"; + +export interface SubRowContextProps { + isSubRow?: boolean; + firstRowId: string; + addRow: (id: string) => void; + removeRow: (id: string) => void; +} + +export const SubRowContext = createContext({ + isSubRow: false, + firstRowId: "", + addRow: () => {}, + removeRow: () => {}, +}); + +const SubRowProvider = ({ children }: { children: React.ReactNode }) => { + const [rowIds, setRowIds] = useState([]); + + const addRow = useCallback((id: string) => { + setRowIds((p) => [...p, id]); + }, []); + + const removeRow = useCallback((id: string) => { + setRowIds((p) => p.filter((rowId) => rowId !== id)); + }, []); + + return ( + + {children} + + ); +}; + +SubRowProvider.displayName = "SubRowProvider"; + +export default SubRowProvider; diff --git a/src/components/flat-table/flat-table-row/flat-table-row.component.tsx b/src/components/flat-table/flat-table-row/flat-table-row.component.tsx index 94576fe35a..a5410933cd 100644 --- a/src/components/flat-table/flat-table-row/flat-table-row.component.tsx +++ b/src/components/flat-table/flat-table-row/flat-table-row.component.tsx @@ -1,11 +1,10 @@ import React, { - useCallback, useContext, useEffect, - useLayoutEffect, useMemo, useRef, useState, + useLayoutEffect, } from "react"; import invariant from "invariant"; import { TableBorderSize } from ".."; @@ -13,13 +12,16 @@ import { TableBorderSize } from ".."; import Event from "../../../__internal__/utils/helpers/events"; import StyledFlatTableRow from "./flat-table-row.style"; import { DrawerSidebarContext } from "../../drawer"; -import FlatTableCheckbox from "../flat-table-checkbox"; import FlatTableRowHeader from "../flat-table-row-header"; import FlatTableRowDraggable, { FlatTableRowDraggableProps, } from "./__internal__/flat-table-row-draggable.component"; import { FlatTableThemeContext } from "../flat-table.component"; import guid from "../../../__internal__/utils/helpers/guid"; +import FlatTableRowContext from "./__internal__/flat-table-row-context"; +import SubRowProvider, { SubRowContext } from "./__internal__/sub-row-provider"; +import { buildPositionMap } from "../__internal__"; +import { FlatTableHeadContext } from "../flat-table-head/flat-table-head.component"; export interface FlatTableRowProps { /** Overrides default cell color, provide design token, any color from palette or any valid css color value. */ @@ -44,18 +46,6 @@ export interface FlatTableRowProps { selected?: boolean; /** Sub rows to be shown when the row is expanded, must be used with the `expandable` prop. */ subRows?: React.ReactNode; - /** @ignore @private */ - isSubRow?: boolean; - /** @ignore @private */ - isFirstSubRow?: boolean; - /** @ignore @private position in header if multiple rows */ - stickyOffset?: number; - /** @ignore @private applies a border-left to the first child */ - applyBorderLeft?: boolean; - /** ID for use in drag and drop functionality - * @private - * @ignore - */ id?: string | number; /** * @private @@ -69,8 +59,6 @@ export interface FlatTableRowProps { moveItem?: FlatTableRowDraggableProps["moveItem"]; /** @ignore @private position in header if multiple rows */ draggable?: boolean; - /** @ignore @private */ - ref?: React.RefObject; } export const FlatTableRow = React.forwardRef< @@ -84,16 +72,12 @@ export const FlatTableRow = React.forwardRef< expandable, expandableArea = "wholeRow", expanded = false, - isSubRow, - isFirstSubRow, - stickyOffset, highlighted, selected, subRows, bgColor, horizontalBorderColor, horizontalBorderSize = "small", - applyBorderLeft, id, draggable, findItem, @@ -102,47 +86,94 @@ export const FlatTableRow = React.forwardRef< }: FlatTableRowProps, ref ) => { - const internalId = useRef(id ?? guid()); + const internalId = useRef(id ? String(id) : guid()); const [isExpanded, setIsExpanded] = useState(expanded); let rowRef = useRef(null); if (ref) { rowRef = ref as React.MutableRefObject; } const firstColumnExpandable = expandableArea === "firstColumn"; - const [leftStickyCellWidths, setLeftStickyCellWidths] = useState( - [] + const [leftPositions, setLeftPositions] = useState>( + {} ); - const [rightStickyCellWidths, setRightStickyCellWidths] = useState< - number[] - >([]); - const [leftPositions, setLeftPositions] = useState([]); - const [rightPositions, setRightPositions] = useState([]); - const childrenArray = useMemo(() => React.Children.toArray(children), [ + const [rightPositions, setRightPositions] = useState< + Record + >({}); + const [firstCellIndex, setFirstCellIndex] = useState(0); + const [lhsRowHeaderIndex, setLhsRowHeaderIndex] = useState(-1); + const [rhsRowHeaderIndex, setRhsRowHeaderIndex] = useState(-1); + const [firstCellId, setFirstCellId] = useState(null); + const [cellsArray, setCellsArray] = useState([]); + const [tabIndex, setTabIndex] = useState(-1); + let interactiveRowProps = {}; + + useLayoutEffect(() => { + const checkForPositionUpdates = ( + updated: Record, + current: Record + ) => { + const updatedKeys = Object.keys(updated); + const currentKeys = Object.keys(current); + if (updatedKeys.length !== currentKeys.length) { + return true; + } + + return updatedKeys.some((key) => updated[key] !== current[key]); + }; + + const cells = rowRef.current?.querySelectorAll("th, td") as + | NodeListOf + | undefined; + + const cellArray = Array.from(cells || []); + setCellsArray(cellArray); + + const firstIndex = cellArray.findIndex( + (cell) => cell.getAttribute("data-component") !== "flat-table-checkbox" + ); + const lhsIndex = cellArray.findIndex( + (cell) => cell.getAttribute("data-sticky-align") === "left" + ); + const rhsIndex = cellArray.findIndex( + (cell) => cell.getAttribute("data-sticky-align") === "right" + ); + + setLhsRowHeaderIndex(lhsIndex); + setRhsRowHeaderIndex(rhsIndex); + + if (firstIndex !== -1) { + setFirstCellIndex(firstIndex); + setFirstCellId(cellArray[firstIndex].getAttribute("id")); + } else { + setFirstCellIndex(0); + } + if (lhsIndex !== -1) { + const updatedLeftPositions = buildPositionMap( + cellArray.slice(0, lhsRowHeaderIndex + 1), + "offsetWidth" + ); + + if (checkForPositionUpdates(updatedLeftPositions, leftPositions)) { + setLeftPositions(updatedLeftPositions); + } + } + if (rhsIndex !== -1) { + const updatedRightPositions = buildPositionMap( + cellArray.slice(rhsRowHeaderIndex, cellArray.length).reverse(), + "offsetWidth" + ); + + if (checkForPositionUpdates(updatedRightPositions, rightPositions)) { + setRightPositions(updatedRightPositions); + } + } + }, [ children, + leftPositions, + lhsRowHeaderIndex, + rhsRowHeaderIndex, + rightPositions, ]); - const lhsRowHeaderIndex = useMemo( - () => - childrenArray.findIndex( - (child) => - React.isValidElement(child) && - (child.type as React.FunctionComponent).displayName === - FlatTableRowHeader.displayName && - child.props.stickyAlignment !== "right" - ), - [childrenArray] - ); - const rhsRowHeaderIndex = useMemo( - () => - childrenArray.findIndex( - (child) => - React.isValidElement(child) && - (child.type as React.FunctionComponent).displayName === - FlatTableRowHeader.displayName && - child.props.stickyAlignment === "right" - ), - [childrenArray] - ); - const [tabIndex, setTabIndex] = useState(-1); const noStickyColumnsOverlap = useMemo(() => { const hasLhsColumn = lhsRowHeaderIndex !== -1; @@ -161,36 +192,7 @@ export const FlatTableRow = React.forwardRef< FlatTableThemeContext ); const { isInSidebar } = useContext(DrawerSidebarContext); - - const reportCellWidth = useCallback( - (width, index) => { - const isLeftSticky = index < lhsRowHeaderIndex; - const copiedArray = isLeftSticky - ? leftStickyCellWidths - : rightStickyCellWidths; - - if (copiedArray[index] !== undefined) { - copiedArray[index] = width; - } else { - copiedArray.push(width); - } - if (isLeftSticky) { - setLeftStickyCellWidths(copiedArray); - } else { - setRightStickyCellWidths(copiedArray); - } - }, - [lhsRowHeaderIndex, leftStickyCellWidths, rightStickyCellWidths] - ); - - let interactiveRowProps = {}; - - const firstCellIndex = - React.isValidElement(childrenArray[0]) && - childrenArray[0].type === FlatTableCheckbox - ? 1 - : 0; - + const { stickyOffsets } = useContext(FlatTableHeadContext); const toggleExpanded = () => setIsExpanded(!isExpanded); function onKeyDown( @@ -243,48 +245,13 @@ export const FlatTableRow = React.forwardRef< } } - const buildPositionArray = ( - setter: React.Dispatch>, - widthsArray: number[], - length: number - ) => { - setter([ - 0, - ...Array.from({ length }).map( - (_, index) => - widthsArray.slice(0, index + 1).reduce((a, b) => a + b, 0), - 0 - ), - ]); - }; - - useLayoutEffect(() => { - if (leftStickyCellWidths.length && lhsRowHeaderIndex !== -1) { - buildPositionArray( - setLeftPositions, - leftStickyCellWidths, - lhsRowHeaderIndex - ); - } - }, [lhsRowHeaderIndex, leftStickyCellWidths]); - - useLayoutEffect(() => { - if (rightStickyCellWidths.length && rhsRowHeaderIndex !== -1) { - buildPositionArray( - setRightPositions, - rightStickyCellWidths, - childrenArray.length - (rhsRowHeaderIndex + 1) - ); - } - }, [rhsRowHeaderIndex, rightStickyCellWidths, childrenArray]); - useEffect(() => { setIsExpanded(expanded); }, [expanded]); useEffect(() => { if (highlighted || selected) { - setSelectedId(String(internalId.current)); + setSelectedId(internalId.current); } }, [highlighted, selected, setSelectedId]); @@ -292,6 +259,21 @@ export const FlatTableRow = React.forwardRef< setTabIndex(selectedId === internalId.current ? 0 : -1); }, [selectedId]); + const { isSubRow, firstRowId, addRow, removeRow } = useContext( + SubRowContext + ); + + useEffect(() => { + const rowId = internalId.current; + addRow(rowId); + + return () => { + removeRow(rowId); + }; + }, [addRow, removeRow]); + + const isFirstSubRow = firstRowId === internalId.current; + const rowComponent = () => ( - {React.Children.map(children, (child, index) => { - return ( - React.isValidElement(child) && - React.cloneElement(child as React.ReactElement, { - expandable: expandable && index === firstCellIndex, - onClick: - expandable && index === firstCellIndex && firstColumnExpandable - ? () => toggleExpanded() - : undefined, - onKeyDown: - expandable && index === firstCellIndex && firstColumnExpandable - ? handleCellKeyDown - : undefined, - cellIndex: index, - reportCellWidth: - index < lhsRowHeaderIndex || - (rhsRowHeaderIndex !== -1 && index > rhsRowHeaderIndex) - ? reportCellWidth - : undefined, - leftPosition: leftPositions[index], - rightPosition: rightPositions[childrenArray.length - (index + 1)], - ...child.props, - }) - ); - })} + toggleExpanded(), + }} + > + {children} + ); @@ -352,6 +321,7 @@ export const FlatTableRow = React.forwardRef< id={internalId.current} moveItem={moveItem} findItem={findItem} + rowRef={rowRef} > {rowComponent()} @@ -360,18 +330,7 @@ export const FlatTableRow = React.forwardRef< return ( <> {draggable ? draggableComponent() : rowComponent()} - {isExpanded && - subRows && - React.Children.map( - subRows, - (child, index) => - child && - React.cloneElement(child as React.ReactElement, { - isSubRow: true, - isFirstSubRow: index === 0, - ...(React.isValidElement(child) && { ...child.props }), - }) - )} + {isExpanded && subRows && {subRows}} ); } diff --git a/src/components/flat-table/flat-table-row/flat-table-row.spec.tsx b/src/components/flat-table/flat-table-row/flat-table-row.spec.tsx index 1f4e81b291..c8fd4e8fed 100644 --- a/src/components/flat-table/flat-table-row/flat-table-row.spec.tsx +++ b/src/components/flat-table/flat-table-row/flat-table-row.spec.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useRef } from "react"; import { mount, ReactWrapper } from "enzyme"; import { act } from "react-dom/test-utils"; @@ -17,11 +17,6 @@ import FlatTableRowHeader from "../flat-table-row-header/flat-table-row-header.c import FlatTableHeader from "../flat-table-header/flat-table-header.component"; import { FlatTableBodyDraggable } from ".."; import { FlatTableThemeContext } from "../flat-table.component"; -import guid from "../../../__internal__/utils/helpers/guid"; - -const mockedGuid = "guid-12345"; -jest.mock("../../../__internal__/utils/helpers/guid"); -(guid as jest.MockedFunction).mockImplementation(() => mockedGuid); const events = { enter: { @@ -584,9 +579,6 @@ describe("FlatTableRow", () => { ); - act(() => - wrapper.find(FlatTableHeader).at(0)?.props()?.reportCellWidth?.(200, 0) - ); assertStyleMatch( { @@ -642,13 +634,6 @@ describe("FlatTableRow", () => { ); - act(() => - wrapper - .find(FlatTableHeader) - .at(1) - ?.props() - ?.reportCellWidth?.(200, 0) - ); assertStyleMatch( { @@ -1125,6 +1110,169 @@ describe("FlatTableRow", () => { }); }); + describe("when passing sub rows as a component", () => { + const SubRowsComponent = () => ( + <> + + sub1cell1 + sub1cell2 + + + sub2cell1 + sub2cell2 + + + ); + + it("should expand the sub rows when row is clicked", () => { + const wrapper = renderFlatTableRow({ + expandable: true, + subRows: , + }); + + act(() => { + wrapper.find(StyledFlatTableRow).at(0).props().onClick(); + }); + + wrapper.update(); + + expect(wrapper.find(StyledFlatTableRow).length).toEqual(3); + + act(() => { + wrapper.find(StyledFlatTableRow).at(0).props().onClick(); + }); + + wrapper.update(); + + expect(wrapper.find(StyledFlatTableRow).length).toEqual(1); + }); + + it("should expand the sub rows when first column clicked and expandable area is first column", () => { + const wrapper = renderFlatTableRow({ + expandable: true, + subRows: , + expandableArea: "firstColumn", + }); + + act(() => { + wrapper + .find(StyledFlatTableRow) + .find(StyledFlatTableCell) + .at(0) + .props() + .onClick(); + }); + + wrapper.update(); + + expect(wrapper.find(StyledFlatTableRow).length).toEqual(3); + + act(() => { + wrapper + .find(StyledFlatTableRow) + .find(StyledFlatTableCell) + .at(0) + .props() + .onClick(); + }); + + wrapper.update(); + + expect(wrapper.find(StyledFlatTableRow).length).toEqual(1); + }); + + it("should toggle the open/close state of the row when enter/space pressed", () => { + const element = document.createElement("div"); + const htmlElement = document.body.appendChild(element); + + const wrapper = mount( + + + }> + cell1 + cell2 + + +
, + { attachTo: htmlElement } + ); + + (wrapper + .find(StyledFlatTableRow) + .at(0) + .getDOMNode() as HTMLElement).focus(); + + act(() => { + wrapper + .find(StyledFlatTableRow) + .at(0) + .props() + .onKeyDown(events.enter); + }); + + wrapper.update(); + + expect(wrapper.find(StyledFlatTableRow).length).toEqual(3); + + act(() => { + wrapper + .find(StyledFlatTableRow) + .at(0) + .props() + .onKeyDown(events.space); + }); + + wrapper.update(); + + expect(wrapper.find(StyledFlatTableRow).length).toEqual(1); + }); + + it("should toggle the open/close state of the row when enter/space pressed and expandable area is first column", () => { + const wrapper = renderFlatTableRow({ + expandable: true, + subRows: , + expandableArea: "firstColumn", + }); + + expect(wrapper.find(StyledFlatTableRow).length).toEqual(1); + + (wrapper + .find(StyledFlatTableRow) + .at(0) + .find("td") + .at(0) + .getDOMNode() as HTMLElement).focus(); + + act(() => { + wrapper + .find(StyledFlatTableRow) + .at(0) + .find(StyledFlatTableCell) + .at(0) + .props() + .onKeyDown(events.enter); + }); + + wrapper.update(); + + expect(wrapper.find(StyledFlatTableRow).length).toEqual(3); + + act(() => { + wrapper + .find(StyledFlatTableRow) + .at(0) + .find(StyledFlatTableCell) + .at(0) + .props() + .onKeyDown(events.space); + }); + + wrapper.update(); + + expect(wrapper.find(StyledFlatTableRow).length).toEqual(1); + }); + }); + describe.each([ ["medium", "2px solid var(--colorsUtilityMajor100)"], ["large", "4px solid var(--colorsUtilityMajor100)"], @@ -1303,9 +1451,6 @@ describe("FlatTableRow", () => { ); - act(() => - wrapper.find(FlatTableHeader).at(0)?.props()?.reportCellWidth?.(200, 0) - ); wrapper.update(); @@ -1517,6 +1662,62 @@ describe("FlatTableRow", () => { { modifier: `${StyledFlatTableCell}` } ); }); + + describe("with a ref", () => { + it("the ref should be forwarded", () => { + let mockRef = { current: null }; + + const WrapperComponent = () => { + mockRef = useRef(null); + + return ( + + + + test 1 + test 2 + + test 3 + test 4 + test 5 + + +
+ ); + }; + + const wrapper = mount(); + + expect(mockRef.current).toBe(wrapper.find("tr").getDOMNode()); + }); + + it("the input callback ref should be called with the DOM element", () => { + let mockRef; + + const WrapperComponent = () => { + mockRef = jest.fn(); + + return ( + + + + test 1 + test 2 + + test 3 + test 4 + test 5 + + +
+ ); + }; + + const wrapper = mount(); + + expect(mockRef).toHaveBeenCalledWith(wrapper.find("tr").getDOMNode()); + }); + }); }); describe("wrapping FlatTableRowHeader", () => { @@ -1526,7 +1727,6 @@ describe("FlatTableRow", () => { }: { children: React.ReactNode; }) => {children}; - FlatTableRowHeaderWrapper.displayName = FlatTableRowHeader.displayName; const rowHeaderIndex = mount( @@ -1547,4 +1747,40 @@ describe("FlatTableRow", () => { expect(rowHeaderIndex).toEqual(3); }); }); + + describe("when only children passed are checkboxes", () => { + it("does not update first cell index", () => { + const wrapper = mount( +
+ + + + + + +
+ ); + + expect(wrapper.find(StyledFlatTableRow).prop("firstCellIndex")).toBe(0); + }); + }); + + describe("when first cell has no id set", () => { + it("does not set a first cell index", () => { + const wrapper = mount( + + + + + I have an ID + + +
I have no ID
+ ); + + expect(wrapper.find(StyledFlatTableRowHeader).prop("leftPositon")).toBe( + undefined + ); + }); + }); }); diff --git a/src/components/flat-table/flat-table-row/flat-table-row.style.ts b/src/components/flat-table/flat-table-row/flat-table-row.style.ts index 7ded9080a3..41eb71e356 100644 --- a/src/components/flat-table/flat-table-row/flat-table-row.style.ts +++ b/src/components/flat-table/flat-table-row/flat-table-row.style.ts @@ -124,13 +124,9 @@ interface StyledFlatTableRowProps FlatTableRowProps, | "bgColor" | "horizontalBorderColor" - | "stickyOffset" | "expandable" | "selected" | "highlighted" - | "isSubRow" - | "isFirstSubRow" - | "applyBorderLeft" | "draggable" > { isRowInteractive?: boolean; @@ -145,6 +141,9 @@ interface StyledFlatTableRowProps size: FlatTableProps["size"]; isDragging?: boolean; horizontalBorderSize: NonNullable; + isSubRow?: boolean; + isFirstSubRow?: boolean; + stickyOffset?: number; } const StyledFlatTableRow = styled.tr` @@ -169,7 +168,6 @@ const StyledFlatTableRow = styled.tr` isFirstSubRow, size, theme, - applyBorderLeft, isDragging, draggable, }) => { @@ -256,7 +254,6 @@ const StyledFlatTableRow = styled.tr` } ${stickyOffset !== undefined && - stickyOffset > 0 && css` && th { top: ${stickyOffset}px; @@ -380,14 +377,6 @@ const StyledFlatTableRow = styled.tr` } `} - - ${applyBorderLeft && - css` - th:first-of-type { - border-left: 1px solid ${customBorderColor || borderColor(colorTheme)}; - } - `} - ${isInSidebar && css` ${allCellTypes} { diff --git a/src/components/flat-table/flat-table-test.stories.tsx b/src/components/flat-table/flat-table-test.stories.tsx index ebe034d302..9e1ecdf880 100644 --- a/src/components/flat-table/flat-table-test.stories.tsx +++ b/src/components/flat-table/flat-table-test.stories.tsx @@ -36,6 +36,7 @@ import guid from "../../__internal__/utils/helpers/guid"; import { FLAT_TABLE_THEMES } from "./flat-table.config"; import { WithSortingHeaders } from "./flat-table.stories"; import { CHARACTERS } from "../../../cypress/support/component-helper/constants"; +import { FlatTableRowContextProps } from "./flat-table-row/__internal__/flat-table-row-context"; type SortType = "ascending" | "descending"; type SortValue = "client" | "total"; @@ -84,7 +85,12 @@ type SubRowsShapeChildrenOnlySelectableStoryKey = keyof SubRowsShapeChildrenOnly export default { title: "Flat Table/Test", - includeStories: ["FlatTableStory", "ExpandableWithLink", "SortableStory"], + includeStories: [ + "FlatTableStory", + "ExpandableWithLink", + "SortableStory", + "SubRowsAsAComponentStory", + ], parameters: { info: { disable: true }, chromatic: { @@ -1841,7 +1847,9 @@ export const FlatTableTitleAlignComponent = ( }; export const FlatTableSortingComponent = ( - props: Partial & FlatTableCellProps + props: Partial & + FlatTableCellProps & + Partial ) => { const headDataItems: HeadDataItems = [ { @@ -3264,3 +3272,81 @@ export const FlatTableLastColumnHasRowspan = () => { ); }; + +export const FlatTableWithMultipleStickyHeaderRows = () => ( + + + + Foo + + + Foo + + + + + Foo + + + Foo + + + +); + +export const SubRowsAsAComponentStory = () => { + const SubRowsComponent = () => ( + <> + + Child one + York + Single + 2 + + + Child two + Edinburgh + Single + 1 + + + ); + return ( + + + + Name + Location + Relationship Status + Dependents + + + + }> + John Doe + London + Single + 0 + + }> + Jane Doe + York + Married + 2 + + }> + John Smith + Edinburgh + Single + 1 + + }> + Jane Smith + Newcastle + Married + 5 + + + + ); +}; diff --git a/src/components/flat-table/flat-table.component.tsx b/src/components/flat-table/flat-table.component.tsx index 926300b74c..97247ff7d7 100644 --- a/src/components/flat-table/flat-table.component.tsx +++ b/src/components/flat-table/flat-table.component.tsx @@ -51,7 +51,8 @@ export const FlatTableThemeContext = React.createContext {} } ); -const FOCUSABLE_ROW_AND_CELL_QUERY = "tbody tr, tbody tr td, tbody tr th"; +const FOCUSABLE_ROW_AND_CELL_QUERY = + "tbody tr[tabindex], tbody tr td[tabindex], tbody tr th[tabindex]"; export const FlatTable = ({ caption, @@ -164,9 +165,7 @@ export const FlatTable = ({ return; } - const focusableElementsArray = Array.from(focusableElements).filter( - (el) => el.getAttribute("tabindex") !== null - ); + const focusableElementsArray = Array.from(focusableElements); const currentFocusIndex = focusableElementsArray.findIndex( (el) => el === document.activeElement @@ -203,17 +202,33 @@ export const FlatTable = ({ }; useLayoutEffect(() => { - const focusableElements = tableRef.current?.querySelectorAll( - FOCUSABLE_ROW_AND_CELL_QUERY - ); - - // if no other menu item is selected, we need to make the first row a tab stop - if (focusableElements && !selectedId) { - const focusableArray = Array.from(focusableElements).filter( - (el) => el.getAttribute("tabindex") !== null + const findSelectedId = () => { + const firstfocusableElement = tableRef.current?.querySelector( + FOCUSABLE_ROW_AND_CELL_QUERY ); - setSelectedId(focusableArray[0]?.getAttribute("id") || ""); + + // if no other menu item is selected, we need to make the first row a tab stop + if (firstfocusableElement && !selectedId) { + const currentlySelectedId = firstfocusableElement?.getAttribute("id"); + + /* istanbul ignore else */ + if (currentlySelectedId && selectedId !== currentlySelectedId) { + setSelectedId(currentlySelectedId); + } + } + }; + + const observer = new MutationObserver(findSelectedId); + + /* istanbul ignore else */ + if (wrapperRef.current) { + observer.observe(wrapperRef.current as Node, { + subtree: true, + childList: true, + attributes: true, + }); } + return () => observer.disconnect(); }, [selectedId]); return ( diff --git a/src/components/flat-table/flat-table.spec.tsx b/src/components/flat-table/flat-table.spec.tsx index 25b89dd8bc..99bde1f520 100644 --- a/src/components/flat-table/flat-table.spec.tsx +++ b/src/components/flat-table/flat-table.spec.tsx @@ -1,6 +1,12 @@ import React from "react"; import { ReactWrapper, mount } from "enzyme"; -import { act } from "react-dom/test-utils"; + +import { + render as rtlRender, + screen, + fireEvent, + waitFor, +} from "@testing-library/react"; import FlatTable, { FlatTableProps } from "./flat-table.component"; import FlatTableHead from "./flat-table-head/flat-table-head.component"; @@ -199,131 +205,6 @@ describe("FlatTable", () => { }); }); - describe("when it has a sticky header with multiple rows", () => { - let wrapper: ReactWrapper; - - const render = () => { - wrapper = mount( -
- - - - header1 - header2 - header3 - header4 - - - header1 - header2 - header3 - header4 - - - - - row header - cell1 - cell2 - cell3 - - - row header - cell1 - - - -
- ); - - jest - .spyOn( - wrapper - .find(StyledFlatTableRow) - .at(0) - .getDOMNode() as HTMLTableRowElement, - "clientHeight", - "get" - ) - .mockImplementation(() => 40); - }; - - beforeEach(() => { - render(); - }); - - afterEach(() => { - wrapper.unmount(); - }); - - it("should set the correct 'top' css on each row", () => { - act(() => render()); - wrapper.update(); - - expect( - wrapper.find(StyledFlatTableRow).at(1).props().stickyOffset - ).toEqual(40); - - assertStyleMatch( - { - top: "40px", - }, - wrapper.find(StyledFlatTableHead).find(StyledFlatTableRow).at(1), - { modifier: `&& th` } - ); - }); - }); - - describe("When FlatTable has sticky header and uses FlatTableRowHeaders so that the second column is made sticky", () => { - let wrapper: ReactWrapper; - const render = () => { - wrapper = mount( - - - - heading one - heading two - heading three - - heading four - - - header 1 - heading 2 - heading 3 - heading 4 - - - - - name - unique id - city - status - 0 - 0 - 0 - - - - ); - }; - - it("should apply left border if preceding row has a FlatTableRowHeader and current one does not and when the preceding row has a rowspan applied", () => { - act(() => render()); - - assertStyleMatch( - { - borderLeft: "1px solid var(--colorsUtilityMajor400)", - }, - wrapper.find(StyledFlatTableHead).find(StyledFlatTableRow).at(1), - { - modifier: `th:first-of-type`, - } - ); - }); - }); - describe("when FlatTable is a child of Sidebar", () => { let wrapper: ReactWrapper; beforeEach(() => { @@ -797,11 +678,8 @@ describe("FlatTable", () => { const arrowLeft = { key: "ArrowLeft" }; describe("when rows are clickable", () => { - it("should not move focus to first row when down arrow pressed and table wrapper focused", () => { - const element = document.createElement("div"); - const htmlElement = document.body.appendChild(element); - - const wrapper = mount( + it("should not move focus to first row when down arrow pressed and table wrapper focused", async () => { + rtlRender( {}}> @@ -813,46 +691,34 @@ describe("FlatTable", () => { four - , - { attachTo: htmlElement } + ); - - act(() => { - (wrapper - .find(StyledFlatTableWrapper) - .getDOMNode() as HTMLDivElement).focus(); - }); - expect(wrapper.find(StyledFlatTableWrapper)).toBeFocused(); - - act(() => { - wrapper.find(StyledFlatTableWrapper).props().onKeyDown(arrowDown); - }); - - expect(wrapper.find(StyledFlatTableWrapper)).toBeFocused(); + const tableWrapper = await screen.findByRole("region"); + tableWrapper?.focus(); + expect(tableWrapper).toHaveFocus(); + fireEvent.keyDown(tableWrapper, arrowDown); + expect(tableWrapper).toHaveFocus(); }); - it("should set the first row's tabindex to 0 if no other rows are selected or highlighted", () => { - const wrapper = mount( + it("should set the first row's tabindex to 0 if no other rows are selected or highlighted", async () => { + rtlRender( - {}}> + {}}> one two - {}}> + {}}> three four ); - - expect( - wrapper.update().find(StyledFlatTableRow).at(0).prop("tabIndex") - ).toBe(0); - expect( - wrapper.update().find(StyledFlatTableRow).at(1).prop("tabIndex") - ).toBe(-1); + await waitFor(() => { + expect(screen.getByTestId("one").getAttribute("tabindex")).toBe("0"); + expect(screen.getByTestId("two").getAttribute("tabindex")).toBe("-1"); + }); }); it("should set the a row's tabindex to 0 when it is selected", () => { @@ -903,345 +769,232 @@ describe("FlatTable", () => { ).toBe(0); }); - it("should move focus to the next row with an onClick when the down arrow key is pressed but not loop to the first when last reached", () => { - const element = document.createElement("div"); - const htmlElement = document.body.appendChild(element); - - const wrapper = mount( + it("should move focus to the next row with an onClick when the down arrow key is pressed but not loop to the first when last reached", async () => { + rtlRender( - {}}> + {}}> one two - {}}> + {}}> three four - {}}> + {}}> five six - {}}> + {}}> seven eight - , - { attachTo: htmlElement } + ); - act(() => { - (wrapper - .find(StyledFlatTableRow) - .at(0) - .getDOMNode() as HTMLTableRowElement).focus(); - }); - - expect(wrapper.update().find(StyledFlatTableRow).at(0)).toBeFocused(); - - act(() => { - wrapper.find(StyledFlatTableWrapper).props().onKeyDown(arrowDown); - }); - - expect(wrapper.update().find(StyledFlatTableRow).at(1)).toBeFocused(); - - act(() => { - wrapper.find(StyledFlatTableWrapper).props().onKeyDown(arrowDown); - }); - - expect(wrapper.update().find(StyledFlatTableRow).at(2)).toBeFocused(); - - act(() => { - wrapper.find(StyledFlatTableWrapper).props().onKeyDown(arrowDown); - }); - - expect(wrapper.update().find(StyledFlatTableRow).at(3)).toBeFocused(); - - act(() => { - wrapper.find(StyledFlatTableWrapper).props().onKeyDown(arrowDown); - }); - - expect(wrapper.update().find(StyledFlatTableRow).at(3)).toBeFocused(); + const tableWrapper = await screen.findByRole("region"); + const firstRow = await screen.findByTestId("one"); + const secondRow = await screen.findByTestId("two"); + const thirdRow = await screen.findByTestId("three"); + const fourthRow = await screen.findByTestId("four"); + firstRow?.focus(); + expect(firstRow).toHaveFocus(); + fireEvent.keyDown(tableWrapper, arrowDown); + expect(secondRow).toHaveFocus(); + fireEvent.keyDown(tableWrapper, arrowDown); + expect(thirdRow).toHaveFocus(); + fireEvent.keyDown(tableWrapper, arrowDown); + expect(fourthRow).toHaveFocus(); + fireEvent.keyDown(tableWrapper, arrowDown); + expect(fourthRow).toHaveFocus(); }); - it("should move focus to the previous row with an onClick when the up arrow key is pressed but not loop to the last when first reached", () => { - const element = document.createElement("div"); - const htmlElement = document.body.appendChild(element); - - const wrapper = mount( + it("should move focus to the previous row with an onClick when the up arrow key is pressed but not loop to the last when first reached", async () => { + rtlRender( - {}}> + {}}> one two - {}}> + {}}> three four - {}}> + {}}> five six - {}}> + {}}> seven eight - , - { attachTo: htmlElement } + ); - act(() => { - (wrapper - .find(StyledFlatTableRow) - .at(3) - .getDOMNode() as HTMLTableRowElement).focus(); - }); - - expect(wrapper.update().find(StyledFlatTableRow).at(3)).toBeFocused(); - - act(() => { - wrapper.find(StyledFlatTableWrapper).props().onKeyDown(arrowUp); - }); - - expect(wrapper.update().find(StyledFlatTableRow).at(2)).toBeFocused(); - - act(() => { - wrapper.find(StyledFlatTableWrapper).props().onKeyDown(arrowUp); - }); - - expect(wrapper.update().find(StyledFlatTableRow).at(1)).toBeFocused(); - - act(() => { - wrapper.find(StyledFlatTableWrapper).props().onKeyDown(arrowUp); - }); - - expect(wrapper.update().find(StyledFlatTableRow).at(0)).toBeFocused(); - - act(() => { - wrapper.find(StyledFlatTableWrapper).props().onKeyDown(arrowUp); - }); - - expect(wrapper.update().find(StyledFlatTableRow).at(0)).toBeFocused(); + const tableWrapper = await screen.findByRole("region"); + const firstRow = await screen.findByTestId("one"); + const secondRow = await screen.findByTestId("two"); + const thirdRow = await screen.findByTestId("three"); + const fourthRow = await screen.findByTestId("four"); + fourthRow?.focus(); + expect(fourthRow).toHaveFocus(); + fireEvent.keyDown(tableWrapper, arrowUp); + expect(thirdRow).toHaveFocus(); + fireEvent.keyDown(tableWrapper, arrowUp); + expect(secondRow).toHaveFocus(); + fireEvent.keyDown(tableWrapper, arrowUp); + expect(firstRow).toHaveFocus(); + fireEvent.keyDown(tableWrapper, arrowUp); + expect(firstRow).toHaveFocus(); }); - it("should not move focus from currently focused row when left arrow key pressed", () => { - const element = document.createElement("div"); - const htmlElement = document.body.appendChild(element); - - const wrapper = mount( + it("should not move focus from currently focused row when left arrow key pressed", async () => { + rtlRender( - {}}> + {}}> one two - {}}> + {}}> three four - {}}> + {}}> five six - {}}> + {}}> seven eight - , - { attachTo: htmlElement } + ); - act(() => { - (wrapper - .find(StyledFlatTableRow) - .at(3) - .getDOMNode() as HTMLTableRowElement).focus(); - }); - - expect(wrapper.update().find(StyledFlatTableRow).at(3)).toBeFocused(); - - act(() => { - wrapper.find(StyledFlatTableWrapper).props().onKeyDown(arrowLeft); - }); - - expect(wrapper.update().find(StyledFlatTableRow).at(3)).toBeFocused(); + const tableWrapper = await screen.findByRole("region"); + const fourthRow = await screen.findByTestId("four"); + fourthRow?.focus(); + expect(fourthRow).toHaveFocus(); + fireEvent.keyDown(tableWrapper, arrowLeft); + expect(fourthRow).toHaveFocus(); }); - it("should move focus to the next expandable row when the down arrow key is pressed but not loop to the first when last reached", () => { - const element = document.createElement("div"); - const htmlElement = document.body.appendChild(element); - - const wrapper = mount( + it("should move focus to the next expandable row when the down arrow key is pressed but not loop to the first when last reached", async () => { + rtlRender( - + one two - + three four - + five six - + seven eight - , - { attachTo: htmlElement } + ); - act(() => { - (wrapper - .find(StyledFlatTableRow) - .at(0) - .getDOMNode() as HTMLTableRowElement).focus(); - }); - - expect(wrapper.update().find(StyledFlatTableRow).at(0)).toBeFocused(); - - act(() => { - wrapper.find(StyledFlatTableWrapper).props().onKeyDown(arrowDown); - }); - - expect(wrapper.update().find(StyledFlatTableRow).at(1)).toBeFocused(); - - act(() => { - wrapper.find(StyledFlatTableWrapper).props().onKeyDown(arrowDown); - }); - - expect(wrapper.update().find(StyledFlatTableRow).at(2)).toBeFocused(); - - act(() => { - wrapper.find(StyledFlatTableWrapper).props().onKeyDown(arrowDown); - }); - - expect(wrapper.update().find(StyledFlatTableRow).at(3)).toBeFocused(); - - act(() => { - wrapper.find(StyledFlatTableWrapper).props().onKeyDown(arrowDown); - }); - - expect(wrapper.update().find(StyledFlatTableRow).at(3)).toBeFocused(); + const tableWrapper = await screen.findByRole("region"); + const firstRow = await screen.findByTestId("one"); + const secondRow = await screen.findByTestId("two"); + const thirdRow = await screen.findByTestId("three"); + const fourthRow = await screen.findByTestId("four"); + firstRow?.focus(); + expect(firstRow).toHaveFocus(); + fireEvent.keyDown(tableWrapper, arrowDown); + expect(secondRow).toHaveFocus(); + fireEvent.keyDown(tableWrapper, arrowDown); + expect(thirdRow).toHaveFocus(); + fireEvent.keyDown(tableWrapper, arrowDown); + expect(fourthRow).toHaveFocus(); + fireEvent.keyDown(tableWrapper, arrowDown); + expect(fourthRow).toHaveFocus(); }); - it("should move focus to the previous expandable row when the up arrow key is pressed but not loop to the last when first reached", () => { - const element = document.createElement("div"); - const htmlElement = document.body.appendChild(element); - - const wrapper = mount( + it("should move focus to the previous expandable row when the up arrow key is pressed but not loop to the last when first reached", async () => { + rtlRender( - + one two - + three four - + five six - + seven eight - , - { attachTo: htmlElement } + ); - act(() => { - (wrapper - .find(StyledFlatTableRow) - .at(3) - .getDOMNode() as HTMLTableRowElement).focus(); - }); - - expect(wrapper.update().find(StyledFlatTableRow).at(3)).toBeFocused(); - - act(() => { - wrapper.find(StyledFlatTableWrapper).props().onKeyDown(arrowUp); - }); - - expect(wrapper.update().find(StyledFlatTableRow).at(2)).toBeFocused(); - - act(() => { - wrapper.find(StyledFlatTableWrapper).props().onKeyDown(arrowUp); - }); - - expect(wrapper.update().find(StyledFlatTableRow).at(1)).toBeFocused(); - - act(() => { - wrapper.find(StyledFlatTableWrapper).props().onKeyDown(arrowUp); - }); - - expect(wrapper.update().find(StyledFlatTableRow).at(0)).toBeFocused(); - - act(() => { - wrapper.find(StyledFlatTableWrapper).props().onKeyDown(arrowUp); - }); - - expect(wrapper.update().find(StyledFlatTableRow).at(0)).toBeFocused(); + const tableWrapper = await screen.findByRole("region"); + const firstRow = await screen.findByTestId("one"); + const secondRow = await screen.findByTestId("two"); + const thirdRow = await screen.findByTestId("three"); + const fourthRow = await screen.findByTestId("four"); + fourthRow?.focus(); + expect(fourthRow).toHaveFocus(); + fireEvent.keyDown(tableWrapper, arrowUp); + expect(thirdRow).toHaveFocus(); + fireEvent.keyDown(tableWrapper, arrowUp); + expect(secondRow).toHaveFocus(); + fireEvent.keyDown(tableWrapper, arrowUp); + expect(firstRow).toHaveFocus(); + fireEvent.keyDown(tableWrapper, arrowUp); + expect(firstRow).toHaveFocus(); }); - it("should move focus to the next row when the down arrow key is pressed whilst a checkbox input child is focused", () => { - const element = document.createElement("div"); - const htmlElement = document.body.appendChild(element); - - const wrapper = mount( + it("should move focus to the next row when the down arrow key is pressed whilst a checkbox input child is focused", async () => { + rtlRender( {}}> two - {}}> + {}}> three four - , - { attachTo: htmlElement } + ); - act(() => { - (wrapper - .find(StyledFlatTableRow) - .at(0) - .find("input") - .getDOMNode() as HTMLInputElement).focus(); - }); - - expect( - wrapper.find(StyledFlatTableRow).at(0).find("input") - ).toBeFocused(); - - act(() => { - wrapper.find(StyledFlatTableWrapper).props().onKeyDown(arrowDown); - }); - - expect(wrapper.update().find(StyledFlatTableRow).at(1)).toBeFocused(); + const tableWrapper = await screen.findByRole("region"); + const secondRow = await screen.findByTestId("two"); + const checkbox = await screen.findByRole("checkbox"); + checkbox.focus(); + expect(checkbox).toHaveFocus(); + fireEvent.keyDown(tableWrapper, arrowDown); + expect(secondRow).toHaveFocus(); }); - it("should move focus to the previous row when the up arrow key is pressed whilst a checkbox input child is focused", () => { - const element = document.createElement("div"); - const htmlElement = document.body.appendChild(element); - - const wrapper = mount( + it("should move focus to the previous row when the up arrow key is pressed whilst a checkbox input child is focused", async () => { + rtlRender( - {}}> + {}}> one two @@ -1250,287 +1003,146 @@ describe("FlatTable", () => { four - , - { attachTo: htmlElement } + ); - act(() => { - (wrapper - .find(StyledFlatTableRow) - .at(1) - .find("input") - .getDOMNode() as HTMLInputElement).focus(); - }); - - expect( - wrapper.find(StyledFlatTableRow).at(1).find("input") - ).toBeFocused(); - - act(() => { - wrapper.find(StyledFlatTableWrapper).props().onKeyDown(arrowUp); - }); - - expect(wrapper.update().find(StyledFlatTableRow).at(0)).toBeFocused(); + const tableWrapper = await screen.findByRole("region"); + const firstRow = await screen.findByTestId("one"); + const checkbox = await screen.findByRole("checkbox"); + checkbox.focus(); + expect(checkbox).toHaveFocus(); + fireEvent.keyDown(tableWrapper, arrowUp); + expect(firstRow).toHaveFocus(); }); }); describe("when the first column is expandable", () => { - it("should set the first cell's tabindex to 0", () => { - const wrapper = mount( + it("should set the first cell's tabindex to 0", async () => { + rtlRender( - one + one two - three + three four ); - - expect( - wrapper - .update() - .find(StyledFlatTableRow) - .at(0) - .find(StyledFlatTableCell) - .at(0) - .prop("tabIndex") - ).toBe(0); - expect( - wrapper - .update() - .find(StyledFlatTableRow) - .at(1) - .find(StyledFlatTableCell) - .at(0) - .prop("tabIndex") - ).toBe(-1); + await waitFor(() => { + expect(screen.getByTestId("one").getAttribute("tabindex")).toBe("0"); + expect(screen.getByTestId("two").getAttribute("tabindex")).toBe("-1"); + }); }); - it("should set the first row header's tabindex to 0", () => { - const wrapper = mount( + it("should set the first row header's tabindex to 0", async () => { + rtlRender( - one - two + + one + + two - three - four + + three + + four ); - - expect( - wrapper.update().find(StyledFlatTableRowHeader).at(0).prop("tabIndex") - ).toBe(0); - expect( - wrapper.update().find(StyledFlatTableRowHeader).at(1).prop("tabIndex") - ).toBe(-1); + await waitFor(() => { + expect(screen.getByTestId("one").getAttribute("tabindex")).toBe("0"); + expect(screen.getByTestId("two").getAttribute("tabindex")).toBe("-1"); + }); }); - it("should move focus to the next focusable cell when the down arrow key is pressed but not loop to the first when last reached", () => { - const element = document.createElement("div"); - const htmlElement = document.body.appendChild(element); - - const wrapper = mount( + it("should move focus to the next focusable cell when the down arrow key is pressed but not loop to the first when last reached", async () => { + rtlRender( - one + one two - three + three four - five + five six - seven + seven eight - , - { attachTo: htmlElement } + ); - act(() => { - (wrapper - .find(StyledFlatTableRow) - .at(0) - .find(StyledFlatTableCell) - .at(0) - .getDOMNode() as HTMLTableCellElement).focus(); - }); - - expect( - wrapper - .update() - .find(StyledFlatTableRow) - .at(0) - .find(StyledFlatTableCell) - .at(0) - ).toBeFocused(); - - act(() => { - wrapper.find(StyledFlatTableWrapper).props().onKeyDown(arrowDown); - }); - - expect( - wrapper - .update() - .find(StyledFlatTableRow) - .at(1) - .find(StyledFlatTableCell) - .at(0) - ).toBeFocused(); - - act(() => { - wrapper.find(StyledFlatTableWrapper).props().onKeyDown(arrowDown); - }); - - expect( - wrapper - .update() - .find(StyledFlatTableRow) - .at(2) - .find(StyledFlatTableCell) - .at(0) - ).toBeFocused(); - - act(() => { - wrapper.find(StyledFlatTableWrapper).props().onKeyDown(arrowDown); - }); - - expect( - wrapper - .update() - .find(StyledFlatTableRow) - .at(3) - .find(StyledFlatTableCell) - .at(0) - ).toBeFocused(); - - act(() => { - wrapper.find(StyledFlatTableWrapper).props().onKeyDown(arrowDown); - }); - - expect( - wrapper - .update() - .find(StyledFlatTableRow) - .at(3) - .find(StyledFlatTableCell) - .at(0) - ).toBeFocused(); + const tableWrapper = await screen.findByRole("region"); + const firstFocusableCell = await screen.findByTestId("one"); + const secondFocusableCell = await screen.findByTestId("two"); + const thirdFocusableCell = await screen.findByTestId("three"); + const fourthFocusableCell = await screen.findByTestId("four"); + firstFocusableCell.focus(); + expect(firstFocusableCell).toHaveFocus(); + fireEvent.keyDown(tableWrapper, arrowDown); + expect(secondFocusableCell).toHaveFocus(); + fireEvent.keyDown(tableWrapper, arrowDown); + expect(thirdFocusableCell).toHaveFocus(); + fireEvent.keyDown(tableWrapper, arrowDown); + expect(fourthFocusableCell).toHaveFocus(); + fireEvent.keyDown(tableWrapper, arrowDown); + expect(fourthFocusableCell).toHaveFocus(); }); - it("should move focus to the previous focusable cell when the up arrow key is pressed but not loop to the last when first reached", () => { - const element = document.createElement("div"); - const htmlElement = document.body.appendChild(element); - - const wrapper = mount( + it("should move focus to the previous focusable cell when the up arrow key is pressed but not loop to the last when first reached", async () => { + rtlRender( - one + one two - three + three four - five + five six - seven + seven eight - , - { attachTo: htmlElement } + ); - act(() => { - (wrapper - .find(StyledFlatTableRow) - .at(3) - .find(StyledFlatTableCell) - .at(0) - .getDOMNode() as HTMLTableCellElement).focus(); - }); - - expect( - wrapper - .update() - .find(StyledFlatTableRow) - .at(3) - .find(StyledFlatTableCell) - .at(0) - ).toBeFocused(); - - act(() => { - wrapper.find(StyledFlatTableWrapper).props().onKeyDown(arrowUp); - }); - - expect( - wrapper - .update() - .find(StyledFlatTableRow) - .at(2) - .find(StyledFlatTableCell) - .at(0) - ).toBeFocused(); - - act(() => { - wrapper.find(StyledFlatTableWrapper).props().onKeyDown(arrowUp); - }); - - expect( - wrapper - .update() - .find(StyledFlatTableRow) - .at(1) - .find(StyledFlatTableCell) - .at(0) - ).toBeFocused(); - - act(() => { - wrapper.find(StyledFlatTableWrapper).props().onKeyDown(arrowUp); - }); - - expect( - wrapper - .update() - .find(StyledFlatTableRow) - .at(0) - .find(StyledFlatTableCell) - .at(0) - ).toBeFocused(); - - act(() => { - wrapper.find(StyledFlatTableWrapper).props().onKeyDown(arrowUp); - }); - - expect( - wrapper - .update() - .find(StyledFlatTableRow) - .at(0) - .find(StyledFlatTableCell) - .at(0) - ).toBeFocused(); + const tableWrapper = await screen.findByRole("region"); + const firstFocusableCell = await screen.findByTestId("one"); + const secondFocusableCell = await screen.findByTestId("two"); + const thirdFocusableCell = await screen.findByTestId("three"); + const fourthFocusableCell = await screen.findByTestId("four"); + fourthFocusableCell.focus(); + expect(fourthFocusableCell).toHaveFocus(); + fireEvent.keyDown(tableWrapper, arrowUp); + expect(thirdFocusableCell).toHaveFocus(); + fireEvent.keyDown(tableWrapper, arrowUp); + expect(secondFocusableCell).toHaveFocus(); + fireEvent.keyDown(tableWrapper, arrowUp); + expect(firstFocusableCell).toHaveFocus(); + fireEvent.keyDown(tableWrapper, arrowUp); + expect(firstFocusableCell).toHaveFocus(); }); }); }); diff --git a/src/components/flat-table/flat-table.stories.tsx b/src/components/flat-table/flat-table.stories.tsx index ec5ab9859a..79a5b53f8e 100644 --- a/src/components/flat-table/flat-table.stories.tsx +++ b/src/components/flat-table/flat-table.stories.tsx @@ -710,6 +710,12 @@ export const WithStickyHead: ComponentStory = () => ( Relationship Status Dependents + + Name + Location + Relationship Status + Dependents +