Skip to content

Commit

Permalink
feat: new studio component StudioIconCard
Browse files Browse the repository at this point in the history
Component that displays a card with an icon, description and optional
ellipsis menu.
Created to be used on the new navigation page for Form Designer.

commit-id:fe161544
  • Loading branch information
Jondyr committed Mar 4, 2025
1 parent 90b6bbd commit ca5b007
Show file tree
Hide file tree
Showing 4 changed files with 223 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
.card {
height: 244px;
width: 244px;
min-width: 244px;

transition:
0.2s transform,
0.2s box-shadow;

background-color: var(--fds-semantic-background-default);
box-shadow: none;
}

/* .card:hover { */
/* box-shadow: var(--fds-shadow-medium); */
/* transform: translateY(-2px); */
/* background-color: var(--fds-semantic-surface-info-subtle-hover); */
/* border: var(--fds-border_radius-small); */
/* border-style: solid; */
/* border-color: var(--fds-semantic-surface-action-checked); */
/* } */

.popoverContent {
display: flex;
flex-direction: column;
align-items: flex-start;
}

.popoverContent button {
width: 100%;
justify-content: left;
}

.link {
padding: 0px;
display: block;
height: 100%;
}

.card:not(:hover) .editIcon {
display: none;
}

.editIcon {
padding: 0px;
position: absolute;
border-radius: var(--fds-border_radius-full);
right: var(--fds-spacing-1);
top: var(--fds-spacing-1);
}

.iconContainer {
height: var(--fds-spacing-26);
display: flex;
align-items: center;
}

.iconBackground {
height: var(--fds-sizing-12);
width: var(--fds-sizing-12);
transform: rotate(45deg);
margin: 0 auto;
display: flex;
align-items: center;
justify-content: center;
}

.iconBackground svg,
.iconBackground img {
transform: rotate(-45deg);
font-size: var(--fds-sizing-9);
color: #4b5563;
}

.blue {
background-color: var(--fds-colors-blue-100);
color: #23262a;
}

.red {
background-color: var(--fds-colors-red-100);
color: #23262a;
}

.green {
background-color: var(--fds-colors-green-200);
color: #23262a;
}

.grey {
background-color: var(--fds-colors-grey-200);
color: #23262a;
}

.yellow {
background-color: var(--fds-colors-yellow-200);
color: #23262a;
}

.title {
padding-bottom: var(--fds-spacing-2);
letter-spacing: 0px;
}

.content {
display: flex;
flex-direction: column;
justify-content: space-between;
row-gap: var(--fds-spacing-1);
padding: var(--fds-spacing-2);
text-align: center;
font-weight: normal;
}

.content button {
width: 100%;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React from 'react';
import userEvent from '@testing-library/user-event';
import { render, screen } from '@testing-library/react';
import { studioIconCardPopoverTrigger } from '@studio/testing/testids';
import { StudioIconCard } from './StudioIconCard';
import { ClipboardIcon } from '@studio/icons';

describe('StudioIconCard', () => {
it('should render children as content', async () => {
const divText = 'test-div';
render(
<StudioIconCard icon={<ClipboardIcon />}>
<div>{divText}</div>
</StudioIconCard>,
);
expect(screen.getByText(divText)).toBeInTheDocument();
});

it('should render icon prop', async () => {
const iconTestId = 'icon-test-id';
render(
<StudioIconCard icon={<ClipboardIcon data-testid={iconTestId} />} iconColor='red'>
<div></div>
</StudioIconCard>,
);
expect(screen.getByTestId(iconTestId)).toBeInTheDocument();
});

it('should render clickable popover trigger for context buttons', async () => {
const user = userEvent.setup();
const buttonText = 'button-text';
const contextButtons = <button>{buttonText}</button>;
render(
<StudioIconCard contextButtons={contextButtons} icon={<ClipboardIcon />}>
<div></div>
</StudioIconCard>,
);
await user.click(screen.getByTestId(studioIconCardPopoverTrigger));
expect(screen.getByRole('button', { name: buttonText })).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React, { type ReactElement, type ReactNode } from 'react';
import {
StudioCard,
StudioHeading,
StudioPopover,
StudioPopoverContent,
StudioPopoverTrigger,
} from '@studio/components';
import classes from './StudioIconCard.module.css';
import cn from 'classnames';
import type { HeadingProps } from '@digdir/designsystemet-react';
import { MenuElipsisVerticalIcon } from '@studio/icons';
import { studioIconCardPopoverTrigger } from '@studio/testing/testids';

export type StudioIconCardIconColors = 'blue' | 'red' | 'green' | 'grey' | 'yellow';

export type StudioIconCardProps = {
icon: ReactElement;
iconColor?: StudioIconCardIconColors;
header?: string;
headerOptions?: HeadingProps;
contextButtons?: ReactNode;
children: ReactNode;
};

export const StudioIconCard = ({
icon,
iconColor = 'grey',
header,
headerOptions,
contextButtons,
children,
}: StudioIconCardProps) => {
return (
<StudioCard className={classes.card}>
{contextButtons && (
<StudioPopover placement='bottom-start' size='sm'>
<StudioPopoverTrigger
data-testid={studioIconCardPopoverTrigger}
variant='tertiary'
className={classes.editIcon}
>
<MenuElipsisVerticalIcon />
</StudioPopoverTrigger>
<StudioPopoverContent className={classes.popoverContent}>
{contextButtons}
</StudioPopoverContent>
</StudioPopover>
)}
<div className={classes.iconContainer}>
<div className={cn(classes.iconBackground, classes[iconColor])} aria-hidden={true}>
{icon}
</div>
</div>

<div className={classes.content}>
<StudioHeading className={classes.title} size='2xs' {...headerOptions}>
{header}
</StudioHeading>
{children}
</div>
</StudioCard>
);
};
1 change: 1 addition & 0 deletions frontend/testing/testids.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ export const resetRepoContainerId = 'reset-repo-container';
export const selectedLayoutSet = 'layout-set-test';
export const typeItemId = (pointer) => `type-item-${pointer}`;
export const userMenuItemId = 'user-menu-item';
export const studioIconCardPopoverTrigger = 'studio-icon-card-popover-trigger';

0 comments on commit ca5b007

Please sign in to comment.