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

Panel Component #2674

Merged
merged 14 commits into from
Nov 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
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
83 changes: 83 additions & 0 deletions src/app-components/panel/Panel.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/* breakpoints-xs */
@media only screen and (min-width: 0) {
.panel {
--y-padding: var(--fds-spacing-6);
--x-padding: var(--fds-spacing-6);
--content-gap: var(--fds-spacing-3);
}
}

/* breakpoints-sm */
@media only screen and (min-width: 540px) {
.panel:not(.panelMobileLayout) {
--y-padding: var(--fds-spacing-9);
--x-padding: var(--fds-spacing-22);
--content-gap: var(--fds-spacing-3);
}
}

/* print style */
@media print {
.panel {
--y-padding: var(--fds-spacing-6);
--x-padding: var(--fds-spacing-6);
--content-gap: var(--fds-spacing-3);
}
}

.panel {
width: 100%;
}

.panelContentWrapper {
display: flex;
gap: var(--content-gap);
padding: var(--y-padding) var(--x-padding);
}

.panelContentWrapper_info {
background-color: var(--fds-semantic-surface-info-subtle);
}

.panelContentWrapper_success {
background-color: var(--fds-semantic-surface-success-subtle);
}

.panelContentWrapper_warning {
background-color: var(--fds-semantic-surface-warning-subtle);
}

.panelContentWrapper_error {
background-color: var(--fds-semantic-surface-danger-no_fill-active);
}

.panelIconWrapper {
display: flex;
flex: none;
}

.panelIconWrapper_success svg {
color: var(--fds-semantic-surface-success-default);
}

.panelIconWrapper_info svg {
color: var(--fds-semantic-border-info-default);
}

.panelIconWrapper_error svg {
color: var(--fds-semantic-surface-danger-default);
}

.panelIconWrapper_warning svg {
color: var(--fds-semantic-border-warning-default);
}

.panelContent {
display: flex;
flex-direction: column;
gap: var(--fds-spacing-1);
}

.panelHeader {
margin: 0;
}
65 changes: 65 additions & 0 deletions src/app-components/panel/Panel.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import React from 'react';

import { render, screen } from '@testing-library/react';

import { PANEL_VARIANT } from 'src/app-components/panel/constants';
import { Panel } from 'src/app-components/panel/Panel';
import type { PanelVariant } from 'src/app-components/panel/Panel';

const MockLang = ({ text }: { text: string }) => <>{text}</>;

describe('Panel', () => {
it('should show title and content', () => {
render(
<Panel
variant={PANEL_VARIANT.Info}
title={<MockLang text='Panel Title' />}
>
Panel Content
</Panel>,
);
expect(screen.getByText('Panel Title')).toBeInTheDocument();
expect(screen.getByText('Panel Content')).toBeInTheDocument();
});

it('should not show icon when showIcon is not set', () => {
render(
<Panel
variant={PANEL_VARIANT.Info}
title={<MockLang text='Panel Title' />}
>
Panel Content
</Panel>,
);
expect(screen.queryByRole('img', { name: 'info' })).not.toBeInTheDocument();
});

it('should not show icon when showIcon is false', () => {
render(
<Panel
variant={PANEL_VARIANT.Info}
title={<MockLang text='Panel Title' />}
showIcon={false}
>
Panel Content
</Panel>,
);
expect(screen.queryByRole('img', { name: 'info' })).not.toBeInTheDocument();
});

it.each<PanelVariant>(['info', 'warning', 'error', 'success'])(
'should apply relevant icon based on variant when showIcon is true',
(variant) => {
render(
<Panel
variant={variant}
title={<MockLang text='Panel Title' />}
showIcon
>
Panel Content
</Panel>,
);
expect(screen.queryByRole('img', { name: variant })).toBeInTheDocument;
},
);
});
106 changes: 106 additions & 0 deletions src/app-components/panel/Panel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import React from 'react';
import type { JSX, PropsWithChildren } from 'react';

import { Heading } from '@digdir/designsystemet-react';
import {
CheckmarkCircleIcon,
ExclamationmarkTriangleIcon,
InformationSquareIcon,
XMarkOctagonIcon,
} from '@navikt/aksel-icons';
import cn from 'classnames';

import { PANEL_VARIANT } from 'src/app-components/panel/constants';
import classes from 'src/app-components/panel/Panel.module.css';
import { useIsMobile } from 'src/hooks/useDeviceWidths';

export type PanelVariant = (typeof PANEL_VARIANT)[keyof typeof PANEL_VARIANT];

type PanelProps = PropsWithChildren<{
variant: PanelVariant;
showIcon?: boolean;
forceMobileLayout?: boolean;
title?: JSX.Element;
}>;

type PanelIconProps = {
isMobileLayout: boolean;
variant: PanelVariant;
};

function PanelIcon({ isMobileLayout, variant }: PanelIconProps) {
const fontSize = isMobileLayout ? '2rem' : '3rem';

switch (variant) {
case PANEL_VARIANT.Info:
return (
<InformationSquareIcon
title='info'
fontSize={fontSize}
/>
);
case PANEL_VARIANT.Warning:
return (
<ExclamationmarkTriangleIcon
title='warning'
fontSize={fontSize}
/>
);
case PANEL_VARIANT.Error:
return (
<XMarkOctagonIcon
title='error'
fontSize={fontSize}
/>
);
case PANEL_VARIANT.Success:
return (
<CheckmarkCircleIcon
title='success'
fontSize={fontSize}
/>
);
}
}

export const Panel: React.FC<PanelProps> = ({
variant,
showIcon = false,
forceMobileLayout = false,
title,
children,
}) => {
const isMobile = useIsMobile();
const isMobileLayout = forceMobileLayout || isMobile;

return (
<div
className={cn(classes.panel, {
[classes.panelMobileLayout]: isMobileLayout,
})}
>
<div className={cn(classes.panelContentWrapper, classes[`panelContentWrapper_${variant}`])}>
{showIcon && (
<div className={cn(classes.panelIconWrapper, classes[`panelIconWrapper_${variant}`])}>
<PanelIcon
isMobileLayout={isMobileLayout}
variant={variant}
/>
</div>
)}
<div className={classes.panelContent}>
{title && (
<Heading
level={2}
size={isMobileLayout ? 'xs' : 'sm'}
className={classes.panelHeader}
>
{title}
</Heading>
)}
<div>{children}</div>
</div>
</div>
</div>
);
};
6 changes: 6 additions & 0 deletions src/app-components/panel/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const PANEL_VARIANT = {
Info: 'info',
Warning: 'warning',
Error: 'error',
Success: 'success',
} as const;
16 changes: 0 additions & 16 deletions src/codegen/Common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,22 +112,6 @@ const common = {
.setTitle('Grid')
.setDescription('Settings for the components grid. Used for controlling horizontal alignment'),

// Panel display mode:
IPanelBase: () =>
new CG.obj(
new CG.prop(
'variant',
new CG.enum('info', 'warning', 'error', 'success')
.optional()
.setTitle('Panel variant')
.setDescription('Change the look of the panel'),
),
new CG.prop(
'showIcon',
new CG.bool().optional({ default: true }).setTitle('Show icon').setDescription('Show icon in the panel header'),
),
),

IDataModelReference: () =>
new CG.obj(
new CG.prop(
Expand Down
84 changes: 0 additions & 84 deletions src/components/form/Panel.test.tsx

This file was deleted.

Loading
Loading