-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
206cbdd
commit 4b61f88
Showing
6 changed files
with
332 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
.fp-CheckboxRoot { | ||
position: relative; | ||
display: grid; | ||
grid-template-columns: var(--space-4) auto; | ||
min-height: 24px; | ||
gap: var(--space-1) var(--space-2); | ||
} | ||
|
||
.fp-CheckboxInput { | ||
all: unset; | ||
box-sizing: border-box; | ||
display: block; | ||
width: var(--space-4); | ||
height: var(--space-4); | ||
margin: var(--space-1) 0; | ||
background-color: transparent; | ||
border: 1px solid var(--figma-color-border-strong); | ||
border-radius: var(--radius-medium); | ||
flex-shrink: 0; | ||
|
||
&:focus-visible { | ||
outline-offset: -1px; | ||
outline: 1px solid var(--figma-color-border-selected); | ||
} | ||
|
||
&:focus-visible:checked { | ||
outline-offset: 0; | ||
outline: 1px solid var(--figma-color-border-selected-strong); | ||
border-color: var(--figma-color-icon-onbrand); | ||
} | ||
|
||
&:checked { | ||
background-color: var(--figma-color-bg-brand); | ||
border-color: transparent; | ||
} | ||
|
||
&:disabled { | ||
border-color: var(--figma-color-border-disabled-strong); | ||
} | ||
|
||
&:disabled:checked { | ||
border-color: transparent; | ||
background-color: var(--figma-color-border-disabled-strong); | ||
} | ||
} | ||
|
||
.fp-CheckboxIndicator { | ||
display: block; | ||
pointer-events: none; | ||
position: absolute; | ||
top: var(--space-1); | ||
} | ||
|
||
.fp-CheckboxCheckmark, | ||
.fp-CheckboxIndeterminate { | ||
display: none; | ||
} | ||
|
||
.fp-CheckboxInput:checked ~ .fp-CheckboxIndicator .fp-CheckboxCheckmark { | ||
color: var(--figma-color-icon-onbrand); | ||
display: block; | ||
} | ||
|
||
.fp-CheckboxInput:indeterminate ~ .fp-CheckboxIndicator .fp-CheckboxIndeterminate { | ||
color: var(--figma-color-icon); | ||
display: block; | ||
} | ||
.fp-CheckboxInput:disabled:indeterminate ~ .fp-CheckboxIndicator .fp-CheckboxIndeterminate { | ||
color: var(--figma-color-icon-disabled); | ||
display: block; | ||
} | ||
|
||
.fp-CheckboxLabel { | ||
margin-top: var(--space-1); | ||
} | ||
|
||
.fp-CheckboxInput:disabled ~ .fp-CheckboxLabel { | ||
color: var(--figma-color-text-disabled); | ||
} | ||
|
||
.fp-CheckboxDescription { | ||
color: var(--figma-color-text-secondary); | ||
grid-area: 2 / 2; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
import type { Meta, StoryObj } from '@storybook/react'; | ||
import * as Checkbox from './checkbox'; | ||
|
||
type Story = StoryObj<typeof Checkbox.Root>; | ||
|
||
const meta: Meta<typeof Checkbox.Root> = { | ||
title: 'Components/Checkbox', | ||
component: Checkbox.Root, | ||
}; | ||
|
||
export default meta; | ||
|
||
export const WithLabel: Story = { | ||
render: () => { | ||
return ( | ||
<Checkbox.Root> | ||
<Checkbox.Input /> | ||
<Checkbox.Label>Clip content</Checkbox.Label> | ||
</Checkbox.Root> | ||
); | ||
}, | ||
}; | ||
|
||
export const WithoutLabel: Story = { | ||
render: () => { | ||
return ( | ||
<Checkbox.Root> | ||
<Checkbox.Input /> | ||
</Checkbox.Root> | ||
); | ||
}, | ||
}; | ||
|
||
export const Indeterminate: Story = { | ||
render: () => { | ||
return ( | ||
<Checkbox.Root> | ||
<Checkbox.Input indeterminate /> | ||
<Checkbox.Label>Clip content</Checkbox.Label> | ||
</Checkbox.Root> | ||
); | ||
}, | ||
}; | ||
|
||
export const Disabled: Story = { | ||
render: () => { | ||
return ( | ||
<Checkbox.Root> | ||
<Checkbox.Input disabled /> | ||
<Checkbox.Label>Clip content</Checkbox.Label> | ||
</Checkbox.Root> | ||
); | ||
}, | ||
}; | ||
export const DisabledChecked: Story = { | ||
render: () => { | ||
return ( | ||
<Checkbox.Root> | ||
<Checkbox.Input disabled checked /> | ||
<Checkbox.Label>Clip content</Checkbox.Label> | ||
</Checkbox.Root> | ||
); | ||
}, | ||
}; | ||
export const DisabledIndeterminate: Story = { | ||
render: () => { | ||
return ( | ||
<Checkbox.Root> | ||
<Checkbox.Input disabled indeterminate /> | ||
<Checkbox.Label>Clip content</Checkbox.Label> | ||
</Checkbox.Root> | ||
); | ||
}, | ||
}; | ||
|
||
export const MultiLineLabel: Story = { | ||
render: () => { | ||
return ( | ||
<Checkbox.Root style={{ width: 128 }}> | ||
<Checkbox.Input /> | ||
<Checkbox.Label>Clip content with label that spans multiple lines</Checkbox.Label> | ||
</Checkbox.Root> | ||
); | ||
}, | ||
}; | ||
|
||
export const Description: Story = { | ||
render: () => { | ||
return ( | ||
<Checkbox.Root style={{ maxWidth: 512 }}> | ||
<Checkbox.Input /> | ||
<Checkbox.Label>Checkbox with description</Checkbox.Label> | ||
<Checkbox.Description> | ||
Helpful description of the option which may briefly highlight side effects or conditions of the option. | ||
</Checkbox.Description> | ||
</Checkbox.Root> | ||
); | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
import React, { useCallback, useId } from 'react'; | ||
import { cx } from 'class-variance-authority'; | ||
import { Text, Label as LabelPrimitive } from '@components/text'; | ||
import { useComposedRefs } from '@lib/react/use-compose-refs'; | ||
import { CheckmarkIcon } from '@components/icons'; | ||
import { CheckmarkIndeterminateIcon } from '@components/icons/checkmark-indeterminate'; | ||
import { createContext } from '@lib/react/create-context'; | ||
|
||
const [CheckboxContextProvider, useCheckboxContext] = createContext<{ id: string }>('Checkbox'); | ||
|
||
type RootElement = React.ElementRef<'div'>; | ||
type RootProps = React.ComponentPropsWithoutRef<'div'>; | ||
|
||
const Root = React.forwardRef<RootElement, RootProps>((props, ref) => { | ||
const { className, id: idProp, ...rootProps } = props; | ||
const generatedId = useId(); | ||
const id = idProp ?? generatedId; | ||
|
||
return ( | ||
<CheckboxContextProvider id={id}> | ||
<div ref={ref} className={cx(className, 'fp-CheckboxRoot')} {...rootProps} /> | ||
</CheckboxContextProvider> | ||
); | ||
}); | ||
|
||
type CheckboxElement = React.ElementRef<'input'>; | ||
type CheckboxProps = React.ComponentPropsWithoutRef<'input'> & { | ||
indeterminate?: boolean; | ||
}; | ||
|
||
const Input = React.forwardRef<CheckboxElement, CheckboxProps>((props, forwardedRef) => { | ||
const { className, indeterminate, ...checkboxProps } = props; | ||
const { id } = useCheckboxContext('Input'); | ||
const inputRef = useIndeterminateState(indeterminate); | ||
const ref = useComposedRefs(forwardedRef, inputRef); | ||
|
||
return ( | ||
<> | ||
<input | ||
ref={ref} | ||
className={cx(className, 'fp-CheckboxInput')} | ||
id={id} | ||
// VoiceOver on Chrome does not announce the label when simply using htmlFor | ||
aria-labelledby={`checkbox-label-${id}`} | ||
aria-describedby={`checkbox-description-${id}`} | ||
{...checkboxProps} | ||
type="checkbox" | ||
/> | ||
<Indicator /> | ||
</> | ||
); | ||
}); | ||
|
||
const Indicator = () => { | ||
return ( | ||
<span className="fp-CheckboxIndicator" aria-hidden="true"> | ||
<CheckmarkIcon className="fp-CheckboxCheckmark" size="4" /> | ||
<CheckmarkIndeterminateIcon className="fp-CheckboxIndeterminate" size="4" /> | ||
</span> | ||
); | ||
}; | ||
|
||
type LabelElement = React.ElementRef<'label'>; | ||
type LabelProps = React.ComponentPropsWithoutRef<'label'>; | ||
|
||
const Label = React.forwardRef<LabelElement, LabelProps>((props, ref) => { | ||
const { className, ...labelProps } = props; | ||
const { id } = useCheckboxContext('Input'); | ||
return ( | ||
<LabelPrimitive | ||
aria-hidden="true" | ||
ref={ref} | ||
className={cx(className, 'fp-CheckboxLabel')} | ||
htmlFor={id} | ||
id={`checkbox-label-${id}`} | ||
{...labelProps} | ||
/> | ||
); | ||
}); | ||
|
||
type DescriptionElement = React.ElementRef<'label'>; | ||
type DescriptionProps = React.ComponentPropsWithoutRef<'label'>; | ||
|
||
const Description = React.forwardRef<DescriptionElement, DescriptionProps>((props, ref) => { | ||
const { className, ...desriptionProps } = props; | ||
const { id } = useCheckboxContext('Description'); | ||
return ( | ||
<Text | ||
aria-hidden="true" | ||
ref={ref} | ||
className={cx(className, 'fp-CheckboxDescription')} | ||
id={`checkbox-description-${id}`} | ||
{...desriptionProps} | ||
/> | ||
); | ||
}); | ||
|
||
function useIndeterminateState(indeterminate: boolean | undefined) { | ||
return useCallback( | ||
(inputElement: HTMLInputElement) => { | ||
if (!inputElement) { | ||
return; | ||
} | ||
|
||
inputElement.indeterminate = !!indeterminate; | ||
}, | ||
[indeterminate] | ||
); | ||
} | ||
|
||
export type { CheckboxProps }; | ||
export { Root, Input, Label, Description }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './checkbox'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
/** | ||
* @license | ||
* MIT License | ||
* Copyright (c) 2022 WorkOS | ||
* https://github.com/radix-ui/primitives/blob/main/LICENSE | ||
* */ | ||
import React from 'react'; | ||
|
||
function createContext<ContextValueType extends object | null>( | ||
rootComponentName: string, | ||
defaultContext?: ContextValueType | ||
) { | ||
const Context = React.createContext<ContextValueType | undefined>(defaultContext); | ||
|
||
function Provider(props: ContextValueType & { children: React.ReactNode }) { | ||
const { children, ...context } = props; | ||
// Only re-memoize when prop values change | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
const value = React.useMemo(() => context, Object.values(context)) as ContextValueType; | ||
return <Context.Provider value={value}>{children}</Context.Provider>; | ||
} | ||
|
||
function useContext(consumerName: string) { | ||
const context = React.useContext(Context); | ||
if (context) return context; | ||
if (defaultContext !== undefined) return defaultContext; | ||
// if a defaultContext wasn't specified, it's a required context. | ||
throw new Error(`\`${consumerName}\` must be used within \`${rootComponentName}\``); | ||
} | ||
|
||
Provider.displayName = rootComponentName + 'Provider'; | ||
return [Provider, useContext] as const; | ||
} | ||
|
||
export { createContext }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters