Skip to content

Commit

Permalink
Implement core base HTML atom Button
Browse files Browse the repository at this point in the history
It consists of the previously implemented styles and variants and
represents a `<button>` (1). Next to this is can also wrap the base HTML
element atom `A` (2) to handle internal and external links.

References:
  (1) https://developer.mozilla.org/de/docs/Web/HTML/Element/button
  (2) #70

Associated epic: GH-63
GH-110
  • Loading branch information
arcticicestudio committed Jan 8, 2019
1 parent 8c45192 commit ff64b75
Show file tree
Hide file tree
Showing 5 changed files with 976 additions and 2 deletions.
105 changes: 105 additions & 0 deletions src/components/atoms/core/Button/Button.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*
* Copyright (C) 2018-present Arctic Ice Studio <[email protected]>
* Copyright (C) 2018-present Sven Greb <[email protected]>
*
* Project: Nord Docs
* Repository: https://github.com/arcticicestudio/nord-docs
* License: MIT
*/

import React from "react";
import PropTypes from "prop-types";
import styled from "styled-components";

import { A } from "atoms/core/HTMLElements";

import styles from "./styles";

const BaseButton = styled.button`
${styles};
`;

/**
* A component that represents the `<button>` HTML element with multiple variants and additional props to toggle more
* styles.
* It also wraps the `A` component to render a link through the `href` and `to` props.
*
* @author Arctic Ice Studio <[email protected]>
* @author Sven Greb <[email protected]>
* @since 0.6.0
* @see https://developer.mozilla.org/de/docs/Web/HTML/Element/button
*/
const Button = ({ children, dashed, disabled, ghost, href, onClick, outlined, quiet, to, variant }) => {
if (href) {
return (
<BaseButton
as={A}
dashed={dashed}
disabled={disabled}
ghost={ghost}
href={href}
outlined={outlined}
quiet={quiet}
variant={variant}
>
{children}
</BaseButton>
);
}
if (to) {
return (
<BaseButton
as={A}
dashed={dashed}
disabled={disabled}
ghost={ghost}
outlined={outlined}
quiet={quiet}
to={to}
variant={variant}
>
{children}
</BaseButton>
);
}
return (
<BaseButton
dashed={dashed}
disabled={disabled}
ghost={ghost}
onClick={onClick}
outlined={outlined}
quiet={quiet}
variant={variant}
>
{children}
</BaseButton>
);
};

Button.propTypes = {
children: PropTypes.node.isRequired,
dashed: PropTypes.bool,
disabled: PropTypes.bool,
ghost: PropTypes.bool,
href: PropTypes.string,
onClick: PropTypes.func,
outlined: PropTypes.bool,
quiet: PropTypes.bool,
to: PropTypes.string,
variant: PropTypes.oneOf(["primary", "secondary", "simple", "subtle"])
};

Button.defaultProps = {
dashed: false,
disabled: false,
ghost: false,
href: "",
onClick: () => {},
outlined: false,
quiet: false,
to: "",
variant: "simple"
};

export default Button;
10 changes: 10 additions & 0 deletions src/components/atoms/core/Button/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/*
* Copyright (C) 2018-present Arctic Ice Studio <[email protected]>
* Copyright (C) 2018-present Sven Greb <[email protected]>
*
* Project: Nord Docs
* Repository: https://github.com/arcticicestudio/nord-docs
* License: MIT
*/

export { default } from "./Button";
7 changes: 5 additions & 2 deletions src/components/atoms/core/Button/styles.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import { css } from "styled-components";
import { darken, lighten, rgba } from "polished";

import { colors, themedModeVariant, MODE_BRIGHT_SNOW_FLURRY, MODE_DARK_NIGHT_FROST } from "styles/theme";
import { colors, motion, themedModeVariant, MODE_BRIGHT_SNOW_FLURRY, MODE_DARK_NIGHT_FROST } from "styles/theme";

const backgroundColor = themedModeVariant({
primary: {
Expand Down Expand Up @@ -292,7 +292,10 @@ const styles = css`
${base};
color: ${fontColor};
background-color: ${({ ghost }) => !ghost && backgroundColor};
transition: background-color 0.2s ease-in, border-color 0.2s ease-in, box-shadow 0.2s ease-in, color 0.2s ease-in;
transition: background-color ${motion.speed.duration.transition.text.fade}ms ease-in,
border-color ${motion.speed.duration.transition.text.fade}ms ease-in,
box-shadow ${motion.speed.duration.transition.text.fade}ms ease-in,
color ${motion.speed.duration.transition.text.fade}ms ease-in;
${({ outlined }) =>
outlined &&
css`
Expand Down
167 changes: 167 additions & 0 deletions test/components/atoms/core/Button/Button.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/*
* Copyright (C) 2018-present Arctic Ice Studio <[email protected]>
* Copyright (C) 2018-present Sven Greb <[email protected]>
*
* Project: Nord Docs
* Repository: https://github.com/arcticicestudio/nord-docs
* License: MIT
*/

import React from "react";
import { waitForElement } from "react-testing-library";

import { renderWithTheme } from "nord-docs-test-utils";
import Button from "atoms/core/Button";
import { ROUTE_DOCS } from "config/routes/mappings";
import { metadataNordDocs } from "data/project";

const handleOnClickMock = jest.fn();

const assertBaseButtonStyles = element => {
const { color } = getComputedStyle(element);

expect(element).toHaveStyleRule("display", "inline-flex");
expect(element).toHaveStyleRule("align-items", "center");
expect(element).toHaveStyleRule("text-align", "center");
expect(element).toHaveStyleRule("vertical-align", "middle");
expect(element).toHaveStyleRule("white-space", "nowrap");
expect(element).toHaveStyleRule("border-radius", expect.stringContaining("em"));
expect(element).toHaveStyleRule("padding", expect.stringContaining("em"));
expect(element).toHaveStyleRule("user-select", "none");
expect(element).toHaveStyleRule("cursor", "pointer", {
modifier: ":hover:not(:disabled)"
});
expect(element).toHaveStyleRule("outline", "none", {
modifier: ":active"
});
expect(element).toHaveStyleRule("outline", "none", {
modifier: ":focus"
});
expect(element).toHaveStyleRule("cursor", "not-allowed", {
modifier: ":disabled"
});
expect(element).toHaveStyleRule("color", expect.not.stringMatching(color), {
modifier: ":disabled"
});
};

const assertBackgroundColorStyle = element => {
const { backgroundColor } = getComputedStyle(element);

expect(element).toHaveStyleRule("background-color", expect.not.stringMatching(backgroundColor), {
modifier: ":active:not(:disabled)"
});
expect(element).toHaveStyleRule("background-color", expect.not.stringMatching(backgroundColor), {
modifier: ":hover:not(:disabled)"
});
};

describe("theme styles", () => {
test("matches the snapshot with 'primary' variant", () => {
const { container } = renderWithTheme(
<Button onClick={handleOnClickMock} variant="primary">
Nord
</Button>
);

assertBaseButtonStyles(container.firstChild);
assertBackgroundColorStyle(container.firstChild);
expect(container.firstChild).toMatchSnapshot();
});

test("matches the snapshot with 'secondary' variant", () => {
const { container } = renderWithTheme(
<Button onClick={handleOnClickMock} variant="secondary">
Nord
</Button>
);

assertBaseButtonStyles(container.firstChild);
assertBackgroundColorStyle(container.firstChild);
expect(container.firstChild).toMatchSnapshot();
});

test("matches the snapshot with 'simple' variant", () => {
const { container } = renderWithTheme(
<Button onClick={handleOnClickMock} variant="simple">
Nord
</Button>
);

assertBaseButtonStyles(container.firstChild);
assertBackgroundColorStyle(container.firstChild);
expect(container.firstChild).toMatchSnapshot();
});

test("matches the snapshot with 'subtle' variant", () => {
const { container } = renderWithTheme(
<Button onClick={handleOnClickMock} variant="subtle">
Nord
</Button>
);

assertBaseButtonStyles(container.firstChild);
assertBackgroundColorStyle(container.firstChild);
expect(container.firstChild).toMatchSnapshot();
});

test("has borders with 'outlined' prop", () => {
const { container } = renderWithTheme(
<Button onClick={handleOnClickMock} outlined>
Nord
</Button>
);

assertBaseButtonStyles(container.firstChild);
assertBackgroundColorStyle(container.firstChild);
expect(container.firstChild).toHaveStyleRule("border", expect.stringContaining("solid"));
expect(container.firstChild).toMatchSnapshot();
});

test("has borders with 'dashed' and 'outlined' props", () => {
const { container } = renderWithTheme(
<Button dashed onClick={handleOnClickMock} outlined>
Nord
</Button>
);

assertBaseButtonStyles(container.firstChild);
assertBackgroundColorStyle(container.firstChild);
expect(container.firstChild).toHaveStyleRule("border", expect.stringContaining("dashed"));
expect(container.firstChild).toMatchSnapshot();
});
});

describe("logical behavior", () => {
test("renders inernal URLs with `to` prop", () => {
const { container } = renderWithTheme(<Button to={ROUTE_DOCS}>Nord</Button>);
expect(container.firstChild).toHaveAttribute("href", ROUTE_DOCS);
expect(container.firstChild).toMatchSnapshot();
});

test("renders inernal URLs with `href` prop", () => {
const { container } = renderWithTheme(<Button href={ROUTE_DOCS}>Nord</Button>);
expect(container.firstChild).toHaveAttribute("href", ROUTE_DOCS);
expect(container.firstChild).toMatchSnapshot();
});

test("renders external URLs with `href` prop", () => {
const { container } = renderWithTheme(<Button href={metadataNordDocs.homepage}>Nord</Button>);
expect(container.firstChild).toHaveAttribute("href", metadataNordDocs.homepage);
expect(container.firstChild).toMatchSnapshot();
});

test("renders external URLs with `to` prop", () => {
const { container } = renderWithTheme(<Button to={metadataNordDocs.homepage}>Nord</Button>);
expect(container.firstChild).toHaveAttribute("href", metadataNordDocs.homepage);
expect(container.firstChild).toMatchSnapshot();
});
});

describe("rendering", () => {
test("renders with text", async () => {
const { getByText } = renderWithTheme(<Button onClick={handleOnClickMock}>Nord</Button>);

await waitForElement(() => getByText(/Nord/i));
});
});
Loading

0 comments on commit ff64b75

Please sign in to comment.