aboutsummaryrefslogtreecommitdiffstats
path: root/src/components/atoms/buttons/button-link
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2023-09-26 18:43:11 +0200
committerArmand Philippot <git@armandphilippot.com>2023-10-24 12:25:00 +0200
commit388e687857345c85ee550cd5da472675e05a6ff5 (patch)
tree0f035a3cad57a75959c028949a57227a83d480e2 /src/components/atoms/buttons/button-link
parent70efcfeaa0603415dd992cb662d8efb960e6e49a (diff)
refactor(components): rewrite Button and ButtonLink components
Both: * move styles to Sass placeholders Button: * add `isPressed` prop to Button * add `isLoading` prop to Button (to differentiate state from disabled) ButtonLink: * replace `external` prop with `isExternal` prop * replace `href` prop with `to` prop
Diffstat (limited to 'src/components/atoms/buttons/button-link')
-rw-r--r--src/components/atoms/buttons/button-link/button-link.module.scss29
-rw-r--r--src/components/atoms/buttons/button-link/button-link.stories.tsx114
-rw-r--r--src/components/atoms/buttons/button-link/button-link.test.tsx129
-rw-r--r--src/components/atoms/buttons/button-link/button-link.tsx67
-rw-r--r--src/components/atoms/buttons/button-link/index.ts1
5 files changed, 340 insertions, 0 deletions
diff --git a/src/components/atoms/buttons/button-link/button-link.module.scss b/src/components/atoms/buttons/button-link/button-link.module.scss
new file mode 100644
index 0000000..0f35a24
--- /dev/null
+++ b/src/components/atoms/buttons/button-link/button-link.module.scss
@@ -0,0 +1,29 @@
+@use "../../../../styles/abstracts/placeholders";
+
+.btn {
+ @extend %button;
+
+ &--circle {
+ @extend %circle-button;
+ }
+
+ &--rectangle {
+ @extend %rectangle-button;
+ }
+
+ &--square {
+ @extend %square-button;
+ }
+
+ &--primary {
+ @extend %primary-button;
+ }
+
+ &--secondary {
+ @extend %secondary-button;
+ }
+
+ &--tertiary {
+ @extend %tertiary-button;
+ }
+}
diff --git a/src/components/atoms/buttons/button-link/button-link.stories.tsx b/src/components/atoms/buttons/button-link/button-link.stories.tsx
new file mode 100644
index 0000000..f048ce9
--- /dev/null
+++ b/src/components/atoms/buttons/button-link/button-link.stories.tsx
@@ -0,0 +1,114 @@
+import type { ComponentMeta, ComponentStory } from '@storybook/react';
+import { ButtonLink } from './button-link';
+
+/**
+ * ButtonLink - Storybook Meta
+ */
+export default {
+ title: 'Atoms/Buttons/ButtonLink',
+ component: ButtonLink,
+ args: {
+ isExternal: false,
+ shape: 'rectangle',
+ },
+ argTypes: {
+ children: {
+ control: {
+ type: 'text',
+ },
+ description: 'The link body.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ isExternal: {
+ control: {
+ type: 'boolean',
+ },
+ description: 'Determine if the link is an external link.',
+ table: {
+ category: 'Options',
+ defaultValue: { summary: false },
+ },
+ type: {
+ name: 'boolean',
+ required: false,
+ },
+ },
+ kind: {
+ control: {
+ type: 'select',
+ },
+ description: 'The link kind.',
+ options: ['primary', 'secondary', 'tertiary'],
+ table: {
+ category: 'Options',
+ defaultValue: { summary: 'secondary' },
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ shape: {
+ control: {
+ type: 'select',
+ },
+ description: 'The link shape.',
+ options: ['circle', 'rectangle', 'square'],
+ table: {
+ category: 'Options',
+ defaultValue: { summary: 'rectangle' },
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ to: {
+ control: {
+ type: 'text',
+ },
+ description: 'The link target.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ },
+} as ComponentMeta<typeof ButtonLink>;
+
+const Template: ComponentStory<typeof ButtonLink> = (args) => (
+ <ButtonLink {...args} />
+);
+
+/**
+ * ButtonLink Story - Primary
+ */
+export const Primary = Template.bind({});
+Primary.args = {
+ children: 'Link',
+ kind: 'primary',
+ to: '#',
+};
+
+/**
+ * ButtonLink Story - Secondary
+ */
+export const Secondary = Template.bind({});
+Secondary.args = {
+ children: 'Link',
+ kind: 'secondary',
+ to: '#',
+};
+
+/**
+ * ButtonLink Story - Tertiary
+ */
+export const Tertiary = Template.bind({});
+Tertiary.args = {
+ children: 'Link',
+ kind: 'tertiary',
+ to: '#',
+};
diff --git a/src/components/atoms/buttons/button-link/button-link.test.tsx b/src/components/atoms/buttons/button-link/button-link.test.tsx
new file mode 100644
index 0000000..d18120b
--- /dev/null
+++ b/src/components/atoms/buttons/button-link/button-link.test.tsx
@@ -0,0 +1,129 @@
+import { describe, expect, it } from '@jest/globals';
+import { render, screen as rtlScreen } from '@testing-library/react';
+import { ButtonLink } from './button-link';
+
+describe('ButtonLink', () => {
+ it('renders a link with anchor and href', () => {
+ const target = 'eum';
+ const body = 'est eaque nostrum';
+
+ render(<ButtonLink to={target}>{body}</ButtonLink>);
+
+ expect(rtlScreen.getByRole('link', { name: body })).toHaveAttribute(
+ 'href',
+ target
+ );
+ });
+
+ it('renders an external link', () => {
+ const target = 'voluptatem';
+ const body = 'impedit';
+
+ render(
+ <ButtonLink isExternal to={target}>
+ {body}
+ </ButtonLink>
+ );
+
+ expect(rtlScreen.getByRole('link', { name: body })).toHaveAttribute(
+ 'rel',
+ expect.stringContaining('external')
+ );
+ });
+
+ it('renders a primary button', () => {
+ const target = 'vero';
+ const body = 'iure';
+
+ render(
+ // eslint-disable-next-line react/jsx-no-literals -- Ignore kind.
+ <ButtonLink kind="primary" to={target}>
+ {body}
+ </ButtonLink>
+ );
+
+ expect(rtlScreen.getByRole('link', { name: body })).toHaveClass(
+ 'btn--primary'
+ );
+ });
+
+ it('renders a secondary button', () => {
+ const target = 'voluptatem';
+ const body = 'et';
+
+ render(
+ // eslint-disable-next-line react/jsx-no-literals -- Ignore kind.
+ <ButtonLink kind="secondary" to={target}>
+ {body}
+ </ButtonLink>
+ );
+
+ expect(rtlScreen.getByRole('link', { name: body })).toHaveClass(
+ 'btn--secondary'
+ );
+ });
+
+ it('renders a tertiary button', () => {
+ const target = 'vitae';
+ const body = 'quo';
+
+ render(
+ // eslint-disable-next-line react/jsx-no-literals -- Ignore kind.
+ <ButtonLink kind="tertiary" to={target}>
+ {body}
+ </ButtonLink>
+ );
+
+ expect(rtlScreen.getByRole('link', { name: body })).toHaveClass(
+ 'btn--tertiary'
+ );
+ });
+
+ it('renders a circle button', () => {
+ const target = 'praesentium';
+ const body = 'laudantium';
+
+ render(
+ // eslint-disable-next-line react/jsx-no-literals -- Ignore kind.
+ <ButtonLink shape="circle" to={target}>
+ {body}
+ </ButtonLink>
+ );
+
+ expect(rtlScreen.getByRole('link', { name: body })).toHaveClass(
+ 'btn--circle'
+ );
+ });
+
+ it('renders a rectangle button', () => {
+ const target = 'tempora';
+ const body = 'ut';
+
+ render(
+ // eslint-disable-next-line react/jsx-no-literals -- Ignore kind.
+ <ButtonLink shape="rectangle" to={target}>
+ {body}
+ </ButtonLink>
+ );
+
+ expect(rtlScreen.getByRole('link', { name: body })).toHaveClass(
+ 'btn--rectangle'
+ );
+ });
+
+ it('renders a square button', () => {
+ const target = 'quia';
+ const body = 'non';
+
+ render(
+ // eslint-disable-next-line react/jsx-no-literals -- Ignore kind.
+ <ButtonLink shape="square" to={target}>
+ {body}
+ </ButtonLink>
+ );
+
+ expect(rtlScreen.getByRole('link', { name: body })).toHaveClass(
+ 'btn--square'
+ );
+ });
+});
diff --git a/src/components/atoms/buttons/button-link/button-link.tsx b/src/components/atoms/buttons/button-link/button-link.tsx
new file mode 100644
index 0000000..f8bbadc
--- /dev/null
+++ b/src/components/atoms/buttons/button-link/button-link.tsx
@@ -0,0 +1,67 @@
+import Link from 'next/link';
+import type { AnchorHTMLAttributes, FC, ReactNode } from 'react';
+import styles from './button-link.module.scss';
+
+export type ButtonLinkProps = Omit<
+ AnchorHTMLAttributes<HTMLAnchorElement>,
+ 'href'
+> & {
+ /**
+ * The button link body.
+ */
+ children: ReactNode;
+ /**
+ * True if it is an external link.
+ *
+ * @default false
+ */
+ isExternal?: boolean;
+ /**
+ * Define the button kind.
+ *
+ * @default 'secondary'
+ */
+ kind?: 'primary' | 'secondary' | 'tertiary';
+ /**
+ * Define the button shape.
+ *
+ * @default 'rectangle'
+ */
+ shape?: 'circle' | 'rectangle' | 'square';
+ /**
+ * Define an URL or anchor as target.
+ */
+ to: string;
+};
+
+/**
+ * ButtonLink component
+ *
+ * Use a button-like link as call to action.
+ */
+export const ButtonLink: FC<ButtonLinkProps> = ({
+ children,
+ className = '',
+ kind = 'secondary',
+ shape = 'rectangle',
+ isExternal = false,
+ rel = '',
+ to,
+ ...props
+}) => {
+ const kindClass = styles[`btn--${kind}`];
+ const shapeClass = styles[`btn--${shape}`];
+ const btnClass = `${styles.btn} ${kindClass} ${shapeClass} ${className}`;
+ const linkRel =
+ isExternal && !rel.includes('external') ? `external ${rel}` : rel;
+
+ return isExternal ? (
+ <a {...props} className={btnClass} href={to} rel={linkRel}>
+ {children}
+ </a>
+ ) : (
+ <Link {...props} className={btnClass} href={to} rel={rel}>
+ {children}
+ </Link>
+ );
+};
diff --git a/src/components/atoms/buttons/button-link/index.ts b/src/components/atoms/buttons/button-link/index.ts
new file mode 100644
index 0000000..68d0a03
--- /dev/null
+++ b/src/components/atoms/buttons/button-link/index.ts
@@ -0,0 +1 @@
+export * from './button-link';