Skip to content

Commit

Permalink
feat(radio-button): standalone radio button component
Browse files Browse the repository at this point in the history
  • Loading branch information
ogermain-kronos committed Jul 12, 2024
1 parent 19a6b9b commit 51d97f1
Show file tree
Hide file tree
Showing 8 changed files with 446 additions and 83 deletions.
48 changes: 48 additions & 0 deletions packages/react/src/components/radio-button/radio-button.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { mountWithTheme } from '../../test-utils/renderer';
import { RadioButton } from './radio-button';
import { getByTestId } from '../../test-utils/enzyme-selectors';
import { doNothing } from '../../test-utils/callbacks';

describe('Radio button', () => {
test('should have controllable data-testid', () => {
const wrapper = mountWithTheme(<RadioButton checked data-testid="radiobutton-testid" />);

expect(getByTestId(wrapper, 'radiobutton-testid').exists()).toBe(true);
});

test('should be checked by default is defaultChecked prop is present', () => {
const wrapper = mountWithTheme(<RadioButton defaultChecked />);

const input = wrapper.find('input[type="radio"]');
expect(input.prop('defaultChecked')).toBe(true);
});

test('onChange callback should be called when radio button is checked', () => {
const callback = jest.fn();
const wrapper = mountWithTheme(<RadioButton onChange={callback} />);

wrapper.find('input[type="radio"]').simulate('change');

expect(callback).toHaveBeenCalledTimes(1);
});

test('should be checked when checked prop is set to true', () => {
const wrapper = mountWithTheme(<RadioButton checked onChange={doNothing} />);

const input = wrapper.find('input[type="radio"]');
expect(input.prop('checked')).toBe(true);
});

test('should be disabled when disabled prop is set to true', () => {
const wrapper = mountWithTheme(<RadioButton disabled />);

const input = wrapper.find('input[type="radio"]');
expect(input.prop('disabled')).toBe(true);
});

test('matches snapshot', () => {
const tree = mountWithTheme(<RadioButton label="This is a label" />);

expect(tree).toMatchSnapshot();
});
});
110 changes: 110 additions & 0 deletions packages/react/src/components/radio-button/radio-button.test.tsx.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Radio button matches snapshot 1`] = `
.c1 {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background-color: #FFFFFF;
border: 1px solid #60666E;
border-radius: 50%;
color: #FFFFFF;
display: inline-block;
height: var(--size-1x);
margin: 0;
position: relative;
width: var(--size-1x);
-webkit-flex-shrink: 0;
-ms-flex-negative: 0;
flex-shrink: 0;
margin-top: var(--spacing-half);
}
.c1 + {
outline: 2px solid transparent;
outline-offset: -2px;
}
.c1:focus + {
box-shadow: 0 0 0 2px #006296;
outline: 2px solid #84C6EA;
outline-offset: -2px;
}
.c1:checked {
border: 2px solid #006296;
}
.c1:checked::after {
background-color: #006296;
border-radius: 50%;
content: '';
display: block;
height: var(--size-half);
left: 50%;
position: absolute;
top: 50%;
-webkit-transform: translate(-50%,-50%);
-ms-transform: translate(-50%,-50%);
transform: translate(-50%,-50%);
width: var(--size-half);
}
.c1:hover [type='radio'] {
border: 1px solid #000000;
}
.c2 {
font-size: 0.875rem;
line-height: 1.5rem;
margin-left: var(--spacing-1x);
}
.c0 {
-webkit-align-items: flex-start;
-webkit-box-align: flex-start;
-ms-flex-align: flex-start;
align-items: flex-start;
display: -webkit-inline-box;
display: -webkit-inline-flex;
display: -ms-inline-flexbox;
display: inline-flex;
margin-top: var(--spacing-1x);
position: relative;
}
<RadioButton
label="This is a label"
>
<styled.div>
<div
className="c0"
>
<styled.input
data-testid="radiobutton-uuid1"
id="uuid1"
type="radio"
>
<input
className="c1"
data-testid="radiobutton-uuid1"
id="uuid1"
type="radio"
/>
</styled.input>
<styled.label
data-testid="radiobutton-uuid1_label"
htmlFor="uuid1"
>
<label
className="c2"
data-testid="radiobutton-uuid1_label"
htmlFor="uuid1"
>
This is a label
</label>
</styled.label>
</div>
</styled.div>
</RadioButton>
`;
117 changes: 117 additions & 0 deletions packages/react/src/components/radio-button/radio-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { ChangeEvent, FunctionComponent } from 'react';
import styled from 'styled-components';
import { focus } from '../../utils/css-state';
import { useId } from '../../hooks/use-id';

const StyledInput = styled.input<{ disabled?: boolean }>`
appearance: none;
background-color: ${({ theme, disabled }) => (disabled ? theme.component['radio-button-disabled-background-color'] : theme.component['radio-button-background-color'])};
border: 1px solid ${({ theme, disabled }) => (disabled ? theme.component['radio-button-disabled-border-color'] : theme.component['radio-button-border-color'])};
border-radius: 50%;
color: ${({ theme, disabled }) => (disabled ? theme.component['radio-button-disabled-background-color'] : theme.component['radio-button-background-color'])};
display: inline-block;
height: var(--size-1x);
margin: 0;
position: relative;
width: var(--size-1x);
flex-shrink: 0;
margin-top: var(--spacing-half);
${(theme) => focus(theme, { selector: '+' })}
&:checked {
border: 2px solid ${({ theme, disabled }) => (disabled ? theme.component['radio-button-disabled-border-color'] : theme.component['radio-button-checked-border-color'])};
&::after {
background-color: ${({ theme, disabled }) => (disabled ? theme.component['radio-button-disabled-border-color'] : theme.component['radio-button-checked-background-color'])};
border-radius: 50%;
content: '';
display: block;
height: var(--size-half);
left: 50%;
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
width: var(--size-half);
}
}
&:hover [type='radio'] {
border: 1px solid ${({ theme, disabled }) => (disabled ? theme.component['radio-button-disabled-hover-border-color'] : theme.component['radio-button-hover-border-color'])};
}
`;

const StyledLabel = styled.label`
font-size: 0.875rem;
line-height: 1.5rem;
margin-left: var(--spacing-1x);
`;

const StyledContainer = styled.div`
align-items: flex-start;
display: inline-flex;
margin-top: var(--spacing-1x);
position: relative;
`;

interface RadioButtonProps {
ariaLabel?: string;
ariaLabelledBy?: string[];
checked?: boolean;
className?: string;
defaultChecked?: boolean;
disabled?: boolean;
id?: string;
label?: string;
name?: string;
value?: string;

onChange?(event: ChangeEvent<HTMLInputElement>): void;
}

export const RadioButton: FunctionComponent<RadioButtonProps> = ({
ariaLabel,
ariaLabelledBy,
checked,
className,
defaultChecked,
disabled,
id,
label,
name,
value,
onChange,
}) => {
const generatedId = useId();
const inputId = id || generatedId;

return (
<StyledContainer>
<StyledInput
data-testid={`radiobutton-${inputId}`}
id={inputId}
type="radio"
name={name}
className={className}
value={value}
defaultChecked={defaultChecked}
checked={checked}
disabled={disabled}
onChange={onChange}
aria-label={ariaLabel}
aria-labelledby={ariaLabelledBy?.join(' ')}
/>
{
label && (
<StyledLabel
data-testid={`radiobutton-${inputId}_label`}
htmlFor={inputId}
>
{label}
</StyledLabel>
)
}
</StyledContainer>
);
};

RadioButton.displayName = 'RadioButton';
1 change: 1 addition & 0 deletions packages/react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export { PhoneInput } from './components/phone-input/phone-input';
export { RadioButtonGroup } from './components/radio-button-group/radio-button-group';
export { SearchContextual } from './components/search/search-contextual';
export { SearchGlobal } from './components/search/search-global';
export { RadioButton } from './components/radio-button/radio-button';
export { StepperInput } from './components/stepper-input/stepper-input';
export { Tab, Tabs } from './components/tabs/tabs';
export { TextArea } from './components/text-area/text-area';
Expand Down
42 changes: 42 additions & 0 deletions packages/storybook/stories/radio-button-group.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { RadioButtonGroup } from '@equisoft/design-elements-react';
import { ArgTypes, Canvas, Meta } from '@storybook/blocks';
import * as RadioButtonGroupStories from './radio-button-group.stories';

<Meta of={RadioButtonGroupStories} />

# Radio Button
1. [Definition](#definition)
2. [Usage](#usage)
3. [Variants](#variants)
4. [Properties](#properties)

## Definition
Radio buttons display mutually exclusive options from which a user selects exactly one [1].
<Canvas of={RadioButtonGroupStories.Default} />

## Usage

### When to use
- When you only want the user to select one item [3].
- Use when the user has to select only one option from a list between 2 to 6 choices [3].
- Use it when validation is needed, such as a submit button [3].

### When not to use
- Radio buttons should not be used to perform actions [2].
- Don't use it when there are more than 6 options, consider the dropdown-list instead [3].

## Variants

### Default
<Canvas of={RadioButtonGroupStories.Default} />

### With conditional content
<Canvas of={RadioButtonGroupStories.WithConditionalContent} />

## Properties
<ArgTypes of={RadioButtonGroup} />

## References
1. [https://www.nngroup.com/articles/radio-buttons-default-selection/](https://www.nngroup.com/articles/radio-buttons-default-selection/)
2. [https://uxplanet.org/radio-buttons-ux-design-588e5c0a50dc](https://uxplanet.org/radio-buttons-ux-design-588e5c0a50dc)
3. [https://uxdesign.cc/ui-cheat-sheet-radio-buttons-checkboxes-and-other-selectors-bf56777ad59e](https://uxdesign.cc/ui-cheat-sheet-radio-buttons-checkboxes-and-other-selectors-bf56777ad59e)
Loading

0 comments on commit 51d97f1

Please sign in to comment.