Skip to content

Commit

Permalink
Update StyledCheckbox design (#42574)
Browse files Browse the repository at this point in the history
* Update StyledCheckbox design

* Correct the line-height attribute

* Push the margin property to the outer container
  • Loading branch information
bl-nero authored Jun 10, 2024
1 parent f194abd commit b212f28
Show file tree
Hide file tree
Showing 7 changed files with 368 additions and 40 deletions.
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;
margin: 3px;
`;

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

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" />
<IconBox IconCmpt={Icon.ChevronCircleDown} text="ChevronCircleDown" />
<IconBox IconCmpt={Icon.ChevronCircleLeft} text="ChevronCircleLeft" />
<IconBox IconCmpt={Icon.ChevronCircleRight} text="ChevronCircleRight" />
Expand Down
Loading

0 comments on commit b212f28

Please sign in to comment.