-
Notifications
You must be signed in to change notification settings - Fork 31
feat(Checkbox): indeterminate checkboxes #3123
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
Changes from all commits
f4f4743
b4758d1
37f70e5
c45c48e
e0b3d13
84924d3
94496e4
03a03b2
c91c454
d53f887
cb72235
e36a69a
a33965d
076f3a6
cde8a2b
a5137e5
8a51bd3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,7 +6,7 @@ import { | |
| } from '@codecademy/gamut-styles'; | ||
| import { StyleProps } from '@codecademy/variance'; | ||
| import styled from '@emotion/styled'; | ||
| import { forwardRef, InputHTMLAttributes, ReactNode } from 'react'; | ||
| import { forwardRef, InputHTMLAttributes, useEffect, useRef } from 'react'; | ||
|
|
||
| import { | ||
| checkboxElement, | ||
|
|
@@ -19,29 +19,17 @@ import { | |
| polyline, | ||
| } from '../styles'; | ||
| import { BaseInputProps } from '../types'; | ||
| import { CheckboxCheckedUnion, CheckboxLabelUnion } from './types'; | ||
|
|
||
| export type CheckboxTextProps = StyleProps<typeof checkboxTextStates>; | ||
| export type CheckboxPaddingProps = StyleProps<typeof checkboxPadding>; | ||
|
|
||
| export type CheckboxStringLabelProps = { | ||
| label: string; | ||
| 'aria-label'?: string; | ||
| }; | ||
|
|
||
| export type CheckboxReactNodeLabelProps = { | ||
| label: ReactNode; | ||
| 'aria-label': string; | ||
| }; | ||
|
|
||
| export type CheckboxLabelProps = | ||
| | CheckboxStringLabelProps | ||
| | CheckboxReactNodeLabelProps; | ||
|
|
||
| export type CheckboxProps = Omit< | ||
| InputHTMLAttributes<HTMLInputElement>, | ||
| 'value' | 'label' | 'aria-label' | ||
| 'checked' | 'value' | 'label' | 'aria-label' | ||
| > & | ||
| CheckboxLabelProps & | ||
| CheckboxLabelUnion & | ||
| CheckboxCheckedUnion & | ||
| CheckboxPaddingProps & | ||
| Pick<BaseInputProps, 'name' | 'required'> & { | ||
| multiline?: boolean; | ||
|
|
@@ -78,6 +66,10 @@ export type CheckboxProps = Omit< | |
| */ | ||
| value?: string | boolean; | ||
| id?: string; | ||
| /** | ||
| * Use if you want both the aria-label and text label to be read by voiceover - this component assumes that the aria-label and visual text label are identical. | ||
| * If you have a link in the Checkbox options, you should set this as true. | ||
| */ | ||
| dontAriaHideLabel?: boolean; | ||
| }; | ||
|
|
||
|
|
@@ -88,17 +80,20 @@ const CheckboxLabel = styled.label<Pick<CheckboxProps, 'disabled' | 'spacing'>>( | |
| checkboxLabelStates | ||
| ); | ||
|
|
||
| const CheckboxElement = styled('div', styledOptions)< | ||
| Pick<CheckboxProps, 'checked' | 'multiline' | 'disabled'> | ||
| >(checkboxElement, checkboxElementStates); | ||
| type CheckboxElementProps = StyleProps<typeof checkboxElementStates>; | ||
|
|
||
| const CheckboxElement = styled('div', styledOptions)<CheckboxElementProps>( | ||
| checkboxElement, | ||
| checkboxElementStates | ||
| ); | ||
|
|
||
| const CheckboxVector = styled.svg` | ||
| position: absolute; | ||
| top: -1px; | ||
| left: -1px; | ||
| `; | ||
|
|
||
| const Polyline = styled.polyline<Pick<CheckboxProps, 'checked'>>` | ||
| const Checkmark = styled.polyline<Pick<CheckboxProps, 'checked'>>` | ||
| ${polyline} | ||
| fill: none; | ||
| stroke: currentColor; | ||
|
|
@@ -110,6 +105,16 @@ const Polyline = styled.polyline<Pick<CheckboxProps, 'checked'>>` | |
| transition: stroke-dashoffset ${timing.fast}; | ||
| `; | ||
|
|
||
| const Line = styled.line<Pick<CheckboxProps, 'indeterminate'>>` | ||
| ${polyline} | ||
| fill: none; | ||
| stroke: currentColor; | ||
| stroke-width: 2; | ||
| stroke-dasharray: 18px; | ||
| stroke-dashoffset: ${({ indeterminate }) => (indeterminate ? 0 : `18px`)}; | ||
| transition: stroke-dashoffset ${timing.fast}; | ||
| `; | ||
|
|
||
| const Input = styled.input` | ||
| ${screenReaderOnly} | ||
| ${checkboxInput} | ||
|
|
@@ -121,20 +126,42 @@ export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>( | |
| ( | ||
| { | ||
| 'aria-label': ariaLabel, | ||
| checked, | ||
| indeterminate, | ||
| className, | ||
| label, | ||
| disabled, | ||
| dontAriaHideLabel, | ||
| htmlFor, | ||
| multiline, | ||
| id, | ||
| checked, | ||
| disabled, | ||
| label, | ||
| multiline, | ||
| spacing, | ||
| value, | ||
| dontAriaHideLabel, | ||
| ...rest | ||
| }, | ||
| ref | ||
| ) => { | ||
| const intRef = useRef<HTMLInputElement | null>(null); | ||
|
|
||
| function syncedRefs(element: HTMLInputElement | null) { | ||
| intRef.current = element; | ||
| if (ref) { | ||
| if (typeof ref === 'object') { | ||
| ref.current = element; | ||
| } else { | ||
| ref(element); | ||
| } | ||
| } | ||
| } | ||
|
Comment on lines
+146
to
+155
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Q: when would
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. callback refs are sometimes used in animations and other things - here's some react official docs with fancy examples: Manipulating the DOM with Refs |
||
|
|
||
| useEffect(() => { | ||
| if (intRef.current && indeterminate !== undefined && !checked) { | ||
| intRef.current.indeterminate = indeterminate; | ||
| } | ||
| }, [checked, indeterminate]); | ||
|
|
||
| const active = checked || indeterminate; | ||
|
|
||
| return ( | ||
| <div className={className}> | ||
| <Input | ||
|
|
@@ -152,27 +179,39 @@ export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>( | |
| type="checkbox" | ||
| value={`${value}`} | ||
| {...rest} | ||
| ref={ref} | ||
| ref={syncedRefs} | ||
| /> | ||
| <CheckboxLabel | ||
| disabled={disabled} | ||
| htmlFor={id || htmlFor} | ||
| spacing={spacing} | ||
| > | ||
| <CheckboxElement | ||
| checked={checked} | ||
| active={active} | ||
| disabled={disabled} | ||
| hasBg={checked || indeterminate} | ||
| hideBorder={disabled && (checked || indeterminate)} | ||
| multiline={multiline} | ||
| > | ||
| <CheckboxVector | ||
| aria-hidden | ||
| color={checked ? 'currentColor' : 'transparent'} | ||
| color={active ? 'currentColor' : 'transparent'} | ||
| height="19px" | ||
| viewBox="0 0 19 19" | ||
| width="19px" | ||
| > | ||
| <path d="M1 1h19v19h-19z" fill="currentColor" /> | ||
| <Polyline checked={checked} points="4 11 8 15 16 6" /> | ||
| <Checkmark | ||
| // This should never happen if the types are working, but is a good back-up. | ||
| checked={checked && !indeterminate} | ||
| points="4 11 8 15 16 6" | ||
| /> | ||
| <Line | ||
| indeterminate={indeterminate} | ||
| x1="4" | ||
| x2="16" | ||
| y1="10" | ||
| y2="10" | ||
| /> | ||
| </CheckboxVector> | ||
| </CheckboxElement> | ||
| <CheckboxText | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| import { ReactNode } from 'react'; | ||
|
|
||
| export type CheckboxStringLabelProps = { | ||
| label: string; | ||
| 'aria-label'?: string; | ||
| }; | ||
|
|
||
| export type CheckboxReactNodeLabelProps = { | ||
| label: ReactNode; | ||
| 'aria-label': string; | ||
| }; | ||
|
|
||
| type CheckboxIndeterminate = { | ||
| indeterminate: boolean; | ||
| checked?: false; | ||
| }; | ||
|
|
||
| type CheckboxRegular = { | ||
| indeterminate?: never; | ||
| checked?: boolean; | ||
| }; | ||
|
|
||
| export type CheckboxCheckedUnion = CheckboxRegular | CheckboxIndeterminate; | ||
|
|
||
| export type CheckboxLabelUnion = | ||
| | CheckboxStringLabelProps | ||
| | CheckboxReactNodeLabelProps; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it help to also check that
checkedis alsofalse(or!true? falsey?) here whenindeterminateistrue?E.g. as a safeguard in case types get changed?