From 7b035819dbde2a925af0d4759be804b6fd43756e Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 17 Dec 2024 15:29:57 +0000 Subject: [PATCH] feat(Accordion): add expandOnTileClick for mobile-first interaction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Jozef Képesi --- .../src/Accordion/Accordion.stories.tsx | 55 ++++++++++++ .../components/SectionHeader.tsx | 83 +++++++++++++++---- .../src/Accordion/AccordionSection/index.tsx | 2 + .../src/Accordion/AccordionSection/types.d.ts | 1 + .../orbit-components/src/Accordion/README.md | 23 ++--- .../src/Accordion/__tests__/index.test.tsx | 83 ++++++++++++++++++- 6 files changed, 216 insertions(+), 31 deletions(-) diff --git a/packages/orbit-components/src/Accordion/Accordion.stories.tsx b/packages/orbit-components/src/Accordion/Accordion.stories.tsx index d80fc8ed8b..53ee4db952 100644 --- a/packages/orbit-components/src/Accordion/Accordion.stories.tsx +++ b/packages/orbit-components/src/Accordion/Accordion.stories.tsx @@ -264,3 +264,58 @@ export const LoadingAccordion: Story = { ), }; + +export const MobileFirstInteraction: Story = { + render: function Render() { + const [expandedSection, setExpandedSection] = React.useState(""); + + return ( + + Traditional button-based expansion: + setExpandedSection(String(id))} + > + + Click the button to expand + Uses traditional button-based expansion + + } + > + This section uses the traditional button-based expansion. + + + + Mobile-first tile click expansion: + setExpandedSection(String(id))} + > + + Click anywhere on the header to expand + Uses mobile-first tile click interaction + + } + > + + This section uses the mobile-first expandOnTileClick interaction. + + + The entire header area is clickable for better mobile usability. + + + Keyboard users can still use Enter or Space to expand/collapse. + + + + + ); + }, +}; diff --git a/packages/orbit-components/src/Accordion/AccordionSection/components/SectionHeader.tsx b/packages/orbit-components/src/Accordion/AccordionSection/components/SectionHeader.tsx index 3adcf20f77..726c7c7a2f 100644 --- a/packages/orbit-components/src/Accordion/AccordionSection/components/SectionHeader.tsx +++ b/packages/orbit-components/src/Accordion/AccordionSection/components/SectionHeader.tsx @@ -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; } @@ -16,25 +17,73 @@ const AccordionSectionHeader = ({ children, actions, expanded, + expandOnTileClick, onExpand, expandable, dataTest, -}: Props) => ( -
-
{children}
- {!expanded && expandable && ( -
- {actions || ( - - )} -
- )} -
-); +}: Props) => { + const isInteractive = expandOnTileClick && !expanded && expandable; + + const handleClick = React.useCallback( + (e: React.MouseEvent) => { + e.stopPropagation(); + onExpand?.(); + }, + [onExpand], + ); + + const handleKeyDown = React.useCallback( + (e: React.KeyboardEvent) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + onExpand?.(); + } + }, + [onExpand], + ); + + const handleButtonClick = React.useCallback( + (e: React.SyntheticEvent) => { + e.stopPropagation(); + onExpand?.(); + }, + [onExpand], + ); + + const content = ( + <> +
{children}
+ {!expanded && expandable && ( +
+ {actions || ( + + )} +
+ )} + + ); + + return ( +
+ {content} +
+ ); +}; export default AccordionSectionHeader; diff --git a/packages/orbit-components/src/Accordion/AccordionSection/index.tsx b/packages/orbit-components/src/Accordion/AccordionSection/index.tsx index 4685ee5ca4..d5f60d55c4 100644 --- a/packages/orbit-components/src/Accordion/AccordionSection/index.tsx +++ b/packages/orbit-components/src/Accordion/AccordionSection/index.tsx @@ -19,6 +19,7 @@ const AccordionSection = ({ actions, dataTest, expandable = true, + expandOnTileClick = false, }: Props) => { const { expanded, onExpand, loading } = useAccordion(); @@ -40,6 +41,7 @@ const AccordionSection = ({ expanded={Boolean(isExpanded)} onExpand={onExpand} expandable={expandable} + expandOnTileClick={expandOnTileClick} dataTest={dataTest} > {header} diff --git a/packages/orbit-components/src/Accordion/AccordionSection/types.d.ts b/packages/orbit-components/src/Accordion/AccordionSection/types.d.ts index 747c2c0213..8c8fd619f3 100644 --- a/packages/orbit-components/src/Accordion/AccordionSection/types.d.ts +++ b/packages/orbit-components/src/Accordion/AccordionSection/types.d.ts @@ -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; diff --git a/packages/orbit-components/src/Accordion/README.md b/packages/orbit-components/src/Accordion/README.md index 1680ed117b..c611d8a9f4 100644 --- a/packages/orbit-components/src/Accordion/README.md +++ b/packages/orbit-components/src/Accordion/README.md @@ -30,17 +30,18 @@ After adding import into your project you can use it simply like: ## Props -| Name | Type | Required | Default | Description | -| ---------- | ------------------ | -------- | ------- | ----------- | -| id | `string \| number` | | | | -| children | `React.ReactNode` | | | | -| actions | `React.ReactNode` | | | | -| expanded | `boolean` | | | | -| expandable | `boolean` | | | | -| onExpand | `Common.Callback` | | | | -| header | `React.ReactNode` | | | | -| footer | `React.ReactNode` | | | | -| dataTest | `string` | | | | +| Name | Type | Required | Default | Description | +| ----------------- | ------------------ | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| id | `string \| number` | | | | +| children | `React.ReactNode` | | | | +| actions | `React.ReactNode` | | | | +| expanded | `boolean` | | | | +| expandable | `boolean` | | | | +| expandOnTileClick | `boolean` | | `false` | If true, enables mobile-first interaction where the entire header area becomes clickable for expansion. The action button serves as a visual indicator only. | +| onExpand | `Common.Callback` | | | | +| header | `React.ReactNode` | | | | +| footer | `React.ReactNode` | | | | +| dataTest | `string` | | | | ### Callback diff --git a/packages/orbit-components/src/Accordion/__tests__/index.test.tsx b/packages/orbit-components/src/Accordion/__tests__/index.test.tsx index 3508112c6c..44e1d4cc1b 100644 --- a/packages/orbit-components/src/Accordion/__tests__/index.test.tsx +++ b/packages/orbit-components/src/Accordion/__tests__/index.test.tsx @@ -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"; @@ -35,7 +35,7 @@ describe(`Accordion`, () => { expect(screen.getByTestId(`${sectionDataTest}Loading`)).toBeInTheDocument(); }); - describe(`AccordionSection`, () => { + describe("AccordionSection", () => { it("should render passed components", () => { render( { 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( + + + , + ); + + 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( + + + , + ); + + 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( + + + , + ); + + const button = screen.getByRole("button", { name: "Open" }); + + await waitFor(() => { + fireEvent.click(button); + }); + + expect(expandHandler).toHaveBeenCalledWith(sectionId); + }); + }); }); });