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

chore: 14263 create studio status radio group #14272

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
.radioGroupContainer {
display: flex;
flex-direction: column;
gap: var(--fds-spacing-4);
}

.radioGroup {
display: flex;
gap: var(--fds-spacing-4);
flex-wrap: wrap;
}

.radioButton {
--radio-button-width: 240px;
--border-width: 3px;

display: flex;
flex-direction: column;
padding-block: var(--fds-spacing-2);
padding-inline: var(--fds-spacing-4);
border-radius: var(--fds-sizing-2);
cursor: pointer;
position: relative;
width: var(--radio-button-width);
border: var(--border-width) solid transparent;
}

.input {
position: absolute;
opacity: 0;
pointer-events: none;
}

.radioButton:has(input:checked) {
border-color: var(--fds-semantic-border-action-default);
}

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

.title {
font-weight: bold;
text-align: left;
}

.text {
word-wrap: break-word;
word-break: break-word;
white-space: normal;
}

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

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

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

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

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

.warning:hover {
background-color: var(--fds-semantic-surface-warning-subtle-hover);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React, { useState } from 'react';
import type { Meta, StoryFn } from '@storybook/react';
import { StudioStatusRadioGroup, type StudioStatusRadioGroupProps } from './StudioStatusRadioGroup';

const options: StudioStatusRadioGroupProps['options'] = [
{
value: 'value1',
title: 'Miljø 1',
text: 'Sist publisert 11.06.2023 kl 14:03',
color: 'success',
},
{
value: 'value2',
title: 'Miljø 2',
text: 'Sist publisert 11.06.2023 kl 14:03',
color: 'success',
},
{ value: 'value3', title: 'Miljø 3', text: 'Forløpig ingen publiseringer', color: 'info' },
{
value: 'value4',
title: 'Miljø 4',
text: 'Applikasjonen er utilgjengelig i miljø',
color: 'warning',
},
];

type Story = StoryFn<typeof StudioStatusRadioGroup>;

const meta: Meta = {
title: 'Components/StudioStatusRadioGroup',
component: StudioStatusRadioGroup,
argTypes: {},
};

export const Preview: Story = () => {
const [selectedValue, setSelectedValue] = useState<string | undefined>();

return (
<StudioStatusRadioGroup
title='Velg et av alternativene under'
options={options}
onChange={(value) => setSelectedValue(value)}
defaultValue={selectedValue}
/>
);
};

export default meta;
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { StudioStatusRadioGroup, type StudioStatusRadioGroupProps } from './StudioStatusRadioGroup';

const mockTitle1: string = 'Success';
const mockTitle2: string = 'Info';
const mockTitle3: string = 'Warning';

const mockText1: string = 'Success text';
const mockText2: string = 'Info text';
const mockText3: string = 'Warning text';

const mockValue1: string = 'success';
const mockValue2: string = 'info';
const mockValue3: string = 'warning';

const mockOption1: StudioStatusRadioGroupProps['options'][number] = {
title: mockTitle1,
text: mockText1,
color: 'success',
value: mockValue1,
};
const mockOption2: StudioStatusRadioGroupProps['options'][number] = {
title: mockTitle2,
text: mockText2,
color: 'info',
value: mockValue2,
};
const mockOption3: StudioStatusRadioGroupProps['options'][number] = {
title: mockTitle3,
text: mockText3,
color: 'warning',
value: mockValue3,
};

const mockOptions: StudioStatusRadioGroupProps['options'] = [mockOption1, mockOption2, mockOption3];
const mockGroupTitle: string = 'Status group';
const mockOnChange = jest.fn();

const defaultProps: StudioStatusRadioGroupProps = {
options: mockOptions,
title: mockGroupTitle,
onChange: mockOnChange,
};

describe('StudioStatusRadioGroup', () => {
it('renders radio buttons with titles and descriptions', () => {
renderStudioStatusRadioGroup();

expect(screen.getByText(mockGroupTitle)).toBeInTheDocument();
expect(screen.getByText(mockTitle1)).toBeInTheDocument();
expect(screen.getByText(mockText1)).toBeInTheDocument();
expect(screen.getByText(mockTitle2)).toBeInTheDocument();
expect(screen.getByText(mockText2)).toBeInTheDocument();
expect(screen.getByText(mockTitle3)).toBeInTheDocument();
expect(screen.getByText(mockText3)).toBeInTheDocument();
});

it('allows selecting a radio button', async () => {
const user = userEvent.setup();
renderStudioStatusRadioGroup();

const successRadioButton = screen.getByRole('radio', { name: `${mockTitle1} ${mockText1}` }); // Any way to do it without having both title and text?
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@framitdavid, do you have a better suggestion here? 🤔

const infoRadioButton = screen.getByRole('radio', { name: `${mockTitle2} ${mockText2}` });
const warningRadioButton = screen.getByRole('radio', { name: `${mockTitle3} ${mockText3}` });

// Initially, no button should be selected
expect(successRadioButton).not.toBeChecked();
expect(infoRadioButton).not.toBeChecked();
expect(warningRadioButton).not.toBeChecked();

await user.click(infoRadioButton);

expect(infoRadioButton).toBeChecked();
expect(successRadioButton).not.toBeChecked();
expect(warningRadioButton).not.toBeChecked();
});

it('calls onChange with correct value when a radio button is selected', async () => {
const user = userEvent.setup();
renderStudioStatusRadioGroup();

const infoRadioButton = screen.getByRole('radio', { name: `${mockTitle2} ${mockText2}` });
await user.click(infoRadioButton);

expect(mockOnChange).toHaveBeenCalledWith(mockValue2);
expect(mockOnChange).toHaveBeenCalledTimes(2); // Why is this being called twice?
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@framitdavid, do you have a suggestion here? 🤔

});

it('renders with default value selected', () => {
renderStudioStatusRadioGroup({ defaultValue: mockValue2 });

const successRadioButton = screen.getByRole('radio', { name: `${mockTitle1} ${mockText1}` });
const infoRadioButton = screen.getByRole('radio', { name: `${mockTitle2} ${mockText2}` });

expect(infoRadioButton).toBeChecked();
expect(successRadioButton).not.toBeChecked();
});

it('applies correct aria attributes for accessibility', () => {
renderStudioStatusRadioGroup();

const successRadioButton = screen.getByRole('radio', { name: `${mockTitle1} ${mockText1}` });
const inputId = `${mockGroupTitle}-${mockValue1}`;

expect(successRadioButton).toHaveAttribute('aria-labelledby', `${inputId}-title`);
expect(successRadioButton).toHaveAttribute('aria-describedby', `${inputId}-text`);
});

it('focuses on the radio button when clicked', async () => {
const user = userEvent.setup();
renderStudioStatusRadioGroup();

const infoRadioButton = screen.getByRole('radio', { name: `${mockTitle2} ${mockText2}` });
await user.click(infoRadioButton);

expect(infoRadioButton).toHaveFocus();
});
});

const renderStudioStatusRadioGroup = (props: Partial<StudioStatusRadioGroupProps> = {}) => {
return render(<StudioStatusRadioGroup {...defaultProps} {...props} />);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import React, { type ChangeEvent, type ReactElement } from 'react';
import classes from './StudioStatusRadioGroup.module.css';
import cn from 'classnames';
import { StudioLabelAsParagraph } from '../StudioLabelAsParagraph';
import { StudioParagraph } from '../StudioParagraph';

type StudioStatusRadioButtonColor = 'success' | 'info' | 'warning';

export type StudioStatusRadioButtonItem = {
title: string;
text: string;
color: StudioStatusRadioButtonColor;
value: string;
};

export type StudioStatusRadioGroupProps = {
options: StudioStatusRadioButtonItem[];
title: string;
defaultValue?: string;
onChange?: (value: string) => void;
};

export const StudioStatusRadioGroup = ({
options,
title: name,
defaultValue,
onChange,
}: StudioStatusRadioGroupProps): ReactElement => {
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
if (onChange) {
onChange(event.target.value);
}
};

return (
<div className={classes.radioGroupContainer}>
<StudioLabelAsParagraph size='md'>{name}</StudioLabelAsParagraph>
<div className={classes.radioGroup} role='radiogroup'>
{options.map(({ title, text, color, value }: StudioStatusRadioButtonItem) => {
const inputId = `${name}-${value}`;
const inputTitleId = `${inputId}-title`;
const inputTextId = `${inputId}-text`;
return (
<label key={value} className={cn(classes.radioButton, classes[color])}>
<input
type='radio'
id={inputId}
name={title}
value={value}
defaultChecked={defaultValue === value}
onChange={handleChange}
className={classes.input}
aria-labelledby={inputTitleId}
aria-describedby={inputTextId}
/>
<div className={classes.textContent}>
<StudioLabelAsParagraph id={inputTitleId} className={classes.title} size='sm'>
{title}
</StudioLabelAsParagraph>
<StudioParagraph id={inputTextId} className={classes.text} size='xs'>
{text}
</StudioParagraph>
</div>
</label>
);
})}
</div>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { StudioStatusRadioGroup } from './StudioStatusRadioGroup';
1 change: 1 addition & 0 deletions frontend/libs/studio-components/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export * from './StudioPageSpinner';
export * from './StudioParagraph';
export * from './StudioPopover';
export * from './StudioProperty';
export * from './StudioStatusRadioGroup';
export * from './StudioRecommendedNextAction';
export * from './StudioRedirectBox';
export * from './StudioResizableLayout';
Expand Down
Loading