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

feat(checkbox)!: remove top level subcomponents #1693

Merged
merged 1 commit into from
Jul 17, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
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;
Copy link
Contributor

Choose a reason for hiding this comment

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

here's that 1/8 thing again. I imagine radio and checkbox need and use it for the same exact reason

}
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