Skip to content

Commit

Permalink
fix(basedropdown): aria & role on header element
Browse files Browse the repository at this point in the history
  • Loading branch information
zettca committed Aug 29, 2023
1 parent ae4c6a1 commit d8d455b
Show file tree
Hide file tree
Showing 2 changed files with 54 additions and 61 deletions.
56 changes: 35 additions & 21 deletions packages/core/src/components/BaseDropdown/BaseDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import React, {
useState,
useCallback,
KeyboardEventHandler,
AriaAttributes,
} from "react";

import { createPortal } from "react-dom";
Expand Down Expand Up @@ -130,7 +131,7 @@ export interface HvBaseDropdownProps

export const HvBaseDropdown = (props: HvBaseDropdownProps) => {
const {
id,
id: idProp,
className,
classes: classesProp,
children,
Expand All @@ -146,13 +147,15 @@ export const HvBaseDropdown = (props: HvBaseDropdownProps) => {
required,
disablePortal,
variableWidth,
placement = "right",
placement: placementProp = "right",
"aria-expanded": ariaExpandedProp,
"aria-label": ariaLabelProp,
"aria-labelledby": ariaLabelledByProp,
popperProps = {},
dropdownHeaderRef: dropdownHeaderRefProp,
onToggle,
onClickOutside,
onContainerCreation,
"aria-expanded": ariaExpandedProp,
...others
} = useDefaultProps("HvBaseDropdown", props);
const { classes, cx } = useClasses(classesProp);
Expand Down Expand Up @@ -183,10 +186,26 @@ export const HvBaseDropdown = (props: HvBaseDropdownProps) => {

const ariaExpanded = ariaExpandedProp ?? (ariaRole ? !!isOpen : undefined);

const elementId = useUniqueId(id, "hvbasedropdown");
const id = useUniqueId(idProp, "hvbasedropdown");
const containerId = setId(id, "children-container");

const headerControlArias = {
"aria-required": required ?? undefined,
"aria-readonly": readOnly ?? undefined,

"aria-expanded": ariaExpanded,
"aria-owns": isOpen ? containerId : undefined,
"aria-controls": isOpen ? containerId : undefined,
} satisfies AriaAttributes;

const bottom: PopperPlacementType =
placement && `bottom-${placement === "right" ? "start" : "end"}`;
const headerAriaLabels = {
"aria-label": ariaLabelProp,
"aria-labelledby": ariaLabelledByProp,
} satisfies AriaAttributes;

const placement: PopperPlacementType = `bottom-${
placementProp === "right" ? "start" : "end"
}`;

const extensionWidth = referenceElement
? referenceElement?.offsetWidth
Expand Down Expand Up @@ -295,7 +314,7 @@ export const HvBaseDropdown = (props: HvBaseDropdownProps) => {
referenceElement,
popperElement,
{
placement: bottom,
placement,
modifiers,
onFirstUpdate,
...otherPopperProps,
Expand Down Expand Up @@ -345,6 +364,7 @@ export const HvBaseDropdown = (props: HvBaseDropdownProps) => {
if (component) {
return React.cloneElement(component as React.ReactElement, {
ref: handleDropdownHeaderRef,
...headerControlArias,
});
}

Expand All @@ -361,12 +381,10 @@ export const HvBaseDropdown = (props: HvBaseDropdownProps) => {
[classes.headerOpenDown]:
isOpen && popperPlacement.includes("bottom"),
})}
// TODO: review "textbox" role
role={ariaRole === "combobox" ? "textbox" : undefined}
{...headerAriaLabels}
style={disabled || readOnly ? { pointerEvents: "none" } : undefined}
aria-label={others["aria-label"] ?? undefined}
aria-labelledby={others["aria-labelledby"] ?? undefined}
aria-required={required ?? undefined}
aria-readonly={readOnly ?? undefined}
// Removes the element from the navigation sequence for keyboard focus if disabled
tabIndex={disabled ? -1 : 0}
ref={handleDropdownHeaderRef}
Expand Down Expand Up @@ -427,7 +445,6 @@ export const HvBaseDropdown = (props: HvBaseDropdownProps) => {

const container = (
<div
role="tooltip"
ref={setPopperElement}
className={classes.container}
style={popperStyles.popper}
Expand All @@ -447,7 +464,7 @@ export const HvBaseDropdown = (props: HvBaseDropdownProps) => {
)}
<BaseDropdownContext.Provider value={popperMaxSize}>
<div
id={setId(elementId, "children-container")}
id={containerId}
className={cx(classes.panel, {
[classes.panelOpenedUp]: popperPlacement.includes("top"),
[classes.panelOpenedDown]: popperPlacement.includes("bottom"),
Expand Down Expand Up @@ -488,14 +505,6 @@ export const HvBaseDropdown = (props: HvBaseDropdownProps) => {
<div className={classes.root}>
<div
id={id}
role={ariaRole}
aria-expanded={ariaExpanded}
aria-owns={isOpen ? setId(elementId, "children-container") : undefined}
aria-controls={
isOpen ? setId(elementId, "children-container") : undefined
}
aria-required={required ?? undefined}
aria-readonly={readOnly ?? undefined}
className={cx(
classes.anchor,
{ [classes.rootDisabled]: disabled },
Expand All @@ -505,6 +514,11 @@ export const HvBaseDropdown = (props: HvBaseDropdownProps) => {
onKeyDown: handleToggle,
onClick: handleToggle,
})}
{...(ariaRole && {
role: ariaRole,
...headerAriaLabels,
...headerControlArias,
})}
// Removes the element from the navigation sequence for keyboard focus
tabIndex={-1}
{...others}
Expand Down
59 changes: 19 additions & 40 deletions packages/core/src/components/BulkActions/BulkActions.test.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,11 @@
import { Add, Delete, Preview, Lock } from "@hitachivantara/uikit-react-icons";
import { fireEvent, render } from "@testing-library/react";
import { fireEvent, render, screen } from "@testing-library/react";
import { useState } from "react";
import { describe, expect, it, vi } from "vitest";
import { HvBulkActions } from "./BulkActions";
import { HvBulkActions, HvBulkActionsProps } from "./BulkActions";

const WithoutActions = () => {
const [numSelected, setNumSelected] = useState<number>(0);

const handleSelectAll = () => {
setNumSelected(8);
};

return (
<HvBulkActions
numTotal={8}
numSelected={numSelected}
onSelectAll={handleSelectAll}
maxVisibleActions={3}
/>
);
};

const WithActions = () => {
const [numSelected, setNumSelected] = useState<number>(0);
const Sample = (props: Partial<HvBulkActionsProps>) => {
const [numSelected, setNumSelected] = useState(0);

const handleSelectAll = () => {
setNumSelected(8);
Expand All @@ -40,39 +23,40 @@ const WithActions = () => {
{ id: "lock", label: "Lock", icon: <Lock /> },
{ id: "put", label: "Preview", icon: <Preview /> },
]}
{...props}
/>
);
};

describe("BulkActions", () => {
describe("Without actions", () => {
it("should render select all component correctly", async () => {
const { getByRole, getByLabelText } = render(<WithoutActions />);
render(<Sample actions={undefined} />);

const checkbox = getByRole("checkbox");
const checkbox = screen.getByRole("checkbox");

expect(checkbox).toBeInTheDocument();
expect(getByLabelText("All (8)")).toBeInTheDocument();
expect(screen.getByLabelText("All (8)")).toBeInTheDocument();

// Select all
fireEvent.click(checkbox);

expect(checkbox).toBeChecked();
expect(getByLabelText("8 / 8")).toBeInTheDocument();
expect(screen.getByLabelText("8 / 8")).toBeInTheDocument();
});

it("should call select all correctly", async () => {
const onSelectAllMock = vi.fn();

const { getAllByRole } = render(
render(
<HvBulkActions
numTotal={5}
numSelected={0}
onSelectAll={onSelectAllMock}
/>
);

const checkboxes = getAllByRole("checkbox");
const checkboxes = screen.getAllByRole("checkbox");

const selectAll = checkboxes[0];

Expand All @@ -83,23 +67,23 @@ describe("BulkActions", () => {
});

it("should render the custom label", () => {
const { getByLabelText } = render(
render(
<HvBulkActions
numTotal={5}
numSelected={0}
selectAllLabel="MockLabel"
/>
);

expect(getByLabelText("MockLabel (5)")).toBeInTheDocument();
expect(screen.getByLabelText("MockLabel (5)")).toBeInTheDocument();
});
});

describe("With actions", () => {
it("should render the actions correctly", async () => {
const { getAllByRole, getByRole } = render(<WithActions />);
render(<Sample />);

const buttons = getAllByRole("button");
const buttons = screen.getAllByRole("button");

expect(buttons.length).toBe(3);

Expand All @@ -111,7 +95,7 @@ describe("BulkActions", () => {
expect(button2).toBeDisabled();
expect(button3).toBeDisabled();

const checkbox = getByRole("checkbox");
const checkbox = screen.getByRole("checkbox");

// Select all
fireEvent.click(checkbox);
Expand All @@ -120,15 +104,10 @@ describe("BulkActions", () => {
expect(button2).toBeEnabled();
expect(button3).toBeEnabled();

// Open tooltip
// Open actions
fireEvent.click(button3);

const tooltip = getByRole("tooltip");

expect(tooltip).toBeInTheDocument();

const menu = getByRole("menu");
const items = getAllByRole("menuitem");
const menu = screen.getByRole("menu");
const items = screen.getAllByRole("menuitem");

expect(menu).toBeInTheDocument();
expect(items.length).toBe(2);
Expand Down

0 comments on commit d8d455b

Please sign in to comment.