Skip to content

Commit

Permalink
feat: refactor radio button and implement radioGroup
Browse files Browse the repository at this point in the history
  • Loading branch information
finhaa committed Mar 22, 2024
1 parent 7cb45ef commit 7f79270
Show file tree
Hide file tree
Showing 7 changed files with 649 additions and 351 deletions.
52 changes: 45 additions & 7 deletions src/components/Radio/index.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,57 @@
import { ChangeEvent, FC } from 'react';
import { CSSProperties, ChangeEvent, FC, useEffect, useState } from 'react';

import { Input, Label, Wrapper } from './styles';

export type Props = {
id: string;
value: string;
label: string;
group: string;
onChange: (event: ChangeEvent<HTMLInputElement>) => void;
disabled?: boolean;
style?: CSSProperties;
checked?: boolean;
};

const Radio: FC<Props> = ({ onChange, id, label, group }) => (
<Wrapper>
<Input onChange={onChange} id={id} name={group} type="radio" />
<Label htmlFor={id}>{label}</Label>
</Wrapper>
);
const Radio: FC<Props> = ({
onChange,
id,
value,
label,
group,
style,
disabled = false,
checked = false,
}) => {
const [internalChecked, setInternalChecked] = useState(checked);

const handleOnChange = (event: ChangeEvent<HTMLInputElement>) => {
if (disabled) return;
onChange(event);
};

useEffect(() => {
setInternalChecked(checked);
}, [checked]);

useEffect(() => {
if (disabled && checked) setInternalChecked(false);
}, [disabled, checked]);

return (
<Wrapper disabled={!!disabled} style={style}>
<Input
onChange={handleOnChange}
id={id}
value={value}
name={group}
type="radio"
disabled={disabled}
checked={internalChecked}
/>
<Label htmlFor={id}>{label}</Label>
</Wrapper>
);
};

export default Radio;
2 changes: 2 additions & 0 deletions src/components/Radio/radio.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ const meta: Meta<typeof Radio> = {
label: 'Radio Test',
group: 'radio-test',
onChange: mockOnChange,
disabled: false,
checked: false,
},
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement);
Expand Down
102 changes: 62 additions & 40 deletions src/components/Radio/styles.ts
Original file line number Diff line number Diff line change
@@ -1,64 +1,86 @@
import styled from 'styled-components';
import { getTheme, ifStyle } from '@platformbuilders/theme-toolkit';

export const Wrapper = styled.div`
margin: 16px 0;
const brandPrimaryMain = getTheme('brand.primary.main');
const brandPrimaryContrast = getTheme('brand.primary.contrast');
const themeRadiusInput = getTheme('themeRadius.input');
const spacingXs = getTheme('spacing.xs');
const spacingSm = getTheme('spacing.sm');
const spacingMd = getTheme('spacing.md');
const textMain = getTheme('text.main');
const isDisabled = ifStyle('disabled');

type WrapperProps = {
disabled?: boolean;
};

export const Wrapper = styled.div<WrapperProps>`
display: flex;
position: relative;
flex-direction: column;
width: 100%;
padding: ${spacingXs}px ${spacingSm}px ${spacingXs}px ${spacingMd}px;
opacity: ${isDisabled('0.3', '1')};
&:hover {
background-color: ${(props) =>
isDisabled('transparent', `${brandPrimaryMain(props)}10`)};
border-radius: ${themeRadiusInput}px;
}
&:hover > label {
color: ${(props) => isDisabled(`${textMain(props)}70`, textMain)};
}
`;

export const Input = styled.input`
display: none;
position: absolute;
opacity: 0;
&:checked + label:before {
border-color: rgb(51, 122, 183);
animation: ripple 0.2s linear forwards;
background: ${brandPrimaryMain};
}
&:checked + label:after {
transform: scale(1);
border-color: ${brandPrimaryContrast};
transition-property: transform, border-color, background-color;
transition-duration: 0.3s;
transition-timing-function: ease;
}
&:checked + label {
color: ${textMain};
}
`;

export const Label = styled.label`
display: inline-block;
min-height: 20px;
position: relative;
padding: 0 30px;
margin-bottom: 0;
cursor: pointer;
vertical-align: bottom;
@keyframes ripple {
0% {
box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 0);
}
50% {
box-shadow: 0px 0px 0px 15px rgba(0, 0, 0, 0.1);
}
100% {
box-shadow: 0px 0px 0px 15px rgba(0, 0, 0, 0);
}
}
color: ${textMain}70;
&:before,
&:after {
position: absolute;
content: '';
border-radius: 50%;
transition: all 0.3s ease;
transition-property: transform, border-color;
position: relative;
display: inline-block;
vertical-align: bottom;
}
&:before {
left: 0;
top: 0;
width: 16px;
height: 16px;
border: 2px solid rgba(0, 0, 0, 0.54);
width: 1rem;
height: 1rem;
border-radius: 100%;
border: 2px solid ${brandPrimaryMain};
margin-right: ${spacingSm}px;
}
&:after {
top: 5px;
left: 5px;
width: 10px;
height: 10px;
transform: scale(0);
background: rgb(51, 122, 183);
position: absolute;
top: 0.5rem;
left: 21px;
width: 0.375rem;
height: 0.1875rem;
border: solid 2px transparent;
border-right: none;
border-top: none;
transform: translate(0.0625rem, 0.125rem) rotate(-45deg);
}
`;
56 changes: 56 additions & 0 deletions src/components/RadioGroup/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { CSSProperties, ChangeEvent, FC, useState } from 'react';

import Radio from '../Radio';
import { Wrapper } from './styles';

type OptionProps = {
id: string;
value: string;
label: string;
style?: CSSProperties;
};

export type Props = {
id: string;
name: string;
options: OptionProps[];
defaultValue: string;
onChange: (event: ChangeEvent<HTMLInputElement>) => void;
disabled: boolean;
};

const RadioGroup: FC<Props> = ({
id,
name,
options,
defaultValue,
onChange,
disabled,
}) => {
const [internalValue, setInternalValue] = useState(defaultValue);

const handleOnChange = (event: ChangeEvent<HTMLInputElement>) => {
setInternalValue(event.target.value);
onChange(event);
};

return (
<Wrapper id={id}>
{options.map(({ id, value, label, style }) => (
<Radio
key={id}
id={id}
value={value}
label={label}
style={style}
group={name}
disabled={disabled}
checked={value === internalValue}
onChange={handleOnChange}
/>
))}
</Wrapper>
);
};

export default RadioGroup;
63 changes: 63 additions & 0 deletions src/components/RadioGroup/radio-group.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { expect, jest } from '@storybook/jest';
import type { Meta, StoryObj } from '@storybook/react';
import { configure, userEvent, within } from '@storybook/testing-library';
import RadioGroup from './index';

configure({ testIdAttribute: 'id' });

// Mocks
const mockTextId = 'radio-group-test-id';
const mockOnChange = jest.fn();

const meta: Meta<typeof RadioGroup> = {
title: 'Components/RadioGroup',
component: RadioGroup,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
args: {
id: 'mockTextId',
options: [
{
id: 'option1',
value: 'option1',
label: 'Opção 1',
},
{
id: 'option2',
value: 'option2',
label: 'Opção 2',
},
{
id: 'option3',
value: 'option3',
label: 'Opção 3',
},
],
defaultValue: 'option1',
name: 'radio-group-test',
onChange: mockOnChange,
disabled: false,
},
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement);

await step('RadioGroup | Test Render', async () => {
expect(canvas.getByTestId(mockTextId)).toBeInTheDocument();
});

await step('RadioGroup | Event Click', async () => {
await userEvent.click(canvas.getByTestId('option1'));
expect(mockOnChange).toHaveBeenCalled();
});
},
};

type Story = StoryObj<typeof RadioGroup>;

export const Default: Story = {
args: {},
};

export default meta;
13 changes: 13 additions & 0 deletions src/components/RadioGroup/styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import styled from 'styled-components';
import { getTheme } from '@platformbuilders/theme-toolkit';

const spacingSm = getTheme('spacing.sm');
const spacingMd = getTheme('spacing.md');

export const Wrapper = styled.div`
display: flex;
flex-direction: column;
width: 100%;
gap:${spacingMd}px
padding: ${spacingSm}px;
`;
Loading

0 comments on commit 7f79270

Please sign in to comment.