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 3, 2025
1 parent dbc9d5f commit c4665ec
Show file tree
Hide file tree
Showing 6 changed files with 274 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('taskcard', () => {
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,62 @@
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])}>{icon}</div>
</div>

<div className={classes.content}>
<StudioHeading className={classes.title} size='2xs' {...headerOptions}>
{header}
</StudioHeading>
{children}
</div>
</StudioCard>
);
};
30 changes: 30 additions & 0 deletions frontend/packages/ux-editor/src/hooks/useLayoutSetIcon.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { renderHook } from '@testing-library/react';
import React from 'react';
import { useLayoutSetIcon } from './useLayoutSetIcon';
import type { LayoutSetModel } from 'app-shared/types/api/dto/LayoutSetModel';
import { QuestionmarkIcon } from '@studio/icons';

describe('useLayoutSetIcon', () => {
it('should return default icon for unknown types', () => {
const layoutSet: LayoutSetModel = {
id: 'unknown-id',
dataType: '',
type: 'unknown-type',
};
const { result } = renderHook(() => useLayoutSetIcon(layoutSet));

expect(result.current.icon.type).toBe((<QuestionmarkIcon />).type);
expect(result.current.iconColor).toBe('grey');
});

it('should return icon and iconColor for known types', () => {
const layoutSet: LayoutSetModel = {
id: 'CustomReceipt',
dataType: '',
type: '',
};
const { result } = renderHook(() => useLayoutSetIcon(layoutSet));
expect(result.current.icon).toBeTruthy();
expect(result.current.iconColor).toBeTruthy();
});
});
23 changes: 23 additions & 0 deletions frontend/packages/ux-editor/src/hooks/useLayoutSetIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React, { type ReactElement } from 'react';
import {
ClipboardIcon,
DataTaskIcon,
QuestionmarkIcon,
ReceiptIcon,
SignTaskIcon,
} from '@studio/icons';
import type { LayoutSetModel } from 'app-shared/types/api/dto/LayoutSetModel';
import type { StudioIconCardIconColors } from '@studio/components/src/components/StudioIconCard/StudioIconCard';

export const useLayoutSetIcon = (
layoutSetModel: LayoutSetModel,
): { icon: ReactElement; iconColor: StudioIconCardIconColors } => {
if (layoutSetModel.type == 'subform') return { icon: <ClipboardIcon />, iconColor: 'blue' };

if (layoutSetModel.task?.id == 'CustomReceipt')
return { icon: <ReceiptIcon />, iconColor: 'green' };
if (layoutSetModel.task?.type == 'data') return { icon: <DataTaskIcon />, iconColor: 'blue' };
if (layoutSetModel.task?.type == 'signing') return { icon: <SignTaskIcon />, iconColor: 'red' };

return { icon: <QuestionmarkIcon />, iconColor: 'grey' };
};
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 c4665ec

Please sign in to comment.