summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2022-04-01 19:03:42 +0200
committerArmand Philippot <git@armandphilippot.com>2022-04-01 22:58:18 +0200
commitd177e0c7c61845b516d4a361a21739bb6486b9b5 (patch)
tree3905aab133889d5d59f8116fbcf301930b858887
parent163f9dc0fe436b708de4e59999e87005c6685a0f (diff)
chore: add a back to top component
-rw-r--r--src/components/atoms/buttons/button-link.stories.tsx15
-rw-r--r--src/components/atoms/buttons/button-link.tsx18
-rw-r--r--src/components/atoms/buttons/button.stories.tsx15
-rw-r--r--src/components/atoms/buttons/button.tsx8
-rw-r--r--src/components/atoms/buttons/buttons.module.scss15
-rw-r--r--src/components/molecules/buttons/back-to-top.module.scss49
-rw-r--r--src/components/molecules/buttons/back-to-top.stories.tsx41
-rw-r--r--src/components/molecules/buttons/back-to-top.test.tsx10
-rw-r--r--src/components/molecules/buttons/back-to-top.tsx40
9 files changed, 204 insertions, 7 deletions
diff --git a/src/components/atoms/buttons/button-link.stories.tsx b/src/components/atoms/buttons/button-link.stories.tsx
index d4df676..6fe786b 100644
--- a/src/components/atoms/buttons/button-link.stories.tsx
+++ b/src/components/atoms/buttons/button-link.stories.tsx
@@ -43,6 +43,21 @@ export default {
required: false,
},
},
+ shape: {
+ control: {
+ type: 'select',
+ },
+ description: 'The link shape.',
+ options: ['rectangle', 'square'],
+ table: {
+ category: 'Options',
+ defaultValue: { summary: 'rectangle' },
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
target: {
control: {
type: null,
diff --git a/src/components/atoms/buttons/button-link.tsx b/src/components/atoms/buttons/button-link.tsx
index c33a4b7..47fe4b0 100644
--- a/src/components/atoms/buttons/button-link.tsx
+++ b/src/components/atoms/buttons/button-link.tsx
@@ -16,6 +16,10 @@ type ButtonLinkProps = {
*/
kind?: 'primary' | 'secondary';
/**
+ * ButtonLink shape. Default: rectangle.
+ */
+ shape?: 'rectangle' | 'square';
+ /**
* Define an URL as target.
*/
target: string;
@@ -30,18 +34,26 @@ const ButtonLink: FC<ButtonLinkProps> = ({
children,
target,
kind = 'secondary',
+ shape = 'rectangle',
external = false,
...props
}) => {
const kindClass = styles[`btn--${kind}`];
+ const shapeClass = styles[`btn--${shape}`];
return external ? (
- <a href={target} className={`${styles.btn} ${kindClass}`} {...props}>
+ <a
+ href={target}
+ className={`${styles.btn} ${kindClass} ${shapeClass}`}
+ {...props}
+ >
{children}
</a>
) : (
- <Link href={target} {...props}>
- <a className={`${styles.btn} ${kindClass}`}>{children}</a>
+ <Link href={target}>
+ <a className={`${styles.btn} ${kindClass} ${shapeClass}`} {...props}>
+ {children}
+ </a>
</Link>
);
};
diff --git a/src/components/atoms/buttons/button.stories.tsx b/src/components/atoms/buttons/button.stories.tsx
index 5af61bd..bec5e5d 100644
--- a/src/components/atoms/buttons/button.stories.tsx
+++ b/src/components/atoms/buttons/button.stories.tsx
@@ -75,6 +75,21 @@ export default {
required: false,
},
},
+ shape: {
+ control: {
+ type: 'select',
+ },
+ description: 'The link shape.',
+ options: ['rectangle', 'square'],
+ table: {
+ category: 'Options',
+ defaultValue: { summary: 'rectangle' },
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
type: {
control: {
type: 'select',
diff --git a/src/components/atoms/buttons/button.tsx b/src/components/atoms/buttons/button.tsx
index 420ee74..08b8d67 100644
--- a/src/components/atoms/buttons/button.tsx
+++ b/src/components/atoms/buttons/button.tsx
@@ -19,6 +19,10 @@ export type ButtonProps = {
*/
onClick?: MouseEventHandler<HTMLButtonElement>;
/**
+ * Button shape. Default: rectangle.
+ */
+ shape?: 'rectangle' | 'square';
+ /**
* Button type attribute. Default: button.
*/
type?: 'button' | 'reset' | 'submit';
@@ -33,16 +37,18 @@ const Button: FC<ButtonProps> = ({
children,
disabled = false,
kind = 'secondary',
+ shape = 'rectangle',
type = 'button',
...props
}) => {
const kindClass = styles[`btn--${kind}`];
+ const shapeClass = styles[`btn--${shape}`];
return (
<button
type={type}
disabled={disabled}
- className={`${styles.btn} ${kindClass}`}
+ className={`${styles.btn} ${kindClass} ${shapeClass}`}
{...props}
>
{children}
diff --git a/src/components/atoms/buttons/buttons.module.scss b/src/components/atoms/buttons/buttons.module.scss
index 9dddf48..6784de9 100644
--- a/src/components/atoms/buttons/buttons.module.scss
+++ b/src/components/atoms/buttons/buttons.module.scss
@@ -1,15 +1,24 @@
@use "@styles/abstracts/functions" as fun;
.btn {
- display: block;
- max-width: max-content;
- padding: var(--spacing-2xs) var(--spacing-md);
+ display: inline-flex;
+ place-content: center;
+ align-items: center;
border: none;
border-radius: fun.convert-px(5);
font-size: var(--font-size-md);
font-weight: 600;
transition: all 0.3s ease-in-out 0s;
+ &--rectangle {
+ padding: var(--spacing-2xs) var(--spacing-md);
+ }
+
+ &--square {
+ padding: var(--spacing-xs);
+ aspect-ratio: 1 / 1;
+ }
+
&:disabled {
cursor: wait;
}
diff --git a/src/components/molecules/buttons/back-to-top.module.scss b/src/components/molecules/buttons/back-to-top.module.scss
new file mode 100644
index 0000000..1abf1f6
--- /dev/null
+++ b/src/components/molecules/buttons/back-to-top.module.scss
@@ -0,0 +1,49 @@
+@use "@styles/abstracts/functions" as fun;
+
+.wrapper {
+ a {
+ svg {
+ width: 100%;
+ }
+
+ :global {
+ .arrow-head {
+ transform: translateY(30%) scale(1.1);
+ transition: all 0.45s ease-in-out 0s;
+ }
+
+ .arrow-bar {
+ opacity: 0;
+ transform: translateY(30%) scaleY(0);
+ transition: transform 0.4s ease-in-out 0s, opacity 0.1s linear 0.4s;
+ }
+ }
+
+ &:hover,
+ &:focus {
+ :global {
+ .arrow-head {
+ transform: translateY(0) scale(1);
+ }
+
+ .arrow-bar {
+ opacity: 1;
+ transform: translateY(0) scaleY(1);
+ transition: transform 0.45s ease-in-out 0s;
+ }
+ }
+
+ svg {
+ :global {
+ animation: pulse 1.2s ease-in-out 0.6s infinite;
+ }
+ }
+ }
+
+ &:active {
+ svg {
+ animation-play-state: paused;
+ }
+ }
+ }
+}
diff --git a/src/components/molecules/buttons/back-to-top.stories.tsx b/src/components/molecules/buttons/back-to-top.stories.tsx
new file mode 100644
index 0000000..f1a36e5
--- /dev/null
+++ b/src/components/molecules/buttons/back-to-top.stories.tsx
@@ -0,0 +1,41 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import { IntlProvider } from 'react-intl';
+import BackToTopComponent from './back-to-top';
+
+export default {
+ title: 'Molecules/Buttons',
+ component: BackToTopComponent,
+ argTypes: {
+ additionalClasses: {
+ control: {
+ type: 'text',
+ },
+ description: 'Add additional classes to the button wrapper.',
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ target: {
+ control: {
+ type: 'text',
+ },
+ description: 'An element id (without hashtag) to use as anchor.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ },
+} as ComponentMeta<typeof BackToTopComponent>;
+
+const Template: ComponentStory<typeof BackToTopComponent> = (args) => (
+ <IntlProvider locale="en">
+ <BackToTopComponent {...args} />
+ </IntlProvider>
+);
+
+export const BackToTop = Template.bind({});
+BackToTop.args = {
+ target: 'top',
+};
diff --git a/src/components/molecules/buttons/back-to-top.test.tsx b/src/components/molecules/buttons/back-to-top.test.tsx
new file mode 100644
index 0000000..2b3a0a9
--- /dev/null
+++ b/src/components/molecules/buttons/back-to-top.test.tsx
@@ -0,0 +1,10 @@
+import { render, screen } from '@test-utils';
+import BackToTop from './back-to-top';
+
+describe('BackToTop', () => {
+ it('renders a BackToTop link', () => {
+ render(<BackToTop target="top" />);
+ expect(screen.getByRole('link')).toHaveAccessibleName('Back to top');
+ expect(screen.getByRole('link')).toHaveAttribute('href', '/#top');
+ });
+});
diff --git a/src/components/molecules/buttons/back-to-top.tsx b/src/components/molecules/buttons/back-to-top.tsx
new file mode 100644
index 0000000..f25d3e9
--- /dev/null
+++ b/src/components/molecules/buttons/back-to-top.tsx
@@ -0,0 +1,40 @@
+import ButtonLink from '@components/atoms/buttons/button-link';
+import Arrow from '@components/atoms/icons/arrow';
+import { FC } from 'react';
+import { useIntl } from 'react-intl';
+import styles from './back-to-top.module.scss';
+
+type BackToTopProps = {
+ /**
+ * Add additional classes to the button wrapper.
+ */
+ additionalClasses?: string;
+ /**
+ * An element id (without hashtag) to use as anchor.
+ */
+ target: string;
+};
+
+/**
+ * BackToTop component
+ *
+ * Render a back to top link.
+ */
+const BackToTop: FC<BackToTopProps> = ({ additionalClasses, target }) => {
+ const intl = useIntl();
+ const linkName = intl.formatMessage({
+ defaultMessage: 'Back to top',
+ description: 'BackToTop: link text',
+ id: 'm+SUSR',
+ });
+
+ return (
+ <div className={`${styles.wrapper} ${additionalClasses}`}>
+ <ButtonLink shape="square" target={`#${target}`} aria-label={linkName}>
+ <Arrow direction="top" />
+ </ButtonLink>
+ </div>
+ );
+};
+
+export default BackToTop;