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

refa: <Checkbox> and <CheckboxGroup> #2016

Merged
merged 15 commits into from
Apr 22, 2022
8 changes: 8 additions & 0 deletions .changeset/chatty-peaches-explode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@marigold/docs": minor
"@marigold/components": minor
"@marigold/theme-b2b": minor
"@marigold/theme-unicorn": minor
---

refa: <Checkbox> and <CheckboxGroup>
120 changes: 39 additions & 81 deletions docs/content/components/checkbox.mdx
Original file line number Diff line number Diff line change
@@ -1,126 +1,84 @@
---
title: Checkbox
figma: https://www.figma.com/file/DFKyTGHAoDxOsUBPszLLxP/%F0%9F%8F%B5%EF%B8%8FMarigold?node-id=467%3A159
---

import { FigmaLink } from '../../src/components/FigmaLink';
import { DoAndDont } from '../../src/components/DoAndDont';

# Checkbox

Checkboxes allow users to select multiple items from a list of individual items,
or to mark one individual item as selected.
The `Checkbox` components allows users to select one or more options from a list of options. In order to group multiple checkboxes, use the `CheckboxGroup` component.

<MarigoldTheme>
<Inline space="medium">
<Checkbox id="unchecked" aria-label="unchecked" />
<Checkbox id="disabled" aria-label="disabled" disabled />
<Checkbox id="checked" aria-label="checked" checked />
<Checkbox
id="disabled-checked"
aria-label="disabled-checked"
checked
disabled
/>
<Checkbox
id="indeterminated-checked"
aria-label="indeterminated-checked"
checked
indeterminated
/>
</Inline>
<Checkbox defaultChecked>I will not talk about Fight Club</Checkbox>
</MarigoldTheme>

```tsx onlyCode
import { Checkbox } from '@marigold/components';
import { Checkbox, CheckboxGroup } from '@marigold/components';
```

<FigmaLink
to={
'https://www.figma.com/file/DFKyTGHAoDxOsUBPszLLxP/%F0%9F%8F%B5%EF%B8%8FMarigold?node-id=467%3A159'
}
/>

## Props

| Property | Type | Default |
| :-------------------------- | :-------- | :---------- |
| `id` | `string` | |
| `variant` (optional) | `string` | `__default` |
| `labelVariant` (optional) | `string` | `inline` |
| `required` (optional) | `boolean` | `false` |
| `disabled` (optional) | `boolean` | `false` |
| `indeterminated` (optional) | `boolean` | `false` |
| `error` (optional) | `boolean` | `false` |
| `errorMessage` (optional) | `string` | |
| Name | Type | Default | Description |
| :----------------- | :-------- | :---------- | :----------------------------------------------------------------------------------------------------------------- |
| `variant` | `string` | | Use a different _variant_ from theme |
| `size` | `string` | `'level-1'` | Use a different _size_ from theme |
| `error` (optional) | `boolean` | `false` | If `true`, the checkbox is considered invalid and if set the `errorMessage` is shown instead of the `description`. |
| ... | | | Yes you can use all regular attributes of `input`! |

## Examples

### Checkbox standard labeled
### Simple Checkbox

```tsx expandCode
() => {
const [isChecked, setChecked] = React.useState(false);
const onChange = () => {
setChecked(!isChecked);
};
return (
<Checkbox
id="standard"
name="standard"
onChange={onChange}
checked={isChecked}
value="Checkbox"
>
Agree to Marigold terms
</Checkbox>
);
};
```tsx
<Checkbox>Agree to business terms</Checkbox>
```

### Checkbox disabled

```tsx
<>
<Checkbox id="disabled" disabled>
Disabled
</Checkbox>
<Checkbox disabled>Disabled</Checkbox>
<br />
<Checkbox id="checkedDisabled" checked disabled>
<Checkbox checked disabled>
Checked and disabled
</Checkbox>
</>
```

### Checkbox required label

```tsx
<Checkbox checked id="required" value="required" required>
Agree to Marigold terms
</Checkbox>
```
### Indeterminate Checkbox

### Checkbox error and errorMessage
Use indeterminate state when it represents both selected and not selected values.

```tsx
<Checkbox
id="error"
value="error"
error
errorMessage="Please check the required checkbox"
required
>
Agree to Marigold terms
<Checkbox defaultChecked indeterminate>
Select all
</Checkbox>
```

### Indeterminated Checkbox

Use indeterminate state when it represents both selected and not selected values.
### Group checkboxes

```tsx
<Checkbox id="selectAll" checked indeterminated>
Select all
</Checkbox>
() => {
const [selected, setSelected] = React.useState([]);
return (
<>
<CheckboxGroup label="Choose your toppings:" onChange={setSelected}>
<Checkbox value="ham">🐖 Ham</Checkbox>
<Checkbox value="beef" disabled>
🐄 Beef (out of stock)
</Checkbox>
<Checkbox value="tuna">🐟 Tuna</Checkbox>
<Checkbox value="tomatos">🍅 Tomatos</Checkbox>
<Checkbox value="onions">🧅 Onions</Checkbox>
<Checkbox value="pineapple">🍍 Pineapple</Checkbox>
</CheckboxGroup>
<hr />
<pre>Selected values: {selected.join(', ')}</pre>
</>
);
};
```

## Dos & Don'ts
Expand Down
4 changes: 2 additions & 2 deletions docs/content/components/text-field.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,14 @@ import { TextField } from '@marigold/components';
| `label` (optional) | `ReactNode` | | The label text. If you don't want to visually display a label, provide an `aria-label` or `aria-labelledby` attribute for accessibility. |
| `description` (optional) | `ReactNode` | | A helpful text |
| `errorMessage` (optional) | `ReactNode` | | An error message |
| `error` (optional) | `boolean` | `false` | If `true`, the is considered invalid and if set the `errorMessage` is shown instead of the `description`. |
| `error` (optional) | `boolean` | `false` | If `true`, the field is considered invalid and if set the `errorMessage` is shown instead of the `description`. |
| `value` (optional) | `string` | | The value of the input field |
| `disabled` (optional) | `boolean` | `false` | If `true`, the input is disabled. |
| `required` (optional) | `boolean` | `false` | If `true`, the input is required. |
| `readOnly` (optional) | `boolean` | `false` | If `true`, the input is readOnly. |
| `type` (optional) | `string` | `text` | The type of the input field. |
| `onChange` (optional) | `function` | | A callback function that is called with the input's current `value` when the input value changes. |
| others | | | Yes you can use all regular attributes of `input`! |
| ... | | | Yes you can use all regular attributes of `input`! |

## Examples

Expand Down
1 change: 1 addition & 0 deletions packages/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"@react-aria/tooltip": "^3.1.3",
"@react-aria/utils": "^3.8.2",
"@react-aria/visually-hidden": "^3.2.3",
"@react-stately/checkbox": "^3.0.6",
"@react-stately/collections": "^3.3.3",
"@react-stately/list": "^3.3.0",
"@react-stately/overlays": "^3.1.3",
Expand Down
107 changes: 76 additions & 31 deletions packages/components/src/Checkbox/Checkbox.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { ReactNode } from 'react';
import { useCheckbox, useCheckboxGroupItem } from '@react-aria/checkbox';
import { useFocusRing } from '@react-aria/focus';
import { useCheckbox } from '@react-aria/checkbox';
import { VisuallyHidden } from '@react-aria/visually-hidden';
import { useHover } from '@react-aria/interactions';
import { useToggleState } from '@react-stately/toggle';
import { AriaCheckboxProps } from '@react-types/checkbox';

Expand All @@ -16,6 +16,8 @@ import {
} from '@marigold/system';
import { ComponentProps } from '@marigold/types';

import { useCheckboxGroupContext } from './CheckboxGroup';

// Theme Extension
// ---------------
export interface CheckboxThemeExtension
Expand Down Expand Up @@ -111,40 +113,69 @@ export const Checkbox = ({
...props
}: CheckboxProps) => {
const ref = React.useRef<HTMLInputElement>(null);
// Adjust props to the react-aria API
const checkboxProps = {
isIndeterminate: indeterminate,
isDisabled: disabled,
isReadOnly: readOnly,
isRequired: required,
validationState: error ? 'invalid' : 'valid',
} as const;

/**
* Use hook depending if the checkbox is used inside a group or standalone.
* This is unusual, but since the checkboxs is not moving out of the group,
* it should be safe.
*/
const groupState = useCheckboxGroupContext();

/* eslint-disable react-hooks/rules-of-hooks */
const { inputProps } = groupState
? useCheckboxGroupItem(
{
...props,
...checkboxProps,
/**
* value is optional for standalone checkboxes, but required when
* used inside a group.
*/
value: props.value as string,
},
groupState,
ref
)
: useCheckbox(
{
isSelected: checked,
defaultSelected: defaultChecked,
...checkboxProps,
...props,
},
useToggleState({
isSelected: checked,
defaultSelected: defaultChecked,
...props,
}),
ref
);
/* eslint-enable react-hooks/rules-of-hooks */

const styles = useComponentStyles(
'Checkbox',
{ variant, size },
{ variant: groupState?.variant || variant, size: groupState?.size || size },
{ parts: ['container', 'label', 'checkbox'] }
);

const state = useToggleState({
isSelected: checked,
defaultSelected: defaultChecked,
...props,
});
const { inputProps } = useCheckbox(
{
isSelected: checked,
defaultSelected: defaultChecked,
isIndeterminate: indeterminate,
isDisabled: disabled,
isReadOnly: readOnly,
isRequired: required,
validationState: error ? 'invalid' : 'valid',
...props,
},
state,
ref
);
const { hoverProps, isHovered } = useHover({});
const { isFocusVisible, focusProps } = useFocusRing();

const stateProps = useStateProps({
checked: state.isSelected,
hover: isHovered,
focus: isFocusVisible,
checked: inputProps.checked,
disabled: inputProps.disabled,
error: groupState?.error || error,
readOnly,
indeterminate,
error,
});

return (
Expand All @@ -155,17 +186,31 @@ export const Checkbox = ({
display: 'flex',
alignItems: 'center',
gap: '1ch',
userSelect: 'none',
'&:hover': { cursor: inputProps.disabled ? 'not-allowed' : 'pointer' },
position: 'relative',
}}
css={styles.container}
{...hoverProps}
{...stateProps}
>
<VisuallyHidden>
<input {...inputProps} {...focusProps} ref={ref} />
</VisuallyHidden>
<Box
as="input"
type="checkbox"
ref={ref}
css={{
position: 'absolute',
width: '100%',
height: '100%',
top: 0,
left: 0,
zIndex: 1,
opacity: 0.0001,
cursor: inputProps.disabled ? 'not-allowed' : 'pointer',
}}
{...inputProps}
{...focusProps}
/>
<Icon
checked={state.isSelected}
checked={inputProps.checked}
indeterminate={indeterminate}
css={styles.checkbox}
{...stateProps}
Expand Down
Loading