Skip to content
114 changes: 114 additions & 0 deletions packages/pxweb2-ui/src/lib/components/LinkCard/LinkCard.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
@use '../../../../style-dictionary/dist/scss/fixed-variables.scss' as fixed;
@use '../../text-styles.scss';

.link-card-medium {
display: flex;
padding: fixed.$spacing-4 fixed.$spacing-5;
align-items: flex-start;
gap: 1rem;

border-radius: var(--px-border-radius-large);
border: 1px solid var(--px-color-border-subtle);
background: var(--px-color-surface-default);
position: relative;
&:hover {
outline: 1px solid var(--px-color-border-subtle);
h1,
h2,
h3,
h4,
h5,
h6,
span {
@extend .heading-underline;
}
cursor: pointer;
}
&:hover .arrow-wrapper {
padding-inline-start: 8px;
transition: 100ms;
}
&:focus-visible {
outline: 3px solid var(--px-color-border-focus-outline);
outline-offset: 6px;
box-shadow: 0 0 0 3px var(--px-color-border-focus-boxshadow);
}
}

.link-card-small {
display: flex;
padding: fixed.$spacing-3 fixed.$spacing-4;
align-items: flex-start;
gap: 0.75rem;

border-radius: var(--px-border-radius-medium);
border: 1px solid var(--px-color-border-subtle);
background: var(--px-color-surface-default);
position: relative;
&:hover {
outline: 1px solid var(--px-color-border-subtle);
h1,
h2,
h3,
h4,
h5,
h6,
span {
@extend .heading-underline;
}
cursor: pointer;
}
&:hover .arrow-wrapper {
padding-inline-start: 8px;
transition: 100ms;
}
&:focus-visible {
outline: 3px solid var(--px-color-border-focus-outline);
outline-offset: 6px;
box-shadow: 0 0 0 3px var(--px-color-border-focus-boxshadow);
}
}
.content-wrapper {
display: inline-flex;
flex-direction: column;
row-gap: 0.25rem;
align-self: center;
}

.icon-wrapper {
display: flex;
width: 44px;
height: 44px;
padding: 0.625rem;
justify-content: center;
align-items: center;
aspect-ratio: 1/1;
background: var(--px-color-surface-moderate);

border-radius: var(--px-border-radius-medium);
}

.heading-wrapper {
align-self: stretch;
}

.child-wrapper {
display: flex;
align-items: flex-start;
justify-content: flex-start;
}
.arrow-wrapper {
display: flex;
align-items: center;
justify-content: center;
align-self: center;
width: 32px;
height: 32px;
border: none;
background-color: transparent;
margin-left: auto;
}
a {
text-decoration: none;
color: inherit;
}
101 changes: 101 additions & 0 deletions packages/pxweb2-ui/src/lib/components/LinkCard/LinkCard.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { LinkCard, LinkCardProps } from './LinkCard';

const meta: Meta<typeof LinkCard> = {
component: LinkCard,
title: 'Components/LinkCard',
};
export default meta;

type Story = StoryObj<typeof LinkCard>;

export const Default: Story = {
args: {
headingText: 'Heading Link card with header',
description: 'This is a medium link card with heading and description.',
href: 'https://www.ssb.no ',
icon: 'Book',
},
render: (args: LinkCardProps) => <LinkCard {...args} />,
};

export const WithoutHeading: Story = {
args: {
icon: 'Book',
headingText: 'Heading Link card with header',
description:
'This is a medium link card without heading, but with description.',
href: 'https://www.ssb.no ',
},
render: (args) => <LinkCard {...args} />,
};

export const WithHeadingMedium: Story = {
args: {
headingText: 'Heading Link card with header level 2',
icon: 'Book',
description: 'This is a medium link card with heading and description.',
headingType: 'h2',
href: 'https://www.ssb.no ',
size: 'medium',
},
render: (args: LinkCardProps) => <LinkCard {...args} />,
};

export const WithHeadingSmall: Story = {
args: {
headingText: 'Heading Link card with header level 2',
icon: 'Book',
description: 'This is a small link card with heading and description.',
headingType: 'h2',
size: 'small',
href: 'https://www.ssb.no ',
newTab: true,
},
render: (args: LinkCardProps) => <LinkCard {...args} />,
};

export const WithoutDescriptionSmall: Story = {
args: {
headingText: 'Heading Link card with header level 2',
icon: 'Book',
headingType: 'h2',
size: 'small',
href: 'https://www.ssb.no ',
newTab: true,
},
render: (args: LinkCardProps) => <LinkCard {...args} />,
};

export const WithSpanHeadingMedium: Story = {
args: {
headingText: 'Heading Link card with header span',
icon: 'Book',
description: 'This is a medium link card with heading and description.',
headingType: 'span',
href: 'https://www.ssb.no ',
size: 'medium',
},
render: (args: LinkCardProps) => <LinkCard {...args} />,
};
export const WithSpanHeadingSmall: Story = {
args: {
headingText: 'Heading Link card with header span',
icon: 'Book',
description: 'This is a small link card with heading and description.',
href: 'https://www.ssb.no ',
headingType: 'span',
size: 'small',
},
render: (args: LinkCardProps) => <LinkCard {...args} />,
};
export const WithoutIcon: Story = {
args: {
headingText: 'Heading Link card without icon',
description: 'This is a small link card with heading and description.',
href: 'https://www.ssb.no ',
headingType: 'span',
size: 'small',
},
render: (args: LinkCardProps) => <LinkCard {...args} />,
};
126 changes: 126 additions & 0 deletions packages/pxweb2-ui/src/lib/components/LinkCard/LinkCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import cl from 'clsx';
import React from 'react';
import styles from './LinkCard.module.scss';
import BodyLong from '../Typography/BodyLong/BodyLong';
import BodyShort from '../Typography/BodyShort/BodyShort';
import { getIconDirection } from '../../util/util';

import { Heading, Icon, IconProps } from '@pxweb2/pxweb2-ui';

export interface LinkCardProps {
icon?: IconProps['iconName'];
headingText: string;
description?: string;
href: string;
newTab?: boolean;
headingType?: 'span' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
size?: 'small' | 'medium';
readonly languageDirection?: 'ltr' | 'rtl';
}

export function LinkCard({
icon,
headingText,
description,
href,
newTab = true,
headingType = 'span',
size = 'medium',
languageDirection = 'ltr',
}: LinkCardProps) {
const iconArrow = getIconDirection(
languageDirection,
'ArrowRight',
'ArrowLeft',
);

let headingLevel: '2' | '3' | '4' | '5' | '6' | undefined;
switch (headingType) {
case 'h2':
headingLevel = '2';
break;
case 'h3':
headingLevel = '3';
break;
case 'h4':
headingLevel = '4';
break;
case 'h5':
headingLevel = '5';
break;
case 'h6':
headingLevel = '6';
break;
default:
headingLevel = undefined;
}

const headingSize = size === 'small' ? 'xsmall' : 'small';

const handleClick = () => {
const link = document.createElement('a');
link.href = href;
link.rel = 'noopener noreferrer';
if (newTab) {
link.target = '_blank';
}
link.click();
};

const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
handleClick();
}
};

return (
<div
className={cl(styles[`link-card-${size}`])}
onClick={handleClick}
onKeyDown={handleKeyDown}
tabIndex={0}
aria-labelledby={`${headingText}${description ? ` ${description}` : ''}`}
role="link"
>
{icon && (
<div className={cl(styles['icon-wrapper'])}>
<Icon iconName={icon} />
</div>
)}
<div className={styles['content-wrapper']}>
{headingText &&
(headingType === 'span' ? (
<span
className={cl(styles['heading-wrapper'], [
styles[`heading-${headingSize}`],
])}
>
{headingText}
</span>
) : (
<Heading
size={headingSize}
level={headingLevel}
className={cl(styles['heading-wrapper'])}
>
{headingText}
</Heading>
))}
{description && (
<div className={styles['child-wrapper']}>
{size === 'medium' && (
<BodyLong size="medium">{description}</BodyLong>
)}
{size === 'small' && (
<BodyShort size="medium">{description}</BodyShort>
)}
</div>
)}
</div>
<div className={cl(styles['arrow-wrapper'])}>
<Icon iconName={iconArrow} />
</div>
</div>
);
}
15 changes: 12 additions & 3 deletions packages/pxweb2-ui/src/lib/components/SideSheet/SideSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ import { useEffect, useRef, useState } from 'react';
import classes from './SideSheet.module.scss';
import Heading from '../Typography/Heading/Heading';
import Button from '../Button/Button';
import { LinkCard } from '../LinkCard/LinkCard';

export interface SideSheetProps {
readonly heading: string;
readonly closeLabel?: string;
readonly isOpen: boolean;
readonly onClose?: () => void;
readonly className?: string;
readonly children: React.ReactNode;
// readonly children: React.ReactNode;
}

export function SideSheet({
Expand All @@ -20,7 +21,7 @@ export function SideSheet({
isOpen,
onClose,
className = '',
children,
// children,
}: SideSheetProps) {
const cssClasses = className.length > 0 ? ' ' + className : '';
const [isSideSheetOpen, setIsSideSheetOpen] = useState(isOpen);
Expand Down Expand Up @@ -87,7 +88,15 @@ export function SideSheet({
></Button>
</div>
</div>
<div className={cl(classes.content)}>{children}</div>
<div className={cl(classes.content)}>
{' '}
<LinkCard
headingText="Link card"
description="This is a small link card with heading and description."
href="#"
size="small"
/>
</div>
</aside>
</dialog>
);
Expand Down
Loading