Feat / 94 toggle button (#223)
* Toggle button built ui-toolkit * Rewrote controlled toggle button story without args * Fixed ts args issue using ComponentStory element * Wrapped controlled version of buttons in a form for Storybook to stop light and dark theme scope collisions * More toggle tests * Update libs/ui-toolkit/src/components/toggle/toggle.stories.tsx Co-authored-by: Dexter Edwards <dexter.edwards93@gmail.com> * Displays checked state as text for controlled toggles in storybook * Used classnames helper * Added toggle to deal ticket * Simplified the toggles array type to allow any number of toggles, removing the need for a cast Co-authored-by: Dexter Edwards <dexter.edwards93@gmail.com>
This commit is contained in:
parent
4c350a74d0
commit
f0e4aded3a
@ -1,38 +0,0 @@
|
|||||||
import { Button } from '@vegaprotocol/ui-toolkit';
|
|
||||||
|
|
||||||
interface ButtonRadioProps {
|
|
||||||
name: string;
|
|
||||||
options: Array<{ value: string; text: string }>;
|
|
||||||
currentOption: string | null;
|
|
||||||
onSelect: (option: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ButtonRadio = ({
|
|
||||||
name,
|
|
||||||
options,
|
|
||||||
currentOption,
|
|
||||||
onSelect,
|
|
||||||
}: ButtonRadioProps) => {
|
|
||||||
return (
|
|
||||||
<div className="flex gap-8">
|
|
||||||
{options.map((option) => {
|
|
||||||
const isSelected = option.value === currentOption;
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
onClick={() => onSelect(option.value)}
|
|
||||||
className="flex-1"
|
|
||||||
variant={isSelected ? 'accent' : undefined}
|
|
||||||
data-testid={
|
|
||||||
isSelected
|
|
||||||
? `${name}-${option.value}-selected`
|
|
||||||
: `${name}-${option.value}`
|
|
||||||
}
|
|
||||||
key={option.value}
|
|
||||||
>
|
|
||||||
{option.text}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,6 +1,6 @@
|
|||||||
import { FormGroup } from '@vegaprotocol/ui-toolkit';
|
import { FormGroup } from '@vegaprotocol/ui-toolkit';
|
||||||
import { OrderSide } from '@vegaprotocol/wallet';
|
import { OrderSide } from '@vegaprotocol/wallet';
|
||||||
import { ButtonRadio } from './button-radio';
|
import { Toggle } from '@vegaprotocol/ui-toolkit';
|
||||||
import type { Order } from './use-order-state';
|
import type { Order } from './use-order-state';
|
||||||
|
|
||||||
interface SideSelectorProps {
|
interface SideSelectorProps {
|
||||||
@ -9,16 +9,18 @@ interface SideSelectorProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const SideSelector = ({ order, onSelect }: SideSelectorProps) => {
|
export const SideSelector = ({ order, onSelect }: SideSelectorProps) => {
|
||||||
|
const toggles = Object.entries(OrderSide).map(([label, value]) => ({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormGroup label="Direction">
|
<FormGroup label="Direction">
|
||||||
<ButtonRadio
|
<Toggle
|
||||||
name="order-side"
|
name="order-side"
|
||||||
options={Object.entries(OrderSide).map(([text, value]) => ({
|
toggles={toggles}
|
||||||
text,
|
checkedValue={order.side}
|
||||||
value,
|
onChange={(e) => onSelect(e.target.value as OrderSide)}
|
||||||
}))}
|
|
||||||
currentOption={order.side}
|
|
||||||
onSelect={(value) => onSelect(value as OrderSide)}
|
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
);
|
);
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { FormGroup } from '@vegaprotocol/ui-toolkit';
|
import { FormGroup } from '@vegaprotocol/ui-toolkit';
|
||||||
import { OrderType } from '@vegaprotocol/wallet';
|
import { OrderType } from '@vegaprotocol/wallet';
|
||||||
import { ButtonRadio } from './button-radio';
|
import { Toggle } from '@vegaprotocol/ui-toolkit';
|
||||||
import type { Order } from './use-order-state';
|
import type { Order } from './use-order-state';
|
||||||
|
|
||||||
interface TypeSelectorProps {
|
interface TypeSelectorProps {
|
||||||
@ -9,16 +9,18 @@ interface TypeSelectorProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const TypeSelector = ({ order, onSelect }: TypeSelectorProps) => {
|
export const TypeSelector = ({ order, onSelect }: TypeSelectorProps) => {
|
||||||
|
const toggles = Object.entries(OrderType).map(([label, value]) => ({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormGroup label="Order type">
|
<FormGroup label="Order type">
|
||||||
<ButtonRadio
|
<Toggle
|
||||||
name="order-type"
|
name="order-type"
|
||||||
options={Object.entries(OrderType).map(([text, value]) => ({
|
toggles={toggles}
|
||||||
text,
|
checkedValue={order.type}
|
||||||
value,
|
onChange={(e) => onSelect(e.target.value as OrderType)}
|
||||||
}))}
|
|
||||||
currentOption={order.type}
|
|
||||||
onSelect={(value) => onSelect(value as OrderType)}
|
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
);
|
);
|
||||||
|
1
libs/ui-toolkit/src/components/toggle/index.ts
Normal file
1
libs/ui-toolkit/src/components/toggle/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './toggle';
|
201
libs/ui-toolkit/src/components/toggle/toggle.spec.tsx
Normal file
201
libs/ui-toolkit/src/components/toggle/toggle.spec.tsx
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { fireEvent, render, screen } from '@testing-library/react';
|
||||||
|
import { Toggle } from './toggle';
|
||||||
|
|
||||||
|
describe('Toggle', () => {
|
||||||
|
it('should render buttons successfully', () => {
|
||||||
|
render(
|
||||||
|
<Toggle
|
||||||
|
name="test"
|
||||||
|
toggles={[
|
||||||
|
{
|
||||||
|
label: 'Option 1',
|
||||||
|
value: 'test-1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Option 2',
|
||||||
|
value: 'test-2',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(screen.getByText('Option 1')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Option 2')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports more than 2 inputs', () => {
|
||||||
|
render(
|
||||||
|
<Toggle
|
||||||
|
name="test"
|
||||||
|
toggles={[
|
||||||
|
{
|
||||||
|
label: 'Option 1',
|
||||||
|
value: 'test-1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Option 2',
|
||||||
|
value: 'test-2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Option 3',
|
||||||
|
value: 'test-3',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(screen.getByText('Option 3')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allow less than 2 inputs', () => {
|
||||||
|
render(
|
||||||
|
<Toggle
|
||||||
|
name="test"
|
||||||
|
toggles={[
|
||||||
|
{
|
||||||
|
label: 'Option 1',
|
||||||
|
value: 'test-1',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(screen.getByText('Option 1')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uncontrolled toggle initially has no checked item', () => {
|
||||||
|
render(
|
||||||
|
<Toggle
|
||||||
|
name="test"
|
||||||
|
toggles={[
|
||||||
|
{
|
||||||
|
label: 'Option 1',
|
||||||
|
value: 'test-1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Option 2',
|
||||||
|
value: 'test-2',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(screen.getByDisplayValue('test-1').matches(':checked')).toBeFalsy();
|
||||||
|
expect(screen.getByDisplayValue('test-2').matches(':checked')).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uncontrolled toggle displays correct checked item after click', () => {
|
||||||
|
render(
|
||||||
|
<Toggle
|
||||||
|
name="test"
|
||||||
|
toggles={[
|
||||||
|
{
|
||||||
|
label: 'Option 1',
|
||||||
|
value: 'test-1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Option 2',
|
||||||
|
value: 'test-2',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
const button = screen.getByText('Option 1');
|
||||||
|
fireEvent.click(button);
|
||||||
|
expect(screen.getByDisplayValue('test-1').matches(':checked')).toBeTruthy();
|
||||||
|
expect(screen.getByDisplayValue('test-2').matches(':checked')).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('controlled toggle displays correct checked value, first option selected', () => {
|
||||||
|
render(
|
||||||
|
<Toggle
|
||||||
|
name="test"
|
||||||
|
toggles={[
|
||||||
|
{
|
||||||
|
label: 'Option 1',
|
||||||
|
value: 'test-1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Option 2',
|
||||||
|
value: 'test-2',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
checkedValue={'test-1'}
|
||||||
|
onChange={() => null}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(screen.getByDisplayValue('test-1')).toHaveProperty('checked', true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('controlled toggle displays correct checked value, second option selected', () => {
|
||||||
|
render(
|
||||||
|
<Toggle
|
||||||
|
name="test"
|
||||||
|
toggles={[
|
||||||
|
{
|
||||||
|
label: 'Option 1',
|
||||||
|
value: 'test-1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Option 2',
|
||||||
|
value: 'test-2',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
checkedValue={'test-2'}
|
||||||
|
onChange={() => null}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(screen.getByDisplayValue('test-2')).toHaveProperty('checked', true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('onchange handler returning null results in nothing happening', () => {
|
||||||
|
render(
|
||||||
|
<Toggle
|
||||||
|
name="test"
|
||||||
|
toggles={[
|
||||||
|
{
|
||||||
|
label: 'Option 1',
|
||||||
|
value: 'test-1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Option 2',
|
||||||
|
value: 'test-2',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
checkedValue={'test-2'}
|
||||||
|
onChange={() => null}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(screen.getByDisplayValue('test-2')).toHaveProperty('checked', true);
|
||||||
|
const button = screen.getByText('Option 1');
|
||||||
|
fireEvent.click(button);
|
||||||
|
expect(screen.getByDisplayValue('test-2')).toHaveProperty('checked', true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('onchange handler controlling state sets new value', () => {
|
||||||
|
const ComponentWrapper = () => {
|
||||||
|
const [value, setValue] = useState('test-2');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Toggle
|
||||||
|
name="test"
|
||||||
|
toggles={[
|
||||||
|
{
|
||||||
|
label: 'Option 1',
|
||||||
|
value: 'test-1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Option 2',
|
||||||
|
value: 'test-2',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
checkedValue={value}
|
||||||
|
onChange={(e) => setValue(e.target.value)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
render(<ComponentWrapper />);
|
||||||
|
expect(screen.getByDisplayValue('test-2')).toHaveProperty('checked', true);
|
||||||
|
const button = screen.getByText('Option 1');
|
||||||
|
fireEvent.click(button);
|
||||||
|
expect(screen.getByDisplayValue('test-1')).toHaveProperty('checked', true);
|
||||||
|
});
|
||||||
|
});
|
74
libs/ui-toolkit/src/components/toggle/toggle.stories.tsx
Normal file
74
libs/ui-toolkit/src/components/toggle/toggle.stories.tsx
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import type { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||||
|
import { Toggle } from './toggle';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
component: Toggle,
|
||||||
|
title: 'Toggle',
|
||||||
|
} as ComponentMeta<typeof Toggle>;
|
||||||
|
|
||||||
|
export const Controlled: ComponentStory<typeof Toggle> = () => {
|
||||||
|
const [checked, setChecked] = useState('test-1');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form>
|
||||||
|
<div className="mb-12">Current checked state: {checked}</div>
|
||||||
|
<Toggle
|
||||||
|
name="controlled"
|
||||||
|
toggles={[
|
||||||
|
{
|
||||||
|
label: 'Option 1',
|
||||||
|
value: 'test-1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Option 2',
|
||||||
|
value: 'test-2',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
checkedValue={checked}
|
||||||
|
onChange={(e) => setChecked(e.target.value)}
|
||||||
|
className="max-w-[400px]"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const UncontrolledTemplate: ComponentStory<typeof Toggle> = (args) => (
|
||||||
|
<Toggle {...args} />
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Uncontrolled = UncontrolledTemplate.bind({});
|
||||||
|
Uncontrolled.args = {
|
||||||
|
name: 'uncontrolled',
|
||||||
|
toggles: [
|
||||||
|
{
|
||||||
|
label: 'Option 1',
|
||||||
|
value: 'test-1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Option 2',
|
||||||
|
value: 'test-2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
className: 'max-w-[400px]',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MoreButtons = UncontrolledTemplate.bind({});
|
||||||
|
MoreButtons.args = {
|
||||||
|
name: 'more',
|
||||||
|
toggles: [
|
||||||
|
{
|
||||||
|
label: 'Option 1',
|
||||||
|
value: 'test-1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Option 2',
|
||||||
|
value: 'test-2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Option 3',
|
||||||
|
value: 'test-3',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
className: 'max-w-[600px]',
|
||||||
|
};
|
71
libs/ui-toolkit/src/components/toggle/toggle.tsx
Normal file
71
libs/ui-toolkit/src/components/toggle/toggle.tsx
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import classnames from 'classnames';
|
||||||
|
import type { ChangeEvent } from 'react';
|
||||||
|
|
||||||
|
// Supports controlled and uncontrolled setups.
|
||||||
|
interface ToggleProps {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ToggleInputProps {
|
||||||
|
name: string;
|
||||||
|
toggles: ToggleProps[];
|
||||||
|
className?: string;
|
||||||
|
onChange?: (e: ChangeEvent<HTMLInputElement>) => void;
|
||||||
|
checkedValue?: string | undefined | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Toggle = ({
|
||||||
|
name,
|
||||||
|
toggles,
|
||||||
|
className,
|
||||||
|
onChange,
|
||||||
|
checkedValue,
|
||||||
|
...props
|
||||||
|
}: ToggleInputProps) => {
|
||||||
|
const fieldsetClasses = classnames(className, 'flex');
|
||||||
|
const labelClasses = classnames(
|
||||||
|
'group flex-1',
|
||||||
|
'-ml-[1px] first-of-type:ml-0'
|
||||||
|
);
|
||||||
|
const radioClasses = classnames('sr-only', 'peer');
|
||||||
|
const buttonClasses = classnames(
|
||||||
|
'relative peer-checked:z-10 inline-block w-full',
|
||||||
|
'border border-black-60 active:border-black dark:border-white-60 dark:active:border-white peer-checked:border-black dark:peer-checked:border-vega-yellow',
|
||||||
|
'group-first-of-type:rounded-tl group-first-of-type:rounded-bl group-last-of-type:rounded-tr group-last-of-type:rounded-br',
|
||||||
|
'px-28 py-4',
|
||||||
|
'peer-checked:bg-vega-yellow hover:bg-black-25 dark:hover:bg-white-25 hover:peer-checked:bg-vega-yellow',
|
||||||
|
'text-ui text-black-60 dark:text-white-60 peer-checked:text-black active:text-black dark:active:text-white peer-checked:font-bold text-center',
|
||||||
|
'cursor-pointer peer-checked:cursor-auto select-none transition-all'
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<fieldset className={fieldsetClasses} {...props}>
|
||||||
|
{toggles.map(({ label, value }, key) => {
|
||||||
|
const isSelected = value === checkedValue;
|
||||||
|
return (
|
||||||
|
<label key={key} className={labelClasses}>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name={name}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
checked={
|
||||||
|
checkedValue === undefined ? undefined : value === checkedValue
|
||||||
|
}
|
||||||
|
className={radioClasses}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className={buttonClasses}
|
||||||
|
data-testid={
|
||||||
|
isSelected ? `${name}-${value}-selected` : `${name}-${value}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</fieldset>
|
||||||
|
);
|
||||||
|
};
|
@ -15,6 +15,7 @@ export { Select } from './components/select';
|
|||||||
export { Splash } from './components/splash';
|
export { Splash } from './components/splash';
|
||||||
export { TextArea } from './components/text-area';
|
export { TextArea } from './components/text-area';
|
||||||
export { ThemeSwitcher } from './components/theme-switcher';
|
export { ThemeSwitcher } from './components/theme-switcher';
|
||||||
|
export { Toggle } from './components/toggle';
|
||||||
export { Dialog } from './components/dialog/dialog';
|
export { Dialog } from './components/dialog/dialog';
|
||||||
export { VegaLogo } from './components/vega-logo';
|
export { VegaLogo } from './components/vega-logo';
|
||||||
export { Tooltip } from './components/tooltip';
|
export { Tooltip } from './components/tooltip';
|
||||||
|
Loading…
Reference in New Issue
Block a user