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..dab4990bc2
--- /dev/null
+++ b/src/components/flat-table/__internal__/build-position-map.ts
@@ -0,0 +1,20 @@
+export default (
+ array: Element[],
+ 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] as HTMLTableCellElement)[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..13d5618a2a
--- /dev/null
+++ b/src/components/flat-table/__internal__/use-calculate-sticky-cells.ts
@@ -0,0 +1,34 @@
+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,
+ firstCellId,
+ firstColumnExpandable,
+ 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..883b84bf55 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 set", () => {
+ it("should set 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..0aa7a52f38 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,10 @@
import React, {
useCallback,
- useEffect,
+ // useLayoutEffect,
useContext,
useState,
useRef,
+ useEffect,
} from "react";
import { PaddingProps } from "styled-system";
import { TableBorderSize, TableCellAlign } from "..";
@@ -15,8 +16,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 +42,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 +52,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(
+
+ )
+ .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..fbd5aaf905 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,92 @@ export const FlatTableRow = React.forwardRef<
}: FlatTableRowProps,
ref
) => {
- const internalId = useRef(id ?? guid());
+ const internalId = useRef(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");
+
+ 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 +190,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 +243,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 +257,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 +319,7 @@ export const FlatTableRow = React.forwardRef<
id={internalId.current}
moveItem={moveItem}
findItem={findItem}
+ rowRef={rowRef}
>
{rowComponent()}
@@ -360,18 +328,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 no ID |
+ I have an 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..7fd05bdd50 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,13 +377,9 @@ const StyledFlatTableRow = styled.tr`
}
`}
-
- ${applyBorderLeft &&
- css`
- th:first-of-type {
- border-left: 1px solid ${customBorderColor || borderColor(colorTheme)};
- }
- `}
+ [data-sticky-align='left'] + th {
+ border-left: 1px solid ${customBorderColor || borderColor(colorTheme)};
+ }
${isInSidebar &&
css`
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..e3ae4fceed 100644
--- a/src/components/flat-table/flat-table.spec.tsx
+++ b/src/components/flat-table/flat-table.spec.tsx
@@ -2,6 +2,13 @@ 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";
import FlatTableBody from "./flat-table-body/flat-table-body.component";
@@ -213,12 +220,6 @@ describe("FlatTable", () => {
header3
header4
-
- header1
- header2
- header3
- header4
-
@@ -235,17 +236,6 @@ describe("FlatTable", () => {
);
-
- jest
- .spyOn(
- wrapper
- .find(StyledFlatTableRow)
- .at(0)
- .getDOMNode() as HTMLTableRowElement,
- "clientHeight",
- "get"
- )
- .mockImplementation(() => 40);
};
beforeEach(() => {
@@ -261,69 +251,19 @@ describe("FlatTable", () => {
wrapper.update();
expect(
- wrapper.find(StyledFlatTableRow).at(1).props().stickyOffset
- ).toEqual(40);
+ wrapper.find(StyledFlatTableRow).at(0).props().stickyOffset
+ ).toEqual(0);
assertStyleMatch(
{
- top: "40px",
+ top: "0px",
},
- wrapper.find(StyledFlatTableHead).find(StyledFlatTableRow).at(1),
+ wrapper.find(StyledFlatTableHead).find(StyledFlatTableRow).at(0),
{ 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 +737,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 +750,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 +828,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 +1062,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..9e3a8db68b 100644
--- a/src/components/flat-table/flat-table.stories.tsx
+++ b/src/components/flat-table/flat-table.stories.tsx
@@ -39,76 +39,54 @@ type SelectedRows = {
type SelectedRow = keyof SelectedRows;
type HighlightedRow = "one" | "two" | "three" | "four" | "";
-export const DefaultStory: ComponentStory = () => (
-
-
-
-
-
- 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
-
-
-
-);
+export const DefaultStory: ComponentStory = () => {
+ const [update, setUpdate] = React.useState(false);
+
+ React.useEffect(() => {
+ setTimeout(() => setUpdate(true), 1000);
+ }, []);
+ return (
+
+
+
+ Name
+ Location
+ Relationship Status
+ Dependents
+
+
+
+ {!update && (
+
+ no update
+
+ )}
+ {update && (
+ <>
+ {}}>
+ foo
+ London
+ Single
+ 0
+
+ {}}>
+ foo
+ York
+ Married
+ 2
+
+ {}}>
+ foo
+ Edinburgh
+ Single
+ 1
+
+ >
+ )}
+
+
+ );
+};
DefaultStory.storyName = "default";
@@ -710,6 +688,12 @@ export const WithStickyHead: ComponentStory = () => (
Relationship Status
Dependents
+
+ Name
+ Location
+ Relationship Status
+ Dependents
+