Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Checkbox and Switch UI #12

Closed
wants to merge 13 commits into from
76 changes: 76 additions & 0 deletions src/components/Checkbox/Checkbox.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import Checkbox from './Checkbox';
import { fn } from '@storybook/test';
import type { Meta, StoryObj } from '@storybook/react';
import { themeColorSubset } from '../../types.ts';

const meta = {
title: 'Components/Checkbox',
component: Checkbox,
argTypes: {
color: { control: 'select', options: Object.keys(themeColorSubset) },
},
args: {
onClick: fn()
},
} satisfies Meta<typeof Checkbox>;

export default meta;
type Story = StoryObj<typeof meta>;

const defaultProps = {
onClick: fn()
};

const disableControls = {
parameters: {
controls: {
disable: true
},
actions: {
disable: true
},
}
};

export const Demo: Story = {
args: {
...defaultProps,
},
tags: ['excludeFromSidebar']
};

export const Default: Story = {
args: {
...defaultProps,
color: 'primary',
},
...disableControls
};

export const Secondary: Story = {
args: {
...defaultProps,
color: 'secondary',
onClick: fn()
},
...disableControls
};

export const PrimaryOutline: Story = {
args: {
...defaultProps,
color: 'primary',
appearance: 'outline'
},
...disableControls
};

export const SecondaryOutline: Story = {
args: {
...defaultProps,
color: 'secondary',
appearance: 'outline'
},
...disableControls
};

62 changes: 62 additions & 0 deletions src/components/Checkbox/Checkbox.style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import styled from 'styled-components';
import { ThemeColor, ThemeElementAppearance, ThemeElementSize } from '../../types.ts';
import { readableColor, shade } from 'polished';

type StyledButtonProps = {
$color: ThemeColor;
$appearance: ThemeElementAppearance;
$size: ThemeElementSize;
};

export const StyledCheckbox = styled.button<StyledButtonProps>`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Semantically, a checkbox should be an input checkbox, not a button. Guessing you did it this way because options for styling actual checkboxes are pretty limited; you can get around this with some trickery using pseudo-elements. Here's a resource with examples: https://css-tricks.com/the-checkbox-hack/

display: inline-block;
background: ${props => props.theme.colors[props.$color]};
color: ${props => readableColor(props.theme.colors[props.$color])};
appearance: none;
border: 0;
border-radius: 0.25rem;
cursor: pointer;
transition: all 0.3s ease;

// Handle appearance
${props => {
if (props.$appearance === 'outline') {

Check warning on line 23 in src/components/Checkbox/Checkbox.style.ts

View workflow job for this annotation

GitHub Actions / tests-and-file-checks

Expected indentation of 2 tabs but found 4 spaces
return `

Check warning on line 24 in src/components/Checkbox/Checkbox.style.ts

View workflow job for this annotation

GitHub Actions / tests-and-file-checks

Expected indentation of 3 tabs but found 6 spaces
background: transparent;
color: ${props.theme.colors[props.$color]};
border: 1px solid ${props.theme.colors[props.$color]};
`;
}

Check warning on line 29 in src/components/Checkbox/Checkbox.style.ts

View workflow job for this annotation

GitHub Actions / tests-and-file-checks

Expected indentation of 2 tabs but found 4 spaces
}};

Check warning on line 30 in src/components/Checkbox/Checkbox.style.ts

View workflow job for this annotation

GitHub Actions / tests-and-file-checks

Expected indentation of 1 tab but found 2 spaces

// Handle sizes
${props => {
if (props.$size === 'sm') {

Check warning on line 34 in src/components/Checkbox/Checkbox.style.ts

View workflow job for this annotation

GitHub Actions / tests-and-file-checks

Expected indentation of 2 tabs but found 4 spaces
return `

Check warning on line 35 in src/components/Checkbox/Checkbox.style.ts

View workflow job for this annotation

GitHub Actions / tests-and-file-checks

Expected indentation of 3 tabs but found 6 spaces
width: 1rem;
height: 1rem;
`;
}

Check warning on line 39 in src/components/Checkbox/Checkbox.style.ts

View workflow job for this annotation

GitHub Actions / tests-and-file-checks

Expected indentation of 2 tabs but found 4 spaces
if (props.$size === 'lg') {

Check warning on line 40 in src/components/Checkbox/Checkbox.style.ts

View workflow job for this annotation

GitHub Actions / tests-and-file-checks

Expected indentation of 2 tabs but found 4 spaces
return `

Check warning on line 41 in src/components/Checkbox/Checkbox.style.ts

View workflow job for this annotation

GitHub Actions / tests-and-file-checks

Expected indentation of 3 tabs but found 6 spaces
width: 2rem;
height: 2rem;
`;
}

Check warning on line 45 in src/components/Checkbox/Checkbox.style.ts

View workflow job for this annotation

GitHub Actions / tests-and-file-checks

Expected indentation of 2 tabs but found 4 spaces
return `
width: 1.5rem;
height: 1.5rem;
`;
}};

&:hover, &:focus, &:active {
text-decoration-color: currentColor;
color: ${props => readableColor(props.theme.colors[props.$color])};

${props => props.$appearance === 'outline' ? `
background: ${props.theme.colors[props.$color]};
` : `
background: ${shade(0.15, props.theme.colors[props.$color])};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work using the theming and Polished colour functions!

`}
}
`;
12 changes: 12 additions & 0 deletions src/components/Checkbox/Checkbox.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { render, screen } from '@testing-library/react';
import Checkbox from './Checkbox';

describe('<Checkbox />', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can wait until next tri, but it would be good to have some more extensive tests here e.g., selecting and deselecting

it('should mount', () => {
render(<Checkbox/>);

const checkbox = screen.getByTestId('Checkbox');

expect(checkbox).toBeInTheDocument();
});
});
25 changes: 25 additions & 0 deletions src/components/Checkbox/Checkbox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { FC } from 'react';
import { StyledCheckbox } from './Checkbox.style';
import { ThemeColor, ThemeElementAppearance, ThemeElementSize } from '../../types.ts';

type CheckboxProps = {
color?: ThemeColor;
onClick: () => void;
appearance?: ThemeElementAppearance;
size?: ThemeElementSize;
}

const Checkbox: FC<CheckboxProps> = ({
color = 'primary',
onClick,
appearance = 'solid',
size = 'md'
}) => {
return (
<StyledCheckbox data-testid="Checkbox" onClick={onClick} $color={color} $appearance={appearance} $size={size}>
{}
</StyledCheckbox>
);
};

export default Checkbox;
75 changes: 75 additions & 0 deletions src/components/Switch/Switch.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import Switch from './Switch';
import { fn } from '@storybook/test';
import type { Meta, StoryObj } from '@storybook/react';
import { themeColorSubset } from '../../types.ts';

const meta = {
title: 'Components/Switch',
component: Switch,
argTypes: {
color: { control: 'select', options: Object.keys(themeColorSubset) },
},
args: {
onClick: fn()
},
} satisfies Meta<typeof Switch>;

export default meta;
type Story = StoryObj<typeof meta>;

const defaultProps = {
onClick: fn()
};

const disableControls = {
parameters: {
controls: {
disable: true
},
actions: {
disable: true
},
}
};

export const Demo: Story = {
args: {
...defaultProps,
},
tags: ['excludeFromSidebar']
};

export const Default: Story = {
args: {
...defaultProps,
color: 'primary',
},
...disableControls
};

export const Secondary: Story = {
args: {
...defaultProps,
color: 'secondary',
onClick: fn()
},
...disableControls
};

export const PrimaryOutline: Story = {
args: {
...defaultProps,
color: 'primary',
appearance: 'outline'
},
...disableControls
};

export const SecondaryOutline: Story = {
args: {
...defaultProps,
color: 'secondary',
appearance: 'outline'
},
...disableControls
};
60 changes: 60 additions & 0 deletions src/components/Switch/Switch.style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import styled from 'styled-components';
import { ThemeColor, ThemeElementAppearance, ThemeElementSize } from '../../types.ts';
import { readableColor, shade } from 'polished';

type StyledSwitch = {
$color: ThemeColor;
$appearance: ThemeElementAppearance;
$size: ThemeElementSize
}
export const StyledSwitch = styled.button<StyledSwitch>`
display: inline-block;
background: ${props => props.theme.colors[props.$color]};
color: ${props => readableColor(props.theme.colors[props.$color])};
appearance: none;
border: 0;
border-radius: 3rem;
cursor: pointer;
transition: all 0.3s ease;
// Handle appearance
${props => {
if(props.$appearance === 'outline') {
return `
background: transparent;
color: ${props.theme.colors[props.$color]};
border: 1px solid ${props.theme.colors[props.$color]};
`;
}
}};
// Handle sizes
${props => {
if(props.$size === 'sm') {
return `
width: 1.5rem;
height: 1rem;
`;
}
if(props.$size === 'lg') {
return `
width: 4rem;
height: 1rem;
`;
}
return `
width: 2.5rem;
height: 1rem;
`;
}};

&:hover, &:focus, &:active {
text-decoration-color: currentColor;
color: ${props => readableColor(props.theme.colors[props.$color])};

${props => props.$appearance === 'outline' ? `
background: ${props.theme.colors[props.$color]};
` : `
background: ${shade(0.15, props.theme.colors[props.$color])};
`
}};
}
`;
12 changes: 12 additions & 0 deletions src/components/Switch/Switch.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { render, screen } from '@testing-library/react';
import Switch from './Switch';

describe('<Switch />', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like the checkbox, it would be good to have tests for the different switch states. But this can wait until next tri. :)

it('should mount', () => {
render(<Switch/>);

const switch = screen.getByTestId('Switch');

expect(switch).toBeInTheDocument();
});
});
25 changes: 25 additions & 0 deletions src/components/Switch/Switch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { FC } from 'react';
import { StyledSwitch } from './Switch.style';
import { ThemeColor, ThemeElementAppearance, ThemeElementSize } from '../../types.ts';

type SwitchProps = {
color?: ThemeColor;
onClick: () => void;
appearance?: ThemeElementAppearance;
size?: ThemeElementSize;
}

const Switch: FC<SwitchProps> = ({
color = 'primary',
onClick,
appearance = 'solid',
size = 'md'
}) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do we know if the switch is on or off? It should probably have a boolean state value or something like that, which gets toggled on click. It's not enough for it to look on or off, because its value would be used somewhere in the application.

return (
<StyledSwitch data-testid="Switch" onClick={onClick} $color={color} $appearance={appearance} $size={size}>
{}
</StyledSwitch>
);
};

export default Switch;
Loading