Skip to content
This repository was archived by the owner on Oct 4, 2022. It is now read-only.

Add an Alert component #349

Merged
merged 14 commits into from
Sep 10, 2019
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
37 changes: 32 additions & 5 deletions apps/components/ComponentsExample.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React from "react";
import styled from "styled-components";
import { __ } from "@wordpress/i18n";

import { CourseDetails, FullHeightCard, Warning } from "@yoast/components";
import { getDirectionalStyle, getCourseFeed, makeOutboundLink } from "@yoast/helpers";
import { Alert, CourseDetails, FullHeightCard, Warning } from "@yoast/components";
import { getCourseFeed, getDirectionalStyle, makeOutboundLink } from "@yoast/helpers";
import React from "react";
import styled from "styled-components";

const Container = styled.div`
max-width: 1024px;
Expand Down Expand Up @@ -65,9 +65,13 @@ export default class ComponentsExample extends React.Component {

this.state = {
courses: null,
isAlertDismissed: false,
};

this.getFeed( "free" );
this.onAlertDismissed = () => {
this.setState( { isAlertDismissed: true } );
};
}

/**
Expand Down Expand Up @@ -120,10 +124,24 @@ export default class ComponentsExample extends React.Component {
};
}

/**
* Renders a Yoast Alert.
*
* @returns {React.Element} The rendered alert.
*/
renderAlert( type ) {
return <Alert key={ type } type={ type }>
{ `This is an Alert of type: "${ type }".` }
<br />
You can add some content.
Including a link <YoastShortLink href="https://yoa.st/why-permalinks/">yoa.st/why-permalinks</YoastShortLink>
</Alert>;
}

/**
* Renders all the Component examples.
*
* @returns {ReactElement} The rendered list of the Component examples.
* @returns {React.Element} The rendered list of the Component examples.
*/
render() {
const courses = this.state.courses;
Expand All @@ -132,6 +150,15 @@ export default class ComponentsExample extends React.Component {
return (
<React.Fragment>
<Container>
<h2>Yoast alerts</h2>
{ [ "error", "info", "success", "warning" ].map( this.renderAlert ) }
{ ! this.state.isAlertDismissed && <Alert type="info" onDismissed={ this.onAlertDismissed }>
This is the dismissable variant.
<br />
You will have to wrap it in order to do actually dismiss it.
<br />
Which is currently done through the state of this example.
</Alert> }
<h2>Yoast warning</h2>
<Warning
message={ [
Expand Down
161 changes: 161 additions & 0 deletions packages/components/src/Alert.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
/* External dependencies */
import PropTypes from "prop-types";
import React from "react";
import styled from "styled-components";
import { __ } from "@wordpress/i18n";

/* Yoast dependencies */
import { getDirectionalStyle } from "@yoast/helpers";
import { colors } from "@yoast/style-guide";

/* Internal dependencies */
import Button from "./buttons/Button";
import SvgIcon from "./SvgIcon";

const AlertContainer = styled.div`
display: flex;
align-items: flex-start;
font-size: 14px;
line-height: 1.5;
border: 1px solid rgba(0, 0, 0, 0.2);
padding: 16px;
color: ${ props => props.alertColor };
background: ${ props => props.alertBackground };
margin-bottom: 20px;
`;

const AlertContent = styled.div`
flex-grow: 1;

a {
color: ${ colors.$color_alert_link_text };
}
`;

const AlertIcon = styled( SvgIcon )`
margin-top: 0.125rem;
${ getDirectionalStyle( "margin-right: 8px", "margin-left: 8px" ) };
`;

const AlertDismiss = styled( Button )`
${ getDirectionalStyle( "margin: -8px -12px -8px 8px", "margin: -8px 8px -12px -8px" ) };
font-size: 24px;
line-height: 1.4;
color: ${ props => props.alertDismissColor };
flex-shrink: 0;
min-width: 36px;
height: 36px;

// Override the base button style: get rid of the button styling.
padding: 0;

&, &:hover, &:active {
/* Inherits box-sizing: border-box so this doesn't change the rendered size. */
border: 2px solid transparent;
background: transparent;
box-shadow: none;
color: ${ props => props.alertDismissColor };
}

/* Inherits focus style from the Button component. */
&:focus {
background: transparent;
color: ${ props => props.alertDismissColor };
border-color: ${ colors.$color_yoast_focus };
box-shadow: 0px 0px 0px 3px ${ colors.$color_yoast_focus_outer };
}
`;

/**
* Alert component.
*
* @param {Object} props The props to use.
* @param {*} props.children The children to render inside the alert.
* @param {string} props.type The type of Alert. Can be: "error", "info", "success" or "warning".
* Controls the colors and icon of the Alert.
* @param {Function} [props.onDismissed] When supplied this Alert will gain an 'X' button.
* Note: the function provided should do the actual dismissing!
* @param {string} dismissAriaLabel The close button aria-label. Must be a translatable string with
* text domain.
*
* @returns {Alert} The Alert component.
*/
class Alert extends React.Component {
/**
* Returns the colors and icon to be used based on the type provided to the props.
*
* @param {string} type The type of Alert.
*
* @returns {object} Options with colors and icons to be used.
*/
getTypeDisplayOptions( type ) {
switch ( type ) {
case "error":
return {
color: colors.$color_alert_error_text,
background: colors.$color_alert_error_background,
icon: "alert-error",
};
case "info":
return {
color: colors.$color_alert_info_text,
background: colors.$color_alert_info_background,
icon: "alert-info",
};
case "success":
return {
color: colors.$color_alert_success_text,
background: colors.$color_alert_success_background,
icon: "alert-success",
};
case "warning":
return {
color: colors.$color_alert_warning_text,
background: colors.$color_alert_warning_background,
icon: "alert-warning",
};
}
}

/**
* Renders the component.
*
* @returns {React.Element} The rendered component.
*/
render() {
const options = this.getTypeDisplayOptions( this.props.type );
const dismissAriaLabel = this.props.dismissAriaLabel || __( "Dismiss this alert", "yoast-components" );

return <AlertContainer alertColor={ options.color } alertBackground={ options.background }>
<AlertIcon icon={ options.icon } color={ options.color } />
<AlertContent>{ this.props.children }</AlertContent>
{
typeof this.props.onDismissed === "function"
? (
<AlertDismiss
alertDismissColor={ options.color }
onClick={ this.props.onDismissed }
aria-label={ dismissAriaLabel }
>
&times;
</AlertDismiss>
)
: null
}
</AlertContainer>;
}
}

Alert.propTypes = {
children: PropTypes.any.isRequired,
type: PropTypes.oneOf( [ "error", "info", "success", "warning" ] ).isRequired,
onDismissed: PropTypes.func,
dismissAriaLabel: PropTypes.string,
};

Alert.defaultProps = {
onDismissed: null,
dismissAriaLabel: "",
};

export default Alert;
4 changes: 4 additions & 0 deletions packages/components/src/SvgIcon.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/components/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export {
wrapInHeading,
} from "./Collapsible";

export { default as Alert } from "./Alert";
export { default as ArticleList } from "./ArticleList";
export { default as Card, FullHeightCard } from "./Card";
export { default as CardBanner } from "./CardBanner";
Expand Down
95 changes: 95 additions & 0 deletions packages/components/tests/AlertTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import React from "react";
import renderer from "react-test-renderer";

import Alert from "../src/Alert";

describe( "Alert", () => {
test( "the Alert types match the snapshot", () => {
const alerts = [
{
type: "error",
color: "#8f1919",
background: "#f9dcdc",
icon: "alert-error",
},
{
type: "info",
color: "#00468f",
background: "#cce5ff",
icon: "alert-info",
},
{
type: "success",
color: "#395315",
background: "#e2f2cc",
icon: "alert-success",
},
{
type: "warning",
color: "#674e00",
background: "#fff3cd",
icon: "alert-warning",
},
];

alerts.forEach( alert => {
const content = `This is of the type: "${ alert.type }".`;
const component = renderer.create(
<Alert type={ alert.type }>
{ content }
</Alert>,
);
const tree = component.toJSON();

// Check the color of the alert.
expect( tree.props.color ).toBe( alert.alertColor );

// 2 children: The type icon and the content.
expect( tree.children.length ).toBe( 2 );

// Check the icon.
expect( tree.children[ 0 ].props.className.indexOf( alert.icon ) ).not.toBe( -1 );
expect( tree.children[ 0 ].props.fill ).toBe( alert.color );

// Check the content.
expect( tree.children[ 1 ].type ).toBe( "div" );
expect( tree.children[ 1 ].children.length ).toBe( 1 );
expect( tree.children[ 1 ].children[ 0 ] ).toBe( content );

// Match the snapshot.
expect( tree ).toMatchSnapshot();

// Check the background of the alert.
const { rendered } = component.toTree();
expect( rendered.props.background ).toBe( alert.alertBackground );
} );
} );

test( "passing onDismissed makes the alert dismissable", () => {
const dismiss = jest.fn();
const component = renderer.create(
<Alert type="info" onDismissed={ dismiss }>
Dismissable alert.
</Alert>,
);
const tree = component.toJSON();

// 3 children: The type icon, the content and the dismiss button.
expect( tree.children.length ).toBe( 3 );

// Check the last child is the dismissable button.
expect( tree.children[ 2 ].type ).toBe( "button" );

// Inside the button should be the times symbol.
expect( tree.children[ 2 ].children.length ).toBe( 1 );
expect( tree.children[ 2 ].children[ 0 ] ).toBe( "×" );

// Check the dismiss action.
expect( typeof tree.children[ 2 ].props.onClick ).toBe( "function" );
tree.children[ 2 ].props.onClick();
expect( dismiss ).toHaveBeenCalledTimes( 1 );

// Match the snapshot.
expect( tree ).toMatchSnapshot();
} );
} );
Loading