Skip to content

Commit

Permalink
fix(menu-fullscreen): ensure correct tab order when Search with butto…
Browse files Browse the repository at this point in the history
…n rendered as a child

Ensures that the correct tab order is maintained when a `MenuItem` with
a `Search` input child with `searchButton` enabled but no value. Currently the
button disappears when the input is blurred and there is no value meaning focus
is placed on the document body instead. This update ensures that the next
item in the Menu is focused instead.

fix #5959
  • Loading branch information
edleeks87 committed Apr 24, 2023
1 parent 82b543b commit ddabef9
Show file tree
Hide file tree
Showing 5 changed files with 181 additions and 2 deletions.
47 changes: 47 additions & 0 deletions cypress/components/menu/menu.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
import {
searchDefaultInput,
searchCrossIcon,
searchButton,
} from "../../locators/search/index";
import { getComponent, closeIconButton, icon } from "../../locators";
import {
Expand Down Expand Up @@ -288,6 +289,24 @@ const MenuComponentItems = ({ ...props }) => {
);
};

/* eslint-disable-next-line react/prop-types */
const MenuFullScreenWithSearchButton = ({ searchValue }) => (
<MenuFullscreen isOpen onClose={() => {}}>
<MenuItem href="#">Menu Item before Search</MenuItem>
<MenuItem variant="alternate">
<Search
placeholder="Dark variant"
variant="dark"
defaultValue={searchValue}
searchButton
/>
</MenuItem>
<MenuItem variant="alternate" href="#">
Menu Item after Search
</MenuItem>
</MenuFullscreen>
);

const MenuComponentScrollableParent = () => {
const items = ["apple", "banana", "carrot", "grapefruit", "melon", "orange"];
const [itemSearch, setItemSearch] = React.useState(items);
Expand Down Expand Up @@ -1457,6 +1476,34 @@ context("Testing Menu component", () => {
);
}
);

it("should focus the next menu item on tab press when the current item has a Search input with searchButton but no value", () => {
CypressMountWithProviders(
<MenuFullScreenWithSearchButton searchValue="" />
);

menuItem().first().find("a").focus();
cy.tab();
searchDefaultInput().should("have.focus");
cy.tab();
menuItem().last().find("a").should("have.focus");
});

it("should focus the search icon and button on tab press when the current item has a Search input with searchButton and has a value", () => {
CypressMountWithProviders(
<MenuFullScreenWithSearchButton searchValue="foo" />
);

menuItem().first().find("a").focus();
cy.tab();
searchDefaultInput().should("have.focus");
cy.tab();
searchCrossIcon().parent().should("have.focus");
cy.tab();
searchButton().should("have.focus");
cy.tab();
menuItem().last().find("a").should("have.focus");
});
});

describe("check events for Menu component", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,34 @@ export const MenuFullscreen = ({
if (Events.isEscKey(ev)) {
onClose(ev);
}

if (Events.isTabKey(ev) && !Events.isShiftKey(ev)) {
const search = menuWrapperRef.current?.querySelector(
'[data-component="search"'
);
const searchInput = search?.querySelector("input");
const searchButton = search?.querySelector("button");

// if there is no value in the search input the button disappears when the input blurs
// this means we need to programatically set focus to the next menu item
if (
searchButton &&
searchInput &&
!searchInput.value &&
searchInput === document.activeElement
) {
ev.preventDefault();

const elements = Array.from(
menuWrapperRef.current?.querySelectorAll(
"a, input, button"
) as NodeListOf<HTMLElement>
);

const index = elements.indexOf(searchInput);
elements[index + 2]?.focus();
}
}
};

return (
Expand Down
105 changes: 104 additions & 1 deletion src/components/menu/menu-full-screen/menu-full-screen.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import {
StyledMenuFullscreenHeader,
} from "./menu-full-screen.style";
import StyledIconButton from "../../icon-button/icon-button.style";
import Search from "../../search";
import StyledSearch from "../../search/search.style";
import StyledSearchButton from "../../search/search-button.style";
import {
assertStyleMatch,
simulate,
Expand Down Expand Up @@ -84,6 +87,33 @@ const render = ({
);
};

const MockMenuWithSearch = ({
isOpen,
focusInput,
}: {
isOpen?: boolean;
focusInput?: boolean;
}) => {
const ref = React.useRef(null);

React.useEffect(() => {
if (focusInput && ref.current) {
(ref.current as HTMLInputElement).focus();
}
}, [focusInput]);

return (
<MenuFullscreen isOpen={isOpen} onClose={() => {}}>
<MenuItem maxWidth="200px">
<Search ref={ref} defaultValue="" searchButton />
</MenuItem>
<MenuItem maxWidth="200px" href="#">
Menu Item One
</MenuItem>
</MenuFullscreen>
);
};

describe("MenuFullscreen", () => {
let wrapper: ReactWrapper;

Expand Down Expand Up @@ -272,7 +302,6 @@ describe("MenuFullscreen", () => {
it("focuses the menu wrapper on open of menu", () => {
wrapper = mount(<TestMenu />);
wrapper.setProps({ isOpen: true });

const element = wrapper.find(StyledMenuFullscreen).getDOMNode();
const startEvent = new Event("transitionstart", {
bubbles: true,
Expand All @@ -287,6 +316,80 @@ describe("MenuFullscreen", () => {

expect(wrapper.find(StyledMenuFullscreen)).toBeFocused();
});

describe("when pressing tab key without shift", () => {
it("does not prevent the browser default behaviour when no Search input with searchButton and no value is rendered", () => {
const container = document.createElement("div");
container.id = "enzymeContainer";
document.body.appendChild(container);
const preventDefault = jest.fn();
wrapper = mount(<TestMenu />, {
attachTo: document.getElementById("enzymeContainer"),
});
wrapper.setProps({ isOpen: true });

const element = wrapper.find(StyledMenuFullscreen).getDOMNode();
const startEvent = new Event("transitionstart", {
bubbles: true,
cancelable: true,
});
const endEvent = new Event("transitionend", {
bubbles: true,
cancelable: true,
});
element.dispatchEvent(startEvent);
element.dispatchEvent(endEvent);

wrapper.find(StyledMenuFullscreen).prop("onKeyDown")({
key: "Tab",
preventDefault,
});
wrapper.find(StyledMenuFullscreen).prop("onKeyDown")({
key: "Tab",
preventDefault,
});
expect(preventDefault).not.toHaveBeenCalled();
wrapper.find(StyledMenuFullscreen).prop("onKeyDown")({
key: "Tab",
preventDefault,
});
expect(preventDefault).not.toHaveBeenCalled();
wrapper.unmount();
});

it("prevents the browser default behaviour when Search input with searchButton and no value rendered", () => {
const container = document.createElement("div");
container.id = "enzymeContainer";
document.body.appendChild(container);
const preventDefault = jest.fn();
wrapper = mount(<MockMenuWithSearch />, {
attachTo: document.getElementById("enzymeContainer"),
});
wrapper.setProps({ isOpen: true });
const element = wrapper.find(StyledMenuFullscreen).getDOMNode();
const startEvent = new Event("transitionstart", {
bubbles: true,
cancelable: true,
});
const endEvent = new Event("transitionend", {
bubbles: true,
cancelable: true,
});
element.dispatchEvent(startEvent);
element.dispatchEvent(endEvent);
wrapper.setProps({ focusInput: true });

expect(wrapper.find(StyledSearch).find("input")).toBeFocused();
expect(wrapper.find(StyledSearchButton).exists()).toBe(true);
wrapper.find(StyledMenuFullscreen).prop("onKeyDown")({
key: "Tab",
preventDefault,
});
expect(preventDefault).toHaveBeenCalled();
expect(wrapper.find(StyledMenuItem).last().find("a")).toBeFocused();
wrapper.unmount();
});
});
});

describe("when clicking outside a submenu", () => {
Expand Down
2 changes: 1 addition & 1 deletion src/components/menu/menu-item/menu-item.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ export const MenuItem = ({
document.activeElement === inputRef.current &&
inputRef.current?.value;

// let natural tab order move focus if input icon is tabbable
// let natural tab order move focus if input icon is tabbable or input with button exists
if (
Events.isTabKey(event) &&
((!Events.isShiftKey(event) && shouldFocusIcon) ||
Expand Down
1 change: 1 addition & 0 deletions src/components/menu/menu.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,7 @@ export const ScrollableSubmenuWithParent: ComponentStory<typeof Menu> = () => {
placeholder="search"
value={searchString}
onChange={handleTextChange}
searchButton
/>
}
>
Expand Down

0 comments on commit ddabef9

Please sign in to comment.