Skip to content

Commit

Permalink
feat: EPMDW-3466 - Allow additional clickable behavior from Card comp…
Browse files Browse the repository at this point in the history
…onent (#454)

Also fix cursor when onClick and card is clickable
  • Loading branch information
athill committed Sep 3, 2024
1 parent 7a3d7ec commit 06ebe73
Show file tree
Hide file tree
Showing 3 changed files with 174 additions and 55 deletions.
19 changes: 17 additions & 2 deletions src/components/PageContent/Card/Card.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ Copyright (C) 2018 The Trustees of Indiana University
SPDX-License-Identifier: BSD-3-Clause
*/
import classNames from "classnames";
import { Button } from "../../Button";
import * as PropTypes from "prop-types";
import * as React from "react";
import * as Rivet from "../../util/Rivet";
Expand All @@ -16,6 +17,7 @@ const Card = ({
eyebrow,
horizontal = false,
image,
onClick,
meta,
raised = false,
testMode = false,
Expand All @@ -34,6 +36,8 @@ const Card = ({
return (
<Component
className={classNames(classNameArr)}
onClick={clickable && onClick ? onClick : null}
style={clickable && onClick ? { cursor: "pointer" } : null}
{...(testMode && { "data-testid": TestUtils.Card.container })}
{...attrs}
>
Expand All @@ -44,8 +48,17 @@ const Card = ({
className="rvt-card__title"
{...(testMode && { "data-testid": TestUtils.Card.title })}
>
{titleUrl && <a href={titleUrl}>{title}</a>}
{!titleUrl && <span>{title}</span>}
{titleUrl ? (
<a href={titleUrl} onClick={onClick}>
{title}
</a>
) : onClick ? (
<Button onClick={onClick} variant="plain">
{title}
</Button>
) : (
<span>{title}</span>
)}
</h2>
<div
className="rvt-card__content"
Expand Down Expand Up @@ -73,6 +86,8 @@ Card.propTypes = {
image: PropTypes.element,
/** Optional meta information */
meta: PropTypes.node,
/** Optional onClick function (applied to title or to entire card if clickable) */
onClick: PropTypes.func,
/** Add a box shadow to the card */
raised: PropTypes.bool,
/** [Developer] Adds data-testId attributes for component testing */
Expand Down
20 changes: 20 additions & 0 deletions src/components/PageContent/Card/Card.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ View the [Rivet documentation for Card](https://rivet.uits.iu.edu/components/car
</Card>
```
<!-- prettier-ignore-end -->

<!-- prettier-ignore-start -->
```jsx
const image = <img class="rvt-width-sm" src="https://rivet.iu.edu/img/placeholder/list-card-3.webp" alt="Smiling students sitting outside on a bench" />;
Expand All @@ -46,6 +47,7 @@ View the [Rivet documentation for Card](https://rivet.uits.iu.edu/components/car
</Card>
```
<!-- prettier-ignore-end -->

<!-- prettier-ignore-start -->
```jsx
const image = <img class="rvt-width-sm" src="https://rivet.iu.edu/img/placeholder/list-card-3.webp" alt="Smiling students sitting outside on a bench" />;
Expand All @@ -62,6 +64,7 @@ View the [Rivet documentation for Card](https://rivet.uits.iu.edu/components/car
</Card>
```
<!-- prettier-ignore-end -->

<!-- prettier-ignore-start -->
```jsx
const image = <img class="rvt-width-sm" src="https://rivet.iu.edu/img/placeholder/list-card-3.webp" alt="Smiling students sitting outside on a bench" />;
Expand All @@ -77,6 +80,22 @@ View the [Rivet documentation for Card](https://rivet.uits.iu.edu/components/car
</Card>
```
<!-- prettier-ignore-end -->

<!-- prettier-ignore-start -->
```jsx
const image = <img class="rvt-width-sm" src="https://rivet.iu.edu/img/placeholder/list-card-3.webp" alt="Smiling students sitting outside on a bench" />;
const meta = <time>November 5, 1955</time>;
<Card
image={image}
title="Card with onClick handler"
onClick={e => {e.preventDefault(); alert('clicked');}}
meta={meta}
>
<p>Using an onClick handler. This can be used with things such as react-router's navigate function.</p>
</Card>
```
<!-- prettier-ignore-end -->
<!-- prettier-ignore-start -->
```jsx
<ul class="rvt-list-plain rvt-flow">
Expand Down Expand Up @@ -110,6 +129,7 @@ View the [Rivet documentation for Card](https://rivet.uits.iu.edu/components/car
</ul>
```
<!-- prettier-ignore-end -->
<!-- prettier-ignore-start -->
```jsx
<div class="rvt-row">
Expand Down
190 changes: 137 additions & 53 deletions src/components/PageContent/Card/Card.test.jsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import "@testing-library/jest-dom";
import React from "react";
import Card from "./Card";
import { TestUtils } from "../../util/TestUtils";

const testIds = TestUtils.Card
const image = <img src="https://rivet.iu.edu/img/placeholder/billboard-2.webp" alt="Student in vintage-style Indiana University t-shirt" />
const imageStr = '<img src="https://rivet.iu.edu/img/placeholder/billboard-2.webp" alt="Student in vintage-style Indiana University t-shirt">'
const title = "test title"
const titleUrl = "/test"
const content = <p>Test content</p>
const contentStr = "<p>Test content</p>"
const customClassName = "custom-style"
const eyebrow = "test Eyebrow"
const meta = <time>November 5, 1955</time>
const metaStr = '<time>November 5, 1955</time>'
const testIds = TestUtils.Card;
const image = (
<img
src="https://rivet.iu.edu/img/placeholder/billboard-2.webp"
alt="Student in vintage-style Indiana University t-shirt"
/>
);
const imageStr =
'<img src="https://rivet.iu.edu/img/placeholder/billboard-2.webp" alt="Student in vintage-style Indiana University t-shirt">';
const title = "test title";
const titleUrl = "https://www.iu.edu/";
const content = <p>Test content</p>;
const contentStr = "<p>Test content</p>";
const customClassName = "custom-style";
const eyebrow = "test Eyebrow";
const meta = <time>November 5, 1955</time>;
const metaStr = "<time>November 5, 1955</time>";

describe("<Card />", () => {
describe("Rendering", () => {
Expand All @@ -32,12 +39,12 @@ describe("<Card />", () => {
{content}
</Card>
);
checkRenderContainer()
checkRenderTitle(titleUrl)
checkRenderContent()
checkRenderImage()
checkRenderMeta()
checkRenderEyebrow()
checkRenderContainer();
checkRenderTitle(titleUrl);
checkRenderContent();
checkRenderImage();
checkRenderMeta();
checkRenderEyebrow();
});
it("without title url, should render without throwing an error", () => {
render(
Expand All @@ -52,12 +59,12 @@ describe("<Card />", () => {
{content}
</Card>
);
checkRenderContainer()
checkRenderTitle()
checkRenderContent()
checkRenderImage()
checkRenderMeta()
checkRenderEyebrow()
checkRenderContainer();
checkRenderTitle();
checkRenderContent();
checkRenderImage();
checkRenderMeta();
checkRenderEyebrow();
});
it("without eyebrow, should render without throwing an error", () => {
render(
Expand All @@ -71,11 +78,11 @@ describe("<Card />", () => {
{content}
</Card>
);
checkRenderContainer()
checkRenderTitle()
checkRenderContent()
checkRenderImage()
checkRenderMeta()
checkRenderContainer();
checkRenderTitle();
checkRenderContent();
checkRenderImage();
checkRenderMeta();
});
it("without image, should render without throwing an error", () => {
render(
Expand All @@ -89,11 +96,11 @@ describe("<Card />", () => {
{content}
</Card>
);
checkRenderContainer()
checkRenderTitle()
checkRenderContent()
checkRenderMeta()
checkRenderEyebrow()
checkRenderContainer();
checkRenderTitle();
checkRenderContent();
checkRenderMeta();
checkRenderEyebrow();
});
it("without meta, should render without throwing an error", () => {
render(
Expand All @@ -107,14 +114,92 @@ describe("<Card />", () => {
{content}
</Card>
);
checkRenderContainer()
checkRenderTitle()
checkRenderContent()
checkRenderImage()
checkRenderEyebrow()
checkRenderContainer();
checkRenderTitle();
checkRenderContent();
checkRenderImage();
checkRenderEyebrow();
});
});
describe("Options", () => {
it("should activate onClick handler on link if titleUrl is also provided", async () => {
const onClick = jest.fn();
const user = userEvent.setup();
render(
<Card
className={customClassName}
onClick={onClick}
titleUrl={titleUrl}
testMode
title={title}
>
{content}
</Card>
);

const element = screen.queryByTestId(testIds.container);
// card is not clickable, so clicking on it should not trigger onClick
await user.click(element);

expect(onClick.mock.calls.length).toBe(0);

const link = element.getElementsByTagName("a")[0];
expect(link.href).toBe(titleUrl);

await user.click(link);

expect(onClick.mock.calls.length).toBe(1);
});

it("should activate onClick handler on button if titleUrl is not provided", async () => {
const onClick = jest.fn();
const user = userEvent.setup();
render(
<Card
className={customClassName}
onClick={onClick}
testMode
title={title}
>
{content}
</Card>
);

const element = screen.queryByTestId(testIds.container);
// card is not clickable, so clicking on it should not trigger onClick
await user.click(element);

expect(onClick.mock.calls.length).toBe(0);

const button = element.getElementsByTagName("button")[0];

await user.click(button);

expect(onClick.mock.calls.length).toBe(1);
});

it("should activate onClick on the entire card if clickable is present", async () => {
const onClick = jest.fn();
const user = userEvent.setup();
render(
<Card
clickable
className={customClassName}
onClick={onClick}
testMode
title={title}
>
{content}
</Card>
);

const element = screen.queryByTestId(testIds.container);
// card is clickable, so clicking on it should trigger onClick
await user.click(element);

expect(onClick.mock.calls.length).toBe(1);
});

it("clickable on, should render as clickable", () => {
render(
<Card
Expand Down Expand Up @@ -181,7 +266,7 @@ describe("<Card />", () => {
</Card>
);
const element = screen.queryByTestId(testIds.container);
expect(element.nodeName).toBe("LI")
expect(element.nodeName).toBe("LI");
});
it("default is test mode off", () => {
render(
Expand All @@ -204,52 +289,51 @@ describe("<Card />", () => {
const checkRenderContainer = () => {
const elemet = screen.getByTestId(testIds.container);
expect(elemet).toBeVisible();
expect(elemet.nodeName).toBe('DIV');
expect(elemet.nodeName).toBe("DIV");
expect(elemet).toHaveClass("rvt-card");
expect(elemet).toHaveClass(customClassName);

}
};

const checkRenderTitle = (url) => {
const element = screen.getByTestId(testIds.title);
expect(element.nodeName).toBe('H2');
expect(element.nodeName).toBe("H2");
expect(element).toBeVisible();
expect(element).toHaveClass("rvt-card__title");
expect(element.children.length).toBe(1)
const content = element.children[0]
expect(element.children.length).toBe(1);
const content = element.children[0];
expect(content.innerHTML).toBe(title);
if(url) {
expect(content.nodeName).toBe('A');
if (url) {
expect(content.nodeName).toBe("A");
expect(content).toHaveAttribute("href", url);
} else {
expect(content.nodeName).toBe('SPAN');
expect(content.nodeName).toBe("SPAN");
}
}
};

const checkRenderContent = () => {
const elemet = screen.getByTestId(testIds.content);
expect(elemet).toBeVisible();
expect(elemet).toHaveClass("rvt-card__content");
expect(elemet.innerHTML).toBe(contentStr);
}
};

const checkRenderImage = () => {
const elemet = screen.getByTestId(testIds.image);
expect(elemet).toBeVisible();
expect(elemet).toHaveClass("rvt-card__image");
expect(elemet.innerHTML).toBe(imageStr);
}
};

const checkRenderEyebrow = () => {
const elemet = screen.getByTestId(testIds.eyebrow);
expect(elemet).toBeVisible();
expect(elemet).toHaveClass("rvt-card__eyebrow");
expect(elemet.innerHTML).toBe(eyebrow);
}
};

const checkRenderMeta = () => {
const elemet = screen.getByTestId(testIds.meta);
expect(elemet).toBeVisible();
expect(elemet).toHaveClass("rvt-card__meta");
expect(elemet.innerHTML).toBe(metaStr);
}
};

0 comments on commit 06ebe73

Please sign in to comment.