feat(UI): implement ToggleButton component (#48567)

This commit is contained in:
Rajkumar Gaur
2023-03-15 16:38:58 +05:30
committed by GitHub
parent 31a03a69ed
commit f03c4a2ecc
6 changed files with 341 additions and 0 deletions

View File

@@ -3,3 +3,4 @@ export { Button } from './button';
export { Alert } from './alert';
export { Image } from './image';
export { Table } from './table';
export { ToggleButton } from './toggle-button';

View File

@@ -0,0 +1,2 @@
export { ToggleButton } from './toggle-button';
export type { ToggleButtonProps } from './types';

View File

@@ -0,0 +1,125 @@
import React, { useState } from 'react';
import { Story } from '@storybook/react';
import { ToggleButton, ToggleButtonProps } from '.';
const story = {
title: 'Example/ToggleButton',
component: ToggleButton,
parameters: {
controls: {
include: [
'children',
'bsStyle',
'bsSize',
'disabled',
'checked',
'onChange',
'value',
'name'
]
}
},
argTypes: {
bsStyle: {
options: ['primary']
},
bsSize: {
options: ['small', 'medium', 'large']
},
disabled: {
options: [true, false],
control: { type: 'radio' }
},
checked: {
options: [true, false],
control: { type: 'radio' }
},
onChange: {
action: 'changed'
},
value: {
type: { name: 'string' }
},
name: {
type: { name: 'string' }
}
}
};
const Template: Story<ToggleButtonProps> = args => {
return <ToggleButton {...args} />;
};
export const Default = Template.bind({});
Default.args = {
children: 'Off'
};
export const Checked = Template.bind({});
Checked.args = {
checked: true,
children: 'On',
value: 'Value'
};
export const Large = Template.bind({});
Large.args = {
bsSize: 'large',
children: 'Off'
};
export const Medium = Template.bind({});
Medium.args = {
bsSize: 'medium',
children: 'Off'
};
export const Disabled = Template.bind({});
Disabled.args = {
children: 'Off',
disabled: true
};
export const RadioChecked = Template.bind({});
RadioChecked.args = {
type: 'radio',
children: 'On',
value: 'radio',
name: 'radio',
checked: true
};
export const RadioUnchecked = Template.bind({});
RadioUnchecked.args = {
type: 'radio',
children: 'Off',
value: 'radio',
name: 'radio'
};
export const InsideToggleGroup = (): JSX.Element => {
const [checked, setChecked] = useState(true);
return (
<>
<ToggleButton
checked={checked}
onChange={checked => {
setChecked(checked);
}}
>
On
</ToggleButton>
<ToggleButton
checked={!checked}
onChange={checked => {
setChecked(!checked);
}}
>
Off
</ToggleButton>
</>
);
};
export default story;

View File

@@ -0,0 +1,78 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ToggleButton } from '.';
describe('<ToggleButton />', () => {
it('should render the toggle button and text', () => {
render(<ToggleButton>On</ToggleButton>);
expect(screen.getByRole('button', { name: /on/i })).toBeInTheDocument();
});
it('should call onChange when clicked', () => {
const onChange = jest.fn();
render(<ToggleButton onChange={onChange}>On</ToggleButton>);
userEvent.click(screen.getByRole('button', { name: /on/i }));
expect(onChange).toBeCalledTimes(1);
});
it('should be checked if checked prop is true', () => {
render(
<ToggleButton checked={true} type='radio'>
On
</ToggleButton>
);
expect(screen.getByRole('radio')).toBeChecked();
});
it('should be unchecked if checked prop is false', () => {
render(
<ToggleButton checked={false} type='radio'>
On
</ToggleButton>
);
expect(screen.getByRole('radio')).not.toBeChecked();
});
it('should be aria-disabled if disabled prop is true', () => {
render(<ToggleButton disabled={true}>On</ToggleButton>);
expect(screen.getByRole('button', { name: /on/i })).toHaveAttribute(
'aria-disabled',
'true'
);
});
it('should not trigger onChange if disabled prop is true', () => {
const onChange = jest.fn();
render(
<ToggleButton disabled={true} onChange={onChange}>
On
</ToggleButton>
);
userEvent.click(screen.getByRole('button', { name: /on/i }));
expect(onChange).not.toBeCalled();
});
it('should have value property if radio', () => {
render(
<form aria-label='form'>
<ToggleButton checked={true} type='radio' value='value' name='radio'>
On
</ToggleButton>
</form>
);
expect(screen.getByRole('form')).toHaveFormValues({
radio: 'value'
});
});
});

View File

@@ -0,0 +1,119 @@
import React from 'react';
import { ButtonSize, ToggleButtonProps } from './types';
const defaultClassNames = [
'relative',
'border-3',
'text-center',
'inline-block',
'cursor-pointer',
'border-foreground-secondary',
'focus:outline-none', // Hide the default browser outline
'focus:ring',
'focus:ring-focus-outline-color',
'focus-within:ring',
'focus-within:ring-focus-outline-color',
'aria-disabled:cursor-not-allowed',
'aria-disabled:opacity-50',
'ml-[-3px]',
'first:ml-0'
];
const computeClassNames = ({
bsSize,
checked,
disabled
}: {
bsSize: ButtonSize;
checked?: boolean;
disabled?: boolean;
}) => {
const classNames = [
...defaultClassNames,
...(checked
? ['cursor-default', 'bg-foreground-primary', 'text-background-primary']
: ['bg-background-quaternary', 'text-foreground-secondary']),
...(disabled
? ['active:before:hidden']
: [
'active:before:w-full',
'active:before:h-full',
'active:before:absolute',
'active:before:inset-0',
'active:before:border-3',
'active:before:border-transparent',
'active:before:bg-gray-900',
'active:before:opacity-20',
'dark:hover:bg-background-primary',
'dark:hover:text-foreground-primary',
...(checked
? [
'hover:bg-background-quaternary',
'hover:text-foreground-secondary'
]
: ['hover:bg-foreground-primary', 'hover:text-background-primary'])
])
];
switch (bsSize) {
case 'large':
classNames.push('px-8 py-2.5 text-lg');
break;
case 'medium':
classNames.push('px-6 py-1.5 text-md');
break;
// default size is 'small'
default:
classNames.push('px-5 py-1 text-sm');
}
return classNames.join(' ');
};
export const ToggleButton = ({
bsSize = 'small',
type = 'button',
disabled,
children,
checked,
onChange,
value,
name
}: ToggleButtonProps): JSX.Element => {
const classNames = computeClassNames({ bsSize, disabled, checked });
const handleChange = () => {
if (!disabled && onChange) {
onChange(true);
}
};
if (type === 'radio') {
return (
<label htmlFor='toggle-btn-radio' className={classNames}>
<input
type='radio'
id='toggle-btn-radio'
name={name}
value={value}
onChange={handleChange}
checked={checked}
aria-disabled={disabled}
className='absolute h-0 w-0 opacity-0'
/>
{children}
</label>
);
}
return (
<button
aria-pressed={checked}
aria-disabled={disabled}
className={classNames}
onClick={handleChange}
>
{children}
</button>
);
};

View File

@@ -0,0 +1,16 @@
export type ButtonStyle = 'primary' | 'danger';
export type ButtonSize = 'small' | 'medium' | 'large';
export interface ToggleButtonProps {
children: React.ReactNode;
bsSize?: ButtonSize;
bsStyle?: ButtonStyle;
disabled?: boolean;
checked?: boolean;
onChange?: (value: boolean) => void;
className?: string;
value?: string;
name?: string;
type?: 'button' | 'radio';
}