Skip to content

Commit

Permalink
fix(checkbox-list): event click
Browse files Browse the repository at this point in the history
  • Loading branch information
philibea committed Dec 24, 2024
1 parent 35833f5 commit 056f82f
Show file tree
Hide file tree
Showing 3 changed files with 343 additions and 354 deletions.
2 changes: 1 addition & 1 deletion e2e/tests/list-checkbox.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { expect, test } from '@playwright/test'

const defaultLocator = 'iframe[title="storybook-preview-iframe"]'

test('has title', async ({ page, baseURL }) => {
test('checkbox list', async ({ page, baseURL }) => {
const url = `${baseURL}/?path=/story/components-data-display-list--selectable`
await page.goto(url)

Expand Down
309 changes: 152 additions & 157 deletions packages/ui/src/components/Checkbox/index.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
import { useTheme } from '@emotion/react'
import styled from '@emotion/styled'
import { AsteriskIcon } from '@ultraviolet/icons'
import type {
ChangeEvent,
ForwardedRef,
InputHTMLAttributes,
ReactNode,
} from 'react'
import { forwardRef, useCallback, useEffect, useId, useState } from 'react'
import type { ChangeEvent, InputHTMLAttributes, ReactNode, Ref } from 'react'
import { useCallback, useEffect, useId, useState } from 'react'
import { Loader } from '../Loader'
import { Stack } from '../Stack'
import { Text } from '../Text'
Expand Down Expand Up @@ -275,6 +270,7 @@ type LabelProp =
}

type CheckboxProps = {
ref?: Ref<HTMLInputElement>
error?: string | ReactNode
/**
* @deprecated Size prop is deprecated and will be removed in next major update.
Expand Down Expand Up @@ -302,164 +298,163 @@ type CheckboxProps = {
| 'id'
| 'onChange'
| 'tabIndex'
| 'onClick'
> &
LabelProp

/**
* Checkbox is an input component used to select or deselect an option.
*/
export const Checkbox = forwardRef(
(
{
id,
checked = false,
onChange,
onFocus,
onBlur,
error,
name,
helper,
value,
size,
children,
progress = false,
disabled = false,
autoFocus = false,
className,
'data-visibility': dataVisibility,
'aria-label': ariaLabel,
required,
'data-testid': dataTestId,
tooltip,
tabIndex,
}: CheckboxProps,
ref: ForwardedRef<HTMLInputElement>,
) => {
const theme = useTheme()
const [state, setState] = useState<boolean | 'indeterminate'>(checked)
const uniqId = useId()
const localId = id ?? uniqId

useEffect(() => {
setState(checked)
}, [checked])

const onLocalChange = useCallback(
(event: ChangeEvent<HTMLInputElement>) => {
if (!progress) onChange?.(event)
setState(current =>
current === 'indeterminate' ? false : event.target.checked,
)
},
[onChange, progress, setState],
)

return (
<Tooltip text={tooltip}>
<CheckboxContainer
className={className}
aria-disabled={disabled}
data-visibility={dataVisibility}
data-checked={state}
data-error={!!error}
data-testid={dataTestId}
>
{progress ? (
<StyledActivityContainer>
<Loader active size={size ?? theme.sizing['300']} />
</StyledActivityContainer>
) : null}
<CheckboxInput
id={localId}
type="checkbox"
aria-invalid={!!error}
aria-describedby={error ? `${localId}-hint` : undefined}
aria-checked={state === 'indeterminate' ? 'mixed' : state}
aria-label={ariaLabel}
checked={state === 'indeterminate' ? false : state}
inputSize={size ?? theme.sizing['300']}
onChange={onLocalChange}
onFocus={onFocus}
onBlur={onBlur}
disabled={disabled}
value={value}
name={name}
autoFocus={autoFocus}
ref={ref}
required={required}
tabIndex={tabIndex}
/>

{!progress ? (
<StyledIcon
size={size ?? theme.sizing['300']}
viewBox="0 0 24 24"
fill="none"
>
<CheckboxIconContainer>
{state !== 'indeterminate' ? (
<path
fillRule="evenodd"
clipRule="evenodd"
width={12}
height={9}
x="5"
y="4"
d="M15.6678 5.26709C16.0849 5.6463 16.113 6.28907 15.7307 6.70276L9.29172 13.6705C9.10291 13.8748 8.83818 13.9937 8.55884 13.9998C8.2795 14.0058 8.0098 13.8984 7.81223 13.7024L4.30004 10.2185C3.89999 9.82169 3.89999 9.17831 4.30004 8.78149C4.70009 8.38467 5.34869 8.38467 5.74874 8.78149L8.50441 11.5149L14.2205 5.32951C14.6028 4.91583 15.2508 4.88788 15.6678 5.26709Z"
fill="white"
/>
export const Checkbox = ({
id,
checked = false,
onChange,
onFocus,
onBlur,
onClick,
error,
name,
helper,
value,
size,
children,
progress = false,
disabled = false,
autoFocus = false,
className,
'data-visibility': dataVisibility,
'aria-label': ariaLabel,
required,
'data-testid': dataTestId,
tooltip,
tabIndex,
ref,
}: CheckboxProps) => {
const theme = useTheme()
const [state, setState] = useState<boolean | 'indeterminate'>(checked)
const uniqId = useId()
const localId = id ?? uniqId

useEffect(() => {
setState(checked)
}, [checked])

const onLocalChange = useCallback(
(event: ChangeEvent<HTMLInputElement>) => {
if (!progress) onChange?.(event)
setState(current =>
current === 'indeterminate' ? false : event.target.checked,
)
},
[onChange, progress, setState],
)

return (
<Tooltip text={tooltip}>
<CheckboxContainer
className={className}
aria-disabled={disabled}
data-visibility={dataVisibility}
data-checked={state}
data-error={!!error}
data-testid={dataTestId}
>
{progress ? (
<StyledActivityContainer>
<Loader active size={size ?? theme.sizing['300']} />
</StyledActivityContainer>
) : null}
<CheckboxInput
id={localId}
type="checkbox"
aria-invalid={!!error}
aria-describedby={error ? `${localId}-hint` : undefined}
aria-checked={state === 'indeterminate' ? 'mixed' : state}
aria-label={ariaLabel}
checked={state === 'indeterminate' ? false : state}
inputSize={size ?? theme.sizing['300']}
onChange={onLocalChange}
onClick={onClick}
onFocus={onFocus}
onBlur={onBlur}
disabled={disabled}
value={value}
name={name}
autoFocus={autoFocus}
ref={ref}
required={required}
tabIndex={tabIndex}
/>

{!progress ? (
<StyledIcon
size={size ?? theme.sizing['300']}
viewBox="0 0 24 24"
fill="none"
>
<CheckboxIconContainer>
{state !== 'indeterminate' ? (
<path
fillRule="evenodd"
clipRule="evenodd"
width={12}
height={9}
x="5"
y="4"
d="M15.6678 5.26709C16.0849 5.6463 16.113 6.28907 15.7307 6.70276L9.29172 13.6705C9.10291 13.8748 8.83818 13.9937 8.55884 13.9998C8.2795 14.0058 8.0098 13.8984 7.81223 13.7024L4.30004 10.2185C3.89999 9.82169 3.89999 9.17831 4.30004 8.78149C4.70009 8.38467 5.34869 8.38467 5.74874 8.78149L8.50441 11.5149L14.2205 5.32951C14.6028 4.91583 15.2508 4.88788 15.6678 5.26709Z"
fill="white"
/>
) : (
<CheckMixedMark x="6" y="11" rx="1" width="12" height="2" />
)}
</CheckboxIconContainer>
</StyledIcon>
) : null}

<Stack gap={0.5} flex={1}>
<Stack gap={0.5} direction="row" alignItems="center" flex={1}>
{children ? (
<>
{typeof children === 'string' ? (
<StyledTextLabel
as="label"
variant="body"
sentiment="neutral"
prominence="default"
htmlFor={localId}
>
{children}
</StyledTextLabel>
) : (
<CheckMixedMark x="6" y="11" rx="1" width="12" height="2" />
<StyledLabel htmlFor={localId}>{children}</StyledLabel>
)}
</CheckboxIconContainer>
</StyledIcon>
) : null}

<Stack gap={0.5} flex={1}>
<Stack gap={0.5} direction="row" alignItems="center" flex={1}>
{children ? (
<>
{typeof children === 'string' ? (
<StyledTextLabel
as="label"
variant="body"
sentiment="neutral"
prominence="default"
htmlFor={localId}
>
{children}
</StyledTextLabel>
) : (
<StyledLabel htmlFor={localId}>{children}</StyledLabel>
)}
</>
) : null}
{required ? (
<sup>
<AsteriskIcon size={8} sentiment="danger" />
</sup>
) : null}
</Stack>

{helper ? (
<Text
variant="caption"
as="span"
prominence="weak"
sentiment="neutral"
>
{helper}
</Text>
</>
) : null}

{error && typeof error !== 'boolean' ? (
<ErrorText variant="caption" as="span" sentiment="danger">
{error}
</ErrorText>
{required ? (
<sup>
<AsteriskIcon size={8} sentiment="danger" />
</sup>
) : null}
</Stack>
</CheckboxContainer>
</Tooltip>
)
},
)

{helper ? (
<Text
variant="caption"
as="span"
prominence="weak"
sentiment="neutral"
>
{helper}
</Text>
) : null}

{error && typeof error !== 'boolean' ? (
<ErrorText variant="caption" as="span" sentiment="danger">
{error}
</ErrorText>
) : null}
</Stack>
</CheckboxContainer>
</Tooltip>
)
}
Loading

0 comments on commit 056f82f

Please sign in to comment.