Skip to content

Commit

Permalink
feat: Disclosure Component (#404)
Browse files Browse the repository at this point in the history
* 373-disclosure: Disclosure Component

* 373-disclosure: Fix toggle and add test
  • Loading branch information
pcberg authored Oct 9, 2023
1 parent 5ef8a19 commit 5b18dd8
Show file tree
Hide file tree
Showing 5 changed files with 255 additions and 0 deletions.
115 changes: 115 additions & 0 deletions src/components/PageContent/Disclosure/Disclosure.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/*
Copyright (C) 2018 The Trustees of Indiana University
SPDX-License-Identifier: BSD-3-Clause
*/
import classNames from "classnames";
import * as PropTypes from "prop-types";
import * as React from "react";
import { createRef, useEffect, useState } from "react";
import * as Rivet from "../../util/Rivet";
import {
handler,
isKeyEvent,
isRightMouseClick,
isTabKeyPress,
targets,
} from "../../util/EventUtils.js";
import { TestUtils } from "../../util/TestUtils.js";

const disclosureClass = "rvt-disclosure";

const Disclosure = ({
children,
className,
closeClickOutside,
id = Rivet.shortuid(),
isOpen,
title,
...attrs
}) => {
const [isOpenState, setIsOpenState] = useState(isOpen);

useEffect(() => {
handleEventRegistration();
return () => {
eventHandler.deregister();
};
});

const handleClickOutside = (event) => {
if (event && shouldToggleDisclosure(event)) {
toggleDisclosure(event);
}
};

const disclosureWrapDiv = createRef();
const eventHandler = handler(handleClickOutside);

const toggleDisclosure = (event) => {
setIsOpenState(!isOpenState);
// if there is a stopPropagation method on the event we need to call it to prevent additional events from firing
event.stopPropagation && event.stopPropagation();
};

const shouldToggleDisclosure = (event) => {
if (isRightMouseClick(event) || isKeyEvent(event)) {
// If the user right clicks anywhere on the screen or they press an unhandled key do not close the menu
return false;
} else if (
targets(disclosureWrapDiv.current, event) &&
(!isKeyEvent(event) || isTabKeyPress(event))
) {
// If the user clicks, touches or tabs inside the disclosure do not close the menu
return false;
}

return true;
};

const handleEventRegistration = () => {
if (isOpenState && closeClickOutside) {
eventHandler.register();
} else {
eventHandler.deregister();
}
};

return (
<div
id={id}
className={classNames(disclosureClass, className)}
ref={disclosureWrapDiv}
{...attrs}
>
<button
className="rvt-disclosure__toggle"
onClick={toggleDisclosure}
aria-expanded={isOpenState ? "true" : "false"}
>
{title}
</button>
{isOpenState && (
<div
className="rvt-disclosure__content"
data-testid={TestUtils.Disclosure.testId}
>
{children}
</div>
)}
</div>
);
};

Disclosure.displayName = "Disclosure";
Disclosure.propTypes = {
/** Determines whether the Disclosure closes when the user clicks anywhere outside of it */
closeClickOutside: PropTypes.bool,
/** A unique identifier for the badge */
id: PropTypes.string,
/** Determines whether the Disclosure is open or not */
isOpen: PropTypes.bool,
/** The content of the Disclosure button */
title: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
};

export default Rivet.rivetize(Disclosure);
15 changes: 15 additions & 0 deletions src/components/PageContent/Disclosure/Disclosure.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
Use the disclosure component to allow the user to show or hide additional content about a topic.

View the [Rivet documentation for Disclosure](https://rivet.iu.edu/components/disclosure/).

### Disclosure Examples

<!-- prettier-ignore-start -->
```jsx
<Disclosure title="Take a look at the numbers" closeClickOutside={true}>
<div class="rvt-prose rvt-flow">
<p>Tuition and fees vary at each Indiana University campus. As you look at total costs, keep in mind that financial aid, scholarships and awards, a part-time job, and student loans can all factor into what you will pay for your degree.</p>
</div>
</Disclosure>
```
<!-- prettier-ignore-end -->
123 changes: 123 additions & 0 deletions src/components/PageContent/Disclosure/Disclosure.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { fireEvent, render, screen } from "@testing-library/react";
import "@testing-library/jest-dom";
import React from "react";
import Disclosure from "./Disclosure";
import { TestUtils } from "../../util/TestUtils.js";
import userEvent from "@testing-library/user-event";

const user = userEvent.setup();

describe("<Disclosure />", () => {
const disclosureTestId = "test-disclosure";
const title = "Test Title";
const child = "Test Child";

describe("Rendering", () => {
it("should render without throwing an error", () => {
render(
<Disclosure data-testid={disclosureTestId} title={title}>
{child}
</Disclosure>
);
const disclosure = screen.getByTestId(disclosureTestId, {});
expect(disclosure).toBeVisible();
expect(disclosure).toHaveClass("rvt-disclosure");

const button = screen.getByRole("button", {});
expect(button).toBeVisible();
expect(button).toHaveClass("rvt-disclosure__toggle");

expect(
screen.queryByTestId(TestUtils.Disclosure.testId, {})
).not.toBeInTheDocument();
});
});

describe("Rendering with isOpen", () => {
it("should have visible content with isOpen", async () => {
render(
<Disclosure title={title} isOpen={true}>
{child}
</Disclosure>
);

const children = screen.queryByTestId(TestUtils.Disclosure.testId, {});
expect(children).toBeVisible();
expect(children).toHaveClass("rvt-disclosure__content");
});
});

describe("Toggle behavior", () => {
it("should have a button that toggles visibility", async () => {
render(<Disclosure title={title}>{child}</Disclosure>);

await user.click(screen.getByRole("button", {}));
const button = screen.getByRole("button", {});
const children = screen.queryByTestId(TestUtils.Disclosure.testId, {});
expect(children).toBeVisible();
expect(children).toHaveClass("rvt-disclosure__content");
expect(button).toHaveAttribute("aria-expanded", "true");
expect(screen.queryByText(child, {})).toBeVisible();

await user.click(screen.getByRole("button", {}));
expect(button).toHaveAttribute("aria-expanded", "false");
expect(
screen.queryByTestId(TestUtils.Disclosure.testId, {})
).not.toBeInTheDocument();
expect(screen.queryByText(child, {})).not.toBeInTheDocument();
});
});

describe("Toggle closeClickOutside behavior", () => {
it("clicking outside should not close the Disclosure when closeClickOutside=false", async () => {
render(
<React.Fragment>
<div data-testid={disclosureTestId}>Outside element</div>
<Disclosure title={title} isOpen={true}>
{child}
</Disclosure>
</React.Fragment>
);

expect(screen.queryByText(child, {})).toBeVisible();
await user.click(screen.getByTestId(disclosureTestId, {}));
expect(screen.queryByText(child, {})).toBeVisible();
});

it("clicking outside should close the Disclosure when closeClickOutside=true", async () => {
render(
<React.Fragment>
<div data-testid={disclosureTestId}>Outside element</div>
<Disclosure title={title} isOpen={true} closeClickOutside={true}>
{child}
</Disclosure>
</React.Fragment>
);

expect(screen.queryByText(child, {})).toBeVisible();
await user.click(screen.getByTestId(disclosureTestId, {}));
expect(screen.queryByText(child, {})).not.toBeInTheDocument();
});

it("clicking inside or a button should not close the Disclosure when closeClickOutside=true", async () => {
render(
<React.Fragment>
<div data-testid={disclosureTestId}>Outside element</div>
<Disclosure title={title} isOpen={true} closeClickOutside={true}>
{child}
</Disclosure>
</React.Fragment>
);

expect(screen.queryByText(child, {})).toBeVisible();
await user.click(screen.queryByText(child, {}));
fireEvent.keyUp(document.body, {
key: "Escape",
});
fireEvent.keyUp(document.body, {
key: "Tab",
});
expect(screen.queryByText(child, {})).toBeVisible();
});
});
});
1 change: 1 addition & 0 deletions src/components/PageContent/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ Copyright (C) 2018 The Trustees of Indiana University
SPDX-License-Identifier: BSD-3-Clause
*/
export { default as Badge } from "./Badge/Badge";
export { default as Disclosure } from "./Disclosure/Disclosure";
1 change: 1 addition & 0 deletions src/components/util/TestUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,5 @@ export const TestUtils = {
RadioButton: { testId: "testId" },
Table: { testId: "testId" },
Footer: { testId: "testId" },
Disclosure: { testId: "disclosure__testId" },
};

0 comments on commit 5b18dd8

Please sign in to comment.