aboutsummaryrefslogtreecommitdiffstats
path: root/src/components/atoms/links/link
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2023-10-02 18:45:30 +0200
committerArmand Philippot <git@armandphilippot.com>2023-11-11 18:14:41 +0100
commitf914ff8376dd91c4f6f8ca149e1cb6becb622d88 (patch)
tree777dc0268eba86721878a715c68f0f09bedb4b18 /src/components/atoms/links/link
parentb52b8183ce299b5a2d3c3b2f4f8cb94bb443d746 (diff)
refactor(components): rewrite Link component
* rename `external` prop to `isExternal` * rename `download` prop to `isDownload` * rewrite CSS to reduce code length and complexity * move link styles in Sass placeholders to avoid repeats because of WordPress articles * move NavLink component to molecules
Diffstat (limited to 'src/components/atoms/links/link')
-rw-r--r--src/components/atoms/links/link/index.ts1
-rw-r--r--src/components/atoms/links/link/link.module.scss70
-rw-r--r--src/components/atoms/links/link/link.stories.tsx178
-rw-r--r--src/components/atoms/links/link/link.test.tsx47
-rw-r--r--src/components/atoms/links/link/link.tsx90
5 files changed, 386 insertions, 0 deletions
diff --git a/src/components/atoms/links/link/index.ts b/src/components/atoms/links/link/index.ts
new file mode 100644
index 0000000..e33728e
--- /dev/null
+++ b/src/components/atoms/links/link/index.ts
@@ -0,0 +1 @@
+export * from './link';
diff --git a/src/components/atoms/links/link/link.module.scss b/src/components/atoms/links/link/link.module.scss
new file mode 100644
index 0000000..8f94a54
--- /dev/null
+++ b/src/components/atoms/links/link/link.module.scss
@@ -0,0 +1,70 @@
+@use "../../../../styles/abstracts/placeholders";
+
+.link {
+ color: var(--color-primary);
+
+ &--regular {
+ @extend %link;
+ }
+
+ &[hreflang],
+ &--download,
+ &--external {
+ @extend %link-with-icon;
+ }
+
+ &[hreflang]:not(#{&}--download):not(#{&}--external) {
+ --is-icon-hidden: "";
+ }
+
+ &[hreflang] {
+ @extend %link-with-lang;
+ }
+
+ &--download {
+ @extend %download-link;
+ }
+
+ &--external {
+ @extend %external-link;
+ }
+
+ &--download,
+ &--external {
+ &:not([hreflang]) {
+ --is-lang-hidden: "";
+ }
+ }
+
+ &--external#{&}--download {
+ @extend %external-download-link;
+ }
+}
+
+:global([data-theme="light"]) {
+ :local {
+ .link {
+ &--download {
+ @extend %light-download-link;
+ }
+
+ &--external {
+ @extend %light-external-link;
+ }
+ }
+ }
+}
+
+:global([data-theme="dark"]) {
+ :local {
+ .link {
+ &--download {
+ @extend %dark-download-link;
+ }
+
+ &--external {
+ @extend %dark-external-link;
+ }
+ }
+ }
+}
diff --git a/src/components/atoms/links/link/link.stories.tsx b/src/components/atoms/links/link/link.stories.tsx
new file mode 100644
index 0000000..0a6a6c5
--- /dev/null
+++ b/src/components/atoms/links/link/link.stories.tsx
@@ -0,0 +1,178 @@
+import type { ComponentMeta, ComponentStory } from '@storybook/react';
+import { Link } from './link';
+
+/**
+ * Link - Storybook Meta
+ */
+export default {
+ title: 'Atoms/Links/Link',
+ component: Link,
+ argTypes: {
+ children: {
+ control: {
+ type: 'text',
+ },
+ description: 'The link body.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ href: {
+ control: {
+ type: 'text',
+ },
+ description: 'The link target.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ isDownload: {
+ control: {
+ type: 'boolean',
+ },
+ description: 'Determine if the link purpose is to download a file.',
+ table: {
+ category: 'Options',
+ defaultValue: { summary: false },
+ },
+ type: {
+ name: 'boolean',
+ required: false,
+ },
+ },
+ isExternal: {
+ control: {
+ type: 'boolean',
+ },
+ description: 'Determine if the link is external of the current website.',
+ table: {
+ category: 'Options',
+ defaultValue: { summary: false },
+ },
+ type: {
+ name: 'boolean',
+ required: false,
+ },
+ },
+ lang: {
+ control: {
+ type: 'text',
+ },
+ table: {
+ category: 'Options',
+ },
+ description: 'The target language as code language.',
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ },
+} as ComponentMeta<typeof Link>;
+
+const Template: ComponentStory<typeof Link> = (args) => <Link {...args} />;
+
+/**
+ * Links Stories - Default
+ */
+export const Default = Template.bind({});
+Default.args = {
+ children: 'A link',
+ href: '#',
+};
+
+/**
+ * Links Stories - Download
+ */
+export const DownloadLink = Template.bind({});
+DownloadLink.args = {
+ children: 'A link to a file',
+ href: '#',
+ isDownload: true,
+ isExternal: false,
+};
+
+/**
+ * Links Stories - Download link with lang
+ */
+export const DownloadLinkWithLang = Template.bind({});
+DownloadLinkWithLang.args = {
+ children: 'A link to a file',
+ href: '#',
+ isDownload: true,
+ isExternal: false,
+ lang: 'en',
+};
+
+/**
+ * Links Stories - External
+ */
+export const ExternalLink = Template.bind({});
+ExternalLink.args = {
+ children: 'A link',
+ href: '#',
+ isDownload: false,
+ isExternal: true,
+};
+
+/**
+ * Links Stories - External download link
+ */
+export const ExternalDownload = Template.bind({});
+ExternalDownload.args = {
+ children: 'A link',
+ href: '#',
+ isDownload: true,
+ isExternal: true,
+};
+
+/**
+ * Links Stories - External link with Lang
+ */
+export const ExternalLinkWithLang = Template.bind({});
+ExternalLinkWithLang.args = {
+ children: 'A link',
+ href: '#',
+ isDownload: false,
+ isExternal: true,
+ lang: 'en',
+};
+
+/**
+ * Links Stories - External download with lang
+ */
+export const ExternalDownloadWithLang = Template.bind({});
+ExternalDownloadWithLang.args = {
+ children: 'A link',
+ href: '#',
+ isDownload: true,
+ isExternal: true,
+ lang: 'en',
+};
+
+/**
+ * Links Stories - With Lang
+ */
+export const LinkLang = Template.bind({});
+LinkLang.args = {
+ children: 'A link',
+ href: '#',
+ isDownload: false,
+ isExternal: false,
+ lang: 'en',
+};
diff --git a/src/components/atoms/links/link/link.test.tsx b/src/components/atoms/links/link/link.test.tsx
new file mode 100644
index 0000000..ad1951d
--- /dev/null
+++ b/src/components/atoms/links/link/link.test.tsx
@@ -0,0 +1,47 @@
+import { describe, expect, it } from '@jest/globals';
+import { render, screen as rtlScreen } from '@testing-library/react';
+import { Link } from './link';
+
+describe('Link', () => {
+ it('renders a link', () => {
+ const anchor = 'porro';
+ const target = '/tempora';
+
+ render(<Link href={target}>{anchor}</Link>);
+
+ expect(rtlScreen.getByRole('link', { name: anchor })).toHaveAttribute(
+ 'href',
+ target
+ );
+ });
+
+ it('can render an external link', () => {
+ const anchor = 'accusamus';
+ const target = 'https://www.example.com';
+
+ render(
+ <Link href={target} isExternal>
+ {anchor}
+ </Link>
+ );
+
+ expect(rtlScreen.getByRole('link', { name: anchor })).toHaveClass(
+ 'link--external'
+ );
+ });
+
+ it('can render a download link', () => {
+ const anchor = 'dolor';
+ const target = '/officiis.pdf';
+
+ render(
+ <Link href={target} isDownload>
+ {anchor}
+ </Link>
+ );
+
+ expect(rtlScreen.getByRole('link', { name: anchor })).toHaveClass(
+ 'link--download'
+ );
+ });
+});
diff --git a/src/components/atoms/links/link/link.tsx b/src/components/atoms/links/link/link.tsx
new file mode 100644
index 0000000..e88cc7c
--- /dev/null
+++ b/src/components/atoms/links/link/link.tsx
@@ -0,0 +1,90 @@
+import NextLink from 'next/link';
+import {
+ forwardRef,
+ type AnchorHTMLAttributes,
+ type ForwardRefRenderFunction,
+ type ReactNode,
+} from 'react';
+import styles from './link.module.scss';
+
+export type LinkProps = Omit<
+ AnchorHTMLAttributes<HTMLAnchorElement>,
+ 'children' | 'download' | 'hrefLang' | 'lang'
+> & {
+ /**
+ * The link body.
+ */
+ children: ReactNode;
+ /**
+ * Should we disable the default transition on links?
+ *
+ * @default false
+ */
+ disableTransition?: boolean;
+ /**
+ * True if it is a download link.
+ *
+ * @default false
+ */
+ isDownload?: boolean;
+ /**
+ * True if it is an external link.
+ *
+ * @default false
+ */
+ isExternal?: boolean;
+ /**
+ * The link target.
+ */
+ href: string;
+ /**
+ * The link target code language.
+ */
+ lang?: string;
+};
+
+const LinkWithRef: ForwardRefRenderFunction<HTMLAnchorElement, LinkProps> = (
+ {
+ children,
+ className = '',
+ disableTransition = false,
+ isDownload = false,
+ isExternal = false,
+ href,
+ lang,
+ rel = '',
+ ...props
+ },
+ ref
+) => {
+ const LinkComponent = isExternal ? 'a' : NextLink;
+ const linkClass = [
+ styles.link,
+ styles[disableTransition ? '' : 'link--regular'],
+ styles[isDownload ? 'link--download' : ''],
+ styles[isExternal ? 'link--external' : ''],
+ className,
+ ].join(' ');
+ const linkRel =
+ isExternal && !rel.includes('external') ? `external ${rel}` : rel;
+
+ return (
+ <LinkComponent
+ {...props}
+ className={linkClass}
+ href={href}
+ hrefLang={lang}
+ ref={ref}
+ rel={linkRel}
+ >
+ {children}
+ </LinkComponent>
+ );
+};
+
+/**
+ * Link Component
+ *
+ * Render a link.
+ */
+export const Link = forwardRef(LinkWithRef);