Skip to content

Commit

Permalink
feat(checkbox)!: remove top level subcomponents (#1693)
Browse files Browse the repository at this point in the history
  • Loading branch information
jinlee93 authored and booc0mtaco committed Jul 19, 2023
1 parent 82da617 commit 87b12e8
Show file tree
Hide file tree
Showing 12 changed files with 171 additions and 225 deletions.
72 changes: 72 additions & 0 deletions src/components/Checkbox/Checkbox.module.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,76 @@
@import '../../design-tokens/mixins.css';

.checkbox {
display: flex;
gap: var(--eds-size-1);
}

.checkbox__input {
height: 18px;
width: 18px;

/* The parent Checkbox component is a flex container. Make sure the input doesn't shrink. */
flex-shrink: 0;

/* Remove the browser's checkbox styles, allowing us to provide our own. */
appearance: none;

/* Magic value to center the checkbox on its label. */
margin: 3px;

color: var(--eds-theme-color-checkbox-brand-background);
border: 2px solid currentColor;

/* Place the ::before content smack in the middle of the input. */
display: grid;
place-content: center;
}

.checkbox__input:checked::before {
background-color: currentColor;
content: '';

/* Clip this element in the shape of a checkbox. The element's background color will be visible
anywhere the svg path is solid. Whatever is behind the checkbox will show through wherever the
svg is transparent. See https://developer.mozilla.org/en-US/docs/Web/CSS/clip-path */
clip-path: path(
'M10.6 16.2L17.65 9.15L16.25 7.75L10.6 13.4L7.75 10.55L6.35 11.95L10.6 16.2ZM3 21V3H21V21H3Z'
);

/* Because the path does not have an explicit viewbox, this element's bounding box establishes
one. In other words, the height/width here need to match the expected viewbox for the path. */
height: 24px;
width: 24px;
}

.checkbox__input:indeterminate::before {
background-color: currentColor;
content: '';

clip-path: path('M7 13H17V11H7V13ZM3 21V3H21V21H3Z');

height: 24px;
width: 24px;
}

.checkbox__input:focus-visible {
@mixin focus;
}

@supports not selector(:focus-visible) {
.checkbox__input:focus {
@mixin focus;
outline-offset: 1px;
}
}

.checkbox__input:disabled {
color: var(--eds-theme-color-icon-disabled);
cursor: not-allowed;
}

.checkbox__label--md {
/* Center the first line of the medium label with the checkbox. */
position: relative;
top: 0.0625rem;
}
8 changes: 3 additions & 5 deletions src/components/Checkbox/Checkbox.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import type { StoryObj, Meta } from '@storybook/react';
import React from 'react';
import { Checkbox } from './Checkbox';
import CheckboxInput from '../CheckboxInput';
import CheckboxLabel from '../CheckboxLabel';

const defaultArgs = {
disabled: false,
Expand Down Expand Up @@ -157,10 +155,10 @@ export const LongLabels = {
export const WithCustomPositioning = {
render: () => (
<div className="flex items-center">
<CheckboxLabel className="mr-2" htmlFor="test">
<Checkbox.Label className="mr-2" htmlFor="test">
Label on Left
</CheckboxLabel>
<CheckboxInput id="test" />
</Checkbox.Label>
<Checkbox.Input id="test" />
</div>
),
};
126 changes: 96 additions & 30 deletions src/components/Checkbox/Checkbox.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,41 @@
import clsx from 'clsx';
import React, { forwardRef } from 'react';
import useForwardedRef from '../../util/useForwardedRef';
import { useId } from '../../util/useId';
import type {
EitherInclusive,
ForwardedRefComponent,
} from '../../util/utility-types';
import type { CheckboxInputProps } from '../CheckboxInput';
import CheckboxInput from '../CheckboxInput';
import type { CheckboxLabelProps } from '../CheckboxLabel';
import CheckboxLabel from '../CheckboxLabel';
import type { EitherInclusive } from '../../util/utility-types';
import { InputLabel, type InputLabelProps } from '../InputLabel/InputLabel';

import styles from './Checkbox.module.css';

type CheckboxLabelProps = InputLabelProps;
type CheckboxHTMLElementProps = Omit<
React.InputHTMLAttributes<HTMLInputElement>,
'checked' | 'id' | 'size'
>;

type CheckboxInputProps = CheckboxHTMLElementProps & {
/**
* Whether checkbox is checked.
*/
checked?: boolean;
/**
* Additional classnames passed in for styling.
*/
className?: string;
/**
* Checkbox ID. Used to connect the input with a label for accessibility purposes.
*/
id: string;
/**
* Whether the checkbox is "indeterminate". Neither checked nor unchecked. The most common use
* case for this is when a checkbox has sub-checkboxes, to represent a "partially checked" state.
*/
indeterminate?: boolean;
};

// id is required in CheckboxInputProps but optional in CheckboxProps, so we
// first remove `id` from CheckboxInputProps before intersecting.
export type CheckboxProps = Omit<CheckboxInputProps, 'id'> & {
type CheckboxProps = Omit<CheckboxInputProps, 'id'> & {
/**
* HTML id attribute. If not passed, this component
* will generate an id to use for accessibility.
Expand All @@ -38,37 +60,81 @@ export type CheckboxProps = Omit<CheckboxInputProps, 'id'> & {
}
>;

type CheckboxType = ForwardedRefComponent<HTMLInputElement, CheckboxProps> & {
Input?: typeof CheckboxInput;
/**
* Checkbox input element, exported for greater flexibility.
* You must provide an `id` prop and connect it to a visible label.
*/
const CheckboxInput = React.forwardRef<HTMLInputElement, CheckboxInputProps>(
({ checked, className, disabled, indeterminate, ...other }, ref) => {
const forwardedRef = useForwardedRef(ref);

// Make this checkbox indeterminate. Can only be done with JS for some reason.
// See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/checkbox#indeterminate_state_checkboxes.
React.useEffect(() => {
if (forwardedRef.current) {
forwardedRef.current.indeterminate = !!indeterminate;
}
}, [forwardedRef, indeterminate]);

return (
<input
checked={checked}
className={clsx(className, styles['checkbox__input'])}
disabled={disabled}
ref={forwardedRef}
type="checkbox"
{...other}
/>
);
},
);

CheckboxInput.displayName = 'CheckboxInput';

const CheckboxLabel = ({ className, size, ...other }: CheckboxLabelProps) => {
const componentClassName = clsx(
size === 'md' && styles['checkbox__label--md'],
className,
);

return <InputLabel className={componentClassName} size={size} {...other} />;
};

/**
* `import {Checkbox} from "@chanzuckerberg/eds";`
*
* `import {CheckboxInput, CheckboxLabel} from '@chanzuckerberg/eds';`
*
* Checkbox control indicating if something is selected or unselected.
*
* NOTE: Requires either a visible label or `aria-label` prop.
*/
export const Checkbox: CheckboxType = forwardRef((props, ref) => {
// All remaining props are passed to the `input` element
const { className, id, label, size = 'lg', disabled, ...other } = props;
export const Checkbox = Object.assign(
forwardRef<HTMLInputElement, CheckboxProps>((props, ref) => {
// All remaining props are passed to the `input` element
const { className, id, label, size = 'lg', disabled, ...other } = props;

const generatedId = useId();
const checkboxId = id || generatedId;
const generatedId = useId();
const checkboxId = id || generatedId;

return (
<div className={clsx(className, styles.checkbox)}>
<CheckboxInput disabled={disabled} id={checkboxId} ref={ref} {...other} />
{label && (
<CheckboxLabel disabled={disabled} htmlFor={checkboxId} size={size}>
{label}
</CheckboxLabel>
)}
</div>
);
});
return (
<div className={clsx(className, styles.checkbox)}>
<CheckboxInput
disabled={disabled}
id={checkboxId}
ref={ref}
{...other}
/>
{label && (
<CheckboxLabel disabled={disabled} htmlFor={checkboxId} size={size}>
{label}
</CheckboxLabel>
)}
</div>
);
}),
{
Input: CheckboxInput,
Label: CheckboxLabel,
},
);

Checkbox.displayName = 'Checkbox';
Checkbox.Input = CheckboxInput;
1 change: 0 additions & 1 deletion src/components/Checkbox/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
export { Checkbox as default } from './Checkbox';
export type { CheckboxProps } from './Checkbox';
65 changes: 0 additions & 65 deletions src/components/CheckboxInput/CheckboxInput.module.css

This file was deleted.

29 changes: 0 additions & 29 deletions src/components/CheckboxInput/CheckboxInput.test.tsx

This file was deleted.

Loading

0 comments on commit 87b12e8

Please sign in to comment.