aboutsummaryrefslogtreecommitdiffstats
path: root/src/components/molecules/buttons/back-to-top
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/molecules/buttons/back-to-top')
-rw-r--r--src/components/molecules/buttons/back-to-top/back-to-top.module.scss54
-rw-r--r--src/components/molecules/buttons/back-to-top/back-to-top.stories.tsx58
-rw-r--r--src/components/molecules/buttons/back-to-top/back-to-top.test.tsx17
-rw-r--r--src/components/molecules/buttons/back-to-top/back-to-top.tsx45
-rw-r--r--src/components/molecules/buttons/back-to-top/index.ts1
5 files changed, 175 insertions, 0 deletions
diff --git a/src/components/molecules/buttons/back-to-top/back-to-top.module.scss b/src/components/molecules/buttons/back-to-top/back-to-top.module.scss
new file mode 100644
index 0000000..10171d2
--- /dev/null
+++ b/src/components/molecules/buttons/back-to-top/back-to-top.module.scss
@@ -0,0 +1,54 @@
+@use "../../../../styles/abstracts/functions" as fun;
+
+.link {
+ --button-height: clamp(#{fun.convert-px(48)}, 8vw, #{fun.convert-px(55)});
+
+ height: var(--button-height);
+ padding: 0;
+
+ svg {
+ max-width: 100%;
+ max-height: 100%;
+ }
+
+ :global {
+ .arrow-head {
+ transform: translateX(-10%) translateY(30%) scale(1.2);
+ transition: all 0.45s ease-in-out 0s;
+ }
+
+ .arrow-bar {
+ opacity: 0;
+ transform: translateY(30%) scaleY(0);
+ transition:
+ transform 0.45s ease-in-out 0s,
+ opacity 0.1s linear 0.2s;
+ }
+ }
+
+ &:hover,
+ &:focus {
+ :global {
+ .arrow-head {
+ transform: translateY(0) scale(1);
+ }
+
+ .arrow-bar {
+ opacity: 1;
+ transform: translateY(0) scaleY(1);
+ }
+ }
+
+ 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/back-to-top.stories.tsx b/src/components/molecules/buttons/back-to-top/back-to-top.stories.tsx
new file mode 100644
index 0000000..3abb59d
--- /dev/null
+++ b/src/components/molecules/buttons/back-to-top/back-to-top.stories.tsx
@@ -0,0 +1,58 @@
+import type { ComponentMeta, ComponentStory } from '@storybook/react';
+import { BackToTop as BackToTopComponent } from './back-to-top';
+
+/**
+ * BackToTop - Storybook Meta
+ */
+export default {
+ title: 'Molecules/Buttons',
+ component: BackToTopComponent,
+ argTypes: {
+ anchor: {
+ control: {
+ type: 'text',
+ },
+ description: 'An element id with leading hashtag to use as anchor.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the button wrapper.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ label: {
+ control: {
+ type: 'text',
+ },
+ description: 'An accessible name for the button.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ },
+} as ComponentMeta<typeof BackToTopComponent>;
+
+const Template: ComponentStory<typeof BackToTopComponent> = (args) => (
+ <BackToTopComponent {...args} />
+);
+
+/**
+ * Buttons Stories - Back to top
+ */
+export const BackToTop = Template.bind({});
+BackToTop.args = {
+ anchor: '#top',
+ label: 'Back to top',
+};
diff --git a/src/components/molecules/buttons/back-to-top/back-to-top.test.tsx b/src/components/molecules/buttons/back-to-top/back-to-top.test.tsx
new file mode 100644
index 0000000..00eb020
--- /dev/null
+++ b/src/components/molecules/buttons/back-to-top/back-to-top.test.tsx
@@ -0,0 +1,17 @@
+import { describe, expect, it } from '@jest/globals';
+import { render, screen as rtlScreen } from '@testing-library/react';
+import { BackToTop } from './back-to-top';
+
+describe('BackToTop', () => {
+ it('renders a BackToTop link', () => {
+ const anchor = '#top';
+ const label = 'eveniet';
+
+ render(<BackToTop anchor={anchor} label={label} />);
+
+ const link = rtlScreen.getByRole('link');
+
+ expect(link).toHaveAccessibleName(label);
+ expect(link).toHaveAttribute('href', anchor);
+ });
+});
diff --git a/src/components/molecules/buttons/back-to-top/back-to-top.tsx b/src/components/molecules/buttons/back-to-top/back-to-top.tsx
new file mode 100644
index 0000000..83e2161
--- /dev/null
+++ b/src/components/molecules/buttons/back-to-top/back-to-top.tsx
@@ -0,0 +1,45 @@
+import type { FC } from 'react';
+import {
+ ButtonLink,
+ type ButtonLinkProps,
+ Icon,
+ VisuallyHidden,
+} from '../../../atoms';
+import styles from './back-to-top.module.scss';
+
+export type BackToTopProps = Omit<
+ ButtonLinkProps,
+ 'children' | 'isExternal' | 'kind' | 'to'
+> & {
+ /**
+ * Define the anchor.
+ */
+ anchor: string;
+ /**
+ * Define an accessible label for the button.
+ */
+ label: string;
+};
+
+/**
+ * BackToTop component
+ *
+ * Render a back to top link.
+ */
+export const BackToTop: FC<BackToTopProps> = ({
+ anchor,
+ className = '',
+ label,
+ shape = 'square',
+ ...props
+}) => {
+ const btnClass = `${styles.link} ${className}`;
+
+ return (
+ <ButtonLink {...props} className={btnClass} shape={shape} to={anchor}>
+ {/* eslint-disable-next-line react/jsx-no-literals -- Config allowed */}
+ <Icon aria-hidden orientation="top" shape="arrow" />
+ <VisuallyHidden>{label}</VisuallyHidden>
+ </ButtonLink>
+ );
+};
diff --git a/src/components/molecules/buttons/back-to-top/index.ts b/src/components/molecules/buttons/back-to-top/index.ts
new file mode 100644
index 0000000..fa6b1c0
--- /dev/null
+++ b/src/components/molecules/buttons/back-to-top/index.ts
@@ -0,0 +1 @@
+export * from './back-to-top';