Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 0 additions & 48 deletions packages/lib/src/dropdown/Dropdown.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,22 +49,6 @@ describe("Dropdown component tests", () => {
expect(menu.getAttribute("aria-labelledby")).toBe(dropdown.id);
expect(getAllByRole("menuitem").length).toBe(4);
});
test("Button trigger opens and closes the menu options when clicked", () => {
const onSelectOption = jest.fn();
const { getByRole, queryByRole, getByText } = render(
<DxcDropdown options={options} label="dropdown-test" onSelectOption={onSelectOption} />
);
const dropdown = getByRole("button");
expect(queryByRole("menu")).toBeFalsy();
userEvent.click(dropdown);
expect(queryByRole("menu")).toBeTruthy();
expect(getByText("Amazon")).toBeTruthy();
expect(getByText("Ebay")).toBeTruthy();
expect(getByText("Wallapop")).toBeTruthy();
expect(getByText("Aliexpress")).toBeTruthy();
userEvent.click(dropdown);
expect(queryByRole("menu")).toBeFalsy();
});
test("Button trigger is not interactive when disabled", () => {
const onSelectOption = jest.fn();
const { getByRole, queryByRole, queryByText } = render(
Expand All @@ -90,30 +74,6 @@ describe("Dropdown component tests", () => {
userEvent.click(option);
expect(onSelectOption).toHaveBeenCalledWith("4");
});
test("When expandOnHover is true, the dropdown trigger shows and hides the menu when it is hovered", () => {
const onSelectOption = jest.fn();
const { queryByText, getByRole, queryByRole } = render(
<DxcDropdown options={options} expandOnHover label="dropdown-test" onSelectOption={onSelectOption} />
);
expect(queryByText("option-test")).toBeFalsy();
expect(queryByRole("menu")).toBeFalsy();
fireEvent.mouseOver(getByRole("button"));
const menu = getByRole("menu");
expect(menu).toBeTruthy();
expect(document.activeElement === menu).toBeTruthy();
expect(menu.getAttribute("aria-activedescendant")).toBe(`${menu.id}-option-0`);
});
test("The menu is closed when the dropdown loses the focus (blur)", () => {
const onSelectOption = jest.fn();
const { getByRole, queryByRole } = render(
<DxcDropdown options={options} label="dropdown-test" onSelectOption={onSelectOption} />
);
const dropdown = getByRole("button");
userEvent.click(dropdown);
expect(getByRole("menu")).toBeTruthy();
fireEvent.blur(getByRole("menu"));
expect(queryByRole("menu")).toBeFalsy();
});
test("Menu button key events — Arrow up opens the list and moves the focus to the last menu item", () => {
const onSelectOption = jest.fn();
const { getByRole } = render(
Expand All @@ -128,7 +88,6 @@ describe("Dropdown component tests", () => {
});
const menu = getByRole("menu");
expect(menu).toBeTruthy();
expect(document.activeElement === menu).toBeTruthy();
expect(getByRole("menu").getAttribute("aria-activedescendant")).toBe(`${menu.id}-option-3`);
});
test("Menu button key events — Arrow down opens the list and moves the focus to the first menu item", () => {
Expand All @@ -145,7 +104,6 @@ describe("Dropdown component tests", () => {
});
const menu = getByRole("menu");
expect(menu).toBeTruthy();
expect(document.activeElement === menu).toBeTruthy();
expect(getByRole("menu").getAttribute("aria-activedescendant")).toBe(`${menu.id}-option-0`);
});
test("Menu button key events — Enter opens the list and moves the focus to the first menu item", () => {
Expand All @@ -162,7 +120,6 @@ describe("Dropdown component tests", () => {
});
const menu = getByRole("menu");
expect(menu).toBeTruthy();
expect(document.activeElement === menu).toBeTruthy();
expect(getByRole("menu").getAttribute("aria-activedescendant")).toBe(`${menu.id}-option-0`);
});
test("Menu button key events — Space opens the list and moves the focus to the first menu item", () => {
Expand All @@ -179,7 +136,6 @@ describe("Dropdown component tests", () => {
});
const menu = getByRole("menu");
expect(menu).toBeTruthy();
expect(document.activeElement === menu).toBeTruthy();
expect(getByRole("menu").getAttribute("aria-activedescendant")).toBe(`${menu.id}-option-0`);
});
test("Menu key events — Arrow up moves the focus to the previous menu item", () => {
Expand All @@ -200,7 +156,6 @@ describe("Dropdown component tests", () => {
keyCode: 38,
charCode: 38,
});
expect(document.activeElement === menu).toBeTruthy();
expect(menu.getAttribute("aria-activedescendant")).toBe(`${menu.id}-option-2`);
fireEvent.keyDown(menu, {
key: "Enter",
Expand All @@ -223,7 +178,6 @@ describe("Dropdown component tests", () => {
keyCode: 38,
charCode: 38,
});
expect(document.activeElement === menu).toBeTruthy();
expect(menu.getAttribute("aria-activedescendant")).toBe(`${menu.id}-option-3`);
fireEvent.keyDown(menu, {
key: "Enter",
Expand Down Expand Up @@ -252,7 +206,6 @@ describe("Dropdown component tests", () => {
keyCode: 40,
charCode: 40,
});
expect(document.activeElement === menu).toBeTruthy();
expect(menu.getAttribute("aria-activedescendant")).toBe(`${menu.id}-option-2`);
fireEvent.keyDown(menu, {
key: "Enter",
Expand Down Expand Up @@ -280,7 +233,6 @@ describe("Dropdown component tests", () => {
keyCode: 40,
charCode: 40,
});
expect(document.activeElement === menu).toBeTruthy();
expect(menu.getAttribute("aria-activedescendant")).toBe(`${menu.id}-option-0`);
fireEvent.keyDown(menu, {
key: "Enter",
Expand Down
146 changes: 65 additions & 81 deletions packages/lib/src/dropdown/Dropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import * as Popover from "@radix-ui/react-popover";
import { FocusEvent, KeyboardEvent, useCallback, useEffect, useId, useLayoutEffect, useRef, useState } from "react";
import { KeyboardEvent, useCallback, useId, useLayoutEffect, useRef, useState } from "react";
import styled from "@emotion/styled";
import { getMargin } from "../common/utils";
import { spaces } from "../common/variables";
import DxcIcon from "../icon/Icon";
import useWidth from "../utils/useWidth";
import DropdownMenu from "./DropdownMenu";
import DropdownPropsType from "./types";
import { Tooltip } from "../tooltip/Tooltip";
import DxcPopover from "../popover/Popover";

const sizes = {
small: "60px",
Expand Down Expand Up @@ -131,10 +130,6 @@ const DxcDropdown = ({
const menuId = `menu-${id}`;
const [isOpen, changeIsOpen] = useState(false);
const [visualFocusIndex, setVisualFocusIndex] = useState(0);
const [portalContainer, setPortalContainer] = useState<HTMLElement | null>(null);
useEffect(() => {
setPortalContainer(document?.getElementById(`${id}-portal`));
}, []);

const triggerRef = useRef<HTMLButtonElement | null>(null);
const menuRef = useRef<HTMLUListElement | null>(null);
Expand All @@ -157,11 +152,6 @@ const DxcDropdown = ({
},
[onSelectOption]
);
const handleOnBlur = (event: FocusEvent<HTMLDivElement>) => {
if (!event.currentTarget.contains(event.relatedTarget)) {
handleOnCloseMenu();
}
};

const handleTriggerOnClick = () => {
changeIsOpen((isCurrentlyOpen) => !isCurrentlyOpen);
Expand Down Expand Up @@ -256,76 +246,70 @@ const DxcDropdown = ({

return (
<>
<DropdownContainer
onMouseEnter={!disabled && expandOnHover ? handleOnOpenMenu : undefined}
onMouseLeave={!disabled && expandOnHover ? handleOnCloseMenu : undefined}
onBlur={!disabled ? handleOnBlur : undefined}
margin={margin}
size={size}
>
<Popover.Root open={isOpen}>
<Tooltip label={title}>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tooltip shouldn't be removed

<Popover.Trigger asChild type={undefined}>
<DropdownTrigger
onClick={handleTriggerOnClick}
onKeyDown={handleTriggerOnKeyDown}
onBlur={(event) => {
event.stopPropagation();
}}
disabled={disabled}
label={label}
margin={margin}
size={size}
id={triggerId}
aria-haspopup="true"
aria-controls={isOpen ? menuId : undefined}
aria-expanded={isOpen ? true : undefined}
aria-label="Show options"
tabIndex={tabIndex}
ref={triggerRef}
type="button"
>
<DropdownTriggerContent iconPosition={iconPosition}>
{icon && (
<DropdownTriggerIcon
disabled={disabled}
role={typeof icon === "string" ? undefined : "img"}
aria-hidden
>
{typeof icon === "string" ? <DxcIcon icon={icon} /> : icon}
</DropdownTriggerIcon>
)}
{label && <DropdownTriggerLabel>{label}</DropdownTriggerLabel>}
</DropdownTriggerContent>
{!caretHidden && (
<CaretIcon disabled={disabled}>
<DxcIcon icon={isOpen ? "keyboard_arrow_up" : "keyboard_arrow_down"} />
</CaretIcon>
)}
</DropdownTrigger>
</Popover.Trigger>
</Tooltip>
{portalContainer && (
<Popover.Portal container={portalContainer}>
<Popover.Content aria-label="Dropdown options" asChild sideOffset={1}>
<DropdownMenu
id={menuId}
dropdownTriggerId={triggerId}
options={options}
iconsPosition={optionsIconPosition}
visualFocusIndex={visualFocusIndex}
menuItemOnClick={handleMenuItemOnClick}
onKeyDown={handleMenuOnKeyDown}
styles={{ width }}
ref={menuRef}
/>
</Popover.Content>
</Popover.Portal>
)}
</Popover.Root>
<DropdownContainer margin={margin} size={size}>
<DxcPopover
asChild
isOpen={isOpen}
onClose={handleOnCloseMenu}
offset={2}
onOpenAutoFocus={() => {
menuRef.current?.focus();
}}
actionToOpen={expandOnHover ? "hover" : "click"}
popoverContent={
<DropdownMenu
id={menuId}
dropdownTriggerId={triggerId}
options={options}
iconsPosition={optionsIconPosition}
visualFocusIndex={visualFocusIndex}
menuItemOnClick={handleMenuItemOnClick}
onKeyDown={handleMenuOnKeyDown}
styles={{ width }}
ref={menuRef}
/>
}
>
<DropdownTrigger
onClick={handleTriggerOnClick}
onKeyDown={handleTriggerOnKeyDown}
onBlur={(event) => {
event.stopPropagation();
}}
disabled={disabled}
label={label}
margin={margin}
size={size}
id={triggerId}
aria-haspopup="true"
aria-controls={isOpen ? menuId : undefined}
aria-expanded={isOpen ? true : undefined}
aria-label="Show options"
tabIndex={tabIndex}
ref={triggerRef}
type="button"
title={title}
>
<DropdownTriggerContent iconPosition={iconPosition}>
{icon && (
<DropdownTriggerIcon
disabled={disabled}
role={typeof icon === "string" ? undefined : "img"}
aria-hidden
>
{typeof icon === "string" ? <DxcIcon icon={icon} /> : icon}
</DropdownTriggerIcon>
)}
{label && <DropdownTriggerLabel>{label}</DropdownTriggerLabel>}
</DropdownTriggerContent>
{!caretHidden && (
<CaretIcon disabled={disabled}>
<DxcIcon icon={isOpen ? "keyboard_arrow_up" : "keyboard_arrow_down"} />
</CaretIcon>
)}
</DropdownTrigger>
</DxcPopover>
</DropdownContainer>

<div id={`${id}-portal`} style={{ position: "absolute" }} />
</>
);
};
Expand Down
63 changes: 30 additions & 33 deletions packages/lib/src/dropdown/DropdownMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,53 +3,50 @@ import styled from "@emotion/styled";
import DropdownMenuItem from "./DropdownMenuItem";
import { DropdownMenuProps } from "./types";
import scrollbarStyles from "../styles/scroll";
import DxcBleed from "../bleed/Bleed";

const DropdownMenuContainer = styled.ul`
max-height: 230px;
min-width: min-content;
padding: 0;
margin: 0;
background-color: var(--color-bg-neutral-lightest);
border-radius: var(--border-radius-s);
box-shadow: var(--shadow-100);
outline: none;
overflow-y: auto;
z-index: var(--z-dropdown);
${scrollbarStyles}
`;

const DropdownMenu = forwardRef<HTMLUListElement, DropdownMenuProps>(
({ id, dropdownTriggerId, iconsPosition, visualFocusIndex, menuItemOnClick, onKeyDown, options, styles }, ref) => (
<DropdownMenuContainer
onMouseDown={(event) => {
// Prevent the onBlur event from closing menu when clicking on the menu since
// it is implemented with a Portal and the menu is not a direct child of the container
event.preventDefault();
}}
onKeyDown={onKeyDown}
id={id}
role="menu"
aria-labelledby={dropdownTriggerId}
aria-orientation="vertical"
aria-activedescendant={visualFocusIndex !== -1 ? `${id}-option-${visualFocusIndex}` : undefined}
tabIndex={-1}
ref={ref}
style={styles}
>
{options.map((option, index) => (
<DropdownMenuItem
id={`${id}-option-${index}`}
key={`${id}-option-${index}`}
visuallyFocused={index === visualFocusIndex}
iconPosition={iconsPosition}
onClick={menuItemOnClick}
option={option}
/>
))}
</DropdownMenuContainer>
<DxcBleed space="var(--spacing-padding-xs)">
<DropdownMenuContainer
onMouseDown={(event) => {
// Prevent the onBlur event from closing menu when clicking on the menu since
// it is implemented with a Portal and the menu is not a direct child of the container
event.preventDefault();
}}
onKeyDown={onKeyDown}
id={id}
role="menu"
aria-labelledby={dropdownTriggerId}
aria-orientation="vertical"
aria-activedescendant={visualFocusIndex !== -1 ? `${id}-option-${visualFocusIndex}` : undefined}
tabIndex={0}
ref={ref}
style={styles}
>
{options.map((option, index) => (
<DropdownMenuItem
id={`${id}-option-${index}`}
key={`${id}-option-${index}`}
visuallyFocused={index === visualFocusIndex}
iconPosition={iconsPosition}
onClick={menuItemOnClick}
option={option}
/>
))}
</DropdownMenuContainer>
</DxcBleed>
)
);

DropdownMenu.displayName = "DropdownMenu";

export default memo(DropdownMenu);
Loading