Skip to content

Commit

Permalink
feat(Accordion): add expandOnTileClick for mobile-first interaction
Browse files Browse the repository at this point in the history
- Allows entire header section to be clickable for expansion
- Maintains backward compatibility with button-based expansion
- Adds cursor-pointer styling when header is clickable
- Includes keyboard navigation support for accessibility
- Includes tests and storybook examples
- Improves code organization by merging conditional rendering logic

Co-Authored-By: Jozef Képesi <[email protected]>
  • Loading branch information
devin-ai-integration[bot] and SScorp committed Dec 17, 2024
1 parent b78d7c5 commit a995633
Show file tree
Hide file tree
Showing 5 changed files with 204 additions and 20 deletions.
55 changes: 55 additions & 0 deletions packages/orbit-components/src/Accordion/Accordion.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -264,3 +264,58 @@ export const LoadingAccordion: Story = {
</Accordion>
),
};

export const MobileFirstInteraction: Story = {
render: function Render() {
const [expandedSection, setExpandedSection] = React.useState("");

return (
<Stack>
<Text>Traditional button-based expansion:</Text>
<Accordion
expandedSection={expandedSection}
onExpand={id => setExpandedSection(String(id))}
>
<AccordionSection
id="traditional"
header={
<Stack spacing="300">
<Text type="primary">Click the button to expand</Text>
<Text size="small">Uses traditional button-based expansion</Text>
</Stack>
}
>
<Text type="primary">This section uses the traditional button-based expansion.</Text>
</AccordionSection>
</Accordion>

<Text spaceAfter="large">Mobile-first tile click expansion:</Text>
<Accordion
expandedSection={expandedSection}
onExpand={id => setExpandedSection(String(id))}
>
<AccordionSection
id="mobile"
expandOnTileClick
header={
<Stack spacing="300">
<Text type="primary">Click anywhere on the header to expand</Text>
<Text size="small">Uses mobile-first tile click interaction</Text>
</Stack>
}
>
<Text type="primary">
This section uses the mobile-first expandOnTileClick interaction.
</Text>
<Text type="primary">
The entire header area is clickable for better mobile usability.
</Text>
<Text type="primary">
Keyboard users can still use Enter or Space to expand/collapse.
</Text>
</AccordionSection>
</Accordion>
</Stack>
);
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ interface Props extends Common.Globals {
readonly children: React.ReactNode;
readonly expanded: boolean;
readonly expandable: boolean;
readonly expandOnTileClick?: boolean;
readonly onExpand?: Common.Callback;
readonly actions?: React.ReactNode;
}
Expand All @@ -16,25 +17,73 @@ const AccordionSectionHeader = ({
children,
actions,
expanded,
expandOnTileClick,
onExpand,
expandable,
dataTest,
}: Props) => (
<div
className={cx("p-600 flex items-center", expanded ? "min-h-[19px]" : "min-h-form-box-normal")}
data-test={dataTest && `${dataTest}Header`}
>
<div className="flex grow items-center">{children}</div>
{!expanded && expandable && (
<div className="ms-600 flex">
{actions || (
<Button onClick={onExpand} type="secondary">
Open
</Button>
)}
</div>
)}
</div>
);
}: Props) => {
const isInteractive = expandOnTileClick && !expanded && expandable;

const handleClick = React.useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation();
onExpand?.();
},
[onExpand],
);

const handleKeyDown = React.useCallback(
(e: React.KeyboardEvent<HTMLDivElement>) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
onExpand?.();
}
},
[onExpand],
);

const handleButtonClick = React.useCallback(
(e: React.SyntheticEvent<HTMLButtonElement | HTMLAnchorElement>) => {
e.stopPropagation();
onExpand?.();
},
[onExpand],
);

const content = (
<>
<div className="flex grow items-center">{children}</div>
{!expanded && expandable && (
<div className="ms-600 flex">
{actions || (
<Button onClick={handleButtonClick} type="secondary">
Open
</Button>
)}
</div>
)}
</>
);

return (
<div
className={cx(
"p-600 flex w-full items-center",
expanded ? "min-h-[19px]" : "min-h-form-box-normal",
isInteractive && "hover:bg-cloud-light cursor-pointer border-0 bg-transparent text-left",
)}
data-test={dataTest && `${dataTest}Header`}
aria-expanded={expanded}
{...(isInteractive && {
role: "button",
onClick: handleClick,
onKeyDown: handleKeyDown,
tabIndex: 0,
})}
>
{content}
</div>
);
};

export default AccordionSectionHeader;
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const AccordionSection = ({
actions,
dataTest,
expandable = true,
expandOnTileClick = false,
}: Props) => {
const { expanded, onExpand, loading } = useAccordion();

Expand All @@ -40,6 +41,7 @@ const AccordionSection = ({
expanded={Boolean(isExpanded)}
onExpand={onExpand}
expandable={expandable}
expandOnTileClick={expandOnTileClick}
dataTest={dataTest}
>
{header}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export interface Props extends Common.Globals {
readonly actions?: React.ReactNode;
readonly expanded?: boolean;
readonly expandable?: boolean;
readonly expandOnTileClick?: boolean; // Mobile-first interaction
readonly onExpand?: Common.Callback;
readonly header?: React.ReactNode;
readonly footer?: React.ReactNode;
Expand Down
83 changes: 80 additions & 3 deletions packages/orbit-components/src/Accordion/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React from "react";

import { render, screen } from "../../test-utils";
import { render, screen, fireEvent, waitFor } from "../../test-utils";
import Accordion, { AccordionSection } from "..";

describe(`Accordion`, () => {
describe("Accordion", () => {
const expandedSection = "0X1";
const dataTest = "Accordion";
const id = "accordionId";
Expand Down Expand Up @@ -35,7 +35,7 @@ describe(`Accordion`, () => {
expect(screen.getByTestId(`${sectionDataTest}Loading`)).toBeInTheDocument();
});

describe(`AccordionSection`, () => {
describe("AccordionSection", () => {
it("should render passed components", () => {
render(
<AccordionSection
Expand All @@ -53,5 +53,82 @@ describe(`Accordion`, () => {
expect(screen.getByTestId(`${sectionDataTest}Content`)).toBeInTheDocument();
expect(screen.getByTestId(`${sectionDataTest}Footer`)).toBeInTheDocument();
});

describe("expandOnTileClick functionality", () => {
const sectionId = "test";

beforeEach(() => {
onExpand.mockClear();
});

it("should expand when header is clicked and expandOnTileClick is true", async () => {
const clickTestId = `${sectionDataTest}-click`;
const expandHandler = onExpand;
render(
<Accordion onExpand={expandHandler} expandedSection={undefined}>
<AccordionSection
id={sectionId}
header="Clickable Header"
expandOnTileClick
dataTest={clickTestId}
/>
</Accordion>,
);

const header = screen.getByTestId(`${clickTestId}Header`);
expect(header).toHaveClass("cursor-pointer");

await waitFor(() => {
fireEvent.click(header);
});

expect(expandHandler).toHaveBeenCalledWith(sectionId);
});

it("should not expand when header is clicked and expandOnTileClick is false", async () => {
const noClickTestId = `${sectionDataTest}-no-click`;
const expandHandler = onExpand;
render(
<Accordion onExpand={expandHandler} expandedSection={undefined}>
<AccordionSection
id={sectionId}
header="Non-Clickable Header"
dataTest={noClickTestId}
/>
</Accordion>,
);

const header = screen.getByTestId(`${noClickTestId}Header`);

await waitFor(() => {
fireEvent.click(header);
});

expect(expandHandler).not.toHaveBeenCalled();
});

it("should maintain button click functionality with expandOnTileClick", async () => {
const buttonTestId = `${sectionDataTest}-button`;
const expandHandler = onExpand;
render(
<Accordion onExpand={expandHandler} expandedSection={undefined}>
<AccordionSection
id={sectionId}
header="Header with Both"
expandOnTileClick
dataTest={buttonTestId}
/>
</Accordion>,
);

const button = screen.getByRole("button", { name: "Open" });

await waitFor(() => {
fireEvent.click(button);
});

expect(expandHandler).toHaveBeenCalledWith(sectionId);
});
});
});
});

0 comments on commit a995633

Please sign in to comment.