From b688349eb84775d4955a66120ebc1510444ff9db Mon Sep 17 00:00:00 2001 From: edleeks87 Date: Thu, 4 May 2023 00:16:50 +0100 Subject: [PATCH] feat(flat-table): update keyboard navigation behaviour when rows are focusable Overrides previous keyboard navigation for focusable rows. Tabbing no longer navigates the rows, this has been replaced by up and down arrow keys. --- .../components/flat-table/flat-table.cy.js | 215 +++- .../__snapshots__/flat-table.spec.js.snap | 456 --------- .../flat-table-cell.component.js | 20 +- .../flat-table-checkbox.component.js | 5 +- .../flat-table-checkbox.spec.js | 18 +- .../flat-table-row-header.component.js | 22 +- .../__snapshots__/flat-table-row.spec.js.snap | 3 + .../flat-table-row.component.js | 34 +- .../flat-table-row/flat-table-row.spec.js | 10 +- .../flat-table/flat-table.component.js | 96 +- src/components/flat-table/flat-table.spec.js | 948 +++++++++++++++++- 11 files changed, 1316 insertions(+), 511 deletions(-) delete mode 100644 src/components/flat-table/__snapshots__/flat-table.spec.js.snap diff --git a/cypress/components/flat-table/flat-table.cy.js b/cypress/components/flat-table/flat-table.cy.js index 2ddbe557f9..4540d9cd5b 100644 --- a/cypress/components/flat-table/flat-table.cy.js +++ b/cypress/components/flat-table/flat-table.cy.js @@ -77,7 +77,6 @@ import { import { keyCode, positionOfElement, - pressTABKey, getRotationAngle, } from "../../support/helper"; @@ -2749,6 +2748,39 @@ const FlatTablePagerStickyHeaderComponent = () => { ); }; +const FlatTablePartiallySelectedOrHighlightedRows = ({ + highlighted, + selected, +}) => { + return ( + + + + Status + + + + {}}> + Not selected or highlighted + + {}} + selected={selected} + highlighted={highlighted} + > + + {selected && "Selected"} + {highlighted && "Highlighted"} + + + {}}> + Not selected or highlighted + + + + ); +}; + context("Tests for Flat Table component", () => { describe("check props for Flat Table component", () => { it("should render Flat Table with ariaDescribedBy", () => { @@ -3998,7 +4030,9 @@ context("Tests for Flat Table component", () => { flatTableBodyRowByPosition(0).click(); flatTableSubrowByPosition(0).should("exist"); flatTableSubrowByPosition(1).should("exist"); - flatTableBodyRowByPosition(0).tab().tab(); + flatTableBodyRowByPosition(0) + .tab() + .trigger("keydown", keyCode("downarrow")); flatTableBodyRowByPosition(3) .find("td") .eq(3) @@ -4017,7 +4051,10 @@ context("Tests for Flat Table component", () => { .trigger("keydown", keyCode("Space")); flatTableSubrowByPosition(0).should("exist"); flatTableSubrowByPosition(1).should("exist"); - flatTableBodyRowByPosition(0).tab().tab().wait(250); + flatTableBodyRowByPosition(0) + .tab() + .trigger("keydown", keyCode("downarrow")) + .wait(250); flatTableBodyRowByPosition(3) .find("td") .eq(3) @@ -4036,7 +4073,10 @@ context("Tests for Flat Table component", () => { .trigger("keydown", keyCode("Enter")); flatTableSubrowByPosition(0).should("exist"); flatTableSubrowByPosition(1).should("exist"); - flatTableBodyRowByPosition(0).tab().tab().wait(250); + flatTableBodyRowByPosition(0) + .tab() + .trigger("keydown", keyCode("downarrow")) + .wait(250); flatTableBodyRowByPosition(3) .find("td") .eq(3) @@ -4051,7 +4091,7 @@ context("Tests for Flat Table component", () => { relLink().click(); }); - it("should render Flat Table with expandable rows expanded by mouse, can tab to subrows", () => { + it("should render Flat Table with expandable rows expanded by mouse, can focus subrows with down arrow keypress", () => { CypressMountWithProviders(); flatTableExpandableIcon(0) @@ -4061,12 +4101,12 @@ context("Tests for Flat Table component", () => { flatTableBodyRowByPosition(0).click(); flatTableSubrowByPosition(0).should("exist"); flatTableSubrowByPosition(1).should("exist"); - flatTableBodyRowByPosition(0).tab(); + flatTableBodyRowByPosition(0).trigger("keydown", keyCode("downarrow")); flatTableBodyRowByPosition(1) .find("td") .eq(3) .should("have.css", "border-right", `2px solid ${gold}`); - flatTableBodyRowByPosition(1).tab(); + flatTableBodyRowByPosition(1).trigger("keydown", keyCode("downarrow")); flatTableBodyRowByPosition(2) .find("td") .eq(3) @@ -4231,41 +4271,174 @@ context("Tests for Flat Table component", () => { .should("have.css", "box-shadow", `${gold} 0px 0px 0px 3px`); }); - it("can tab through correct order with multiple tabbable elements in Flat Table content", () => { + it("can focus the first row by tabbing but no further rows are focused on tab press", () => { + CypressMountWithProviders(); + + cy.get("body").tab(); + + cy.focused().tab(); + flatTableBodyRowByPosition(0).then(checkFocus); + cy.focused().tab(); + flatTableBodyRowByPosition(0).should("not.be.focused"); + flatTableBodyRowByPosition(1).should("not.be.focused"); + flatTableBodyRowByPosition(3).should("not.be.focused"); + }); + + it("sets the last selected row as the tab stop and removes it from any other ones", () => { + CypressMountWithProviders( + + ); + + cy.get("body").tab(); + + cy.focused().tab(); + + flatTableBodyRowByPosition(0).should("not.be.focused"); + flatTableBodyRowByPosition(1).then(checkFocus); + }); + + it("sets the last highlighted row as the tab stop and removes it from any other ones", () => { + CypressMountWithProviders( + + ); + + cy.get("body").tab(); + + cy.focused().tab(); + + flatTableBodyRowByPosition(0).should("not.be.focused"); + flatTableBodyRowByPosition(1).then(checkFocus); + }); + + it("can use tab and down arrow key to navigate the clickable rows and tabbable elements", () => { CypressMountWithProviders(); cy.get("body").tab(); - for (let i = 0; i < 8; i++) { + + // tab through batch selection + for (let i = 0; i < 5; i++) { cy.focused().tab(); } + + flatTableBodyRowByPosition(0).then(checkFocus); + + cy.focused().tab(); + flatTableBodyRowByPosition(0).find("input").eq(0).should("be.focused"); + cy.focused().trigger("keydown", keyCode("downarrow")); flatTableBodyRowByPosition(1).then(checkFocus); + cy.focused().trigger("keydown", keyCode("downarrow")); + flatTableBodyRowByPosition(2).then(checkFocus); + cy.focused().trigger("keydown", keyCode("downarrow")); + flatTableBodyRowByPosition(3).then(checkFocus); + cy.focused().trigger("keydown", keyCode("downarrow")); + flatTableBodyRowByPosition(3).then(checkFocus); }); - it("can Shift tab through correct order with multiple tabbable elements in Flat Table content", () => { + it("can use up arrow to navigate the clickable rows and tabbable elements", () => { CypressMountWithProviders(); cyRoot(); cy.get("body").tab(); - for (let i = 0; i < 6; i++) { - cy.focused().tab({ shift: true }); - } + + flatTableBodyRowByPosition(3).find("input").eq(0).focus(); + flatTableBodyRowByPosition(3).find("input").eq(0).should("be.focused"); + cy.focused().trigger("keydown", keyCode("uparrow")); flatTableBodyRowByPosition(2).then(checkFocus); + cy.focused().trigger("keydown", keyCode("uparrow")); + flatTableBodyRowByPosition(1).then(checkFocus); + cy.focused().trigger("keydown", keyCode("uparrow")); + flatTableBodyRowByPosition(0).then(checkFocus); + cy.focused().trigger("keydown", keyCode("uparrow")); + flatTableBodyRowByPosition(0).then(checkFocus); }); - it.each([["up"], ["down"], ["left"], ["right"]])( - "can not navigate through Flat Table rows using arrow keys", + it.each([["leftarrow"], ["rightarrow"]])( + "can not navigate through Flat Table rows using %s keys", (arrow) => { CypressMountWithProviders(); - cyRoot(); - pressTABKey(6); - flatTableBodyRowByPosition(0) - .focus() - .trigger("keydown", keyCode(arrow)) - .then(checkFocus); + cy.get("body").tab(); + + // tab through batch selection + for (let i = 0; i < 5; i++) { + cy.focused().tab(); + } + + flatTableBodyRowByPosition(0).then(checkFocus); + flatTableBodyRowByPosition(0).trigger("keydown", keyCode(arrow)); + flatTableBodyRowByPosition(0).then(checkFocus); } ); + it("should navigate the first column of cells with down arrow key press when expandableArea is set to 'firstColumn'", () => { + CypressMountWithProviders(); + + cy.get("body").tab(); + cy.focused().tab(); + flatTableCell(0).should("be.focused"); + flatTableCell(0).trigger("keydown", keyCode("downarrow")); + flatTableCell(4).should("be.focused"); + flatTableCell(4).trigger("keydown", keyCode("downarrow")); + flatTableCell(8).should("be.focused"); + flatTableCell(8).trigger("keydown", keyCode("downarrow")); + flatTableCell(12).should("be.focused"); + flatTableCell(12).trigger("keydown", keyCode("downarrow")); + flatTableCell(12).should("be.focused"); + }); + + it("should navigate the first column of cells with up arrow key press when expandableArea is set to 'firstColumn'", () => { + CypressMountWithProviders(); + + flatTableCell(12).focus(); + flatTableCell(12).should("be.focused"); + flatTableCell(12).trigger("keydown", keyCode("uparrow")); + flatTableCell(8).should("be.focused"); + flatTableCell(8).trigger("keydown", keyCode("uparrow")); + flatTableCell(4).should("be.focused"); + flatTableCell(4).trigger("keydown", keyCode("uparrow")); + flatTableCell(0).should("be.focused"); + flatTableCell(0).trigger("keydown", keyCode("uparrow")); + flatTableCell(0).should("be.focused"); + }); + + it("should navigate any focusable rows, including expanded sub rows, with down arrow key", () => { + CypressMountWithProviders(); + + cy.get("body").tab(); + cy.focused().tab(); + flatTableBodyRowByPosition(0).should("be.focused"); + flatTableBodyRowByPosition(0).click(); + flatTableBodyRowByPosition(1).trigger("keydown", keyCode("downarrow")); + flatTableBodyRowByPosition(1).should("be.focused"); + flatTableBodyRowByPosition(1).trigger("keydown", keyCode("downarrow")); + flatTableBodyRowByPosition(2).should("be.focused"); + flatTableBodyRowByPosition(2).trigger("keydown", keyCode("downarrow")); + flatTableBodyRowByPosition(3).should("be.focused"); + flatTableBodyRowByPosition(3).trigger("keydown", keyCode("downarrow")); + flatTableBodyRowByPosition(4).should("be.focused"); + flatTableBodyRowByPosition(4).trigger("keydown", keyCode("downarrow")); + flatTableBodyRowByPosition(5).should("be.focused"); + }); + + it("should navigate any focusable rows, including expanded sub rows, with up arrow key", () => { + CypressMountWithProviders(); + + cy.get("body").tab(); + cy.focused().tab(); + flatTableBodyRowByPosition(0).should("be.focused"); + flatTableBodyRowByPosition(0).click(); + flatTableBodyRowByPosition(1).trigger("keydown", keyCode("downarrow")); + flatTableBodyRowByPosition(1).should("be.focused"); + flatTableBodyRowByPosition(1).trigger("keydown", keyCode("downarrow")); + flatTableBodyRowByPosition(2).should("be.focused"); + flatTableBodyRowByPosition(2).trigger("keydown", keyCode("downarrow")); + flatTableBodyRowByPosition(3).should("be.focused"); + flatTableBodyRowByPosition(3).trigger("keydown", keyCode("downarrow")); + flatTableBodyRowByPosition(4).should("be.focused"); + flatTableBodyRowByPosition(4).trigger("keydown", keyCode("downarrow")); + flatTableBodyRowByPosition(5).should("be.focused"); + }); + it("should render Flat Table with action popover in a cell opened by mouse", () => { CypressMountWithProviders(); diff --git a/src/components/flat-table/__snapshots__/flat-table.spec.js.snap b/src/components/flat-table/__snapshots__/flat-table.spec.js.snap deleted file mode 100644 index 9501aafcca..0000000000 --- a/src/components/flat-table/__snapshots__/flat-table.spec.js.snap +++ /dev/null @@ -1,456 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`FlatTable when rendered with proper table data should have expected structure and styles 1`] = ` -.c10 { - background-color: transparent; - border-width: 0; - box-sizing: border-box; - font-weight: 700; - left: auto; - text-align: left; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - vertical-align: middle; - white-space: nowrap; - word-break: keep-all; - padding: 0; -} - -.c10:first-child { - padding-left: 1px; -} - -.c10.c10.c10 > div { - box-sizing: border-box; -} - -.c12 { - background-color: var(--colorsUtilityYang100); - border-width: 0; - border-bottom: 1px solid var(--colorsUtilityMajor100); - text-align: left; - vertical-align: middle; - padding: 0; -} - -.c12.c12.c12.c12 > div { - box-sizing: border-box; -} - -.c12:first-of-type { - border-left: 1px solid var(--colorsUtilityMajor100); -} - -.c12:last-of-type { - border-right: 1px solid var(--colorsUtilityMajor100); -} - -.c13 { - background-color: var(--colorsUtilityYang100); - border-width: 0; - border-bottom: 1px solid var(--colorsUtilityMajor100); - text-align: left; - vertical-align: middle; - padding: 0; -} - -.c13.c13.c13.c13 > div { - box-sizing: border-box; -} - -.c13:first-of-type { - border-left: 1px solid var(--colorsUtilityMajor100); -} - -.c13:last-of-type { - border-right: 1px solid var(--colorsUtilityMajor100); -} - -.c13:first-of-type + .c11 { - border-left: 1px solid var(--colorsUtilityMajor100); -} - -.c8 { - background-color: var(--colorsUtilityYang100); - border: 1px solid var(--colorsUtilityMajor100); - border-top: none; - box-sizing: border-box; - left: 0; - font-weight: normal; - position: -webkit-sticky; - position: sticky; - text-align: left; - top: auto; - vertical-align: middle; - padding: 0; - z-index: 1000; -} - -.c8.c8.c8.c8 { - left: 0px; -} - -.c8.c8.c8.c8 > div { - box-sizing: border-box; - padding-top: 10px; - padding-bottom: 10px; - padding-left: var(--spacing300); - padding-right: var(--spacing300); -} - -.c6 { - border-collapse: separate; - border-radius: 0px; - border-spacing: 0; - min-width: 100%; - table-layout: fixed; - width: auto; -} - -.c6 [data-component="icon"]:not([color]) { - color: var(--colorsActionMinor500); -} - -.c6 .c7 + td { - border-left: none; -} - -.c6 .c7:nth-child(1) { - border-right: 2px solid var(--colorsUtilityMajor100); -} - -.c6 .c9 { - border-bottom: 1px solid var(--colorsUtilityMajor400); -} - -.c6 .c9:first-child { - border-left: 1px solid var(--colorsUtilityMajor400); -} - -.c4.c4.c4 .c15 { - border-left: none; - border-right: none; -} - -.c4.c4.c4 .c7, -.c4.c4.c4 .c15 { - font-weight: 700; -} - -.c1 { - display: grid; - grid-auto-rows: min-content; -} - -.c2 { - border-collapse: separate; - border-spacing: 0; - width: 100%; -} - -.c2 .c5 { - height: 40px; -} - -.c2 .c11 > div, -.c2 .c9 > div, -.c2 .c7 > div, -.c2 .c15 > div { - font-size: 14px; - padding-left: 16px; - padding-right: 16px; -} - -.c0 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; - border-radius: var(--borderRadius000); - border-top-left-radius: var(--borderRadius100); - border-top-right-radius: var(--borderRadius100); - border-bottom-left-radius: var(--borderRadius100); - border-bottom-right-radius: var(--borderRadius100); - box-sizing: border-box; - box-shadow: inset 0px 0px 0px 1px var(--colorsUtilityMajor100); -} - -.c0:focus { - outline: 2px solid var(--colorsSemanticFocus500); -} - -.c0:focus:not(:focus-visible) { - outline: none; -} - -.c0:focus:focus-visible { - outline: 2px solid var(--colorsSemanticFocus500); -} - -.c0 .c3 .c15, -.c0 .c9, -.c0 .c3 { - background-color: var(--colorsUtilityMajor400); - border-right: 1px solid var(--colorsUtilityMajor300); - color: var(--colorsUtilityYang100); - border-bottom-color: var(--colorsUtilityMajor300); -} - -.c0 .c3 .c7 { - background-color: var(--colorsUtilityMajor400); - color: var(--colorsUtilityYang100); - border-bottom-color: var(--colorsUtilityMajor300); - border-right-color: var(--colorsUtilityMajor300); - border-left-color: var(--colorsUtilityMajor300); -} - -.c0 .c3 .c7, -.c0 .c9.isSticky, -.c0 .c3 .c15.isSticky { - z-index: 1005; -} - -.c0 thead .c9.isSticky, -.c0 .c15.isSticky { - border-right: none; -} - -.c0 .c9, -.c0 .c15 { - z-index: 1003; -} - -.c0 thead .c5:first-of-type th:first-of-type { - border-top-left-radius: var(--borderRadius100); -} - -.c0 thead .c5:first-of-type th:last-of-type { - border-top-right-radius: var(--borderRadius100); -} - -.c0 tbody .c7, -.c0 .c11.isSticky, -.c0 tbody .c15.isSticky { - z-index: 1000; -} - -.c0 tbody .c5:last-of-type th:first-child, -.c0 tbody .c5:last-of-type td:first-child { - border-bottom-left-radius: var(--borderRadius100); -} - -.c0 tbody .c5:last-of-type th:last-child, -.c0 tbody .c5:last-of-type td:last-child { - border-bottom-right-radius: var(--borderRadius100); -} - -.c14 .c3 .c15, -.c14 .c9, -.c14 .c3 { - background-color: var(--colorsUtilityMajor400); - border-right: 1px solid var(--colorsUtilityMajor300); - color: var(--colorsUtilityYang100); - border-bottom-color: var(--colorsUtilityMajor300); -} - -.c14 .c3 .c7 { - background-color: var(--colorsUtilityMajor400); - color: var(--colorsUtilityYang100); - border-bottom-color: var(--colorsUtilityMajor300); - border-right-color: var(--colorsUtilityMajor300); - border-left-color: var(--colorsUtilityMajor300); -} - -.c14 .c3 .c7, -.c14 .c9.isSticky, -.c14 .c3 .c15.isSticky { - z-index: 1005; -} - -.c14 thead .c9.isSticky, -.c14 .c15.isSticky { - border-right: none; -} - -.c14 .c9, -.c14 .c15 { - z-index: 1003; -} - -.c14 thead .c5:first-of-type th:first-of-type { - border-top-left-radius: var(--borderRadius100); -} - -.c14 thead .c5:first-of-type th:last-of-type { - border-top-right-radius: var(--borderRadius100); -} - -.c14 tbody .c7, -.c14 .c11.isSticky, -.c14 tbody .c15.isSticky { - z-index: 1000; -} - -.c14 tbody .c5:nth-of-type(1) th:last-child, -.c14 tbody .c5:nth-of-type(1) td:last-child { - border-bottom-right-radius: var(--borderRadius100); -} - -.c14 tbody .c5:last-of-type th:first-child, -.c14 tbody .c5:last-of-type td:first-child { - border-bottom-left-radius: var(--borderRadius100); -} - -
-
- - - - - - - - - - - - - - - - - - - - - -
-
- row header -
-
-
- header1 -
-
-
- header2 -
-
-
- header3 -
-
-
- row header -
-
-
- cell1 -
-
-
- cell2 -
-
-
- cell3 -
-
-
- row header -
-
-
- cell1 -
-
-
-
-`; diff --git a/src/components/flat-table/flat-table-cell/flat-table-cell.component.js b/src/components/flat-table/flat-table-cell/flat-table-cell.component.js index 0a8560bf7e..ac7932db15 100644 --- a/src/components/flat-table/flat-table-cell/flat-table-cell.component.js +++ b/src/components/flat-table/flat-table-cell/flat-table-cell.component.js @@ -1,4 +1,10 @@ -import React, { useLayoutEffect, useRef } from "react"; +import React, { + useLayoutEffect, + useRef, + useState, + useEffect, + useContext, +} from "react"; import PropTypes from "prop-types"; import styledSystemPropTypes from "@styled-system/prop-types"; @@ -8,6 +14,8 @@ import { } from "./flat-table-cell.style"; import { filterStyledSystemPaddingProps } from "../../../style/utils"; import Icon from "../../icon"; +import { FlatTableThemeContext } from "../flat-table.component"; +import guid from "../../../__internal__/utils/helpers/guid"; const paddingPropTypes = filterStyledSystemPaddingProps( styledSystemPropTypes.space @@ -32,6 +40,9 @@ const FlatTableCell = ({ ...rest }) => { const ref = useRef(null); + const id = useRef(guid()); + const [tabIndex, setTabIndex] = useState(-1); + const { selectedId } = useContext(FlatTableThemeContext); useLayoutEffect(() => { if (ref.current && reportCellWidth) { @@ -39,6 +50,10 @@ const FlatTableCell = ({ } }, [reportCellWidth, cellIndex]); + useEffect(() => { + setTabIndex(selectedId === id.current ? 0 : -1); + }, [selectedId]); + return ( { - event.stopPropagation(); + if (!Events.isDownKey(event) && !Events.isUpKey(event)) { + event.stopPropagation(); + } }; return ( diff --git a/src/components/flat-table/flat-table-checkbox/flat-table-checkbox.spec.js b/src/components/flat-table/flat-table-checkbox/flat-table-checkbox.spec.js index d87efaa32e..17c2e0034b 100644 --- a/src/components/flat-table/flat-table-checkbox/flat-table-checkbox.spec.js +++ b/src/components/flat-table/flat-table-checkbox/flat-table-checkbox.spec.js @@ -39,20 +39,30 @@ describe("FlatTableCheckbox", () => { ).toEqual("test"); }); }); - describe("should stop the event propagation", () => { - it("when is clicked", () => { + describe("event propagation", () => { + it("is stopped on click", () => { const stopPropagation = jest.fn(); const wrapper = render({ onClick: () => {} }); wrapper.find(Checkbox).props().onClick({ stopPropagation }); expect(stopPropagation).toHaveBeenCalledTimes(1); }); - it("when key is pressed", () => { + it("is stopped on keydown and key is not ArrowDown or ArrowUp", () => { const stopPropagation = jest.fn(); const wrapper = render({}); - wrapper.find(Checkbox).props().onKeyDown({ stopPropagation }); + wrapper.find(Checkbox).props().onKeyDown({ key: "a", stopPropagation }); expect(stopPropagation).toHaveBeenCalledTimes(1); }); + + it.each(["ArrowDown", "ArrowUp"])( + "is not stopped when key is %s", + (key) => { + const stopPropagation = jest.fn(); + const wrapper = render({}); + wrapper.find(Checkbox).props().onKeyDown({ key, stopPropagation }); + expect(stopPropagation).not.toHaveBeenCalled(); + } + ); }); describe("onClick handler", () => { diff --git a/src/components/flat-table/flat-table-row-header/flat-table-row-header.component.js b/src/components/flat-table/flat-table-row-header/flat-table-row-header.component.js index 739f58d57b..2374637ede 100644 --- a/src/components/flat-table/flat-table-row-header/flat-table-row-header.component.js +++ b/src/components/flat-table/flat-table-row-header/flat-table-row-header.component.js @@ -1,13 +1,20 @@ -import React, { useCallback } from "react"; +import React, { + useCallback, + useEffect, + useContext, + useState, + useRef, +} from "react"; import PropTypes from "prop-types"; import styledSystemPropTypes from "@styled-system/prop-types"; - import { filterStyledSystemPaddingProps } from "../../../style/utils"; import Icon from "../../icon"; import { StyledFlatTableRowHeader, StyledFlatTableRowHeaderContent, } from "./flat-table-row-header.style"; +import { FlatTableThemeContext } from "../flat-table.component"; +import guid from "../../../__internal__/utils/helpers/guid"; const paddingPropTypes = filterStyledSystemPaddingProps( styledSystemPropTypes.space @@ -29,6 +36,10 @@ const FlatTableRowHeader = ({ stickyAlignment = "left", ...rest }) => { + const id = useRef(guid()); + const [tabIndex, setTabIndex] = useState(-1); + const { selectedId } = useContext(FlatTableThemeContext); + const handleOnClick = useCallback( (ev) => { if (expandable && onClick) onClick(ev); @@ -42,6 +53,10 @@ const FlatTableRowHeader = ({ [expandable, onKeyDown] ); + useEffect(() => { + setTabIndex(selectedId === id.current ? 0 : -1); + }, [selectedId]); + return (
{ + const internalId = useRef(id ?? guid()); const [isExpanded, setIsExpanded] = useState(expanded); let rowRef = useRef(); if (ref) rowRef = ref; @@ -73,6 +75,7 @@ const FlatTableRow = React.forwardRef( ), [childrenArray] ); + const [tabIndex, setTabIndex] = useState(-1); const noStickyColumnsOverlap = useMemo(() => { const hasLhsColumn = lhsRowHeaderIndex !== -1; @@ -87,7 +90,9 @@ const FlatTableRow = React.forwardRef( `Do not render a right hand side \`${FlatTableRowHeader.displayName}\` before left hand side \`${FlatTableRowHeader.displayName}\`` ); - const themeContext = useContext(FlatTableThemeContext); + const { colorTheme, size, setSelectedId, selectedId } = useContext( + FlatTableThemeContext + ); const reportCellWidth = useCallback( (width, index) => { @@ -135,7 +140,9 @@ const FlatTableRow = React.forwardRef( } function handleClick(ev) { - if (onClick) onClick(ev); + if (onClick) { + onClick(ev); + } if (expandable && !firstColumnExpandable) { toggleExpanded(); } @@ -144,7 +151,7 @@ const FlatTableRow = React.forwardRef( if (onClick || expandable) { interactiveRowProps = { isRowInteractive: !firstColumnExpandable, - tabIndex: firstColumnExpandable ? undefined : 0, + tabIndex: firstColumnExpandable ? undefined : tabIndex, onKeyDown, isFirstColumnInteractive: firstColumnExpandable, isExpanded, @@ -195,6 +202,16 @@ const FlatTableRow = React.forwardRef( setIsExpanded(expanded); }, [expanded]); + useEffect(() => { + if (highlighted || selected) { + setSelectedId(internalId.current); + } + }, [highlighted, selected, setSelectedId]); + + useEffect(() => { + setTabIndex(selectedId === internalId.current ? 0 : -1); + }, [selectedId]); + const rowComponent = (isInSidebar) => ( @@ -250,7 +268,11 @@ const FlatTableRow = React.forwardRef( ); const draggableComponent = (isInSidebar) => ( - + {rowComponent(isInSidebar)} ); diff --git a/src/components/flat-table/flat-table-row/flat-table-row.spec.js b/src/components/flat-table/flat-table-row/flat-table-row.spec.js index 0a2e6966c6..4afb8a6a55 100644 --- a/src/components/flat-table/flat-table-row/flat-table-row.spec.js +++ b/src/components/flat-table/flat-table-row/flat-table-row.spec.js @@ -18,6 +18,12 @@ 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.mockImplementation(() => mockedGuid); const events = { enter: { @@ -62,7 +68,7 @@ describe("FlatTableRow", () => { }); it("then the component should have tabIndex set to 0", () => { - expect(wrapper.find(StyledFlatTableRow).prop("tabIndex")).toBe(0); + expect(wrapper.find(StyledFlatTableRow).prop("tabIndex")).toBe(-1); }); it("then the component should have isRowInteractive prop set to true", () => { @@ -300,7 +306,7 @@ describe("FlatTableRow", () => { ); }); - it('applies a "background-color" to the "FLatTableRowHeader"', () => { + it('applies a "background-color" to the "FlatTableRowHeader"', () => { wrapper = renderFlatTableRow({ highlighted: true, onClick: jest.fn(), diff --git a/src/components/flat-table/flat-table.component.js b/src/components/flat-table/flat-table.component.js index b254a6241f..b074a78f5c 100644 --- a/src/components/flat-table/flat-table.component.js +++ b/src/components/flat-table/flat-table.component.js @@ -9,13 +9,18 @@ import { } from "./flat-table.style"; import { DrawerSidebarContext } from "../drawer"; import { filterStyledSystemMarginProps } from "../../style/utils"; +import Events from "../../__internal__/utils/helpers/events/events"; -export const FlatTableThemeContext = React.createContext({}); +export const FlatTableThemeContext = React.createContext({ + setSelectedId: () => {}, +}); const marginPropTypes = filterStyledSystemMarginProps( styledSystemPropTypes.space ); +const FOCUSABLE_ROW_AND_CELL_QUERY = "tbody tr, tbody tr td, tbody tr th"; + const FlatTable = ({ caption, children, @@ -39,6 +44,7 @@ const FlatTable = ({ const [hasHorizontalScrollbar, setHasHorizontalScrollbar] = useState(false); const [firstColRowSpanIndex, setFirstColRowSpanIndex] = useState(-1); const [lastColRowSpanIndex, setLastColRowSpanIndex] = useState(-1); + const [selectedId, setSelectedId] = useState(""); const addDefaultHeight = !height && (hasStickyHead || hasStickyFooter); const tableStylingProps = { caption, @@ -61,6 +67,7 @@ const FlatTable = ({ return rowSpan >= index + 1; }); + /* istanbul ignore else */ if (wrapperRef.current && tableRef.current) { const { offsetHeight, offsetWidth } = wrapperRef.current; const { @@ -91,6 +98,88 @@ const FlatTable = ({ } }, [footer, children, height, minHeight]); + const findParentIndexOfFocusedChild = (array) => + array.findIndex((el) => { + const focusableRowElements = el.querySelectorAll( + "button, input, a, [tabindex]" + ); + + /* istanbul ignore else */ + if (focusableRowElements) { + const focusableRowElementsArray = Array.from(focusableRowElements); + + if ( + focusableRowElementsArray.find( + (el2) => el2 === document.activeElement + ) + ) { + return true; + } + } + + return false; + }); + + const handleKeyDown = (ev) => { + const focusableElements = tableRef.current?.querySelectorAll( + FOCUSABLE_ROW_AND_CELL_QUERY + ); + + /* istanbul ignore if */ + if (!focusableElements) { + return; + } + + const focusableElementsArray = Array.from(focusableElements).filter( + (el) => el.getAttribute("tabindex") !== null + ); + + const currentFocusIndex = focusableElementsArray.findIndex( + (el) => el === document.activeElement + ); + + if (Events.isDownKey(ev)) { + if ( + currentFocusIndex !== -1 && + currentFocusIndex < focusableElementsArray.length + ) { + focusableElementsArray[currentFocusIndex + 1]?.focus(); + } else { + // it may be that an element within the row currently has focus + const index = findParentIndexOfFocusedChild(focusableElementsArray); + + if (index !== -1 && index < focusableElementsArray.length) { + focusableElementsArray[index + 1]?.focus(); + } + } + } else if (Events.isUpKey(ev)) { + if (currentFocusIndex > 0) { + focusableElementsArray[currentFocusIndex - 1]?.focus(); + } else { + // it may be that an element within the row currently has focus + const index = findParentIndexOfFocusedChild(focusableElementsArray); + + if (index > 0) { + focusableElementsArray[index - 1]?.focus(); + } + } + } + }; + + 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 + ); + setSelectedId(focusableArray[0]?.getAttribute("id") || ""); + } + }, [selectedId]); + return ( {({ isInSidebar }) => ( @@ -123,6 +212,7 @@ const FlatTable = ({ hasFooter={!!footer} firstColRowSpanIndex={firstColRowSpanIndex} lastColRowSpanIndex={lastColRowSpanIndex} + onKeyDown={handleKeyDown} {...rest} > @@ -132,7 +222,9 @@ const FlatTable = ({ {...tableStylingProps} > {caption ? {caption} : null} - + {children} diff --git a/src/components/flat-table/flat-table.spec.js b/src/components/flat-table/flat-table.spec.js index 5f98813800..ddcf8f3370 100644 --- a/src/components/flat-table/flat-table.spec.js +++ b/src/components/flat-table/flat-table.spec.js @@ -10,6 +10,7 @@ import FlatTableRow from "./flat-table-row/flat-table-row.component"; import FlatTableHeader from "./flat-table-header/flat-table-header.component"; import FlatTableCell from "./flat-table-cell/flat-table-cell.component"; import FlatTableRowHeader from "./flat-table-row-header/flat-table-row-header.component"; +import FlatTableCheckbox from "./flat-table-checkbox"; import { assertStyleMatch, testStyledSystemMargin, @@ -43,13 +44,13 @@ const RenderComponent = (props) => ( - + row header cell1 cell2 cell3 - + row header cell1 @@ -77,18 +78,6 @@ describe("FlatTable", () => { }); }); - describe("when rendered with proper table data", () => { - let wrapper; - - beforeEach(() => { - wrapper = renderFlatTable(); - }); - - it("should have expected structure and styles", () => { - expect(wrapper.toJSON()).toMatchSnapshot(); - }); - }); - describe("when the table wrapper is focused", () => { let wrapper; @@ -816,6 +805,937 @@ describe("FlatTable", () => { ); }); }); + + describe("keyboard navigation", () => { + const arrowDown = { key: "ArrowDown" }; + const arrowUp = { key: "ArrowUp" }; + 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( + + + {}}> + one + two + + {}}> + three + four + + + , + { attachTo: htmlElement } + ); + + act(() => { + wrapper.find(StyledFlatTableWrapper).getDOMNode().focus(); + }); + expect(wrapper.find(StyledFlatTableWrapper)).toBeFocused(); + + act(() => { + wrapper.find(StyledFlatTableWrapper).props().onKeyDown(arrowDown); + }); + + expect(wrapper.find(StyledFlatTableWrapper)).toBeFocused(); + }); + + it("should set the first row's tabindex to 0 if no other rows are selected or highlighted", () => { + const wrapper = mount( + + + {}}> + 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); + }); + + it("should set the a row's tabindex to 0 when it is selected", () => { + const wrapper = mount( + + + {}}> + one + two + + {}} selected> + three + four + + + + ); + + expect( + wrapper.update().find(StyledFlatTableRow).at(0).prop("tabIndex") + ).toBe(-1); + expect( + wrapper.update().find(StyledFlatTableRow).at(1).prop("tabIndex") + ).toBe(0); + }); + + it("should set a row's tabindex to 0 when it is highlighted", () => { + const wrapper = mount( + + + {}}> + one + two + + {}} highlighted> + three + four + + + + ); + + expect( + wrapper.update().find(StyledFlatTableRow).at(0).prop("tabIndex") + ).toBe(-1); + expect( + wrapper.update().find(StyledFlatTableRow).at(1).prop("tabIndex") + ).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( + + + {}}> + one + two + + {}}> + three + four + + {}}> + five + six + + {}}> + seven + eight + + + , + { attachTo: htmlElement } + ); + + act(() => { + wrapper.find(StyledFlatTableRow).at(0).getDOMNode().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(); + }); + + 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( + + + {}}> + one + two + + {}}> + three + four + + {}}> + five + six + + {}}> + seven + eight + + + , + { attachTo: htmlElement } + ); + + act(() => { + wrapper.find(StyledFlatTableRow).at(3).getDOMNode().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(); + }); + + 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( + + + {}}> + one + two + + {}}> + three + four + + {}}> + five + six + + {}}> + seven + eight + + + , + { attachTo: htmlElement } + ); + + act(() => { + wrapper.find(StyledFlatTableRow).at(3).getDOMNode().focus(); + }); + + expect(wrapper.update().find(StyledFlatTableRow).at(3)).toBeFocused(); + + act(() => { + wrapper.find(StyledFlatTableWrapper).props().onKeyDown(arrowLeft); + }); + + expect(wrapper.update().find(StyledFlatTableRow).at(3)).toBeFocused(); + }); + + 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( + + + + one + two + + + three + four + + + five + six + + + seven + eight + + + , + { attachTo: htmlElement } + ); + + act(() => { + wrapper.find(StyledFlatTableRow).at(0).getDOMNode().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(); + }); + + 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( + + + + one + two + + + three + four + + + five + six + + + seven + eight + + + , + { attachTo: htmlElement } + ); + + act(() => { + wrapper.find(StyledFlatTableRow).at(3).getDOMNode().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(); + }); + + 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( + + + {}}> + + two + + {}}> + three + four + + + , + { attachTo: htmlElement } + ); + + act(() => { + wrapper + .find(StyledFlatTableRow) + .at(0) + .find("input") + .getDOMNode() + .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(); + }); + + 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( + + + {}}> + one + two + + {}}> + + four + + + , + { attachTo: htmlElement } + ); + + act(() => { + wrapper + .find(StyledFlatTableRow) + .at(1) + .find("input") + .getDOMNode() + .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(); + }); + }); + + describe("when the first column is expandable", () => { + it("should set the first cell's tabindex to 0", () => { + const wrapper = mount( + + + + one + two + + + 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); + }); + + it("should set the first row header's tabindex to 0", () => { + const wrapper = mount( + + + + one + two + + + three + four + + + + ); + + expect( + wrapper.update().find(StyledFlatTableRowHeader).at(0).prop("tabIndex") + ).toBe(0); + expect( + wrapper.update().find(StyledFlatTableRowHeader).at(1).prop("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( + + + + one + two + + + three + four + + + five + six + + + seven + eight + + + , + { attachTo: htmlElement } + ); + + act(() => { + wrapper + .find(StyledFlatTableRow) + .at(0) + .find(StyledFlatTableCell) + .at(0) + .getDOMNode() + .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(); + }); + + 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( + + + + one + two + + + three + four + + + five + six + + + seven + eight + + + , + { attachTo: htmlElement } + ); + + act(() => { + wrapper + .find(StyledFlatTableRow) + .at(3) + .find(StyledFlatTableCell) + .at(0) + .getDOMNode() + .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(); + }); + + 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( + + + + one + two + + + three + four + + + five + six + + + seven + eight + + + , + { attachTo: htmlElement } + ); + + act(() => { + wrapper + .find(StyledFlatTableRow) + .at(0) + .find(StyledFlatTableRowHeader) + .at(0) + .getDOMNode() + .focus(); + }); + + expect( + wrapper + .update() + .find(StyledFlatTableRow) + .at(0) + .find(StyledFlatTableRowHeader) + .at(0) + ).toBeFocused(); + + act(() => { + wrapper.find(StyledFlatTableWrapper).props().onKeyDown(arrowDown); + }); + + expect( + wrapper + .update() + .find(StyledFlatTableRow) + .at(1) + .find(StyledFlatTableRowHeader) + .at(0) + ).toBeFocused(); + + act(() => { + wrapper.find(StyledFlatTableWrapper).props().onKeyDown(arrowDown); + }); + + expect( + wrapper + .update() + .find(StyledFlatTableRow) + .at(2) + .find(StyledFlatTableRowHeader) + .at(0) + ).toBeFocused(); + + act(() => { + wrapper.find(StyledFlatTableWrapper).props().onKeyDown(arrowDown); + }); + + expect( + wrapper + .update() + .find(StyledFlatTableRow) + .at(3) + .find(StyledFlatTableRowHeader) + .at(0) + ).toBeFocused(); + + act(() => { + wrapper.find(StyledFlatTableWrapper).props().onKeyDown(arrowDown); + }); + + expect( + wrapper + .update() + .find(StyledFlatTableRow) + .at(3) + .find(StyledFlatTableRowHeader) + .at(0) + ).toBeFocused(); + }); + + 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( + + + + one + two + + + three + four + + + five + six + + + seven + eight + + + , + { attachTo: htmlElement } + ); + + act(() => { + wrapper + .find(StyledFlatTableRow) + .at(3) + .find(StyledFlatTableRowHeader) + .at(0) + .getDOMNode() + .focus(); + }); + + expect( + wrapper + .update() + .find(StyledFlatTableRow) + .at(3) + .find(StyledFlatTableRowHeader) + .at(0) + ).toBeFocused(); + + act(() => { + wrapper.find(StyledFlatTableWrapper).props().onKeyDown(arrowUp); + }); + + expect( + wrapper + .update() + .find(StyledFlatTableRow) + .at(2) + .find(StyledFlatTableRowHeader) + .at(0) + ).toBeFocused(); + + act(() => { + wrapper.find(StyledFlatTableWrapper).props().onKeyDown(arrowUp); + }); + + expect( + wrapper + .update() + .find(StyledFlatTableRow) + .at(1) + .find(StyledFlatTableRowHeader) + .at(0) + ).toBeFocused(); + + act(() => { + wrapper.find(StyledFlatTableWrapper).props().onKeyDown(arrowUp); + }); + + expect( + wrapper + .update() + .find(StyledFlatTableRow) + .at(0) + .find(StyledFlatTableRowHeader) + .at(0) + ).toBeFocused(); + + act(() => { + wrapper.find(StyledFlatTableWrapper).props().onKeyDown(arrowUp); + }); + + expect( + wrapper + .update() + .find(StyledFlatTableRow) + .at(0) + .find(StyledFlatTableRowHeader) + .at(0) + ).toBeFocused(); + }); + }); + }); }); function renderFlatTable(props = {}, renderer = TestRenderer.create) {