Skip to content

Commit

Permalink
feat(component): add indeterminate state to checkboxes (#197)
Browse files Browse the repository at this point in the history
  • Loading branch information
chanceaclark authored Sep 18, 2019
1 parent b9effd4 commit 5146fdb
Show file tree
Hide file tree
Showing 9 changed files with 239 additions and 17 deletions.
28 changes: 28 additions & 0 deletions packages/big-design-icons/src/components/RemoveIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// **********************************
// Auto-generated file, do NOT modify
// **********************************
import React from 'react';

import { createStyledIcon, IconProps } from '../base';

const Icon =
/*#__PURE__*/
React.memo<Partial<IconProps>>(({ title, theme, ...props }) => (
<svg
width={24}
height={24}
viewBox="0 0 24 24"
stroke="currentColor"
fill="currentColor"
strokeWidth="0"
{...props}
>
<title>{title}</title>
<path fill="none" d="M0 0h24v24H0V0z" />
<path d="M18 13H6c-.55 0-1-.45-1-1s.45-1 1-1h12c.55 0 1 .45 1 1s-.45 1-1 1z" />
</svg>
));

export const RemoveIcon =
/*#__PURE__*/
createStyledIcon(Icon);
1 change: 1 addition & 0 deletions packages/big-design-icons/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export * from './OpenInNewIcon';
export * from './PublicIcon';
export * from './ReceiptIcon';
export * from './RemoveCircleOutlineIcon';
export * from './RemoveIcon';
export * from './RestoreIcon';
export * from './SearchIcon';
export * from './SettingsIcon';
Expand Down
1 change: 1 addition & 0 deletions packages/big-design-icons/svgs/material/remove.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
34 changes: 29 additions & 5 deletions packages/big-design/src/components/Checkbox/Checkbox.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CheckIcon } from '@bigcommerce/big-design-icons';
import { CheckIcon, RemoveIcon } from '@bigcommerce/big-design-icons';
import { ThemeInterface } from '@bigcommerce/big-design-theme';
import hoistNonReactStatics from 'hoist-non-react-statics';
import React, { memo, Ref } from 'react';
Expand All @@ -8,6 +8,7 @@ import { uniqueId } from '../../utils';
import { CheckboxContainer, HiddenCheckbox, StyledCheckbox, StyledLabel } from './styled';

interface Props {
isIndeterminate?: boolean;
label: React.ReactChild;
theme?: ThemeInterface;
}
Expand All @@ -24,7 +25,7 @@ class RawCheckbox extends React.PureComponent<CheckboxProps & PrivateProps> {
private readonly labelUniqueId = uniqueId('checkBox_label_');

render() {
const { checked, className, label, forwardedRef, style, ...props } = this.props;
const { checked, className, isIndeterminate, label, forwardedRef, style, ...props } = this.props;
const id = this.getInputId();

return (
Expand All @@ -35,10 +36,33 @@ class RawCheckbox extends React.PureComponent<CheckboxProps & PrivateProps> {
id={id}
{...props}
aria-labelledby={this.labelUniqueId}
ref={forwardedRef}
ref={checkbox => {
if (checkbox && typeof isIndeterminate === 'boolean') {
checkbox.indeterminate = !checked && isIndeterminate;
}

if (forwardedRef) {
if (typeof forwardedRef === 'function') {
forwardedRef(checkbox);
} else {
// RefObject.current is readonly in DefinitelyTyped but in practice you
// can still write to it.
// See https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31065
// @ts-ignore
forwardedRef.current = checkbox;
}
}
}}
/>
<StyledCheckbox checked={checked} htmlFor={id} aria-hidden={true} theme={props.theme}>
<CheckIcon theme={props.theme} />

<StyledCheckbox
isIndeterminate={isIndeterminate}
checked={checked}
htmlFor={id}
aria-hidden={true}
theme={props.theme}
>
{!checked && isIndeterminate ? <RemoveIcon theme={props.theme} /> : <CheckIcon theme={props.theme} />}
</StyledCheckbox>
{this.renderLabel()}
</CheckboxContainer>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`render Checkbox (checked) 1`] = `
exports[`render Checkbox checked 1`] = `
.c4 {
vertical-align: middle;
height: 1.5rem;
Expand Down Expand Up @@ -126,7 +126,136 @@ exports[`render Checkbox (checked) 1`] = `
</div>
`;

exports[`render Checkbox (unchecked) 1`] = `
exports[`render Checkbox indeterminate 1`] = `
.c4 {
vertical-align: middle;
height: 1.5rem;
width: 1.5rem;
}
.c0 {
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
}
.c2 {
border: 0;
-webkit-clip: rect(0 0 0 0);
clip: rect(0 0 0 0);
-webkit-clip-path: inset(50%);
clip-path: inset(50%);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
white-space: nowrap;
width: 1px;
}
.c3 {
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
background: #3C64F4;
border: 1px solid #D9DCE9;
border-color: #3C64F4;
border-radius: 0.25rem;
color: #FFFFFF;
display: -webkit-inline-box;
display: -webkit-inline-flex;
display: -ms-inline-flexbox;
display: inline-flex;
height: 1.25rem;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
-webkit-transition: all 150ms;
transition: all 150ms;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
width: 1.25rem;
}
.c1:focus + .c3 {
box-shadow: 0 0 0 0.25rem #DBE3FE;
}
.c3 svg {
visibility: visible;
}
.c1:focus + .c6 {
box-shadow: 0 0 0 0.25rem #DBE3FE;
}
.c5 {
color: #313440;
margin: 0 0 1rem;
font-size: 1rem;
font-weight: 400;
line-height: 1.5rem;
margin-left: 1rem;
}
.c5:last-child {
margin-bottom: 0;
}
<div
class="c0"
>
<input
aria-labelledby="checkBox_label_1"
class="c1 c2"
id="checkBox_0"
type="checkbox"
/>
<label
aria-hidden="true"
class="c3"
for="checkBox_0"
>
<svg
class="c4"
fill="currentColor"
height="24"
stroke="currentColor"
stroke-width="0"
viewBox="0 0 24 24"
width="24"
>
<title />
<path
d="M0 0h24v24H0V0z"
fill="none"
/>
<path
d="M18 13H6c-.55 0-1-.45-1-1s.45-1 1-1h12c.55 0 1 .45 1 1s-.45 1-1 1z"
/>
</svg>
</label>
<label
class="c5"
for="checkBox_0"
id="checkBox_label_1"
>
Unchecked
</label>
</div>
`;

exports[`render Checkbox unchecked 1`] = `
.c4 {
vertical-align: middle;
height: 1.5rem;
Expand Down
31 changes: 24 additions & 7 deletions packages/big-design/src/components/Checkbox/spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,24 @@ import React from 'react';
import { Checkbox } from './index';
import { StyleableCheckbox } from './private';

test('render Checkbox (checked)', () => {
const { container } = render(<Checkbox label="Checked" checked={true} onChange={() => null} />);
describe('render Checkbox', () => {
test('checked', () => {
const { container } = render(<Checkbox label="Checked" checked={true} onChange={() => null} />);

expect(container.firstChild).toMatchSnapshot();
});
expect(container.firstChild).toMatchSnapshot();
});

test('unchecked', () => {
const { container } = render(<Checkbox label="Unchecked" checked={false} onChange={() => null} />);

expect(container.firstChild).toMatchSnapshot();
});

test('render Checkbox (unchecked)', () => {
const { container } = render(<Checkbox label="Unchecked" checked={false} onChange={() => null} />);
test('indeterminate', () => {
const { container } = render(<Checkbox label="Unchecked" isIndeterminate={true} onChange={() => null} />);

expect(container.firstChild).toMatchSnapshot();
expect(container.firstChild).toMatchSnapshot();
});
});

test('has correct value for checked', () => {
Expand All @@ -36,6 +44,15 @@ test('has correct value for unchecked', () => {
expect(input.checked).toBe(false);
});

test('has correct value for indeterminate', () => {
const { getByTestId } = render(
<Checkbox label="Checked" isIndeterminate checked={false} onChange={() => null} data-testid="checkbox" />,
);
const input = getByTestId('checkbox') as HTMLInputElement;

expect(input.checked).toBe(false);
});

test('triggers onChange when clicking the checkbox', () => {
const onChange = jest.fn();
const { getByTestId } = render(
Expand Down
9 changes: 6 additions & 3 deletions packages/big-design/src/components/Checkbox/styled.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { StyleableText } from '../Typography/private';

interface StyledCheckboxProps {
checked?: boolean;
isIndeterminate?: boolean;
}

export const CheckboxContainer = styled.div`
Expand All @@ -19,9 +20,11 @@ export const HiddenCheckbox = styled.input`

export const StyledCheckbox = styled.label<StyledCheckboxProps>`
align-items: center;
background: ${props => (props.checked ? props.theme.colors.primary : props.theme.colors.white)};
background: ${({ checked, isIndeterminate, theme }) =>
checked || isIndeterminate ? theme.colors.primary : theme.colors.white};
border: ${({ theme }) => theme.border.box};
border-color: ${props => (props.checked ? props.theme.colors.primary : props.theme.colors.secondary30)};
border-color: ${({ checked, isIndeterminate, theme }) =>
checked || isIndeterminate ? theme.colors.primary : theme.colors.secondary30};
border-radius: ${({ theme }) => theme.borderRadius.normal};
color: ${({ theme }) => theme.colors.white};
display: inline-flex;
Expand All @@ -36,7 +39,7 @@ export const StyledCheckbox = styled.label<StyledCheckboxProps>`
}
svg {
visibility: ${props => (props.checked ? 'visible' : 'hidden')};
visibility: ${({ checked, isIndeterminate }) => (checked || isIndeterminate ? 'visible' : 'hidden')};
}
`;

Expand Down
4 changes: 4 additions & 0 deletions packages/docs/PropTables/CheckboxPropTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,9 @@ export const CheckboxPropTable: React.FC = () => (
<PropTable.Prop name="label" types="ReactChild" required>
Label to display next to a <Code>Checkbox</Code> component.
</PropTable.Prop>
<PropTable.Prop name="isIndeterminate" types="boolean">
Styles and sets the checkbox into a indeterminate state. Note that the <Code primary>checked</Code> prop will take
precedence over <Code primary>isIndeterminate</Code>.
</PropTable.Prop>
</PropTable>
);
15 changes: 15 additions & 0 deletions packages/docs/pages/Checkbox/CheckboxPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,20 @@ export default () => (
</Text>

<CheckboxPropTable />

<H1>Indeterminate</H1>

<Text>
Checkboxs support <Code primary>isIndeterminate</Code> passed as a prop to show a combined state of partially
selected checkboxes.
</Text>

<CodePreview>
{/* jsx-to-string:start */}
<Form.Group>
<Checkbox label="Indeterminate" isIndeterminate />
</Form.Group>
{/* jsx-to-string:end */}
</CodePreview>
</>
);

0 comments on commit 5146fdb

Please sign in to comment.