Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: added link component #321

Merged
merged 9 commits into from
Nov 20, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/weak-weeks-rush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@lux-design-system/components-react": minor
---

Nieuw component: 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,3 +11,4 @@ export {
type LuxHeadingProps,
} from './heading/Heading';
export { LuxParagraph, type LuxParagraphProps } from './paragraph/Paragraph';
export { LuxLink, type LuxLinkProps } from './link/Link';
43 changes: 43 additions & 0 deletions packages/components-react/src/link/Link.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
.lux-link {
display: inline-flex;
MrSkippy marked this conversation as resolved.
Show resolved Hide resolved
gap: var(--lux-link-column-gap);
color: var(--lux-link-color);
font-weight: var(--lux-link-font-weight);
font-size: var(--lux-link-font-size);
line-height: var(--lux-link-line-height);
font-family: var(--lux-link-font-family);
text-decoration: var(--lux-link-text-decoration);

&:active {
color: var(--lux-link-active-color);
text-decoration: var(--lux-link-active-text-decoration);
}

&:focus {
background-color: var(--lux-link-focus-background-color);
color: var(--lux-link-focus-color);
text-decoration: var(--lux-link-focus-text-decoration);
}

&:hover {
color: var(--lux-link-hover-color);
text-decoration: var(--lux-link-hover-text-decoration);
}

&:visited {
color: var(--lux-link-visited-color);
}
}

.lux-link__icon {
inline-size: var(--lux-link-icon-size);
block-size: var(--lux-link-icon-size);
}

.lux-link-icon--start {
order: 0;
MrSkippy marked this conversation as resolved.
Show resolved Hide resolved
}

.lux-link-icon--end {
order: 1;
}
82 changes: 82 additions & 0 deletions packages/components-react/src/link/Link.tsx
MrSkippy marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import {
Link as UtrechtLink,
type LinkProps as UtrechtLinkProps,
} from '@utrecht/component-library-react/dist/css-module';
import clsx from 'clsx';
import React, { ReactElement } from 'react';
import './Link.css';

type IconPosition = 'start' | 'end';

export type LuxLinkProps = UtrechtLinkProps & {
external?: boolean;
placeholder?: boolean;
icon?: ReactElement;
iconPosition?: IconPosition;
};

const MODIFIER_CLASSNAMES = {
external: 'utrecht-link--external',
placeholder: 'utrecht-link--placeholder',
};

const ICON_POSITIONS: { [key: string]: string } = {
start: 'lux-link-icon--start',
end: 'lux-link-icon--end',
};

export const LuxLink = (props: LuxLinkProps) => {
const {
external = false,
placeholder = false,
className: providedClassName = '',
children,
icon: iconNode,
iconPosition: providedIconPosition,
...otherProps
} = props;

// Set default icon position to 'start' if there's an icon but no position specified
const iconPosition = iconNode ? providedIconPosition || 'start' : undefined;

const className = clsx('utrecht-link', 'utrecht-link--html-a', 'lux-link', providedClassName, {
[MODIFIER_CLASSNAMES.external]: external,
[MODIFIER_CLASSNAMES.placeholder]: placeholder,
});

const positionedIcon = React.Children.map(iconNode, (iconElement) => {
if (!iconElement) {
return null;
}

if (!React.isValidElement<HTMLElement>(iconElement)) {
return iconElement;
}

return React.cloneElement(iconElement as ReactElement, {
className: clsx('lux-link__icon', iconElement?.props?.className, iconPosition && ICON_POSITIONS[iconPosition]),
});
});

// const content = (
MrSkippy marked this conversation as resolved.
Show resolved Hide resolved
// <>
// {iconPosition === 'start' && positionedIcon}
// <span className="utrecht-link__text">{children}</span>
// {iconPosition === 'end' && positionedIcon}
// </>
// );

const externalProps = external
? {
target: '_blank',
rel: 'external noopener noreferrer',
}
: {};

return (
<UtrechtLink {...otherProps} {...externalProps} className={className}>
{positionedIcon}
<span className="utrecht-link__text">{children}</span>
</UtrechtLink>
);
};
64 changes: 64 additions & 0 deletions packages/components-react/src/link/test/Link.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { describe, expect, it } from '@jest/globals';
import { render, screen } from '@testing-library/react';
import { LuxLink } from '../Link';

const ExampleIcon = (
<svg height="16" width="16" xmlns="http://www.w3.org/2000/svg">
<circle r="6" cx="7" cy="7" fill="currentColor" />
</svg>
);

describe('Link', () => {
it('renders a link', () => {
render(<LuxLink href="#">Test Link</LuxLink>);

const link = screen.getByRole('link', {
name: 'Test Link',
});
expect(link).toBeInTheDocument();
});

it('renders an external link with correct attributes', () => {
render(
<LuxLink href="https://example.com" external>
External Link
</LuxLink>,
);

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

expect(link).toHaveAttribute('target', '_blank');
expect(link).toHaveAttribute('rel', 'external noopener noreferrer');
});

it('renders a link with an icon', () => {
render(
<LuxLink href="#" icon={ExampleIcon}>
Link with Icon
</LuxLink>,
);

const link = screen.getByRole('link', {
name: 'Link with Icon',
});

expect(link.querySelector('svg')).toBeInTheDocument();
});

it('renders a link with correct language attributes', () => {
render(
<LuxLink href="#" hrefLang="nl" lang="nl">
Nederlandse Link
</LuxLink>,
);

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

expect(link).toHaveAttribute('hrefLang', 'nl');
expect(link).toHaveAttribute('lang', 'nl');
});
});
68 changes: 68 additions & 0 deletions packages/storybook/src/react-components/link/link.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { Canvas, Controls, Markdown, Meta } from "@storybook/blocks";
import markdown from "@utrecht/link-css/README.md?raw";
import * as LinkStories from "./link.stories.tsx";
import { CitationDocumentation } from "../../utils/CitationDocumentation.tsx";

<Meta of={LinkStories} />

# Link

<CitationDocumentation
component="Utrecht Link"
url="https://nl-design-system.github.io/utrecht/storybook-css/index.html?path=/docs/css-link--docs"
/>

<Markdown>{markdown}</Markdown>

## Playground

Experimenteer met de verschillende mogelijkheden van de Link component:

<Canvas of={LinkStories.Playground} />
<Controls of={LinkStories.Playground} />

## Variants

### Standaard Link

Een basis link zonder extra toevoegingen.

<Canvas of={LinkStories.Playground} />

### External Link

Een externe link opent in een nieuw tabblad en heeft de juiste security attributes.

<Canvas of={LinkStories.External} />

## States

Links kunnen verschillende states hebben voor interactie:

### Hover

<Canvas of={LinkStories.Hover} />

### Active

<Canvas of={LinkStories.Active} />

### Focus

<Canvas of={LinkStories.Focus} />

## Link met Icoon

Links kunnen worden verrijkt met een icoon voor extra visuele context.

### Default Positie

<Canvas of={LinkStories.LinkWithIcon} />

### Icoon aan het Begin

<Canvas of={LinkStories.LinkWithIconStart} />

### Icoon aan het Einde

<Canvas of={LinkStories.LinkWithIconEnd} />
Loading