Skip to content

Commit

Permalink
Feature/link (#520)
Browse files Browse the repository at this point in the history
Closes #474 
Closes #448

---------

Co-authored-by: Ruben Smit <[email protected]>
  • Loading branch information
2 people authored and Mees Work committed Aug 22, 2024
1 parent 5979129 commit a3ee4dd
Show file tree
Hide file tree
Showing 12 changed files with 247 additions and 33 deletions.
13 changes: 5 additions & 8 deletions apps/rhc-templates/src/app/collage/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import { AccordionProvider, Paragraph } from '@rijkshuisstijl-community/components-react';
import { AccordionProvider, Link, Paragraph } from '@rijkshuisstijl-community/components-react';
import { Avatar, Pagination } from '@amsterdam/design-system-react';
import {
IconAlertTriangle,
Expand Down Expand Up @@ -28,7 +28,6 @@ import {
FormLabel,
Heading,
Image,
Link,
LinkList,
LinkListLink,
OrderedList,
Expand Down Expand Up @@ -119,12 +118,10 @@ export default function Collage() {
<div className="unstarted">
<Separator></Separator>
</div>
<div className="unstarted">
<Link href="#">
{`Hello, I'm a link `}
<IconArrowRight />
</Link>
</div>
<Link href="#">
{`Hello, I'm a link `}
<IconArrowRight />
</Link>
<div className="unstarted">
<Button appearance="primary-action-button">Primary button</Button>
</div>
Expand Down
9 changes: 2 additions & 7 deletions apps/rhc-templates/src/app/form/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import { AccordionProvider, Logo, Paragraph } from '@rijkshuisstijl-community/components-react';
import { AccordionProvider, Link, Logo, Paragraph } from '@rijkshuisstijl-community/components-react';
import { FormLabel, Heading } from '@utrecht/component-library-react';
import {
BreadcrumbNav,
Expand All @@ -10,7 +10,6 @@ import {
Fieldset,
FieldsetLegend,
FormField,
Link,
LinkList,
LinkListLink,
PageContent,
Expand Down Expand Up @@ -68,11 +67,7 @@ export default function Form() {
</div>
<br />
<Paragraph>
Lees verder over de{' '}
<span className="unstarted">
<Link href="#">verantwoordingsmethode SiSa</Link>
</span>
.
Lees verder over de <Link href="#">verantwoordingsmethode SiSa</Link>.
</Paragraph>
<form>
<div className="unstarted">
Expand Down
15 changes: 5 additions & 10 deletions apps/rhc-templates/src/app/page/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
ButtonLink,
Heading,
Icon,
Link,
LinkList,
LinkListLink,
OrderedList,
Expand All @@ -31,7 +30,7 @@ import {
Figure,
FigureCaption,
} from '@utrecht/component-library-react/dist/css-module';
import { ActionGroup, Logo, Paragraph } from '@rijkshuisstijl-community/components-react';
import { ActionGroup, Link, Logo, Paragraph } from '@rijkshuisstijl-community/components-react';
import { HeadingGroup } from '@utrecht/component-library-react';

export default function Page() {
Expand Down Expand Up @@ -122,11 +121,9 @@ export default function Page() {
<Heading level={3}>Dit is een H3</Heading>
</div>
<Paragraph>Dit is een paragraaf.</Paragraph>
<div className="unstarted">
<Link external={true} href="example.com">
Dit is een externe link
</Link>
</div>
<Link external={true} href="example.com">
Dit is een externe link
</Link>
<div className="unstarted">
<Table>
<TableCaption>Caption van tabel</TableCaption>
Expand Down Expand Up @@ -162,9 +159,7 @@ export default function Page() {
laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit
esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui
officia deserunt mollit anim id est laborum.
<span className="unstarted">
<Link href="/">Dit is een normale link</Link>
</span>
<Link href="/">Dit is een normale link</Link>
</Paragraph>
<div className="unstarted">
<Blockquote attribution="Label">
Expand Down
2 changes: 2 additions & 0 deletions packages/components-css/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,7 @@
* @license EUPL-1.2
* Copyright (c) 2021 Community for NL Design System
*/

@import "accordion/index";
@import "link/index";
@import "logo/index";
10 changes: 10 additions & 0 deletions packages/components-css/link/index.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* @license EUPL-1.2
* Copyright (c) 2021 Community for NL Design System
*/

.utrecht-link:any-link {
align-items: center;
column-gap: var(--utrecht-link-column-gap, inherit);
display: inline-flex;
}
145 changes: 145 additions & 0 deletions packages/components-react/src/Link.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { render, screen } from '@testing-library/react';
import { createRef } from 'react';
import { Link } from './Link';
import '@testing-library/jest-dom';

describe('Link', () => {
it('renders an link role element', () => {
render(<Link href="/">Home</Link>);

const link = screen.getByRole('link', { name: 'Home' });

expect(link).toBeInTheDocument();
expect(link).toBeVisible();
});

it('renders an HTML a element', () => {
const { container } = render(<Link>{'https://example.com/'}</Link>);

const link = container.querySelector('a:only-child');

expect(link).toBeInTheDocument();
});

it('renders a design system BEM class name', () => {
const { container } = render(<Link />);

const link = container.querySelector(':only-child');

expect(link).toHaveClass('utrecht-link');
});

it('renders rich text content', () => {
const { container } = render(
<Link href="https://example.com/">
<strong>https:</strong>
{'//example.com/'}
</Link>,
);

const link = container.querySelector(':only-child');

const richText = link?.querySelector('strong');

expect(richText).toBeInTheDocument();
});

it('can be hidden', () => {
const { container } = render(<Link hidden />);

const link = container.querySelector(':only-child');

expect(link).not.toBeVisible();
});

it('can have a custom class name', () => {
const { container } = render(<Link className="visited">{'https://example.com/'}</Link>);

const link = container.querySelector(':only-child');

expect(link).toHaveClass('visited');
});
it('can have a additional class name', () => {
const { container } = render(<Link className="large" />);

const link = container.querySelector(':only-child');

expect(link).toHaveClass('large');

expect(link).toHaveClass('utrecht-link');
});
it('supports ForwardRef in React', () => {
const ref = createRef<HTMLAnchorElement>();

const { container } = render(<Link ref={ref}>{'https://example.com/'}</Link>);

const link = container.querySelector(':only-child');

expect(ref.current).toBe(link);
});

describe('variant for external links', () => {
it('renders a design system BEM class name', () => {
const { container } = render(<Link external />);

const link = container.querySelector(':only-child');

expect(link).toHaveClass('utrecht-link--external');
});

it('prevents sharing referer information', () => {
const { container } = render(<Link external />);

const link = container.querySelector(':only-child');

expect(link).toHaveAttribute('rel');

expect(link?.getAttribute('rel')).toContain('noreferrer');
});

it('prevents access to window.opener', () => {
const { container } = render(<Link external />);

const link = container.querySelector(':only-child');

expect(link).toHaveAttribute('rel');

expect(link?.getAttribute('rel')).toContain('noopener');
});

it('provides semantic information that the link is external', () => {
const { container } = render(<Link external />);

const link = container.querySelector(':only-child');

expect(link).toHaveAttribute('rel');

expect(link?.getAttribute('rel')).toContain('external');
});
describe('External link icon', () => {
it('contains a visual icon that the link is external', () => {
const { container } = render(<Link external />);

const link = container.querySelector(':only-child');

const icon = link?.querySelector('.utrecht-icon');

expect(icon).toBeInTheDocument();
});

it('contains a visual icon that has an aria-label that comes from a property', () => {
const { container } = render(<Link external externalLabel="some-external-label" />);

const link = container.querySelector(':only-child');

const icon = link?.querySelector('.utrecht-icon');

expect(icon).toBeInTheDocument();

expect(icon).toHaveAttribute('aria-label');

expect(icon?.getAttribute('aria-label')).toContain('some-external-label');
});
});
});
});
37 changes: 37 additions & 0 deletions packages/components-react/src/Link.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import {
Icon,
Link as UtrechtLink,
type LinkProps as UtrechtLinkProps,
} from '@utrecht/component-library-react/dist/css-module';
import { ForwardedRef, forwardRef } from 'react';
import '@rijkshuisstijl-community/components-css/index.scss';

const IconExternalLink = () => (
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M12 2C11.4477 2 11 1.55228 11 1C11 0.447715 11.4477 0 12 0H17C17.5523 0 18 0.447715 18 1V6C18 6.55228 17.5523 7 17 7C16.4477 7 16 6.55228 16 6V3.41421L7.70711 11.7071C7.31658 12.0976 6.68342 12.0976 6.29289 11.7071C5.90237 11.3166 5.90237 10.6834 6.29289 10.2929L14.5858 2H12ZM0 6C0 4.34315 1.34315 3 3 3H8C8.55228 3 9 3.44772 9 4C9 4.55228 8.55228 5 8 5H3C2.44772 5 2 5.44772 2 6V15C2 15.5523 2.44772 16 3 16H12C12.5523 16 13 15.5523 13 15V10C13 9.44771 13.4477 9 14 9C14.5523 9 15 9.44771 15 10V15C15 16.6569 13.6569 18 12 18H3C1.34315 18 0 16.6569 0 15V6Z"
fill="#01689B"
/>
</svg>
);

interface RhcLinkProps extends UtrechtLinkProps {
externalLabel?: string;
}

export const Link = forwardRef(
({ children, externalLabel, ...restProps }: RhcLinkProps, ref: ForwardedRef<HTMLAnchorElement>) => (
<UtrechtLink {...restProps} ref={ref}>
{children}
{restProps.external && (
<Icon role="img" aria-label={externalLabel}>
<IconExternalLink />
</Icon>
)}
</UtrechtLink>
),
);

Link.displayName = 'Link';
1 change: 1 addition & 0 deletions packages/components-react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export {
type AccordionSectionProps,
type AccordionSectionProviderProps,
} from './Accordion';
export { Link } from './Link';
export { ActionGroup } from './ActionGroup';
export { Logo } from './Logo';
export type { LogoProps } from './Logo';
1 change: 1 addition & 0 deletions packages/storybook/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"@storybook/react-vite": "8.2.4",
"@storybook/testing-library": "0.2.2",
"@storybook/theming": "8.2.4",
"@tabler/icons-react": "3.11.0",
"@types/react": "18.3.3",
"@types/react-dom": "18.3.0",
"@utrecht/component-library-css": "4.0.0",
Expand Down
42 changes: 35 additions & 7 deletions packages/storybook/src/community/link.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,30 @@
/* @license CC0-1.0 */

import { RhcIconArrowRight, RhcIconCalendar } from '@rijkshuisstijl-community/web-components-react';
import { Link } from '@rijkshuisstijl-community/components-react';
import type { Meta, StoryObj } from '@storybook/react';
import { Icon, Link } from '@utrecht/component-library-react/dist/css-module';
import { IconArrowRight, IconCalendarEvent } from '@tabler/icons-react';
import { Icon } from '@utrecht/component-library-react/dist/css-module';
import { PropsWithChildren } from 'react';
import readme from './link.md?raw';
interface LinkStoryProps {
href: string;
iconLeft?: boolean;
iconRight?: boolean;
external?: boolean;
}

const LinkStory = ({ href, children, iconLeft, iconRight, ...props }: PropsWithChildren<LinkStoryProps>) => (
<Link href={href} {...props}>
const LinkStory = ({ href, children, iconLeft, iconRight, external, ...props }: PropsWithChildren<LinkStoryProps>) => (
<Link href={href} external={external} {...props}>

{iconLeft && (
<Icon>
<RhcIconCalendar></RhcIconCalendar>
<IconCalendarEvent />
</Icon>
)}
{children}
{iconRight && (
<Icon>
<RhcIconArrowRight></RhcIconArrowRight>
<IconArrowRight />
</Icon>
)}
</Link>
Expand All @@ -42,6 +45,24 @@ const meta = {
category: 'Property',
},
},
external: {
description: 'Whether the link is external',
type: {
name: 'boolean',
},
table: {
category: 'Property',
},
},
externalLabel: {
description: 'Aria label for external link icon',
type: {
name: 'string',
},
table: {
category: 'Property',
},
},
children: {
description: 'Link text - default webcomponent slot',
type: {
Expand Down Expand Up @@ -113,5 +134,12 @@ export const IconRight: Story = {
children: 'Label',
iconRight: true,
},
name: 'Link',
};

export const External: Story = {
args: {
href: 'https://example.com/',
children: 'Label',
external: true,
},
};
Loading

0 comments on commit a3ee4dd

Please sign in to comment.