Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Disclosure Component #404

Merged
merged 2 commits into from
Oct 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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" },
};