Skip to content

Commit

Permalink
Accordion (#1720)
Browse files Browse the repository at this point in the history
  • Loading branch information
joshwooding authored Jul 14, 2023
1 parent 79e9f4b commit 27139c5
Show file tree
Hide file tree
Showing 36 changed files with 1,107 additions and 1,365 deletions.

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,8 @@
"webpack-dev-server": "4.9.3",
"@changesets/assemble-release-plan@^5.2.2": "patch:@changesets/assemble-release-plan@npm%3A5.2.2#./.yarn/patches/@changesets-assemble-release-plan-npm-5.2.2-11f5894b70.patch",
"rollup": "2.79.1",
"@salt-ds/lab": "workspace:*"
"@salt-ds/lab": "workspace:*",
"@jpmorganchase/mosaic-components": "patch:@jpmorganchase/mosaic-components@npm%3A0.1.0-beta.37#./.yarn/patches/@jpmorganchase-mosaic-components-npm-0.1.0-beta.37-6d8686e3dc.patch"
},
"browserslist": {
"production": [
Expand Down
294 changes: 90 additions & 204 deletions packages/lab/src/__tests__/__e2e__/accordion/Accordion.cy.tsx
Original file line number Diff line number Diff line change
@@ -1,234 +1,120 @@
import {
AccordionPanel,
Accordion,
AccordionDetails,
AccordionProps,
AccordionSection,
AccordionSummary,
AccordionHeader,
} from "@salt-ds/lab";
import { useReducer, useState } from "react";
import { Component, ReactNode } from "react";

interface DetailsSpyProps {
children?: ReactNode;
onMount?: () => void;
onUnmount?: () => void;
onUpdated?: () => void;
}

class DetailsSpy extends Component<DetailsSpyProps> {
render() {
return <p>Detailed text</p>;
}

const AccordionExample = (props: AccordionProps) => {
return (
<Accordion {...props}>
<AccordionSection id="section-0" key="AccordionSection0">
<AccordionSummary>AccordionSummary0</AccordionSummary>
<AccordionDetails>AccordionDetails0</AccordionDetails>
</AccordionSection>
<AccordionSection id="section-1" key="AccordionSection1">
<AccordionSummary>AccordionSummary1</AccordionSummary>
<AccordionDetails>AccordionDetails1</AccordionDetails>
</AccordionSection>
<AccordionSection id="section-2" key="AccordionSection2">
<AccordionSummary>AccordionSummary2</AccordionSummary>
<AccordionDetails>AccordionDetails2</AccordionDetails>
</AccordionSection>
</Accordion>
);
};
componentDidMount() {
this.props.onMount?.();
}

componentDidUpdate() {
this.props.onUpdated?.();
}

const ControlledAccordionExample = (props: AccordionProps) => {
const { expandedSectionIds, onChange, ...rest } = props;
const [expanded, setExpanded] = useState(expandedSectionIds);
componentWillUnmount() {
this.props.onUnmount?.();
}
}

const handleChange = (ids: string[] | null) => {
setExpanded(ids ?? []);
onChange?.(ids);
};
type AccordionExampleProps = Pick<AccordionProps, "onToggle"> & DetailsSpyProps;

const AccordionExample = ({
onToggle,
onMount,
onUnmount,
onUpdated,
}: AccordionExampleProps) => {
return (
<Accordion expandedSectionIds={expanded} onChange={handleChange} {...rest}>
<AccordionSection id="section-0" key="AccordionSection0">
<AccordionSummary>AccordionSummary0</AccordionSummary>
<AccordionDetails>AccordionDetails0</AccordionDetails>
</AccordionSection>
<AccordionSection id="section-1" key="AccordionSection1">
<AccordionSummary>AccordionSummary1</AccordionSummary>
<AccordionDetails>AccordionDetails1</AccordionDetails>
</AccordionSection>
<AccordionSection id="section-2" key="AccordionSection2">
<AccordionSummary>AccordionSummary2</AccordionSummary>
<AccordionDetails>AccordionDetails2</AccordionDetails>
</AccordionSection>
<Accordion onToggle={onToggle} value="example">
<AccordionHeader>Summary Text</AccordionHeader>
<AccordionPanel>
<DetailsSpy
onMount={onMount}
onUnmount={onUnmount}
onUpdated={onUpdated}
>
<div data-testid="details-content" />
</DetailsSpy>
</AccordionPanel>
</Accordion>
);
};

const expectExpandedSections = (expected: number[]) => {
const s = new Set(expected);
for (let i of Array(3).keys()) {
cy.findByRole("button", { name: `AccordionSummary${i}` }).should(
"have.attr",
"aria-expanded",
s.has(i) ? "true" : "false"
);
}
};

const expectOnChangeLastCalled = (expected: number[]) => {
cy.get("@changeSpy").should(
"have.been.calledWith",
expected.map((index) => `section-${index}`)
);
};

describe("GIVEN an Accordion", () => {
describe("WHEN it is used in uncontrolled mode", () => {
describe("WHEN user expands up to maxExpandedItems sections", () => {
it("THEN sections should expand", () => {
const changeSpy = cy.stub().as("changeSpy");
cy.mount(
<AccordionExample onChange={changeSpy} maxExpandedItems={2} />
);
cy.findByRole("button", { name: "AccordionSummary0" }).realClick();
cy.findByRole("button", { name: "AccordionSummary1" }).realClick();
it("THEN it should render in collapsed state", () => {
cy.mount(<AccordionExample />);

cy.findByRole("button").should("have.attr", "aria-expanded", "false");
});

cy.get("@changeSpy").should("have.been.calledTwice");
it("THEN it should render the details", () => {
const mountSpy = cy.stub().as("mountSpy");
cy.mount(<AccordionExample onMount={mountSpy} />);
cy.findByRole("button").realClick();

expectExpandedSections([0, 1]);
cy.get("@mountSpy").should("have.been.calledOnce");
});

expectOnChangeLastCalled([0, 1]);
});
describe("AND WHEN the summary is clicked", () => {
it("THEN should render in expanded state", () => {
const toggleSpy = cy.stub().as("toggleSpy");
cy.mount(<AccordionExample onToggle={toggleSpy} />);

describe("AND WHEN user keeps opening more sections", () => {
it("THEN panels should begin collapsing starting from the ones opened first", () => {
const changeSpy = cy.stub().as("changeSpy");
cy.mount(
<AccordionExample onChange={changeSpy} maxExpandedItems={2} />
);
cy.findByRole("button", { name: "AccordionSummary0" }).realClick();
cy.findByRole("button", { name: "AccordionSummary1" }).realClick();
cy.findByRole("button", { name: "AccordionSummary2" }).realClick();

expectExpandedSections([1, 2]);
expectOnChangeLastCalled([1, 2]);
});
});
cy.findByRole("button").realClick();
cy.findByRole("button").should("have.attr", "aria-expanded", "true");

describe("AND WHEN user changes maxExpandedItems to a lower number", () => {
function DynamicMaxExpandedItemsExample(props: AccordionProps) {
const [isToggled, toggle] = useReducer((state) => {
return !state;
}, false);
return (
<>
<button onClick={toggle}>Toggle Max Expanded Items</button>
<Accordion {...props} maxExpandedItems={isToggled ? 1 : 2}>
<AccordionSection id="section-0" key="AccordionSection0">
<AccordionSummary>AccordionSummary0</AccordionSummary>
<AccordionDetails>AccordionDetails0</AccordionDetails>
</AccordionSection>
<AccordionSection id="section-1" key="AccordionSection1">
<AccordionSummary>AccordionSummary1</AccordionSummary>
<AccordionDetails>AccordionDetails1</AccordionDetails>
</AccordionSection>
<AccordionSection id="section-2" key="AccordionSection2">
<AccordionSummary>AccordionSummary2</AccordionSummary>
<AccordionDetails>AccordionDetails2</AccordionDetails>
</AccordionSection>
</Accordion>
</>
);
}
it("THEN oldest panels should close", () => {
const changeSpy = cy.stub().as("changeSpy");
cy.mount(<DynamicMaxExpandedItemsExample onChange={changeSpy} />);
cy.findByRole("button", { name: "AccordionSummary0" }).realClick();
cy.findByRole("button", { name: "AccordionSummary1" }).realClick();

cy.findByRole("button", {
name: "Toggle Max Expanded Items",
}).realClick();

expectExpandedSections([1]);
expectOnChangeLastCalled([1]);
});
});
cy.get("@toggleSpy").should("have.been.calledOnce");
});
});

describe("WHEN it is used in controlled mode", () => {
it("THEN sections with specified IDs should be expanded", () => {
const changeSpy = cy.stub().as("changeSpy");
cy.mount(
<ControlledAccordionExample
expandedSectionIds={["section-0", "section-2"]}
onChange={changeSpy}
/>
);
expectExpandedSections([0, 2]);
cy.get("@changeSpy").should("not.have.been.called");
it("THEN should not remount the details", () => {
const mountSpy = cy.stub().as("mountSpy");
const unmountSpy = cy.stub().as("unmountSpy");
cy.mount(<AccordionExample onMount={mountSpy} onUnmount={unmountSpy} />);

cy.findByRole("button").realClick();
cy.findByRole("button").should("have.attr", "aria-expanded", "true");

cy.get("@mountSpy").should("have.been.calledOnce");
cy.get("@unmountSpy").should("not.have.been.called");
});

describe("THEN user expands sections", () => {
it("THEN onChange event should be raised but expanded actions remain the same", () => {
const changeSpy = cy.stub().as("changeSpy");
cy.mount(
<AccordionExample
expandedSectionIds={["section-0", "section-2"]}
onChange={changeSpy}
/>
);
cy.findByRole("button", { name: "AccordionSummary1" }).realClick();
cy.get("@changeSpy").should("have.been.calledOnce");
expectExpandedSections([0, 2]);
});
describe("AND WHEN the summary is clicked again", () => {
it("THEN should render is collapsed state", () => {
const toggleSpy = cy.stub().as("toggleSpy");
cy.mount(<AccordionExample onToggle={toggleSpy} />);

cy.findByRole("button").realClick();
cy.findByRole("button").realClick();

describe("AND WHEN expandedSectionIds prop changes", () => {
it("THEN expanded sections should change", () => {
cy.mount(
<ControlledAccordionExample
expandedSectionIds={["section-0", "section-2"]}
/>
);
cy.findByRole("button", { name: "AccordionSummary1" }).realClick();

expectExpandedSections([0, 1, 2]);
});
cy.findByRole("button").should("have.attr", "aria-expanded", "false");

cy.get("@toggleSpy").should("have.been.calledTwice");
});
});
});

describe("WHEN expanded prop is set directly on accordion sections", () => {
it("THEN sections with expanded property set to true should be expanded", () => {
cy.mount(
<Accordion>
<AccordionSection id="section-0" key="AccordionSection0">
<AccordionSummary>AccordionSummary0</AccordionSummary>
<AccordionDetails>AccordionDetails0</AccordionDetails>
</AccordionSection>
<AccordionSection id="section-1" key="AccordionSection1" expanded>
<AccordionSummary>AccordionSummary1</AccordionSummary>
<AccordionDetails>AccordionDetails1</AccordionDetails>
</AccordionSection>
<AccordionSection id="section-2" key="AccordionSection2">
<AccordionSummary>AccordionSummary2</AccordionSummary>
<AccordionDetails>AccordionDetails2</AccordionDetails>
</AccordionSection>
</Accordion>
);
expectExpandedSections([1]);
});
it("THEN should keep the details mounted", () => {
const unmountSpy = cy.stub().as("unmountSpy");
cy.mount(<AccordionExample onUnmount={unmountSpy} />);

// TODO Should this work?
it.skip("THEN expanded property set on sections should have priority over accordion's properties", () => {
cy.mount(
<Accordion expandedSectionIds={["section-0", "section-1"]}>
<AccordionSection id="section-0" key="AccordionSection0">
<AccordionSummary>AccordionSummary0</AccordionSummary>
<AccordionDetails>AccordionDetails0</AccordionDetails>
</AccordionSection>
<AccordionSection id="section-1" key="AccordionSection1">
<AccordionSummary>AccordionSummary1</AccordionSummary>
<AccordionDetails>AccordionDetails1</AccordionDetails>
</AccordionSection>
<AccordionSection id="section-2" key="AccordionSection2" expanded>
<AccordionSummary>AccordionSummary2</AccordionSummary>
<AccordionDetails>AccordionDetails2</AccordionDetails>
</AccordionSection>
</Accordion>
);

expectExpandedSections([2]);
cy.findByRole("button").realClick();
cy.findByRole("button").realClick();

cy.get("@unmountSpy").should("not.have.been.called");
});
});
});
});
Loading

1 comment on commit 27139c5

@vercel
Copy link

@vercel vercel bot commented on 27139c5 Jul 14, 2023

Choose a reason for hiding this comment

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

Please sign in to comment.