Skip to content

Commit

Permalink
Merge pull request #35 from kodiak-packages/31-radio-button
Browse files Browse the repository at this point in the history
Create Radio button
  • Loading branch information
bramvanhoutte authored May 11, 2020
2 parents 3a12c4b + bf599ac commit 6830711
Show file tree
Hide file tree
Showing 23 changed files with 640 additions and 27 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"main": "dist/index.js",
"scripts": {
"start": "docz dev",
"build": "rm -rf dist && tsc && cpx './src/**/*.css' 'dist'",
"build": "rm -rf dist && tsc && cpx './src/**/!(docAssets)/*.css' 'dist'",
"test": "jest --detectOpenHandles --watchAll",
"document": "docz build",
"lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'",
Expand Down
3 changes: 1 addition & 2 deletions src/components/Button/Button.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ route: /button
---

import { Playground, Props } from 'docz'
import { Activity, Trash2 } from '../../index.ts'
import Button from './Button.tsx'
import { Button, Activity, Trash2 } from '../../index.ts'

# Button

Expand Down
4 changes: 2 additions & 2 deletions src/components/Button/Button.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@

.typePrimary:focus {
outline: none;
box-shadow: 0 0 0 3px var(--color-primary-focus);
box-shadow: 0 0 0 3px var(--color-focus);
}

.typeSecondary:focus {
outline: none;
box-shadow: 0 0 0 3px var(--color-secondary-focus);
box-shadow: 0 0 0 3px var(--color-focus);
}

.typePrimary,
Expand Down
4 changes: 2 additions & 2 deletions src/components/Button/Button.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ describe('Button', () => {
expect(renderedClassNames.indexOf(className)).toBe(renderedClassNames.length - 1);
});

test('onClick should be triggered when clicked', async () => {
test('onClick should be triggered when clicked', () => {
const onClickFn = jest.fn();
const component = (
<Button onClick={onClickFn} name="click">
Expand All @@ -42,7 +42,7 @@ describe('Button', () => {
);
const { getByTestId } = render(component);

const button = await getByTestId('button-click');
const button = getByTestId('button-click');
fireEvent.click(button);

expect(onClickFn).toHaveBeenCalledTimes(1);
Expand Down
2 changes: 1 addition & 1 deletion src/components/Button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import Spinner from '../utils/Spinner/Spinner';

import styles from './Button.module.css';

export interface Props {
interface Props {
children: React.ReactNode;
type?: 'primary' | 'secondary';
onClick?: MouseEventHandler<HTMLButtonElement>;
Expand Down
4 changes: 2 additions & 2 deletions src/components/Icon/Icon.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import React, { CSSProperties } from 'react';

export interface IconProps {
interface Props {
color?: string;
className?: string;
style?: CSSProperties;
}

const Icon: React.FC<IconProps> = ({ className, style, color = 'currentColor' }: IconProps) => {
const Icon: React.FC<Props> = ({ className, style, color = 'currentColor' }: Props) => {
return (
<>
{color}
Expand Down
4 changes: 2 additions & 2 deletions src/components/Input/Input.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ route: /input

import { useState } from 'react';
import { Playground, Props } from 'docz';
import Input from './Input.tsx';
import styles from './Input.module.css';
import { Input } from '../../index.ts'
import styles from './docAssets/docs.module.css';

# Input

Expand Down
12 changes: 1 addition & 11 deletions src/components/Input/Input.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

.input:focus {
outline: none;
box-shadow: 0 0 0 3px var(--color-secondary-focus);
box-shadow: 0 0 0 3px var(--color-focus);
}

.containsError {
Expand All @@ -34,13 +34,3 @@
margin-top: 4px;
color: var(--color-error);
}

/* MDX */

.countryCode {
width: 50px;
}

.countryCode input {
text-align: center;
}
4 changes: 2 additions & 2 deletions src/components/Input/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import classNames from 'classnames';

import styles from './Input.module.css';

type Props = {
interface Props {
name: string;
value?: string;
onChange?: ChangeEventHandler<HTMLInputElement>;
Expand All @@ -14,7 +14,7 @@ type Props = {
autoComplete?: boolean;
maxLength?: number;
className?: string;
};
}

const Input = React.forwardRef<HTMLInputElement, Props>(
(
Expand Down
7 changes: 7 additions & 0 deletions src/components/Input/docAssets/docs.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.countryCode {
width: 50px;
}

.countryCode input {
text-align: center;
}
82 changes: 82 additions & 0 deletions src/components/Radio/Radio.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
---
name: Radio
menu: Components
route: /radio
---

import { Playground, Props } from 'docz'
import { Radio, Loader } from '../../index.ts'

import reactImage from './docAssets/react.png'
import dockerImage from './docAssets/docker.png'
import pythonImage from './docAssets/python.png'

# Radio

## Examples

### Basic usage

Radio buttons are made up of two components working together. The `Radio.Group` and `Radio.Item` component.

The `Radio.Group` component controls the name of the radio buttons and the onChange event. Each `Radio.Group` name property should be unique. The `Radio.Item` componet contains the value and label of the radio buttons. You add one `Radio.Item` per option.

The `Radio.Item` component must be wrapped in a `Radio.Group` component to function properly.

<Playground>
<Radio.Group name="classes1" defaultValue="react">
<Radio.Item value="react" label="Introduction To React" />
<br />
<Radio.Item value="docker" label="Introduction To Docker" />
<br />
<Radio.Item value="python" label="Introduction To Python" />
</Radio.Group>
</Playground>

### Custom label with Icon

The label property on the button component also accepts JSX. This allows you to display custom components in the radio button label, like icons.

<Playground>
<Radio.Group name="classes2" defaultValue="react">
<Radio.Item value="react" label={<React.Fragment><img src={reactImage} style={{ height: '20px', marginRight: '6px'}} />Introduction To React</React.Fragment>} />
<br />
<Radio.Item value="docker" label={<React.Fragment><img src={dockerImage} style={{ height: '20px', marginRight: '6px'}} />Introduction To Docker</React.Fragment>}/>
<br />
<Radio.Item value="python" label={<React.Fragment><img src={pythonImage} style={{ height: '20px', marginRight: '6px'}} />Introduction To Python</React.Fragment>} />
</Radio.Group>
</Playground>

### Radio item with description

<Playground>
<Radio.Group name="classes3" defaultValue="react">
<Radio.Item value="react" label="Introduction To React" description="This course is for React newbies and anyone looking to build a solid foundation. It’s designed to teach you everything you need to start building web applications in React right away."/>
<br />
<Radio.Item value="docker" label="Introduction To Docker" description="In this course we will take a simple Node.js app that connects to a MongoDB database and uses an Express web server, and how to properly “Dockerize” the app."/>
<br />
<Radio.Item value="python" label="Introduction To Python" description="At the end of this course, you’ll have a great working knowledge of Python, fully capable of creating your own Python projects from scratch or jumping into an existing application."/>
</Radio.Group>
</Playground>

### Disabled radio items

<Playground>
<Radio.Group name="classes4" defaultValue="react">
<Radio.Item isDisabled value="react" label="Introduction To React" description="This course is for React newbies and anyone looking to build a solid foundation. It’s designed to teach you everything you need to start building web applications in React right away."/>
<br />
<Radio.Item isDisabled value="docker" label="Introduction To Docker" description="In this course we will take a simple Node.js app that connects to a MongoDB database and uses an Express web server, and how to properly “Dockerize” the app."/>
<br />
<Radio.Item isDisabled value="python" label="Introduction To Python" description="At the end of this course, you’ll have a great working knowledge of Python, fully capable of creating your own Python projects from scratch or jumping into an existing application."/>
</Radio.Group>
</Playground>

## API

### Radio Group

<Props of={Radio.Group} />

### Radio Item

<Props of={Radio.Item} />
95 changes: 95 additions & 0 deletions src/components/Radio/Radio.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import React from 'react';
import { fireEvent, render } from '@testing-library/react';

import Radio from './Radio';

describe('Radio', () => {
test('default snapshot', () => {
const component = (
<Radio.Group name="default" defaultValue="test1">
<Radio.Item value="test1" label="Test option 1" />
<Radio.Item value="test2" label="Test option 2" />
<Radio.Item value="test3" label="Test option 3" />
</Radio.Group>
);
const { asFragment } = render(component);
expect(asFragment()).toMatchSnapshot();
});

test('description snapshot', () => {
const component = (
<Radio.Group name="default" defaultValue="test1">
<Radio.Item value="test1" label="Test option 1" description="Lalalalalalala" />
<Radio.Item value="test2" label="Test option 2" description="Lololololololo" />
<Radio.Item value="test3" label="Test option 3" description="Lelelelelelele" />
</Radio.Group>
);
const { asFragment } = render(component);
expect(asFragment()).toMatchSnapshot();
});

test('onChange should trigger correctly', () => {
const onChangeFn = jest.fn();

const component = (
<Radio.Group name="default" defaultValue="test1" onChange={onChangeFn}>
<Radio.Item value="test1" label="Test option 1" />
<Radio.Item value="test2" label="Test option 2" />
<Radio.Item value="test3" label="Test option 3" />
</Radio.Group>
);
const { getByTestId } = render(component);

const radioButton1 = getByTestId('radio-default-test1');
const radioButton2 = getByTestId('radio-default-test2');
const radioButton3 = getByTestId('radio-default-test3');
fireEvent.click(radioButton1);
fireEvent.click(radioButton2);
fireEvent.click(radioButton3);
fireEvent.click(radioButton3);
fireEvent.click(radioButton2);
fireEvent.click(radioButton1);

expect(onChangeFn).toHaveBeenCalledTimes(4);
});

test('disabled should block onChange', () => {
const onChangeFn = jest.fn();

const component = (
<Radio.Group name="default" defaultValue="test1" onChange={onChangeFn}>
<Radio.Item isDisabled value="test1" label="Test option 1" />
<Radio.Item isDisabled value="test2" label="Test option 2" />
<Radio.Item isDisabled value="test3" label="Test option 3" />
</Radio.Group>
);
const { getByTestId } = render(component);

const radioButton1 = getByTestId('radio-default-test1');
const radioButton2 = getByTestId('radio-default-test2');
const radioButton3 = getByTestId('radio-default-test3');
fireEvent.click(radioButton1);
fireEvent.click(radioButton2);
fireEvent.click(radioButton3);

expect(onChangeFn).toHaveBeenCalledTimes(0);
});

test('className prop', () => {
const className = 'center';
const component = (
<Radio.Group name="default" defaultValue="test1">
<Radio.Item className={className} value="test1" label="Test option 1" />
<Radio.Item value="test2" label="Test option 2" />
<Radio.Item value="test3" label="Test option 3" />
</Radio.Group>
);
const { getByTestId } = render(component);
const containerElement = getByTestId('default-test1-container');

const renderedClassNames = containerElement.className.split(' ');
expect(renderedClassNames).toContain(className);
// className in prop should be the last in the row
expect(renderedClassNames.indexOf(className)).toBe(renderedClassNames.length - 1);
});
});
4 changes: 4 additions & 0 deletions src/components/Radio/Radio.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import RadioGroup from './RadioGroup/RadioGroup';
import RadioItem from './RadioItem/RadioItem';

export default { Group: RadioGroup, Item: RadioItem };
43 changes: 43 additions & 0 deletions src/components/Radio/RadioGroup/RadioGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React, { useState } from 'react';

export interface RadioContext {
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
selectedValue: string;
groupRef?: React.Ref<HTMLInputElement>;
name: string;
}

export interface Props {
children: React.ReactNode;
defaultValue: RadioContext['selectedValue'];
name: string;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
groupRef?: React.Ref<HTMLInputElement>;
}

export const radioContext = React.createContext<RadioContext | null>(null);

const RadioGroup: React.FC<Props> = ({
children,
defaultValue,
onChange,
name,
groupRef,
}: Props) => {
const [selectedValue, setSelectedValue] = useState(defaultValue);

const onRadioItemChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setSelectedValue(e.target.value);
if (onChange) {
onChange(e);
}
};

return (
<radioContext.Provider value={{ selectedValue, onChange: onRadioItemChange, name, groupRef }}>
{children}
</radioContext.Provider>
);
};

export default RadioGroup;
Loading

0 comments on commit 6830711

Please sign in to comment.