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

Improve checkbox examples #2456

Merged
merged 3 commits into from
May 23, 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/checkbox-examples-1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@ariakit/react-core": patch
"@ariakit/react": patch
---

The `Checkbox` component now accepts `string[]` as the `value` prop. This is to conform with the native input prop type. If a string array is passed, it will be stringified, just like in the native input element. ([#2456](https://github.com/ariakit/ariakit/pull/2456))
6 changes: 6 additions & 0 deletions .changeset/checkbox-examples-2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@ariakit/react-core": patch
"@ariakit/react": patch
---

Fixed the `clickOnEnter` prop on `Checkbox` not working when rendering the component as a native input element. ([#2456](https://github.com/ariakit/ariakit/pull/2456))
25 changes: 20 additions & 5 deletions components/checkbox.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@

<a href="../examples/checkbox/index.tsx" data-playground>Example</a>

## Installation
## Examples

```sh
npm i @ariakit/react
```
<div data-cards="examples">

Learn more on the [Getting started](/guide/getting-started) guide.
- [](/examples/checkbox-as-button)
- [](/examples/checkbox-custom)
- [](/examples/checkbox-group)

</div>

## API

Expand All @@ -23,3 +25,16 @@ Learn more on the [Getting started](/guide/getting-started) guide.
&lt;<a href="/apis/checkbox-check">CheckboxCheck</a> /&gt;
&lt;/Checkbox&gt;
</pre>

## Related components

<div data-cards="components">

- [](/components/button)
- [](/components/form)
- [](/components/menu)
- [](/components/radio)
- [](/components/select)
- [](/components/command)

</div>
22 changes: 22 additions & 0 deletions components/group.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Group

<p data-description>
Group related elements in a generic container that may have a label. This abstract component is based on the <a href="https://w3c.github.io/aria/#group">WAI-ARIA Group Role</a>.
</p>

## API

<pre data-api>
&lt;<a href="/apis/group">Group</a>&gt;
&lt;<a href="/apis/group-label">GroupLabel</a> /&gt;
&lt;/Group&gt;
</pre>

## Related components

<div data-cards="components">

- [](/components/form)
- [](/components/composite)

</div>
43 changes: 42 additions & 1 deletion examples/checkbox-as-button/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,58 @@
Rendering a custom <a href="/components/checkbox">Checkbox</a> as a <code>button</code> element in React, while keeping it accessible to screen reader and keyboard users.
</p>

<aside data-type="note" title="Need to render a native checkbox element?">

This example demonstrates the rendering of a button element. However, if you intend to render the `Checkbox` as a form control or require the preservation of native input element properties for any specific purpose, please refer to the [Custom Checkbox](/examples/checkbox-custom) example.

</aside>

<a href="./index.tsx" data-playground>Example</a>

## Components

<div data-cards="components">

- [](/components/checkbox)
- [](/components/button)

</div>

## Activating on <kbd>Enter</kbd>

By default, native checkbox elements are activated on <kbd>Space</kbd>, but not on <kbd>Enter</kbd>. The Ariakit `Checkbox` component allows you to control this behavior using the [`clickOnEnter`](/apis/checkbox#clickonenter) and [`clickOnSpace`](/apis/checkbox#clickonspace) props. However, when rendering the `Checkbox` as any non-native input element, the `clickOnEnter` prop will be automatically set to `true`.

## Reading the state

In this example, we're reading the [`value`](/apis/checkbox-store#value) state from the checkbox store to render the button's text. This is done by using the selector form of the [`useState`](/apis/checkbox-store#usestate) hook:

```jsx
const label = checkbox.useState((state) =>
state.value ? "Checked" : "Unchecked"
);
```

Learn more about reading the state on the [Component stores](/guide/component-stores#reading-the-state) guide.

## Styling

When rendering the `Checkbox` component as a non-native `input` element, the `:checked` pseudo-class is not supported. To style the checked state, use the `aria-checked` attribute selector:

```css
.button[aria-checked="true"] {
background-color: hsl(204 100% 40%);
color: hsl(204, 20%, 100%);
color: hsl(204 20% 100%);
}
```

Learn more on the [Styling](/guide/styling) guide.

## Related examples

<div data-cards="examples">

- [](/examples/checkbox-custom)
- [](/examples/checkbox-group)
- [](/examples/menu-item-checkbox)

</div>
20 changes: 20 additions & 0 deletions examples/checkbox-as-button/site-icon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export default function Icon() {
return (
<svg viewBox="0 0 128 128" width={128} height={128}>
<foreignObject width={128} height={128}>
<div className="flex h-full w-full items-center justify-center p-4">
<div className="w-20 gap-2 rounded bg-blue-600 p-2">
<svg
fill="none"
viewBox="0 0 24 24"
strokeWidth={4}
className="h-6 w-6 stroke-white"
>
<path d="M4.5 12.75l6 6 9-13.5" />
</svg>
</div>
</div>
</foreignObject>
</svg>
);
}
3 changes: 0 additions & 3 deletions examples/checkbox-as-button/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,12 @@
text-blue-900
bg-blue-200/40
hover:bg-blue-200/60

dark:text-blue-100
dark:bg-blue-600/25
dark:hover:bg-blue-600/40

aria-checked:text-white
aria-checked:bg-blue-600
aria-checked:hover:bg-blue-800

dark:aria-checked:text-white
dark:aria-checked:bg-blue-600
dark:aria-checked:hover:bg-blue-800
Expand Down
7 changes: 0 additions & 7 deletions examples/checkbox-controlled/readme.md

This file was deleted.

48 changes: 48 additions & 0 deletions examples/checkbox-custom/checkbox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { forwardRef, useState } from "react";
import type { ComponentPropsWithoutRef, ReactNode } from "react";
import * as Ariakit from "@ariakit/react";

interface CheckboxProps extends ComponentPropsWithoutRef<"input"> {
children?: ReactNode;
}

export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
function Checkbox({ children, ...props }, ref) {
const [checked, setChecked] = useState(props.defaultChecked ?? false);
const [focusVisible, setFocusVisible] = useState(false);
return (
<label
className="checkbox"
data-checked={checked}
data-focus-visible={focusVisible || undefined}
>
<Ariakit.VisuallyHidden>
<Ariakit.Checkbox
{...props}
ref={ref}
clickOnEnter
onFocusVisible={() => setFocusVisible(true)}
onBlur={() => setFocusVisible(false)}
onChange={(event) => {
setChecked(event.target.checked);
props.onChange?.(event);
}}
/>
</Ariakit.VisuallyHidden>
<div className="check" data-checked={checked}>
<svg
fill="none"
stroke="currentColor"
strokeWidth="2"
viewBox="0 0 16 16"
height="1em"
width="1em"
>
<polyline points="4,8 7,12 12,4" />
</svg>
</div>
{children}
</label>
);
}
);
22 changes: 2 additions & 20 deletions examples/checkbox-custom/index.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,6 @@
import { useState } from "react";
import * as Ariakit from "@ariakit/react";
import { Checkbox } from "./checkbox.jsx";
import "./style.css";

export default function Example() {
const checkbox = Ariakit.useCheckboxStore({ defaultValue: false });
const [focusVisible, setFocusVisible] = useState(false);
const checked = checkbox.useState("value");
return (
<label className="label">
<Ariakit.VisuallyHidden>
<Ariakit.Checkbox
store={checkbox}
onFocusVisible={() => setFocusVisible(true)}
onBlur={() => setFocusVisible(false)}
/>
</Ariakit.VisuallyHidden>
<div className="checkbox" data-focus-visible={focusVisible ? "" : null}>
<Ariakit.CheckboxCheck checked={checked} />
</div>
I have read and agree to the terms and conditions
</label>
);
return <Checkbox defaultChecked>Ariakit</Checkbox>;
}
19 changes: 19 additions & 0 deletions examples/checkbox-custom/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,22 @@
</p>

<a href="./index.tsx" data-playground>Example</a>

## Components

<div data-cards="components">

- [](/components/checkbox)
- [](/components/visually-hidden)

</div>

## Related examples

<div data-cards="examples">

- [](/examples/checkbox-as-button)
- [](/examples/checkbox-group)
- [](/examples/menu-item-checkbox)

</div>
23 changes: 23 additions & 0 deletions examples/checkbox-custom/site-icon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export default function Icon() {
return (
<svg viewBox="0 0 128 128" width={128} height={128}>
<foreignObject width={128} height={128}>
<div className="flex h-full items-center justify-center p-4">
<div className="flex flex-col gap-2 rounded-lg border-[3px] border-blue-600 bg-white p-2 dark:bg-gray-700">
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-blue-600 p-2">
<svg
fill="none"
viewBox="0 0 24 24"
strokeWidth={4}
className="h-full w-full stroke-white"
>
<path d="M4.5 12.75l6 6 9-13.5" />
</svg>
</div>
<div className="h-2 w-14 bg-black/50 dark:bg-white/50" />
</div>
</div>
</foreignObject>
</svg>
);
}
49 changes: 36 additions & 13 deletions examples/checkbox-custom/style.css
Original file line number Diff line number Diff line change
@@ -1,19 +1,42 @@
@import url("../checkbox/style.css");

.checkbox {
@apply
w-5
h-5
rounded
flex
justify-center
items-center
border
text-blue-900
bg-blue-200/40
border-blue-600
dark:text-blue-100
dark:bg-blue-600/25
dark:border-blue-200/40
gap-2
p-4
pr-6
rounded-lg
select-none
border-2
border-black/30
dark:border-white/30
data-[checked=true]:border-blue-600
data-[checked=true]:dark:border-blue-600
bg-white
dark:bg-gray-700
shadow
dark:shadow-dark
data-[focus-visible]:outline
data-[focus-visible]:outline-4
data-[focus-visible]:outline-blue-600/25
;
}

.check {
@apply
block
rounded-full
p-0.5
text-lg
bg-gray-150
dark:bg-gray-850
[border:inherit]
[stroke-dasharray:15]
[stroke-dashoffset:15]
data-[checked=true]:bg-blue-600
data-[checked=true]:dark:bg-blue-600
data-[checked=true]:text-white
data-[checked=true]:[stroke-dashoffset:0]
transition-[stroke-dashoffset]
;
}
17 changes: 13 additions & 4 deletions examples/checkbox-custom/test.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,27 @@
import { click, getByRole, press } from "@ariakit/test";

test("check/uncheck on click", async () => {
expect(getByRole("checkbox")).not.toBeChecked();
await click(getByRole("checkbox"));
expect(getByRole("checkbox")).toBeChecked();
await click(getByRole("checkbox"));
expect(getByRole("checkbox")).not.toBeChecked();
await click(getByRole("checkbox"));
expect(getByRole("checkbox")).toBeChecked();
});

test("check/uncheck on space", async () => {
expect(getByRole("checkbox")).not.toBeChecked();
expect(getByRole("checkbox")).toBeChecked();
await press.Tab();
await press.Space();
expect(getByRole("checkbox")).toBeChecked();
expect(getByRole("checkbox")).not.toBeChecked();
await press.Space();
expect(getByRole("checkbox")).toBeChecked();
});

test("check/uncheck on enter", async () => {
expect(getByRole("checkbox")).toBeChecked();
await press.Tab();
await press.Enter();
expect(getByRole("checkbox")).not.toBeChecked();
await press.Enter();
expect(getByRole("checkbox")).toBeChecked();
});