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: add Rivet 2.x alerts #304

Merged
merged 9 commits into from
Nov 9, 2022
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
24 changes: 12 additions & 12 deletions babel.config.cjs
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
module.exports = {
presets: [
[
'@babel/env',
{
modules: false,
useBuiltIns: 'usage',
corejs: '3.25.5'
}
],
'@babel/react',
]
}
presets: [
[
"@babel/env",
{
modules: "auto",
useBuiltIns: "usage",
corejs: "3.25.5",
},
],
"@babel/react",
],
};
3 changes: 3 additions & 0 deletions jest.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
testEnvironment: "jsdom",
};
9,776 changes: 2,635 additions & 7,141 deletions package-lock.json

Large diffs are not rendered by default.

9 changes: 7 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,37 +17,42 @@
"scripts": {
"start": "styleguidist server --config styleguide.config.cjs",
"dev": "vite",
"test": "NODE_ENV=test jest",
"build": "vite build",
"preview": "vite preview",
"prepare": "husky install",
"pretty-quick": "pretty-quick"
},
"dependencies": {
"classnames": "2.3.1",
"rivet-core": "2.1.0"
},
"peerDependencies": {
"classnames": "2.3.1",
"react": ">= 16",
"react-dom": ">= 16"
},
"devDependencies": {
"@babel/core": "7.19.3",
"@babel/preset-env": "7.19.4",
"@babel/preset-react": "7.18.6",
"@testing-library/jest-dom": "5.16.5",
"@testing-library/react": "13.4.0",
"@testing-library/user-event": "14.4.3",
"@types/react": "18.0.17",
"@types/react-dom": "18.0.6",
"@vitejs/plugin-react": "2.1.0",
"babel-loader": "8.2.5",
"core-js": "3.25.5",
"css-loader": "6.7.1",
"husky": "8.0.0",
"jest": "27.5.1",
"path": "0.12.7",
"prettier": "2.7.1",
"pretty-quick": "3.1.3",
"prop-types": "15.8.1",
"react-docgen": "5.4.3",
"react-scripts": "5.0.1",
"react-styleguidist": "11.2.0",
"react-styleguidist": "13.0.0",
"style-loader": "3.3.1",
"vite": "3.1.0",
"vite-plugin-externalize-deps": "0.1.5"
Expand Down
22 changes: 12 additions & 10 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import React, { useState } from 'react'
import './App.css'
import {Alert} from "./components/Alert"
import React, { useState } from "react";
import "./App.css";
import { Alert } from "./components/Alert";

function App() {
const [count, setCount] = useState(0)
const [count, setCount] = useState(0);

return (
<div className="App">
<Alert variant={"danger"}>Some alerts don't need a title. This is one of them.</Alert>
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
<Alert variant={"danger"}>
Some alerts don't need a title. This is one of them.
</Alert>
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
</div>
)
);
}

export default App
export default App;
125 changes: 78 additions & 47 deletions src/components/Alert/Alert.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,59 +2,90 @@
Copyright (C) 2018 The Trustees of Indiana University
SPDX-License-Identifier: BSD-3-Clause
*/
import classNames from 'classnames';
import * as React from 'react';
import * as PropTypes from 'prop-types';
import classNames from "classnames";
import * as React from "react";
import * as PropTypes from "prop-types";

import * as Rivet from '../util/Rivet';
import * as Rivet from "../util/Rivet";

/**
* Use the alert component to show brief important messages to the user like errors, action confirmations, or system status.
*/
const Alert = ({title, onDismiss, variant, isOpen = true, id = Rivet.shortuid(), className, children, ...attrs}) => {

const alertId = id;
const titleId = alertId + '-title';

const headerFragment = () => (<div class="rvt-alert__title" id={titleId}>{title}</div>);

const dismissFragment = () =>
onDismiss
? (
<button class="rvt-alert__dismiss" data-rvt-alert-close>
<span class="rvt-sr-only">Dismiss this alert</span>
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path fill="currentColor" d="M9.41,8l5.29-5.29a1,1,0,0,0-1.41-1.41L8,6.59,2.71,1.29A1,1,0,0,0,1.29,2.71L6.59,8,1.29,13.29a1,1,0,1,0,1.41,1.41L8,9.41l5.29,5.29a1,1,0,0,0,1.41-1.41Z" />
</svg>
</button>
)
: null;

const classes = classNames('rvt-alert', `rvt-alert--${variant}`, className);

const ariaProps = title ? {'aria-labelledby': titleId} : {};

return isOpen
? <div id={alertId} className={classes} role='alert' aria-labelledby={titleId} data-rvt-alert={variant} {...ariaProps} {...attrs}>
{headerFragment()}
<p className='rvt-alert__message'>{children}</p>
{dismissFragment()}
</div>
: null
}

Alert.displayName = 'Alert';
const Alert = ({
title,
onDismiss,
variant,
isOpen = true,
id = Rivet.shortuid(),
className,
children,
...attrs
}) => {
const alertId = id;
const titleId = alertId + "-title";

const headerFragment = () => (
<div className="rvt-alert__title" id={titleId}>
{title}
</div>
);

const dismissFragment = () =>
onDismiss ? (
<button
className="rvt-alert__dismiss"
data-rvt-alert-close
onClick={onDismiss}
>
<span className="rvt-sr-only">Dismiss this alert</span>
<svg
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 16 16"
>
<path
fill="currentColor"
d="M9.41,8l5.29-5.29a1,1,0,0,0-1.41-1.41L8,6.59,2.71,1.29A1,1,0,0,0,1.29,2.71L6.59,8,1.29,13.29a1,1,0,1,0,1.41,1.41L8,9.41l5.29,5.29a1,1,0,0,0,1.41-1.41Z"
/>
</svg>
</button>
) : null;

const classes = classNames("rvt-alert", `rvt-alert--${variant}`, className);

const ariaProps = title ? { "aria-labelledby": titleId } : {};

return isOpen ? (
<div
id={alertId}
className={classes}
role="alert"
aria-labelledby={titleId}
data-rvt-alert={variant}
{...ariaProps}
{...attrs}
>
{headerFragment()}
<p className="rvt-alert__message">{children}</p>
{dismissFragment()}
</div>
) : null;
};

Alert.displayName = "Alert";
Alert.propTypes = {
/** The variant type which determines how the alert is styled */
variant: PropTypes.oneOf(['info', 'success', 'warning', 'danger']).isRequired,
/** An extremely brief title for the alert */
title: PropTypes.oneOfType([PropTypes.string, PropTypes.element]).isRequired,
/** A unique identifier for the alert */
id: PropTypes.string,
/** A function that can be called to have side-effects when the alert is dismissed */
onDismiss: PropTypes.func,
/** Determines whether the alert is displayed or hidden */
isOpen: PropTypes.bool
/** A unique identifier for the alert */
id: PropTypes.string,
/** Determines whether the alert is displayed or hidden */
isOpen: PropTypes.bool,
/** A function that can be called to have side-effects when the alert dismissal button is selected */
onDismiss: PropTypes.func,
/** An extremely brief title for the alert */
title: PropTypes.oneOfType([PropTypes.string, PropTypes.element]).isRequired,
/** The variant type which determines how the alert is styled */
variant: PropTypes.oneOf(["info", "success", "warning", "danger"]).isRequired,
};

export default Rivet.rivetize(Alert);
119 changes: 119 additions & 0 deletions src/components/Alert/Alert.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*
Copyright (C) 2018 The Trustees of Indiana University
SPDX-License-Identifier: BSD-3-Clause
*/
import { render, screen } from "@testing-library/react";
import "@testing-library/jest-dom";
import user from "@testing-library/user-event";
import React from "react";

import Alert from "./Alert";

describe("<Alert />", () => {
const titleText = "A Test Component";

describe("Rendering and text", () => {
it("should render without throwing an error", () => {
render(<Alert title={titleText} variant="info" />);
expect(screen.getByRole("alert")).toHaveTextContent(titleText);
});
it("should include alert text", () => {
const bodyText = "This is the message.";
render(
<Alert title={titleText} variant="info">
{bodyText}
</Alert>
);
expect(screen.getByRole("alert")).toHaveTextContent(bodyText);
});
it("should apply the id", () => {
const testId = "the_id";
const cut = render(
<Alert title={titleText} variant="info" id={testId} />
);
expect(screen.getByRole("alert")).toHaveProperty("id", testId);
});
});

describe("Styling", () => {
it("should specify style: error", () => {
render(<Alert title={titleText} variant="danger" />);
expect(
screen
.getByRole("alert")
.firstChild.classList.contains("rvt-alert--danger")
);
});
it("should specify style: info", () => {
render(<Alert title={titleText} variant="info" />);
expect(
screen
.getByRole("alert")
.firstChild.classList.contains("rvt-alert--info")
);
});
it("should specify style: warning", () => {
render(<Alert title={titleText} variant="warning" />);
expect(
screen
.getByRole("alert")
.firstChild.classList.contains("rvt-alert--warning")
);
});
it("should specify style: success", () => {
render(<Alert title={titleText} variant="success" />);
expect(
screen
.getByRole("alert")
.firstChild.classList.contains("rvt-alert--success")
);
});
it("should apply custom style", () => {
render(
<Alert title={titleText} variant="info" className="rvt-alert--custom" />
);
expect(
screen
.getByRole("alert")
.firstChild.classList.contains("rvt-alert--custom")
);
});
});

describe("Visbility", () => {
it("is visible by default", () => {
render(<Alert title={titleText} variant="success" />);
expect(screen.getByRole("alert")).toBeVisible();
});
it("can be made visible", () => {
render(<Alert title={titleText} variant="success" isOpen />);
expect(screen.getByRole("alert")).toBeVisible();
});
it("can be made invisible", () => {
render(<Alert title={titleText} variant="success" isOpen={false} />);
expect(screen.queryAllByRole("alert")).toHaveLength(0);
});
});

describe("Dismiss behavior", () => {
it("should include dismiss button when dismissible", () => {
render(<Alert title={titleText} variant="info" onDismiss={() => {}} />);
expect(screen.getByRole("button")).toBeVisible();
});
it("should fire dismiss delegate", async () => {
let fired = false;
const delegate = () => (fired = true);
render(<Alert title={titleText} variant="info" onDismiss={delegate} />);
await user.click(screen.getByRole("button"));

expect(fired).toEqual(true);
});
it("the alert should remain visible when dismiss button clicked", async () => {
render(<Alert title={titleText} variant="info" onDismiss={() => {}} />);
expect(screen.getByRole("alert")).toBeVisible();
await user.click(screen.getByRole("button"));

expect(screen.getByRole("alert")).toBeVisible();
});
});
});
Loading