Skip to content
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
1 change: 1 addition & 0 deletions components/radio-group/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
src
1 change: 1 addition & 0 deletions components/radio-group/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# `@byndyusoft-ui/radio-group`
38 changes: 38 additions & 0 deletions components/radio-group/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"name": "@byndyusoft-ui/radio",
"version": "0.0.1",
"description": "Byndyusoft UI RadioGroup React Component",
"keywords": [
"byndyusoft",
"byndyusoft-ui",
"react",
"radio"
],
"author": "Denis Timashev <pleasenonaga@gmail.com>",
"homepage": "https://github.com/Byndyusoft/ui/tree/master/components/radio-group#readme",
"license": "ISC",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"repository": {
"type": "git",
"url": "git+https://github.com/Byndyusoft/ui.git"
},
"scripts": {
"build": "rollup --config",
"clean": "rimraf dist",
"lint": "eslint src --config ../../eslint.config.js",
"test": "jest --config ../../jest.config.js --roots components/radio-group/src",
"storybook": "start-storybook -p 6006",
"build-storybook": "build-storybook"
},
"bugs": {
"url": "https://github.com/Byndyusoft/ui/issues"
},
"publishConfig": {
"access": "public"
},
"peerDependencies": {
"react": ">=17",
"react-dom": ">=17"
}
}
15 changes: 15 additions & 0 deletions components/radio-group/rollup.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import typescript from '@rollup/plugin-typescript';
import baseConfig from '../../rollup.base.config';

export default {
...baseConfig,
input: ['src/index.ts'],
plugins: [
...baseConfig.plugins,
typescript({
tsconfig: './tsconfig.json',
module: 'ESNext',
exclude: ['src/*.stories.tsx', 'src/*.tests.tsx', 'node_modules']
})
]
};
42 changes: 42 additions & 0 deletions components/radio-group/src/RadioGroup.tests.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React, { useState } from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import RadioGroup from './components/RadioGroup';
import Radio from './components/Radio';

const TestComponent = ({ selectedOption = 'apple' }: { selectedOption?: string }) => {
const [value, setValue] = useState(selectedOption);

return (
<>
<strong>Fruit: {value}</strong>
<RadioGroup value={value} name="fruit" onChange={setValue}>
<Radio value="apple">Apple</Radio>
<Radio value="banana">Banana</Radio>
<Radio value="pineapple">Pineapple</Radio>
</RadioGroup>
</>
);
};

const setup = () => render(<TestComponent />);

describe('RadioGroup', () => {
test('renders radio group with labels', () => {
setup();

expect(screen.getByText('Apple')).toBeInTheDocument();
expect(screen.getByText('Banana')).toBeInTheDocument();
expect(screen.getByText('Pineapple')).toBeInTheDocument();
});

test('selecting option is changing value', async () => {
setup();

const bananaRadio = screen.getByRole('radio', { name: 'Banana' });
await userEvent.click(bananaRadio);

expect(screen.getByText('Fruit: banana')).toBeInTheDocument();
});
});
35 changes: 35 additions & 0 deletions components/radio-group/src/RadioGroup.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { ReactNode } from 'react';

export type TOnChange = (value: string) => void;

export interface IRadioProps {
value: string;
isDisabled?: boolean;
children: ReactNode;
}

export interface IRadioGroupProps {
value: string;
children: ReactNode;
name: string;
onChange: TOnChange;
}

export interface IUseRadioGroupStateProps {
name: string;
value: string;
onChange: TOnChange;
}

export interface IUseRadioGroup {
name: string;
value: string;
setValue: (value: string) => void;
}

export interface IRadioGroupContextProviderProps {
name: string;
value: string;
children: ReactNode;
onChange: TOnChange;
}
30 changes: 30 additions & 0 deletions components/radio-group/src/components/Radio.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React, { ReactElement } from 'react';
import { IRadioProps } from '../RadioGroup.types';
import { useRadioGroupContext } from './RadioGroupContext';

const Radio = ({ value, isDisabled, children }: IRadioProps): ReactElement => {
const { name, value: groupValue, setValue } = useRadioGroupContext();

const radioId = `${name}-${value}`;

const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
setValue(event.target.value);
};

return (
<div>
<input
type="radio"
name={name}
id={radioId}
value={value}
checked={groupValue === value}
onChange={handleInputChange}
disabled={isDisabled}
/>
<label htmlFor={radioId}>{children}</label>
</div>
);
};

export default Radio;
11 changes: 11 additions & 0 deletions components/radio-group/src/components/RadioGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import React, { ReactElement } from 'react';
import { IRadioGroupProps } from '../RadioGroup.types';
import { RadioGroupContextProvider } from './RadioGroupContext';

const RadioGroup = ({ name, value, children, onChange }: IRadioGroupProps): ReactElement => (
<RadioGroupContextProvider name={name} value={value} onChange={onChange}>
{children}
</RadioGroupContextProvider>
);

export default RadioGroup;
22 changes: 22 additions & 0 deletions components/radio-group/src/components/RadioGroupContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React, { createContext, useContext } from 'react';
import { IRadioGroupContextProviderProps, IUseRadioGroup } from '../RadioGroup.types';
import useRadioGroupState from '../useRadioGroupState';

const RadioGroupContext = createContext<IUseRadioGroup>({} as IUseRadioGroup);

export function useRadioGroupContext(): IUseRadioGroup {
const context = useContext(RadioGroupContext);

// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (context === undefined) {
throw new Error('useRadioGroupContext must be used within the RadioGroupContextProvider');
}

return context;
}

export const RadioGroupContextProvider = ({ name, value, onChange, children }: IRadioGroupContextProviderProps) => {
const radioGroupState = useRadioGroupState({ name, value, onChange });

return <RadioGroupContext.Provider value={radioGroupState}>{children}</RadioGroupContext.Provider>;
};
6 changes: 6 additions & 0 deletions components/radio-group/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export { default as RadioGroup } from './components/RadioGroup';
export { default as Radio } from './components/Radio';
export { default as useRadioGroupState } from './useRadioGroupState';
export { RadioGroupContextProvider, useRadioGroupContext } from './components/RadioGroupContext';

export { IRadioGroupProps, IRadioProps, IUseRadioGroupStateProps, IUseRadioGroup } from './RadioGroup.types';
29 changes: 29 additions & 0 deletions components/radio-group/src/stories/CustomRadio.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React, { ReactNode, useCallback } from 'react';
import { useRadioGroupContext } from '../components/RadioGroupContext';

const CustomRadioComponent = ({ value, children }: { value: string; children: ReactNode }): JSX.Element => {
const { name, value: groupValue, setValue } = useRadioGroupContext();

const onChangeHandler = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
setValue(event.target.value);
},
[setValue]
);

return (
<div>
<input
type="radio"
name={name}
id={value}
value={value}
checked={groupValue === value}
onChange={onChangeHandler}
/>
<label htmlFor={value}>{children}</label>
</div>
);
};

export default CustomRadioComponent;
68 changes: 68 additions & 0 deletions components/radio-group/src/stories/Radio.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import React, { useState } from 'react';
import { Story } from '@storybook/react';
import RadioGroup from '../components/RadioGroup';
import Radio from '../components/Radio';
import CustomRadioComponent from './CustomRadio';

const DefaultStory: Story = () => {
const [value, setValue] = useState('apple');

return (
<div>
<strong>Fruit: {value}</strong>
<RadioGroup value={value} name="fruit" onChange={setValue}>
<Radio value="apple">🍎 Apple</Radio>
<Radio value="banana">🍌 Banana</Radio>
<Radio value="pineapple">🍍 Pineapple</Radio>
</RadioGroup>
</div>
);
};

export const Default = DefaultStory.bind({});

const WithCustomRadioStory: Story = () => {
const [value, setValue] = useState('happy');

return (
<div>
<strong>Mood: {value}</strong>
<RadioGroup value={value} name="mood" onChange={setValue}>
<CustomRadioComponent value="happy">😊 Happy</CustomRadioComponent>
<CustomRadioComponent value="neutral">😐 Neutral</CustomRadioComponent>
<CustomRadioComponent value="upset">😔 Upset</CustomRadioComponent>
</RadioGroup>
</div>
);
};

export const WithCustomRadio = WithCustomRadioStory.bind({});

const ResetStateStory: Story = () => {
const [value, setValue] = useState('happy');

const handleResetState = (): void => {
setValue('happy');
};

return (
<div>
<strong>Mood: {value}</strong>
<RadioGroup value={value} name="mood" onChange={setValue}>
<CustomRadioComponent value="happy">😊 Happy</CustomRadioComponent>
<CustomRadioComponent value="neutral">😐 Neutral</CustomRadioComponent>
<CustomRadioComponent value="upset">😔 Upset</CustomRadioComponent>
</RadioGroup>
<button type="button" onClick={handleResetState}>
Reset state
</button>
</div>
);
};

export const ResetState = ResetStateStory.bind({});

export default {
title: 'components/RadioGroup',
component: RadioGroup
};
29 changes: 29 additions & 0 deletions components/radio-group/src/useRadioGroupState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { useCallback, useEffect, useState } from 'react';
import { IUseRadioGroupStateProps, IUseRadioGroup } from './RadioGroup.types';

const useRadioGroupState = ({ name, value, onChange }: IUseRadioGroupStateProps): IUseRadioGroup => {
const [stateValue, setStateValue] = useState(value);

useEffect(() => {
if (value !== stateValue) {
setStateValue(value);
onChange?.(value);
}
}, [value, stateValue]);

const setValueHandler = useCallback(
(targetValue: string) => {
setStateValue(targetValue);
onChange?.(targetValue);
},
[onChange]
);

return {
name,
value: stateValue,
setValue: setValueHandler
};
};

export default useRadioGroupState;
10 changes: 10 additions & 0 deletions components/radio-group/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"declaration": true,
"declarationDir": "dist",
"outDir": "dist",
"module": "commonjs"
},
"include": ["../../types.d.ts", "src"]
}
13 changes: 13 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.