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

Update StyledCheckbox design #42574

Merged
merged 4 commits into from
Jun 10, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
131 changes: 115 additions & 16 deletions web/packages/design/src/Checkbox/Checkbox.story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,27 +18,126 @@

import React from 'react';

import { Box } from 'design';
import styled from 'styled-components';

import { CheckboxWrapper, CheckboxInput } from './Checkbox';
import { Flex } from '..';

import { StyledCheckbox } from './Checkbox';

export default {
title: 'Design/Checkbox',
};

export const Checkbox = () => (
<Box>
<CheckboxWrapper key={1}>
<CheckboxInput type="checkbox" name="input1" id={'input1'} />
Input 1
</CheckboxWrapper>
<CheckboxWrapper key={2}>
<CheckboxInput type="checkbox" name="input2" id={'input2'} />
Input 2
</CheckboxWrapper>
<CheckboxWrapper key={3}>
<CheckboxInput type="checkbox" name="input3" id={'input3'} />
Input 3
</CheckboxWrapper>
</Box>
<Flex
alignItems="start"
flexDirection="column"
gap={3}
bg="levels.surface"
p={5}
>
<Table border={1}>
<tr>
<th colSpan={2} />
<th>Large</th>
<th>Small</th>
</tr>
<tr>
<th rowSpan={4}>Enabled</th>
<th>Default</th>
<td>
<StyledCheckbox type="checkbox" />
<StyledCheckbox checked />
</td>
<td>
<StyledCheckbox size="small" />
<StyledCheckbox size="small" checked />
</td>
</tr>
<tr className="teleport-checkbox__force-hover">
<th>Hover</th>
<td>
<StyledCheckbox type="checkbox" />
<StyledCheckbox checked />
</td>
<td>
<StyledCheckbox size="small" />
<StyledCheckbox size="small" checked />
</td>
</tr>
<tr className="teleport-checkbox__force-active">
<th>Active</th>
<td>
<StyledCheckbox type="checkbox" />
<StyledCheckbox checked />
</td>
<td>
<StyledCheckbox size="small" />
<StyledCheckbox size="small" checked />
</td>
</tr>
<tr className="teleport-checkbox__force-focus-visible">
<th>Focus</th>
<td>
<StyledCheckbox type="checkbox" />
<StyledCheckbox checked />
</td>
<td>
<StyledCheckbox size="small" />
<StyledCheckbox size="small" checked />
</td>
</tr>
<tr>
<th rowSpan={4}>Disabled</th>
<th>Default</th>
<td>
<StyledCheckbox disabled />
<StyledCheckbox disabled checked />
</td>
<td>
<StyledCheckbox size="small" disabled />
<StyledCheckbox size="small" disabled checked />
</td>
</tr>
<tr className="teleport-checkbox__force-hover">
<th>Hover</th>
<td>
<StyledCheckbox disabled />
<StyledCheckbox disabled checked />
</td>
<td>
<StyledCheckbox size="small" disabled />
<StyledCheckbox size="small" disabled checked />
</td>
</tr>
<tr className="teleport-checkbox__force-active">
<th>Active</th>
<td>
<StyledCheckbox disabled />
<StyledCheckbox disabled checked />
</td>
<td>
<StyledCheckbox size="small" disabled />
<StyledCheckbox size="small" disabled checked />
</td>
</tr>
</Table>
<label>
<StyledCheckbox size="small" defaultChecked={false} /> Uncontrolled
checkbox, unchecked
</label>
<label>
<StyledCheckbox size="small" defaultChecked={true} /> Uncontrolled
checkbox, checked
</label>
</Flex>
);

const Table = styled.table`
border-collapse: collapse;
th,
td {
border: ${p => p.theme.borders[1]};
padding: 10px;
}
`;
202 changes: 179 additions & 23 deletions web/packages/design/src/Checkbox/Checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@

import styled from 'styled-components';

import React from 'react';

import { Flex } from 'design';
import { space } from 'design/system';
import * as Icon from 'design/Icon';

export const CheckboxWrapper = styled(Flex)`
padding: 8px;
Expand All @@ -39,47 +42,200 @@ export const CheckboxInput = styled.input`
margin-right: 10px;
accent-color: ${props => props.theme.colors.brand};

&:hover {
// The "force" class is required for Storybook, where we want to show all the
// states, even though we can't enforce them.
&:hover,
.teleport-checkbox__force-hover & {
cursor: pointer;
}

${space}
`;

// TODO (avatus): Make this the default checkbox
export const StyledCheckbox = styled.input.attrs(props => ({
type CheckboxSize = 'large' | 'small';

interface StyledCheckboxProps {
size?: CheckboxSize;

// Input properties
autoFocus?: boolean;
checked?: boolean;
defaultChecked?: boolean;
disabled?: boolean;
id?: string;
name?: string;
placeholder?: string;
readonly?: boolean;
role?: string;
type?: 'checkbox' | 'radio';
value?: string;

// TODO(bl-nero): Support the "indeterminate" property.

// Container properties
className?: string;
style?: React.CSSProperties;

onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
}

// TODO (bl-nero): Make this the default checkbox
export function StyledCheckbox(props: StyledCheckboxProps) {
const { style, className, size, ...inputProps } = props;
return (
// The outer wrapper and inner wrapper are separate to allow using
// positioning CSS attributes on the checkbox while still maintaining its
// internal integrity that requires the internal wrapper to be positioned.
<OuterWrapper style={style} className={className}>
<InnerWrapper>
{/* The checkbox is rendered as two items placed on top of each other:
the actual checkbox, which is a native input control, and an SVG
checkmark. Note that we avoid the usual "label with content" trick,
because we want to be able to use this component both with and
without surrounding labels. Instead, we use absolute positioning and
an actually rendered input with a custom appearance. */}
<StyledCheckboxInternal cbSize={size} {...inputProps} />
<Checkmark />
</InnerWrapper>
</OuterWrapper>
);
}

const OuterWrapper = styled.span`
line-height: 0;
`;

const InnerWrapper = styled.span`
display: inline-block;
position: relative;
margin: 3px;
`;

const Checkmark = styled(Icon.CheckThick)`
position: absolute;
left: 1px;
top: 1px;
right: 1px;
bottom: 1px;
pointer-events: none;
color: ${props => props.theme.colors.text.primaryInverse};
opacity: 0;

transition: all 150ms;

input:checked + & {
opacity: 1;
}

input:disabled + & {
color: ${props => props.theme.colors.text.main};
}
`;

export const StyledCheckboxInternal = styled.input.attrs(props => ({
// TODO(bl-nero): Make radio buttons a separate control.
type: props.type || 'checkbox',
}))`
// reset the appearance so we can style the background
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
width: 16px;
height: 16px;
border: 1px solid ${props => props.theme.colors.text.muted};
border-radius: ${props => props.theme.radii[1]}px;
border: 1.5px solid ${props => props.theme.colors.text.muted};
border-radius: ${props => props.theme.radii[2]}px;
background: transparent;
cursor: ${props => (props.disabled ? 'not-allowed' : 'pointer')};

position: relative;
margin: 0;

&:checked {
border: 1px solid ${props => props.theme.colors.brand};
background-color: ${props => props.theme.colors.brand};
}
// Give it some animation, but don't animate focus-related properties.
transition:
border-color 150ms,
background-color 150ms,
box-shadow 150ms;

&:hover {
cursor: ${props => (props.disabled ? 'not-allowed' : 'pointer')};
}
// State-specific styles. Note: the "force" classes are required for
// Storybook, where we want to show all the states, even though we can't
// enforce them.
&:enabled {
&:checked {
background-color: ${props => props.theme.colors.buttons.primary.default};
border-color: transparent;
}

&:hover,
.teleport-checkbox__force-hover & {
background-color: ${props =>
props.theme.colors.interactive.tonal.neutral[0]};
border-color: ${props => props.theme.colors.text.slightlyMuted};

&:checked {
background-color: ${props => props.theme.colors.buttons.primary.hover};
border-color: transparent;
box-shadow:
0px 2px 1px -1px rgba(0, 0, 0, 0.2),
0px 1px 1px 0px rgba(0, 0, 0, 0.14),
0px 1px 3px 0px rgba(0, 0, 0, 0.12);
}
}

&::before {
content: '';
display: block;
&:focus-visible,
.teleport-checkbox__force-focus-visible & {
background-color: ${props =>
props.theme.colors.interactive.tonal.neutral[0]};
border-color: ${props => props.theme.colors.buttons.primary.default};
outline: none;
border-width: 2px;

&:checked {
background-color: ${props =>
props.theme.colors.buttons.primary.default};
border-color: transparent;
outline: 2px solid
${props => props.theme.colors.buttons.primary.default};
outline-offset: 1px;
}
}

&:active,
.teleport-checkbox__force-active & {
background-color: ${props =>
props.theme.colors.interactive.tonal.neutral[1]};
border-color: ${props => props.theme.colors.text.slightlyMuted};

&:checked {
background-color: ${props => props.theme.colors.buttons.primary.active};
border-color: transparent;
}
}
}

&:checked::before {
content: '✓';
color: ${props => props.theme.colors.levels.deep};
position: absolute;
right: 1px;
top: -1px;
&:disabled {
background-color: ${props =>
props.theme.colors.interactive.tonal.neutral[0]};
border-color: transparent;
}

${size}
`;

/**
* Returns dimensions of a checkbox with a given `size` property. Since its name
* conflicts with the native `size` attribute with a different type and
* semantics, we use `cbSize` here.
*/
function size(props: { cbSize?: CheckboxSize }) {
const { cbSize = 'large' } = props;
let s = '';
switch (cbSize) {
case 'large':
s = '18px';
break;
case 'small':
s = '14px';
break;
default:
cbSize satisfies never;
}
return { width: s, height: s };
}
1 change: 1 addition & 0 deletions web/packages/design/src/Icon/Icons.story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export const Icons = () => (
<IconBox IconCmpt={Icon.ChatBubble} text="ChatBubble" />
<IconBox IconCmpt={Icon.ChatCircleSparkle} text="ChatCircleSparkle" />
<IconBox IconCmpt={Icon.Check} text="Check" />
<IconBox IconCmpt={Icon.CheckThick} text="CheckThick" />
Copy link
Contributor

Choose a reason for hiding this comment

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

🎉

<IconBox IconCmpt={Icon.ChevronCircleDown} text="ChevronCircleDown" />
<IconBox IconCmpt={Icon.ChevronCircleLeft} text="ChevronCircleLeft" />
<IconBox IconCmpt={Icon.ChevronCircleRight} text="ChevronCircleRight" />
Expand Down
Loading
Loading