fix: prevent asset conflicts between React and Grid.js versions

Add coexistence checks to all enqueue methods to prevent loading
both React and Grid.js assets simultaneously.

Changes:
- ReactAdmin.php: Only enqueue React assets when ?react=1
- Init.php: Skip Grid.js when React active on admin pages
- Form.php, Coupon.php, Access.php: Restore classic assets when ?react=0
- Customer.php, Product.php, License.php: Add coexistence checks

Now the toggle between Classic and React versions works correctly.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
dwindown
2026-04-18 17:02:14 +07:00
parent bd9cdac02e
commit e8fbfb14c1
74973 changed files with 6658406 additions and 71 deletions

42
node_modules/reakit/src/Box/Box.ts generated vendored Normal file
View File

@@ -0,0 +1,42 @@
import * as React from "react";
import { createComponent } from "reakit-system/createComponent";
import { createHook } from "reakit-system/createHook";
import { shallowEqual } from "reakit-utils/shallowEqual";
import { BOX_KEYS } from "./__keys";
export type BoxOptions = {
/**
* Options passed to `reakit-system-*`
* @private
*/
unstable_system?: any;
};
export type BoxHTMLProps = React.HTMLAttributes<any> &
React.RefAttributes<any> & {
/**
* Function returned by the hook to wrap the element to which html props
* will be passed.
*/
wrapElement?: (element: React.ReactNode) => React.ReactNode;
};
export type BoxProps = BoxOptions & BoxHTMLProps;
export const useBox = createHook<BoxOptions, BoxHTMLProps>({
name: "Box",
keys: BOX_KEYS,
propsAreEqual(prev, next) {
const { unstable_system: prevSystem, ...prevProps } = prev;
const { unstable_system: nextSystem, ...nextProps } = next;
if (prevSystem !== nextSystem && !shallowEqual(prevSystem, nextSystem)) {
return false;
}
return shallowEqual(prevProps, nextProps);
},
});
export const Box = createComponent({
as: "div",
useHook: useBox,
});

86
node_modules/reakit/src/Box/README.md generated vendored Normal file
View File

@@ -0,0 +1,86 @@
---
path: /docs/box/
redirect_from:
- /components/box/
- /components/block/
- /components/flex/
- /components/grid/
- /components/inline/
- /components/inlineblock/
- /components/inlineflex/
- /components/avatar/
- /components/blockquote/
- /components/card/
- /components/card/cardfit/
- /components/code/
- /components/heading/
- /components/image/
- /components/link/
- /components/list/
- /components/navigation/
- /components/paragraph/
- /components/table/
- /components/table/tablewrapper/
---
# Box
`Box` is the most abstract component on top of which all other Reakit components are built. By default, it renders a `div` element.
<carbon-ad></carbon-ad>
## Installation
```sh
npm install reakit
```
Learn more in [Get started](/docs/get-started/).
## Usage
```jsx
import { Box } from "reakit/Box";
function Example() {
return <Box>Box</Box>;
}
```
### `as` prop
Learn more about the `as` prop in [Composition](/docs/composition/#as-prop).
```jsx
import { Box } from "reakit/Box";
function Example() {
return <Box as="button">Button</Box>;
}
```
### Render props
Learn more about render props in [Composition](/docs/composition/#render-props).
```jsx
import { Box } from "reakit/Box";
function Example() {
return <Box>{(props) => <button {...props}>Button</button>}</Box>;
}
```
## Composition
- `Box` is used by all Reakit components.
Learn more in [Composition](/docs/composition/#props-hooks).
## Props
<!-- Automatically generated -->
### `Box`
No props to show

2
node_modules/reakit/src/Box/__keys.ts generated vendored Normal file
View File

@@ -0,0 +1,2 @@
// Automatically generated
export const BOX_KEYS = ["unstable_system"] as const;

26
node_modules/reakit/src/Box/__tests__/Box-test.tsx generated vendored Normal file
View File

@@ -0,0 +1,26 @@
import * as React from "react";
import { render } from "reakit-test-utils";
import { Box, BoxProps } from "../Box";
test("render", () => {
const { getByText } = render(<Box>box</Box>);
expect(getByText("box")).toMatchInlineSnapshot(`
<div>
box
</div>
`);
});
test("do not re-render if unstable_system is the same", () => {
const onRender = jest.fn();
const Test = React.memo(({ unstable_system }: BoxProps) => {
React.useEffect(onRender);
return <Box unstable_system={unstable_system} />;
}, Box.unstable_propsAreEqual);
const { rerender } = render(<Test />);
expect(onRender).toHaveBeenCalledTimes(1);
rerender(<Test unstable_system={{ b: "b" }} />);
expect(onRender).toHaveBeenCalledTimes(2);
rerender(<Test unstable_system={{ b: "b" }} />);
expect(onRender).toHaveBeenCalledTimes(2);
});

1
node_modules/reakit/src/Box/index.ts generated vendored Normal file
View File

@@ -0,0 +1 @@
export * from "./Box";

62
node_modules/reakit/src/Button/Button.ts generated vendored Normal file
View File

@@ -0,0 +1,62 @@
import * as React from "react";
import { createComponent } from "reakit-system/createComponent";
import { createHook } from "reakit-system/createHook";
import { useForkRef } from "reakit-utils/useForkRef";
import { isButton } from "reakit-utils/isButton";
import { warning } from "reakit-warning";
import {
ClickableOptions,
ClickableHTMLProps,
useClickable,
} from "../Clickable/Clickable";
import { BUTTON_KEYS } from "./__keys";
export const useButton = createHook<ButtonOptions, ButtonHTMLProps>({
name: "Button",
compose: useClickable,
keys: BUTTON_KEYS,
useProps(_, { ref: htmlRef, ...htmlProps }) {
const ref = React.useRef<HTMLElement>(null);
const [role, setRole] = React.useState<"button" | undefined>(undefined);
const [type, setType] = React.useState<"button" | undefined>("button");
React.useEffect(() => {
const element = ref.current;
if (!element) {
warning(
true,
"Can't determine whether the element is a native button because `ref` wasn't passed to the component",
"See https://reakit.io/docs/button"
);
return;
}
if (!isButton(element)) {
if (element.tagName !== "A") {
setRole("button");
}
setType(undefined);
}
}, []);
return {
ref: useForkRef(ref, htmlRef),
role,
type,
...htmlProps,
};
},
});
export const Button = createComponent({
as: "button",
memo: true,
useHook: useButton,
});
export type ButtonOptions = ClickableOptions;
export type ButtonHTMLProps = ClickableHTMLProps &
React.ButtonHTMLAttributes<any>;
export type ButtonProps = ButtonOptions & ButtonHTMLProps;

160
node_modules/reakit/src/Button/README.md generated vendored Normal file
View File

@@ -0,0 +1,160 @@
---
path: /docs/button/
redirect_from:
- /components/button/
---
# Button
Accessible `Button` component that enables users to trigger an action or event, such as submitting a [Form](/docs/form/), opening a [Dialog](/docs/dialog/), canceling an action, or performing a delete operation. It follows the [WAI-ARIA Button Pattern](https://www.w3.org/TR/wai-aria-practices/#button).
<carbon-ad></carbon-ad>
## Installation
```sh
npm install reakit
```
Learn more in [Get started](/docs/get-started/).
## Usage
```jsx
import { Button } from "reakit/Button";
function Example() {
return <Button>Button</Button>;
}
```
## Styling
Reakit components are unstyled by default. You're free to use whatever approach you want. Each component returns a single HTML element that accepts all HTML props, including `className` and `style`.
> The example below uses [Emotion](https://emotion.sh/docs/introduction). But these styles can be reproduced using static CSS and other CSS-in-JS libraries, such as [styled-components](https://styled-components.com/).
```jsx unstyled
import { Button } from "reakit/Button";
import { css } from "emotion";
const className = css`
outline: 0;
color: #ffffff;
background: #006dff;
padding: 0.375em 0.75em;
line-height: 1.5;
border: transparent;
border-radius: 0.25rem;
cursor: pointer;
font-size: 16px;
&:focus {
box-shadow: 0 0 0 0.2em rgba(0, 109, 255, 0.4);
}
&[disabled],
&[aria-disabled="true"] {
cursor: auto;
opacity: 0.5;
}
&:not([disabled]),
&:not([aria-disabled="true"]) {
&:hover {
color: #ffffff;
background-color: #0062e6;
}
&:active,
&[data-active="true"] {
color: #ffffff;
background-color: #004eb8;
}
}
`;
function Example() {
return <Button className={className}>Button</Button>;
}
```
Learn more in [Styling](/docs/styling/).
## Accessibility
- `Button` has role `button`.
- When `Button` has focus, <kbd>Space</kbd> and <kbd>Enter</kbd> activates it.
<!-- eslint-disable no-alert -->
```jsx
import { Button } from "reakit/Button";
function Example() {
return (
<Button as="div" onClick={() => alert("clicked")}>
Button
</Button>
);
}
```
- If `disabled` prop is `true`, `Button` has `disabled` and `aria-disabled` attributes set to `true`.
<!-- eslint-disable no-alert -->
```jsx
import { Button } from "reakit/Button";
function Example() {
return (
<Button disabled onClick={() => alert("clicked")}>
Button
</Button>
);
}
```
- If `disabled` and `focusable` props are `true`, `Button` has `aria-disabled` attribute set to `true`, but not `disabled`.
<!-- eslint-disable no-alert -->
```jsx
import { Button } from "reakit/Button";
function Example() {
return (
<Button disabled focusable onClick={() => alert("clicked")}>
Button
</Button>
);
}
```
This is useful when the presence of a `Button` is important enough so users can perceive it by tabbing.
Learn more in [Accessibility](/docs/accessibility/).
## Composition
- `Button` uses [Clickable](/docs/clickable/), and is used by [FormPushButton](/docs/form/), [FormRemoveButton](/docs/form/), [Disclosure](/docs/disclosure/) and all their derivatives.
Learn more in [Composition](/docs/composition/#props-hooks).
## Props
<!-- Automatically generated -->
### `Button`
- **`disabled`**
<code>boolean | undefined</code>
Same as the HTML attribute.
- **`focusable`**
<code>boolean | undefined</code>
When an element is `disabled`, it may still be `focusable`. It works
similarly to `readOnly` on form elements. In this case, only
`aria-disabled` will be set.

View File

@@ -0,0 +1,19 @@
import * as React from "react";
import { render, screen, axe } from "reakit-test-utils";
import AccessibleButton from "..";
test("a11y", async () => {
const { baseElement } = render(<AccessibleButton />);
expect(await axe(baseElement)).toHaveNoViolations();
});
test("markup", () => {
render(<AccessibleButton />);
expect(screen.getByRole("button")).toMatchInlineSnapshot(`
<button
type="button"
>
Button
</button>
`);
});

View File

@@ -0,0 +1,8 @@
import * as React from "react";
import { Button } from "reakit/Button";
import "./style.css";
export default function AccessibleButton() {
return <Button>Button</Button>;
}

View File

@@ -0,0 +1,35 @@
:root {
--font-family: var(--font-family-body, sans-serif);
--font-size: var(--font-size-body, 16px);
--button-color: var(--color-primary-700, #1976d2);
--button-background: transparent;
--button-background-hover: var(--color-primary-50, #e3f2fd);
--button-background-active: var(--color-primary-100, #bbdefb);
--button-border-focus: var(--color-primary-700, #1976d2);
}
button {
font-family: var(--font-family);
background-color: var(--button-background);
color: var(--button-color);
font-size: var(--font-size);
position: relative;
border: 0;
padding: 0 1em;
line-height: 2.5em;
border-radius: 4px;
cursor: pointer;
outline: 0;
}
button:hover {
background-color: var(--button-background-hover);
}
button:active {
background-color: var(--button-background-active);
}
button:focus {
box-shadow: 0 0 0 2px var(--button-border-focus);
}

View File

@@ -0,0 +1,20 @@
import * as React from "react";
import { render, screen, axe } from "reakit-test-utils";
import ButtonAsDiv from "..";
test("a11y", async () => {
const { baseElement } = render(<ButtonAsDiv />);
expect(await axe(baseElement)).toHaveNoViolations();
});
test("markup", () => {
render(<ButtonAsDiv />);
expect(screen.getByRole("button")).toMatchInlineSnapshot(`
<div
role="button"
tabindex="0"
>
Div
</div>
`);
});

View File

@@ -0,0 +1,6 @@
import * as React from "react";
import { Button } from "reakit/Button";
export default function ButtonAsDiv() {
return <Button as="div">Div</Button>;
}

View File

@@ -0,0 +1,19 @@
import * as React from "react";
import { render, screen, axe } from "reakit-test-utils";
import ButtonAsLink from "..";
test("a11y", async () => {
const { baseElement } = render(<ButtonAsLink />);
expect(await axe(baseElement)).toHaveNoViolations();
});
test("markup", () => {
render(<ButtonAsLink />);
expect(screen.getByRole("link")).toMatchInlineSnapshot(`
<a
href="#"
>
Link
</a>
`);
});

View File

@@ -0,0 +1,10 @@
import * as React from "react";
import { Button } from "reakit/Button";
export default function ButtonAsLink() {
return (
<Button as="a" href="#">
Link
</Button>
);
}

View File

@@ -0,0 +1,42 @@
import * as React from "react";
import { render, axe } from "reakit-test-utils";
import { Provider } from "../../../../Provider";
import ButtonWithTooltip from "..";
test("a11y", async () => {
const { baseElement } = render(<ButtonWithTooltip />);
expect(await axe(baseElement)).toHaveNoViolations();
});
test("markup", () => {
const { baseElement } = render(
<Provider>
<ButtonWithTooltip />
</Provider>
);
expect(baseElement).toMatchInlineSnapshot(`
<body>
<div>
<button
aria-describedby="id-1"
tabindex="0"
type="button"
>
Button
</button>
</div>
<div
class="__reakit-portal"
>
<div
hidden=""
id="id-1"
role="tooltip"
style="display: none; position: fixed; left: 100%; top: 100%; pointer-events: none;"
>
Tooltip
</div>
</div>
</body>
`);
});

View File

@@ -0,0 +1,17 @@
import * as React from "react";
import { Button } from "reakit/Button";
import { Tooltip, TooltipReference, useTooltipState } from "reakit/Tooltip";
import "./style.css";
export default function ButtonWithTooltip() {
const tooltip = useTooltipState();
return (
<>
<TooltipReference {...tooltip} as={Button}>
Button
</TooltipReference>
<Tooltip {...tooltip}>Tooltip</Tooltip>
</>
);
}

View File

@@ -0,0 +1 @@
@import "../AccessibleButton/style.css";

View File

@@ -0,0 +1,11 @@
import { Button } from "../Button";
export { default as AccessibleButton } from "./AccessibleButton";
export { default as ButtonAsDiv } from "./ButtonAsDiv";
export { default as ButtonAsLink } from "./ButtonAsLink";
export { default as ButtonWithTooltip } from "./ButtonWithTooltip";
export default {
title: "Button",
component: Button,
};

2
node_modules/reakit/src/Button/__keys.ts generated vendored Normal file
View File

@@ -0,0 +1,2 @@
// Automatically generated
export const BUTTON_KEYS = [] as const;

1
node_modules/reakit/src/Button/index.ts generated vendored Normal file
View File

@@ -0,0 +1 @@
export * from "./Button";

197
node_modules/reakit/src/Checkbox/Checkbox.ts generated vendored Normal file
View File

@@ -0,0 +1,197 @@
import * as React from "react";
import { createComponent } from "reakit-system/createComponent";
import { removeIndexFromArray } from "reakit-utils/removeIndexFromArray";
import { createHook } from "reakit-system/createHook";
import { useForkRef } from "reakit-utils/useForkRef";
import { createEvent } from "reakit-utils/createEvent";
import { warning } from "reakit-warning";
import { useLiveRef } from "reakit-utils/useLiveRef";
import {
ClickableOptions,
ClickableHTMLProps,
useClickable,
} from "../Clickable/Clickable";
import { CheckboxStateReturn } from "./CheckboxState";
import { CHECKBOX_KEYS } from "./__keys";
export type CheckboxOptions = ClickableOptions &
Pick<Partial<CheckboxStateReturn>, "state" | "setState"> & {
/**
* Checkbox's value is going to be used when multiple checkboxes share the
* same state. Checking a checkbox with value will add it to the state
* array.
*/
value?: string | number;
/**
* Checkbox's checked state. If present, it's used instead of `state`.
*/
checked?: boolean;
};
export type CheckboxHTMLProps = ClickableHTMLProps &
React.InputHTMLAttributes<any> & {
value?: string | number;
};
export type CheckboxProps = CheckboxOptions & CheckboxHTMLProps;
function getChecked(options: CheckboxOptions) {
if (typeof options.checked !== "undefined") {
return options.checked;
}
if (typeof options.value === "undefined") {
return !!options.state;
}
const state = Array.isArray(options.state) ? options.state : [];
return state.indexOf(options.value) !== -1;
}
function fireChange(element: HTMLElement, onChange?: React.ChangeEventHandler) {
const event = createEvent(element, "change");
Object.defineProperties(event, {
type: { value: "change" },
target: { value: element },
currentTarget: { value: element },
});
onChange?.(event as any);
}
function useIndeterminateState(
ref: React.RefObject<HTMLInputElement>,
options: CheckboxOptions
) {
React.useEffect(() => {
const element = ref.current;
if (!element) {
warning(
options.state === "indeterminate",
"Can't set indeterminate state because `ref` wasn't passed to component.",
"See https://reakit.io/docs/checkbox/#indeterminate-state"
);
return;
}
if (options.state === "indeterminate") {
element.indeterminate = true;
} else if (element.indeterminate) {
element.indeterminate = false;
}
}, [options.state, ref]);
}
export const useCheckbox = createHook<CheckboxOptions, CheckboxHTMLProps>({
name: "Checkbox",
compose: useClickable,
keys: CHECKBOX_KEYS,
useOptions(
{ unstable_clickOnEnter = false, ...options },
{ value, checked }
) {
return {
unstable_clickOnEnter,
value,
checked: getChecked({ checked, ...options }),
...options,
};
},
useProps(
options,
{ ref: htmlRef, onChange: htmlOnChange, onClick: htmlOnClick, ...htmlProps }
) {
const ref = React.useRef<HTMLInputElement>(null);
const [isNativeCheckbox, setIsNativeCheckbox] = React.useState(true);
const onChangeRef = useLiveRef(htmlOnChange);
const onClickRef = useLiveRef(htmlOnClick);
React.useEffect(() => {
const element = ref.current;
if (!element) {
warning(
true,
"Can't determine whether the element is a native checkbox because `ref` wasn't passed to the component",
"See https://reakit.io/docs/checkbox"
);
return;
}
if (element.tagName !== "INPUT" || element.type !== "checkbox") {
setIsNativeCheckbox(false);
}
}, []);
useIndeterminateState(ref, options);
const onChange = React.useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
const element = event.currentTarget;
if (options.disabled) {
event.stopPropagation();
event.preventDefault();
return;
}
if (onChangeRef.current) {
// If component is NOT rendered as a native input, it will not have
// the `checked` property. So we assign it for consistency.
if (!isNativeCheckbox) {
element.checked = !element.checked;
}
onChangeRef.current(event);
}
if (!options.setState) return;
if (typeof options.value === "undefined") {
options.setState(!options.checked);
} else {
const state = Array.isArray(options.state) ? options.state : [];
const index = state.indexOf(options.value);
if (index === -1) {
options.setState([...state, options.value]);
} else {
options.setState(removeIndexFromArray(state, index));
}
}
},
[
options.disabled,
isNativeCheckbox,
options.setState,
options.value,
options.checked,
options.state,
]
);
const onClick = React.useCallback(
(event: React.MouseEvent<HTMLElement, MouseEvent>) => {
onClickRef.current?.(event);
if (event.defaultPrevented) return;
if (isNativeCheckbox) return;
fireChange(event.currentTarget, onChange);
},
[isNativeCheckbox, onChange]
);
return {
ref: useForkRef(ref, htmlRef),
role: !isNativeCheckbox ? "checkbox" : undefined,
type: isNativeCheckbox ? "checkbox" : undefined,
value: isNativeCheckbox ? options.value : undefined,
checked: options.checked,
"aria-checked":
options.state === "indeterminate" ? "mixed" : options.checked,
onChange,
onClick,
...htmlProps,
};
},
});
export const Checkbox = createComponent({
as: "input",
memo: true,
useHook: useCheckbox,
});

36
node_modules/reakit/src/Checkbox/CheckboxState.ts generated vendored Normal file
View File

@@ -0,0 +1,36 @@
import * as React from "react";
import {
useSealedState,
SealedInitialState,
} from "reakit-utils/useSealedState";
export type CheckboxState = {
/**
* Stores the state of the checkbox.
* If checkboxes that share this state have defined a `value` prop, it's
* going to be an array.
*/
state: boolean | "indeterminate" | Array<number | string>;
};
export type CheckboxActions = {
/**
* Sets `state`.
*/
setState: React.Dispatch<React.SetStateAction<CheckboxState["state"]>>;
};
export type CheckboxInitialState = Partial<Pick<CheckboxState, "state">>;
export type CheckboxStateReturn = CheckboxState & CheckboxActions;
/**
* As simple as `React.useState(false)`
*/
export function useCheckboxState(
initialState: SealedInitialState<CheckboxInitialState> = {}
): CheckboxStateReturn {
const { state: initialValue = false } = useSealedState(initialState);
const [state, setState] = React.useState(initialValue);
return { state, setState };
}

292
node_modules/reakit/src/Checkbox/README.md generated vendored Normal file
View File

@@ -0,0 +1,292 @@
---
path: /docs/checkbox/
---
# Checkbox
Accessible `Checkbox` component that follows the [WAI-ARIA Checkbox Pattern](https://www.w3.org/TR/wai-aria-practices/#checkbox), which means you'll have a working dual or tri-state toggle button regardless of the type of the underlying element. By default, it renders the native `<input type="checkbox">`.
<carbon-ad></carbon-ad>
## Installation
```sh
npm install reakit
```
Learn more in [Get started](/docs/get-started/).
## Usage
It receives the same props as [controlled inputs](https://reactjs.org/docs/forms.html), such as `checked` and `onChange`:
```jsx
import React from "react";
import { Checkbox } from "reakit/Checkbox";
function Example() {
const [checked, setChecked] = React.useState(false);
const toggle = () => setChecked(!checked);
return (
<label>
<Checkbox checked={checked} onChange={toggle} />
Checkbox
</label>
);
}
```
### Rendering as a different element
You can render `Checkbox` as any component. Reakit will ensure that it's accessible by adding proper ARIA attributes and event handlers.
> When styling, instead of using `:checked` or the `[checked]` selector, you will have to use `[aria-checked="true"]` to select non-native checked checkboxes.
```jsx
import React from "react";
import { Checkbox } from "reakit/Checkbox";
import { Button } from "reakit/Button";
function Example() {
const [checked, setChecked] = React.useState(false);
const toggle = () => setChecked(!checked);
return (
<Checkbox as={Button} checked={checked} onChange={toggle}>
{checked ? "Uncheck" : "Check"}
</Checkbox>
);
}
```
### `useCheckboxState`
For convenience and consistency with the other components, Reakit provides a `useCheckboxState` that already implements the state logic for you:
```jsx
import { useCheckboxState, Checkbox } from "reakit/Checkbox";
function Example() {
const checkbox = useCheckboxState({ state: true });
return (
<label>
<Checkbox {...checkbox} />
Checkbox
</label>
);
}
```
### Multiple checkboxes
Oftentimes we need to render multiple checkboxes and store the checked values in an array. It can be easily done with Reakit:
```jsx
import { useCheckboxState, Checkbox } from "reakit/Checkbox";
function Example() {
const checkbox = useCheckboxState({ state: [] });
return (
<>
<div>Choices: {checkbox.state.join(", ")}</div>
<label>
<Checkbox {...checkbox} value="apple" />
Apple
</label>
<label>
<Checkbox {...checkbox} value="orange" />
Orange
</label>
<label>
<Checkbox {...checkbox} value="watermelon" />
Watermelon
</label>
</>
);
}
```
### Indeterminate or mixed state
You can programmatically set checkbox value as `indeterminate`:
```jsx
import React from "react";
import { Checkbox, useCheckboxState } from "reakit/Checkbox";
function useTreeState({ values }) {
const group = useCheckboxState();
const items = useCheckboxState();
// updates items when group is toggled
React.useEffect(() => {
if (group.state === true) {
items.setState(values);
} else if (group.state === false) {
items.setState([]);
}
}, [group.state]);
// updates group when items is toggled
React.useEffect(() => {
if (items.state.length === values.length) {
group.setState(true);
} else if (items.state.length) {
group.setState("indeterminate");
} else {
group.setState(false);
}
}, [items.state]);
return { group, items };
}
function Example() {
const values = ["Apple", "Orange", "Watermelon"];
const { group, items } = useTreeState({ values });
return (
<ul>
<li>
<label>
<Checkbox {...group} /> Fruits
</label>
</li>
<ul>
{values.map((value, i) => (
<li key={i}>
<label>
<Checkbox {...items} value={value} /> {value}
</label>
</li>
))}
</ul>
</ul>
);
}
```
## Styling
Reakit components are [un-styled by default](/docs/styling/). Each component returns a single HTML element that accepts all HTML props, including `className` and `style`.
> The example below uses [Emotion](https://emotion.sh/docs/introduction). But these styles can be reproduced using static CSS and other CSS-in-JS libraries, such as [styled-components](https://styled-components.com/).
```jsx unstyled
import * as React from "react";
import { Checkbox } from "reakit/Checkbox";
import { css } from "emotion";
const labelStyle = css`
display: flex;
align-items: center;
`;
const checkboxStyle = css`
appearance: none;
border: 1px solid #a860ff;
border-radius: 4px;
outline: none;
cursor: pointer;
width: 20px;
height: 20px;
display: flex;
justify-content: center;
align-items: center;
margin-right: 5px;
&:after {
content: "✔";
display: none;
color: white;
font-size: 70%;
}
&:checked {
background-color: #6a50ee;
border: 2px solid #a860ff;
&:after {
display: block;
}
}
`;
function Example() {
const [checked, setChecked] = React.useState(false);
const toggle = () => setChecked(!checked);
return (
<label className={labelStyle}>
<Checkbox checked={checked} onChange={toggle} className={checkboxStyle} />
Checkbox
</label>
);
}
```
## Accessibility
- `Checkbox` has role `checkbox`.
- When checked, `Checkbox` has `aria-checked` set to `true`.
- When not checked, `Checkbox` has `aria-checked` set to `false`.
- When partially checked, `Checkbox` has `aria-checked` set to `mixed`.
Learn more in [Accessibility](/docs/accessibility/).
## Composition
- `Checkbox` uses [Clickable](/docs/clickable/), and is used by [FormCheckbox](/docs/form/) and [MenuItemCheckbox](/docs/menu/).
Learn more in [Composition](/docs/composition/#props-hooks).
## Props
<!-- Automatically generated -->
### `useCheckboxState`
- **`state`**
<code>boolean | &#34;indeterminate&#34; | (string | number)[]</code>
Stores the state of the checkbox.
If checkboxes that share this state have defined a `value` prop, it's
going to be an array.
### `Checkbox`
- **`disabled`**
<code>boolean | undefined</code>
Same as the HTML attribute.
- **`focusable`**
<code>boolean | undefined</code>
When an element is `disabled`, it may still be `focusable`. It works
similarly to `readOnly` on form elements. In this case, only
`aria-disabled` will be set.
- **`value`**
<code>string | number | undefined</code>
Checkbox's value is going to be used when multiple checkboxes share the
same state. Checking a checkbox with value will add it to the state
array.
- **`checked`**
<code>boolean | undefined</code>
Checkbox's checked state. If present, it's used instead of `state`.
<details><summary>2 state props</summary>
> These props are returned by the state hook. You can spread them into this component (`{...state}`) or pass them separately. You can also provide these props from your own state logic.
- **`state`**
<code>boolean | &#34;indeterminate&#34; | (string | number)[]</code>
Stores the state of the checkbox.
If checkboxes that share this state have defined a `value` prop, it's
going to be an array.
- **`setState`**
<code title="(value: SetStateAction&#60;boolean | &#34;indeterminate&#34; | (string | number)[]&#62;) =&#62; void">(value: SetStateAction&#60;boolean | &#34;indeterminate...</code>
Sets `state`.
</details>

View File

@@ -0,0 +1,8 @@
import * as React from "react";
import { render, axe } from "reakit-test-utils";
import Checkbox from "..";
test("a11y", async () => {
const { baseElement } = render(<Checkbox />);
expect(await axe(baseElement)).toHaveNoViolations();
});

View File

@@ -0,0 +1,46 @@
import * as React from "react";
import { Checkbox as BaseCheckbox, useCheckboxState } from "reakit/Checkbox";
import "./style.css";
export default function Checkbox() {
const checkbox = useCheckboxState();
return (
<label className="checkbox">
<BaseCheckbox
{...checkbox}
as="button"
type="button"
className="checkbox-control"
aria-labelledby="checkbox-label"
>
<CheckIcon className="checkbox-check-icon" aria-hidden />
</BaseCheckbox>
<span id="checkbox-label">Checkbox</span>
</label>
);
}
type CheckIconProps = {
className?: string;
};
function CheckIcon({ className }: CheckIconProps) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
width="1em"
height="1em"
className={className}
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
);
}

View File

@@ -0,0 +1,54 @@
:root {
--font-family: var(--font-family-body, sans-serif);
--font-size: var(--font-size-body, 16px);
--checkbox-color: var(--color-white, #fff);
--checkbox-background: var(--color-white, #fff);
--checkbox-border: var(--color-gray-100, #eee);
--checkbox-background-checked: var(--color-primary-700, #1976d2);
--checkbox-border-checked: var(--color-primary-700, #1976d2);
--checkbox-border-hover: var(--color-primary-400, #51a8f0);
--checkbox-background-active: var(--color-primary-900, #0d5399);
--checkbox-border-active: var(--color-primary-900, #0d5399);
}
.checkbox {
display: inline-flex;
align-items: center;
font-family: var(--font-family);
font-size: var(--font-size);
cursor: pointer;
}
.checkbox-control {
display: inline-flex;
align-items: center;
justify-content: center;
height: 1.25rem;
width: 1.25rem;
padding: 0;
margin-right: 0.5rem;
appearance: none;
border: 2px solid var(--checkbox-border);
color: var(--checkbox-color);
background: var(--checkbox-background);
border-radius: 4px;
transition: all 0.2s;
}
.checkbox-control[aria-checked="true"] {
background: var(--checkbox-background-checked);
border-color: var(--checkbox-border-checked);
}
.checkbox-control:hover {
border-color: var(--checkbox-border-hover);
}
.checkbox-control:active {
background-color: var(--checkbox-background-active);
border-color: var(--checkbox-border-active);
}
.checkbox-check-icon {
font-size: 1rem;
}

View File

@@ -0,0 +1,8 @@
import { Checkbox } from "../Checkbox";
export { default as Checkbox } from "./Checkbox";
export default {
title: "Checkbox",
component: Checkbox,
};

7
node_modules/reakit/src/Checkbox/__keys.ts generated vendored Normal file
View File

@@ -0,0 +1,7 @@
// Automatically generated
const CHECKBOX_STATE_KEYS = ["state", "setState"] as const;
export const CHECKBOX_KEYS = [
...CHECKBOX_STATE_KEYS,
"value",
"checked",
] as const;

View File

@@ -0,0 +1,244 @@
import * as React from "react";
import { render, click, press } from "reakit-test-utils";
import { Checkbox, CheckboxOptions, CheckboxHTMLProps } from "../Checkbox";
test("render", () => {
const { baseElement } = render(<Checkbox />);
expect(baseElement).toMatchInlineSnapshot(`
<body>
<div>
<input
aria-checked="false"
type="checkbox"
/>
</div>
</body>
`);
});
test("render disabled", () => {
const { baseElement } = render(<Checkbox disabled />);
expect(baseElement).toMatchInlineSnapshot(`
<body>
<div>
<input
aria-checked="false"
aria-disabled="true"
disabled=""
style="pointer-events: none;"
type="checkbox"
/>
</div>
</body>
`);
});
test("render disabled focusable", () => {
const { baseElement } = render(<Checkbox disabled focusable />);
expect(baseElement).toMatchInlineSnapshot(`
<body>
<div>
<input
aria-checked="false"
aria-disabled="true"
style="pointer-events: none;"
type="checkbox"
/>
</div>
</body>
`);
});
test("render checked", () => {
const { baseElement } = render(<Checkbox checked />);
expect(baseElement).toMatchInlineSnapshot(`
<body>
<div>
<input
aria-checked="true"
checked=""
type="checkbox"
/>
</div>
</body>
`);
});
test("click", () => {
const fn = jest.fn();
const { getByLabelText } = render(
<label>
<Checkbox onClick={fn} /> checkbox
</label>
);
const checkbox = getByLabelText("checkbox");
expect(fn).toHaveBeenCalledTimes(0);
click(checkbox);
expect(fn).toHaveBeenCalledTimes(1);
});
test("click disabled", () => {
const fn = jest.fn();
const { getByLabelText } = render(
<label>
<Checkbox onClick={fn} disabled /> checkbox
</label>
);
const checkbox = getByLabelText("checkbox");
click(checkbox);
expect(fn).toHaveBeenCalledTimes(0);
});
test("click disabled focusable", () => {
const fn = jest.fn();
const { getByLabelText } = render(
<label>
<Checkbox onClick={fn} disabled focusable /> checkbox
</label>
);
const checkbox = getByLabelText("checkbox");
click(checkbox);
expect(fn).toHaveBeenCalledTimes(0);
});
test("focus", () => {
const { getByLabelText } = render(
<label>
<Checkbox /> checkbox
</label>
);
const checkbox = getByLabelText("checkbox");
expect(checkbox).not.toHaveFocus();
checkbox.focus();
expect(checkbox).toHaveFocus();
});
test("focus disabled", () => {
const { getByLabelText } = render(
<label>
<Checkbox disabled /> checkbox
</label>
);
const checkbox = getByLabelText("checkbox");
expect(checkbox).not.toHaveFocus();
checkbox.focus();
expect(checkbox).not.toHaveFocus();
});
test("focus disabled focusable", () => {
const { getByLabelText } = render(
<label>
<Checkbox disabled focusable /> checkbox
</label>
);
const checkbox = getByLabelText("checkbox");
expect(checkbox).not.toHaveFocus();
checkbox.focus();
expect(checkbox).toHaveFocus();
});
test("non-native checkbox click", () => {
const fn = jest.fn();
const { getByLabelText } = render(
<Checkbox as="div" onClick={fn} aria-label="checkbox" />
);
const checkbox = getByLabelText("checkbox");
expect(fn).toHaveBeenCalledTimes(0);
click(checkbox);
expect(fn).toHaveBeenCalledTimes(1);
});
test("non-native checkbox click disabled", () => {
const fn = jest.fn();
const { getByLabelText } = render(
<Checkbox as="div" onClick={fn} aria-label="checkbox" disabled />
);
const checkbox = getByLabelText("checkbox");
click(checkbox);
expect(fn).toHaveBeenCalledTimes(0);
});
test("non-native checkbox click disabled focusable", () => {
const fn = jest.fn();
const { getByLabelText } = render(
<Checkbox as="div" onClick={fn} aria-label="checkbox" disabled focusable />
);
const checkbox = getByLabelText("checkbox");
click(checkbox);
expect(fn).toHaveBeenCalledTimes(0);
});
test("non-native checkbox focus", () => {
const { getByLabelText } = render(
<Checkbox as="div" aria-label="checkbox" />
);
const checkbox = getByLabelText("checkbox");
expect(checkbox).not.toHaveFocus();
checkbox.focus();
expect(checkbox).toHaveFocus();
});
test("non-native checkbox focus disabled", () => {
const { getByLabelText } = render(
<Checkbox as="div" aria-label="checkbox" disabled />
);
const checkbox = getByLabelText("checkbox");
expect(checkbox).not.toHaveFocus();
checkbox.focus();
expect(checkbox).not.toHaveFocus();
});
test("non-native checkbox focus disabled focusable", () => {
const { getByLabelText } = render(
<Checkbox as="div" aria-label="checkbox" disabled focusable />
);
const checkbox = getByLabelText("checkbox");
expect(checkbox).not.toHaveFocus();
checkbox.focus();
expect(checkbox).toHaveFocus();
});
test("non-native checkbox space", () => {
const fn = jest.fn();
const { getByLabelText } = render(
<Checkbox as="div" onClick={fn} aria-label="checkbox" />
);
const checkbox = getByLabelText("checkbox");
press.Space(checkbox);
expect(fn).toHaveBeenCalledTimes(1);
});
test("non-native checkbox space disabled", () => {
const fn = jest.fn();
const { getByLabelText } = render(
<Checkbox as="div" onClick={fn} aria-label="checkbox" disabled />
);
const checkbox = getByLabelText("checkbox");
press.Space(checkbox);
expect(fn).toHaveBeenCalledTimes(0);
});
test("non-native checkbox space disabled focusable", () => {
const fn = jest.fn();
const { getByLabelText } = render(
<Checkbox as="div" onClick={fn} aria-label="checkbox" disabled focusable />
);
const checkbox = getByLabelText("checkbox");
press.Space(checkbox);
expect(fn).toHaveBeenCalledTimes(0);
});
test("indeterminate", () => {
const Comp = (props: CheckboxOptions & CheckboxHTMLProps) => (
<label>
<Checkbox {...props} /> checkbox
</label>
);
const { getByLabelText, rerender } = render(<Comp />);
const checkbox = getByLabelText("checkbox");
expect(checkbox).toHaveAttribute("aria-checked", "false");
rerender(<Comp state="indeterminate" />);
expect(checkbox).toHaveAttribute("aria-checked", "mixed");
rerender(<Comp state />);
expect(checkbox).toHaveAttribute("aria-checked", "true");
});

View File

@@ -0,0 +1,49 @@
import { renderHook, act } from "reakit-test-utils/hooks";
import { jestSerializerStripFunctions } from "reakit-test-utils/jestSerializerStripFunctions";
import { useCheckboxState } from "../CheckboxState";
expect.addSnapshotSerializer(jestSerializerStripFunctions);
function render(...args: Parameters<typeof useCheckboxState>) {
return renderHook(() => useCheckboxState(...args)).result;
}
test("initial state", () => {
const result = render();
expect(result.current).toMatchInlineSnapshot(`
Object {
"state": false,
}
`);
});
test("initial state", () => {
const result = render({ state: true });
expect(result.current).toMatchInlineSnapshot(`
Object {
"state": true,
}
`);
});
test("initial state array", () => {
const result = render({ state: ["a", "b"] });
expect(result.current).toMatchInlineSnapshot(`
Object {
"state": Array [
"a",
"b",
],
}
`);
});
test("setState", () => {
const result = render();
act(() => result.current.setState(true));
expect(result.current).toMatchInlineSnapshot(`
Object {
"state": true,
}
`);
});

View File

@@ -0,0 +1,253 @@
import * as React from "react";
import { render, click } from "reakit-test-utils";
import { Checkbox, useCheckbox, useCheckboxState } from "..";
test("single checkbox", () => {
const Test = () => {
const checkbox = useCheckboxState();
return (
<label>
<Checkbox {...checkbox} />
checkbox
</label>
);
};
const { getByLabelText } = render(<Test />);
const checkbox = getByLabelText("checkbox") as HTMLInputElement;
expect(checkbox.checked).toBe(false);
click(checkbox);
expect(checkbox.checked).toBe(true);
});
test("group checkbox", () => {
const Test = () => {
const checkbox = useCheckboxState();
return (
<div role="group">
<Checkbox {...checkbox} as="div" aria-label="apple" value="apple" />
<label>
<Checkbox {...checkbox} value="orange" />
orange
</label>
<label>
<Checkbox {...checkbox} value="watermelon" />
watermelon
</label>
</div>
);
};
const { getByLabelText } = render(<Test />);
const apple = getByLabelText("apple") as HTMLInputElement;
const orange = getByLabelText("orange") as HTMLInputElement;
const watermelon = getByLabelText("watermelon") as HTMLInputElement;
expect(apple.checked).toBe(false);
expect(orange.checked).toBe(false);
expect(watermelon.checked).toBe(false);
click(orange);
click(apple);
expect(apple.checked).toBe(true);
expect(orange.checked).toBe(true);
expect(watermelon.checked).toBe(false);
});
test("group checkbox with initial state", () => {
const Test = () => {
const checkbox = useCheckboxState({ state: ["orange"] });
return (
<div role="group">
<Checkbox {...checkbox} as="div" aria-label="apple" value="apple" />
<label>
<Checkbox {...checkbox} value="orange" />
orange
</label>
<label>
<Checkbox {...checkbox} value="watermelon" />
watermelon
</label>
</div>
);
};
const { getByLabelText } = render(<Test />);
const apple = getByLabelText("apple") as HTMLInputElement;
const orange = getByLabelText("orange") as HTMLInputElement;
const watermelon = getByLabelText("watermelon") as HTMLInputElement;
expect(apple.checked).toBe(false);
expect(orange.checked).toBe(true);
expect(watermelon.checked).toBe(false);
click(apple);
expect(apple.checked).toBe(true);
expect(orange.checked).toBe(true);
expect(watermelon.checked).toBe(false);
});
test("checkbox onChange checked value", () => {
const onChange = jest.fn();
const Test = () => {
const checkbox = useCheckboxState();
return (
<label>
<Checkbox
{...checkbox}
onChange={(event) => onChange(event.target.checked)}
/>
checkbox
</label>
);
};
const { getByLabelText } = render(<Test />);
const checkbox = getByLabelText("checkbox") as HTMLInputElement;
expect(checkbox.checked).toBe(false);
expect(onChange).not.toBeCalled();
click(checkbox);
expect(checkbox.checked).toBe(true);
expect(onChange).toBeCalledWith(true);
click(checkbox);
expect(checkbox.checked).toBe(false);
expect(onChange).toBeCalledWith(false);
});
test("non-native checkbox onChange checked value", () => {
const onChange = jest.fn();
const Test = () => {
const checkbox = useCheckboxState();
return (
<Checkbox
{...checkbox}
as="div"
aria-label="checkbox"
onChange={(event: any) => onChange(event.target.checked)}
/>
);
};
const { getByLabelText } = render(<Test />);
const checkbox = getByLabelText("checkbox") as HTMLInputElement;
expect(checkbox.checked).toBe(false);
expect(onChange).not.toBeCalled();
click(checkbox);
expect(checkbox.checked).toBe(true);
expect(onChange).toBeCalledWith(true);
click(checkbox);
expect(checkbox.checked).toBe(false);
expect(onChange).toBeCalledWith(false);
});
test("checkbox onChange checked value without useCheckboxState", () => {
const onChange = jest.fn();
const Test = () => {
const [checked, setChecked] = React.useState(false);
return (
<label>
<Checkbox
checked={checked}
onChange={(event) => {
setChecked(event.target.checked);
onChange(event.target.checked);
}}
/>
checkbox
</label>
);
};
const { getByLabelText } = render(<Test />);
const checkbox = getByLabelText("checkbox") as HTMLInputElement;
expect(checkbox.checked).toBe(false);
expect(onChange).not.toBeCalled();
click(checkbox);
expect(checkbox.checked).toBe(true);
expect(onChange).toBeCalledWith(true);
click(checkbox);
expect(checkbox.checked).toBe(false);
expect(onChange).toBeCalledWith(false);
});
test("non-native checkbox onChange checked value without useCheckboxState", () => {
const onChange = jest.fn();
const Test = () => {
const [checked, setChecked] = React.useState(false);
return (
<Checkbox
as="div"
aria-label="checkbox"
checked={checked}
onChange={(event: any) => {
setChecked(event.target.checked);
onChange(event.target.checked);
}}
/>
);
};
const { getByLabelText } = render(<Test />);
const checkbox = getByLabelText("checkbox") as HTMLInputElement;
expect(checkbox.checked).toBe(false);
expect(onChange).not.toBeCalled();
click(checkbox);
expect(checkbox.checked).toBe(true);
expect(onChange).toBeCalledWith(true);
click(checkbox);
expect(checkbox.checked).toBe(false);
expect(onChange).toBeCalledWith(false);
});
test("non-native checkbox onClick preventDefault", () => {
const Test = () => {
const checkbox = useCheckboxState();
return (
<Checkbox
{...checkbox}
as="div"
aria-label="checkbox"
onClick={(event: React.MouseEvent) => event.preventDefault()}
/>
);
};
const { getByLabelText } = render(<Test />);
const checkbox = getByLabelText("checkbox") as HTMLInputElement;
expect(checkbox.checked).toBe(false);
click(checkbox);
expect(checkbox.checked).toBe(false);
});
test("useCheckbox", () => {
const Test = () => {
const [checked, setChecked] = React.useState(false);
const props = useCheckbox(
{},
{
checked,
onChange: (event: React.ChangeEvent<HTMLInputElement>) =>
setChecked(event.target.checked),
}
);
return (
<label>
<input {...props} />
checkbox
</label>
);
};
const { getByLabelText } = render(<Test />);
const checkbox = getByLabelText("checkbox") as HTMLInputElement;
expect(checkbox.checked).toBe(false);
expect(checkbox).toMatchInlineSnapshot(`
<input
aria-checked="false"
type="checkbox"
/>
`);
click(checkbox);
expect(checkbox.checked).toBe(true);
expect(checkbox).toMatchInlineSnapshot(`
<input
aria-checked="true"
type="checkbox"
/>
`);
click(checkbox);
expect(checkbox.checked).toBe(false);
expect(checkbox).toMatchInlineSnapshot(`
<input
aria-checked="false"
type="checkbox"
/>
`);
});

2
node_modules/reakit/src/Checkbox/index.ts generated vendored Normal file
View File

@@ -0,0 +1,2 @@
export * from "./Checkbox";
export * from "./CheckboxState";

129
node_modules/reakit/src/Clickable/Clickable.ts generated vendored Normal file
View File

@@ -0,0 +1,129 @@
import * as React from "react";
import { createComponent } from "reakit-system/createComponent";
import { createHook } from "reakit-system/createHook";
import { isButton } from "reakit-utils/isButton";
import { useLiveRef } from "reakit-utils/useLiveRef";
import { isSelfTarget } from "reakit-utils/isSelfTarget";
import {
TabbableOptions,
TabbableHTMLProps,
useTabbable,
} from "../Tabbable/Tabbable";
import { CLICKABLE_KEYS } from "./__keys";
export type ClickableOptions = TabbableOptions & {
/**
* Whether or not trigger click on pressing <kbd>Enter</kbd>.
* @private
*/
unstable_clickOnEnter?: boolean;
/**
* Whether or not trigger click on pressing <kbd>Space</kbd>.
* @private
*/
unstable_clickOnSpace?: boolean;
};
export type ClickableHTMLProps = TabbableHTMLProps;
export type ClickableProps = ClickableOptions & ClickableHTMLProps;
function isNativeClick(event: React.KeyboardEvent) {
const element = event.currentTarget;
if (!event.isTrusted) return false;
// istanbul ignore next: can't test trusted events yet
return (
isButton(element) ||
element.tagName === "INPUT" ||
element.tagName === "TEXTAREA" ||
element.tagName === "A" ||
element.tagName === "SELECT"
);
}
export const useClickable = createHook<ClickableOptions, ClickableHTMLProps>({
name: "Clickable",
compose: useTabbable,
keys: CLICKABLE_KEYS,
useOptions({
unstable_clickOnEnter = true,
unstable_clickOnSpace = true,
...options
}) {
return {
unstable_clickOnEnter,
unstable_clickOnSpace,
...options,
};
},
useProps(
options,
{ onKeyDown: htmlOnKeyDown, onKeyUp: htmlOnKeyUp, ...htmlProps }
) {
const [active, setActive] = React.useState(false);
const onKeyDownRef = useLiveRef(htmlOnKeyDown);
const onKeyUpRef = useLiveRef(htmlOnKeyUp);
const onKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLElement>) => {
onKeyDownRef.current?.(event);
if (event.defaultPrevented) return;
if (options.disabled) return;
if (event.metaKey) return;
if (!isSelfTarget(event)) return;
const isEnter = options.unstable_clickOnEnter && event.key === "Enter";
const isSpace = options.unstable_clickOnSpace && event.key === " ";
if (isEnter || isSpace) {
if (isNativeClick(event)) return;
event.preventDefault();
if (isEnter) {
event.currentTarget.click();
} else if (isSpace) {
setActive(true);
}
}
},
[
options.disabled,
options.unstable_clickOnEnter,
options.unstable_clickOnSpace,
]
);
const onKeyUp = React.useCallback(
(event: React.KeyboardEvent<HTMLElement>) => {
onKeyUpRef.current?.(event);
if (event.defaultPrevented) return;
if (options.disabled) return;
if (event.metaKey) return;
const isSpace = options.unstable_clickOnSpace && event.key === " ";
if (active && isSpace) {
setActive(false);
event.currentTarget.click();
}
},
[options.disabled, options.unstable_clickOnSpace, active]
);
return {
"data-active": active || undefined,
onKeyDown,
onKeyUp,
...htmlProps,
};
},
});
export const Clickable = createComponent({
as: "button",
memo: true,
useHook: useClickable,
});

73
node_modules/reakit/src/Clickable/README.md generated vendored Normal file
View File

@@ -0,0 +1,73 @@
---
path: /docs/clickable/
---
# Clickable
`Clickable` is an abstract component that implements all the interactions an interactive element needs to be fully accessible when it's not rendered as its respective native element.
<carbon-ad></carbon-ad>
## Installation
```sh
npm install reakit
```
Learn more in [Get started](/docs/get-started/).
## Usage
<!-- eslint-disable no-alert -->
```jsx
import { Clickable } from "reakit/Clickable";
function Example() {
const onClick = () => alert("clicked");
return (
<>
<Clickable as="div" onClick={onClick}>
Clickable
</Clickable>
<Clickable as="div" onClick={onClick} disabled>
Disabled
</Clickable>
<Clickable as="div" onClick={onClick} disabled focusable>
Focusable
</Clickable>
</>
);
}
```
## Accessibility
- Pressing <kbd>Enter</kbd> or <kbd>Space</kbd> triggers a click event on `Clickable` regardless of its rendered element.
- `Clickable` extends the accessibility features of [Tabbable](/docs/tabbable/#accessibility).
Learn more in [Accessibility](/docs/accessibility/).
## Composition
- `Clickable` uses [Tabbable](/docs/tabbable/), and is used by [Button](/docs/button/), [Checkbox](/docs/checkbox/), and [CompositeItem](/docs/composite/).
Learn more in [Composition](/docs/composition/#props-hooks).
## Props
<!-- Automatically generated -->
### `Clickable`
- **`disabled`**
<code>boolean | undefined</code>
Same as the HTML attribute.
- **`focusable`**
<code>boolean | undefined</code>
When an element is `disabled`, it may still be `focusable`. It works
similarly to `readOnly` on form elements. In this case, only
`aria-disabled` will be set.

5
node_modules/reakit/src/Clickable/__keys.ts generated vendored Normal file
View File

@@ -0,0 +1,5 @@
// Automatically generated
export const CLICKABLE_KEYS = [
"unstable_clickOnEnter",
"unstable_clickOnSpace",
] as const;

View File

@@ -0,0 +1,252 @@
import * as React from "react";
import { render, click, focus, press } from "reakit-test-utils";
import { Clickable, ClickableProps } from "../Clickable";
test("render", () => {
const { getByText } = render(<Clickable>clickable</Clickable>);
expect(getByText("clickable")).toMatchInlineSnapshot(`
<button>
clickable
</button>
`);
});
test("render disabled", () => {
const { getByText } = render(<Clickable disabled>clickable</Clickable>);
expect(getByText("clickable")).toMatchInlineSnapshot(`
<button
aria-disabled="true"
disabled=""
style="pointer-events: none;"
>
clickable
</button>
`);
});
test("render disabled focusable", () => {
const { getByText } = render(
<Clickable disabled focusable>
clickable
</Clickable>
);
expect(getByText("clickable")).toMatchInlineSnapshot(`
<button
aria-disabled="true"
style="pointer-events: none;"
>
clickable
</button>
`);
});
test("click", () => {
const fn = jest.fn();
const { getByText } = render(<Clickable onClick={fn}>clickable</Clickable>);
const clickable = getByText("clickable");
expect(fn).toHaveBeenCalledTimes(0);
click(clickable);
expect(fn).toHaveBeenCalledTimes(1);
});
test("click disabled", () => {
const fn = jest.fn();
const { getByText } = render(
<Clickable onClick={fn} disabled>
clickable
</Clickable>
);
const clickable = getByText("clickable");
click(clickable);
expect(fn).toHaveBeenCalledTimes(0);
});
test("click enabled after disabled", () => {
const fn = jest.fn();
const { getByText, rerender } = render(
<Clickable onClick={fn} disabled>
clickable
</Clickable>
);
const clickable = getByText("clickable");
rerender(<Clickable onClick={fn}>clickable</Clickable>);
click(clickable);
expect(fn).toHaveBeenCalledTimes(1);
});
test("click disabled focusable", () => {
const fn = jest.fn();
const { getByText } = render(
<Clickable onClick={fn} disabled focusable>
clickable
</Clickable>
);
const clickable = getByText("clickable");
click(clickable);
expect(fn).toHaveBeenCalledTimes(0);
});
test("focus", () => {
const { getByText } = render(<Clickable>clickable</Clickable>);
const clickable = getByText("clickable");
expect(clickable).not.toHaveFocus();
focus(clickable);
expect(clickable).toHaveFocus();
});
test("focus disabled", () => {
const { getByText } = render(<Clickable disabled>clickable</Clickable>);
const clickable = getByText("clickable");
expect(clickable).not.toHaveFocus();
focus(clickable);
expect(clickable).not.toHaveFocus();
});
test("focus disabled focusable", () => {
const { getByText } = render(
<Clickable disabled focusable>
clickable
</Clickable>
);
const clickable = getByText("clickable");
expect(clickable).not.toHaveFocus();
focus(clickable);
expect(clickable).toHaveFocus();
});
test("non-native button click", () => {
const fn = jest.fn();
const { getByText } = render(
<Clickable as="div" onClick={fn}>
clickable
</Clickable>
);
const clickable = getByText("clickable");
expect(fn).toHaveBeenCalledTimes(0);
click(clickable);
expect(fn).toHaveBeenCalledTimes(1);
});
test("non-native button click disabled", () => {
const fn = jest.fn();
const { getByText } = render(
<Clickable as="div" onClick={fn} disabled>
clickable
</Clickable>
);
const clickable = getByText("clickable");
click(clickable);
expect(fn).toHaveBeenCalledTimes(0);
});
test("non-native button click disabled focusable", () => {
const fn = jest.fn();
const { getByText } = render(
<Clickable as="div" onClick={fn} disabled focusable>
clickable
</Clickable>
);
const clickable = getByText("clickable");
click(clickable);
expect(fn).toHaveBeenCalledTimes(0);
});
test("non-native button focus", () => {
const { getByText } = render(<Clickable as="div">clickable</Clickable>);
const clickable = getByText("clickable");
expect(clickable).not.toHaveFocus();
focus(clickable);
expect(clickable).toHaveFocus();
});
test("non-native button focus disabled", () => {
const { getByText } = render(
<Clickable as="div" disabled>
clickable
</Clickable>
);
const clickable = getByText("clickable");
expect(clickable).not.toHaveFocus();
focus(clickable);
expect(clickable).not.toHaveFocus();
});
test("non-native button focus disabled focusable", () => {
const { getByText } = render(
<Clickable as="div" disabled focusable>
clickable
</Clickable>
);
const clickable = getByText("clickable");
expect(clickable).not.toHaveFocus();
focus(clickable);
expect(clickable).toHaveFocus();
});
test("non-native button space/enter", () => {
const fn = jest.fn();
const { getByText } = render(
<Clickable as="div" onClick={fn}>
clickable
</Clickable>
);
const clickable = getByText("clickable");
press.Enter(clickable);
expect(fn).toHaveBeenCalledTimes(1);
press.Space(clickable);
expect(fn).toHaveBeenCalledTimes(2);
});
test("non-native button space/enter disabled", () => {
const fn = jest.fn();
const { getByText } = render(
<Clickable as="div" disabled onClick={fn}>
clickable
</Clickable>
);
const clickable = getByText("clickable");
press.Enter(clickable);
press.Space(clickable);
expect(fn).toHaveBeenCalledTimes(0);
});
test("non-native button space/enter metaKey", () => {
const fn = jest.fn();
const { getByText } = render(
<Clickable as="div" onClick={fn}>
clickable
</Clickable>
);
const clickable = getByText("clickable");
press.Enter(clickable, { metaKey: true });
press.Space(clickable, { metaKey: true });
expect(fn).toHaveBeenCalledTimes(0);
});
test("non-native button space/enter disabled focusable", () => {
const fn = jest.fn();
const { getByText } = render(
<Clickable as="div" disabled focusable onClick={fn}>
clickable
</Clickable>
);
const clickable = getByText("clickable");
press.Enter(clickable);
press.Space(clickable);
expect(fn).toHaveBeenCalledTimes(0);
});
test("press enter on Clickable as another non-native Clickable", () => {
const onClick = jest.fn();
const NonNativeClickable = React.forwardRef<HTMLDivElement, ClickableProps>(
(props, ref) => <Clickable as="div" ref={ref} {...props} />
);
const { getByText } = render(
<Clickable as={NonNativeClickable} onClick={onClick}>
clickable
</Clickable>
);
const clickable = getByText("clickable");
press.Enter(clickable);
expect(onClick).toHaveBeenCalledTimes(1);
});

1
node_modules/reakit/src/Clickable/index.ts generated vendored Normal file
View File

@@ -0,0 +1 @@
export * from "./Clickable";

380
node_modules/reakit/src/Combobox/Combobox.ts generated vendored Normal file
View File

@@ -0,0 +1,380 @@
import * as React from "react";
import { createComponent } from "reakit-system/createComponent";
import { createHook } from "reakit-system/createHook";
import { useLiveRef } from "reakit-utils/useLiveRef";
import { useForkRef } from "reakit-utils/useForkRef";
import { warning } from "reakit-warning";
import { useUpdateEffect } from "reakit-utils/useUpdateEffect";
import {
CompositeOptions,
CompositeHTMLProps,
useComposite,
} from "../Composite/Composite";
import { COMBOBOX_KEYS } from "./__keys";
import { unstable_ComboboxStateReturn } from "./ComboboxState";
import { getMenuId } from "./__utils/getMenuId";
function getControls(baseId: string, ariaControls?: string) {
const menuId = getMenuId(baseId);
if (ariaControls) {
return `${ariaControls} ${menuId}`;
}
return menuId;
}
function getAutocomplete(options: unstable_ComboboxOptions) {
if (options.list && options.inline) return "both";
if (options.list) return "list";
if (options.inline) return "inline";
return "none";
}
function isFirstItemAutoSelected(
items: unstable_ComboboxOptions["items"],
autoSelect: unstable_ComboboxOptions["autoSelect"],
currentId: unstable_ComboboxOptions["currentId"]
) {
if (!autoSelect) return false;
const firstItem = items.find((item) => !item.disabled);
return currentId && firstItem?.id === currentId;
}
function hasCompletionString(inputValue: string, currentValue?: string) {
return (
!!currentValue &&
currentValue.length > inputValue.length &&
currentValue.toLowerCase().indexOf(inputValue.toLowerCase()) === 0
);
}
function getCompletionString(inputValue: string, currentValue?: string) {
if (!currentValue) return "";
const index = currentValue.toLowerCase().indexOf(inputValue.toLowerCase());
return currentValue.slice(index + inputValue.length);
}
function useValue(options: unstable_ComboboxOptions) {
return React.useMemo(() => {
if (!options.inline) {
return options.inputValue;
}
const firstItemAutoSelected = isFirstItemAutoSelected(
options.items,
options.autoSelect,
options.currentId
);
if (firstItemAutoSelected) {
if (hasCompletionString(options.inputValue, options.currentValue)) {
return (
options.inputValue +
getCompletionString(options.inputValue, options.currentValue)
);
}
return options.inputValue;
}
return options.currentValue || options.inputValue;
}, [
options.inline,
options.inputValue,
options.autoSelect,
options.items,
options.currentId,
options.currentValue,
]);
}
function getFirstEnabledItemId(items: unstable_ComboboxOptions["items"]) {
return items.find((item) => !item.disabled)?.id;
}
export const unstable_useCombobox = createHook<
unstable_ComboboxOptions,
unstable_ComboboxHTMLProps
>({
name: "Combobox",
compose: useComposite,
keys: COMBOBOX_KEYS,
useOptions({ menuRole = "listbox", hideOnEsc = true, ...options }) {
return { menuRole, hideOnEsc, ...options };
},
useProps(
options,
{
ref: htmlRef,
onKeyDown: htmlOnKeyDown,
onKeyPress: htmlOnKeyPress,
onChange: htmlOnChange,
onClick: htmlOnClick,
onBlur: htmlOnBlur,
"aria-controls": ariaControls,
...htmlProps
}
) {
const ref = React.useRef<HTMLInputElement>(null);
const [updated, update] = React.useReducer(() => ({}), {});
const onKeyDownRef = useLiveRef(htmlOnKeyDown);
const onKeyPressRef = useLiveRef(htmlOnKeyPress);
const onChangeRef = useLiveRef(htmlOnChange);
const onClickRef = useLiveRef(htmlOnClick);
const onBlurRef = useLiveRef(htmlOnBlur);
const value = useValue(options);
const hasInsertedTextRef = React.useRef(false);
// Completion string
React.useEffect(() => {
if (!options.inline) return;
if (!options.autoSelect) return;
if (!options.currentValue) return;
if (options.currentId !== getFirstEnabledItemId(options.items)) return;
if (!hasCompletionString(options.inputValue, options.currentValue)) {
return;
}
const element = ref.current;
warning(
!element,
"Can't auto select combobox because `ref` wasn't passed to the component",
"See https://reakit.io/docs/combobox"
);
element?.setSelectionRange(
options.inputValue.length,
options.currentValue.length
);
}, [
updated,
options.inline,
options.autoSelect,
options.currentValue,
options.inputValue,
options.currentId,
options.items,
]);
// Auto select on type
useUpdateEffect(() => {
if (
options.autoSelect &&
options.items.length &&
hasInsertedTextRef.current
) {
// If autoSelect is set to true and the last change was a text
// insertion, we want to automatically focus on the first suggestion.
// This effect will run both when inputValue changes and when items
// change so we can also catch async items.
options.setCurrentId(undefined);
} else {
// Without autoSelect, we'll always blur the combobox option and move
// focus onto the combobox input.
options.setCurrentId(null);
}
}, [
options.items,
options.inputValue,
options.autoSelect,
options.setCurrentId,
]);
const onKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLInputElement>) => {
onKeyDownRef.current?.(event);
// Resets the reference on key down so we can figure it out later on
// key press.
hasInsertedTextRef.current = false;
if (event.defaultPrevented) return;
if (event.key === "Escape" && options.hideOnEsc) {
options.hide?.();
}
},
[options.hideOnEsc, options.hide]
);
const onKeyPress = React.useCallback(
(event: React.KeyboardEvent<HTMLInputElement>) => {
onKeyPressRef.current?.(event);
// onKeyPress will catch only printable character presses, so we skip
// text removal and paste.
hasInsertedTextRef.current = true;
},
[]
);
const onChange = React.useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
onChangeRef.current?.(event);
if (event.defaultPrevented) return;
options.show?.();
options.setInputValue?.(event.target.value);
update();
if (!options.autoSelect || !hasInsertedTextRef.current) {
// If autoSelect is not set or it's not an insertion of text, focus
// on the combobox input after changing the value.
options.setCurrentId?.(null);
} else {
// Selects first item
options.setCurrentId?.(undefined);
}
},
[
options.show,
options.autoSelect,
options.setCurrentId,
options.setInputValue,
]
);
const onClick = React.useCallback(
(event: React.MouseEvent<HTMLInputElement, MouseEvent>) => {
onClickRef.current?.(event);
if (event.defaultPrevented) return;
// https://github.com/reakit/reakit/issues/808
if (!options.minValueLength || value.length >= options.minValueLength) {
options.show?.();
}
options.setCurrentId?.(null);
options.setInputValue(value);
},
[
options.show,
options.setCurrentId,
options.setInputValue,
options.minValueLength,
value,
]
);
const onBlur = React.useCallback(
(event: React.FocusEvent<HTMLInputElement>) => {
onBlurRef.current?.(event);
if (event.defaultPrevented) return;
options.setInputValue(value);
},
[options.setInputValue, value]
);
return {
ref: useForkRef(ref, useForkRef(options.unstable_referenceRef, htmlRef)),
role: "combobox",
autoComplete: "off",
"aria-controls": getControls(options.baseId, ariaControls),
"aria-haspopup": options.menuRole,
"aria-expanded": options.visible,
"aria-autocomplete": getAutocomplete(options),
value,
onKeyDown,
onKeyPress,
onChange,
onClick,
onBlur,
...htmlProps,
};
},
useComposeProps(
options,
{
onKeyUp,
onKeyDownCapture: htmlOnKeyDownCapture,
onKeyDown: htmlOnKeyDown,
...htmlProps
}
) {
const compositeHTMLProps = useComposite(options, htmlProps, true);
const onKeyDownCaptureRef = useLiveRef(htmlOnKeyDownCapture);
const onKeyDownRef = useLiveRef(htmlOnKeyDown);
const onKeyDownCapture = React.useCallback(
(event: React.KeyboardEvent<HTMLInputElement>) => {
onKeyDownCaptureRef.current?.(event);
if (event.defaultPrevented) return;
if (options.menuRole !== "grid") {
// If menu is a one-dimensional list and there's an option with
// focus, we don't want Home/End and printable characters to perform
// actions on the option, only on the combobox input.
if (event.key === "Home") return;
if (event.key === "End") return;
}
if (event.key.length === 1) return;
// Composite's onKeyDownCapture will proxy this event to the active
// item.
compositeHTMLProps.onKeyDownCapture?.(event);
},
[options.menuRole, compositeHTMLProps.onKeyDownCapture]
);
const onKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLInputElement>) => {
onKeyDownRef.current?.(event);
if (event.defaultPrevented) return;
const onlyInputHasFocus = options.currentId === null;
if (!onlyInputHasFocus) return;
// Do not perform list actions when pressing horizontal arrow keys when
// focusing the combobox input while no option has focus.
if (event.key === "ArrowLeft") return;
if (event.key === "ArrowRight") return;
if (event.key === "Home") return;
if (event.key === "End") return;
if (
!event.ctrlKey &&
!event.altKey &&
!event.shiftKey &&
!event.metaKey &&
(event.key === "ArrowUp" ||
event.key === "ArrowDown" ||
event.key.length === 1)
) {
// Up/Down arrow keys and printable characters should open the
// combobox popover.
options.show?.();
}
compositeHTMLProps.onKeyDown?.(event);
},
[options.currentId, options.show, compositeHTMLProps.onKeyDown]
);
return {
...compositeHTMLProps,
onKeyDownCapture,
onKeyDown,
onKeyUp,
};
},
});
export const unstable_Combobox = createComponent({
as: "input",
memo: true,
useHook: unstable_useCombobox,
});
export type unstable_ComboboxOptions = CompositeOptions &
Pick<
Partial<unstable_ComboboxStateReturn>,
| "currentValue"
| "menuRole"
| "list"
| "inline"
| "autoSelect"
| "visible"
| "show"
| "hide"
| "unstable_referenceRef"
| "minValueLength"
> &
Pick<
unstable_ComboboxStateReturn,
"baseId" | "inputValue" | "setInputValue"
> & {
/**
* When enabled, user can hide the combobox popover by pressing
* <kbd>Esc</kbd> while focusing on the combobox input.
* @default true
*/
hideOnEsc?: boolean;
};
export type unstable_ComboboxHTMLProps = CompositeHTMLProps &
React.InputHTMLAttributes<any>;
export type unstable_ComboboxProps = unstable_ComboboxOptions &
unstable_ComboboxHTMLProps;

37
node_modules/reakit/src/Combobox/ComboboxGridCell.ts generated vendored Normal file
View File

@@ -0,0 +1,37 @@
import { createComponent } from "reakit-system/createComponent";
import { createHook } from "reakit-system/createHook";
import {
unstable_GridCellOptions as GridCellOptions,
unstable_GridCellHTMLProps as GridCellHTMLProps,
unstable_useGridCell as useGridCell,
} from "../Grid/GridCell";
import { COMBOBOX_GRID_CELL_KEYS } from "./__keys";
import {
unstable_ComboboxItemOptions as ComboboxItemOptions,
unstable_ComboboxItemHTMLProps as ComboboxItemHTMLProps,
unstable_useComboboxItem as useComboboxItem,
} from "./ComboboxItem";
export const unstable_useComboboxGridCell = createHook<
unstable_ComboboxGridCellOptions,
unstable_ComboboxGridCellHTMLProps
>({
name: "ComboboxGridCell",
compose: [useComboboxItem, useGridCell],
keys: COMBOBOX_GRID_CELL_KEYS,
});
export const unstable_ComboboxGridCell = createComponent({
as: "span",
memo: true,
useHook: unstable_useComboboxGridCell,
});
export type unstable_ComboboxGridCellOptions = GridCellOptions &
ComboboxItemOptions;
export type unstable_ComboboxGridCellHTMLProps = GridCellHTMLProps &
ComboboxItemHTMLProps;
export type unstable_ComboboxGridCellProps = unstable_ComboboxGridCellOptions &
unstable_ComboboxGridCellHTMLProps;

29
node_modules/reakit/src/Combobox/ComboboxGridRow.ts generated vendored Normal file
View File

@@ -0,0 +1,29 @@
import { createComponent } from "reakit-system/createComponent";
import { createHook } from "reakit-system/createHook";
import {
unstable_GridRowOptions as GridRowOptions,
unstable_GridRowHTMLProps as GridRowHTMLProps,
unstable_useGridRow as useGridRow,
} from "../Grid/GridRow";
import { COMBOBOX_GRID_ROW_KEYS } from "./__keys";
export const unstable_useComboboxGridRow = createHook<
unstable_ComboboxGridRowOptions,
unstable_ComboboxGridRowHTMLProps
>({
name: "ComboboxGridRow",
compose: useGridRow,
keys: COMBOBOX_GRID_ROW_KEYS,
});
export const unstable_ComboboxGridRow = createComponent({
as: "div",
useHook: unstable_useComboboxGridRow,
});
export type unstable_ComboboxGridRowOptions = GridRowOptions;
export type unstable_ComboboxGridRowHTMLProps = GridRowHTMLProps;
export type unstable_ComboboxGridRowProps = unstable_ComboboxGridRowOptions &
unstable_ComboboxGridRowHTMLProps;

36
node_modules/reakit/src/Combobox/ComboboxGridState.ts generated vendored Normal file
View File

@@ -0,0 +1,36 @@
import {
SealedInitialState,
useSealedState,
} from "reakit-utils/useSealedState";
import {
unstable_ComboboxListGridState as ComboboxListGridState,
unstable_ComboboxListGridActions as ComboboxListGridActions,
unstable_ComboboxListGridInitialState as ComboboxListGridInitialState,
unstable_useComboboxListGridState as useComboboxListGridState,
} from "./ComboboxListGridState";
import {
ComboboxPopoverState,
ComboboxPopoverActions,
ComboboxPopoverInitialState,
useComboboxPopoverState,
} from "./__utils/ComboboxPopoverState";
export function unstable_useComboboxGridState(
initialState: SealedInitialState<unstable_ComboboxGridInitialState> = {}
): unstable_ComboboxGridStateReturn {
const sealed = useSealedState(initialState);
const combobox = useComboboxListGridState(sealed);
return useComboboxPopoverState(combobox, sealed);
}
export type unstable_ComboboxGridState = ComboboxPopoverState &
ComboboxListGridState;
export type unstable_ComboboxGridActions = ComboboxPopoverActions &
ComboboxListGridActions;
export type unstable_ComboboxGridInitialState = ComboboxPopoverInitialState &
ComboboxListGridInitialState;
export type unstable_ComboboxGridStateReturn = unstable_ComboboxGridState &
unstable_ComboboxGridActions;

125
node_modules/reakit/src/Combobox/ComboboxItem.ts generated vendored Normal file
View File

@@ -0,0 +1,125 @@
import * as React from "react";
import { createComponent } from "reakit-system/createComponent";
import { createHook } from "reakit-system/createHook";
import { useLiveRef } from "reakit-utils/useLiveRef";
import { BoxOptions, BoxHTMLProps, useBox } from "../Box/Box";
import {
CompositeItemOptions,
CompositeItemHTMLProps,
useCompositeItem,
} from "../Composite/CompositeItem";
import { unstable_ComboboxStateReturn } from "./ComboboxState";
import { COMBOBOX_ITEM_KEYS } from "./__keys";
import { getItemId } from "./__utils/getItemId";
import { Item } from "./__utils/types";
export const unstable_useComboboxItem = createHook<
unstable_ComboboxItemOptions,
unstable_ComboboxItemHTMLProps
>({
name: "ComboboxItem",
compose: useBox,
keys: COMBOBOX_ITEM_KEYS,
propsAreEqual(prev, next) {
if (prev.value !== next.value) return false;
if (!prev.value || !next.value || !prev.baseId || !next.baseId) {
return useCompositeItem.unstable_propsAreEqual(prev, next);
}
const {
currentValue: prevCurrentValue,
inputValue: prevInputValue,
// @ts-ignore
matches: prevMatches,
...prevProps
} = prev;
const {
currentValue: nextCurrentValue,
inputValue: nextInputValue,
// @ts-ignore
matches: nextMatches,
...nextProps
} = next;
if (prevCurrentValue !== nextCurrentValue) {
if (next.value === prevCurrentValue || next.value === nextCurrentValue) {
return false;
}
}
const prevId = getItemId(prev.baseId, prev.value, prev.id);
const nextId = getItemId(next.baseId, next.value, prev.id);
return useCompositeItem.unstable_propsAreEqual(
{ ...prevProps, id: prevId },
{ ...nextProps, id: nextId }
);
},
useOptions(options) {
const trulyDisabled = options.disabled && !options.focusable;
const value = trulyDisabled ? undefined : options.value;
const registerItem = React.useCallback(
(item: Item) => {
if (options.visible) {
options.registerItem?.({ ...item, value });
}
},
[options.registerItem, options.visible, value]
);
if (options.id || !options.baseId || !options.value) {
return { ...options, registerItem };
}
const id = getItemId(options.baseId, options.value, options.id);
return { ...options, registerItem, id };
},
useProps(options, { onClick: htmlOnClick, ...htmlProps }) {
const onClickRef = useLiveRef(htmlOnClick);
const onClick = React.useCallback(
(event: React.MouseEvent<HTMLElement, MouseEvent>) => {
onClickRef.current?.(event);
if (event.defaultPrevented) return;
if (!options.value) return;
options.hide?.();
options.setInputValue?.(options.value);
},
[options.hide, options.setInputValue, options.value]
);
return {
children: options.value,
onClick,
tabIndex: -1,
...htmlProps,
};
},
});
export const unstable_ComboboxItem = createComponent({
as: "span",
memo: true,
useHook: unstable_useComboboxItem,
});
export type unstable_ComboboxItemOptions = BoxOptions &
CompositeItemOptions &
Pick<
Partial<unstable_ComboboxStateReturn>,
"currentValue" | "inputValue" | "hide" | "visible"
> &
Pick<unstable_ComboboxStateReturn, "setInputValue" | "registerItem"> & {
/**
* Item's value that will be used to fill input value and filter `matches`
* based on the input value. You can omit this for items that perform
* actions other than filling a form. For example, items may open a dialog.
*/
value?: string;
};
export type unstable_ComboboxItemHTMLProps = BoxHTMLProps &
CompositeItemHTMLProps;
export type unstable_ComboboxItemProps = unstable_ComboboxItemOptions &
unstable_ComboboxItemHTMLProps;

51
node_modules/reakit/src/Combobox/ComboboxList.ts generated vendored Normal file
View File

@@ -0,0 +1,51 @@
import { createComponent } from "reakit-system/createComponent";
import { createHook } from "reakit-system/createHook";
import { useWarning } from "reakit-warning";
import { useCreateElement } from "reakit-system/useCreateElement";
import { BoxOptions, BoxHTMLProps, useBox } from "../Box/Box";
import { getMenuId } from "./__utils/getMenuId";
import { unstable_ComboboxStateReturn } from "./ComboboxState";
import { COMBOBOX_LIST_KEYS } from "./__keys";
export const unstable_useComboboxList = createHook<
unstable_ComboboxListOptions,
unstable_ComboboxListHTMLProps
>({
name: "ComboboxList",
compose: useBox,
keys: COMBOBOX_LIST_KEYS,
useOptions({ menuRole = "listbox", ...options }) {
return { menuRole, ...options };
},
useProps(options, htmlProps) {
return {
role: options.menuRole,
id: getMenuId(options.baseId),
...htmlProps,
};
},
});
export const unstable_ComboboxList = createComponent({
as: "div",
useHook: unstable_useComboboxList,
useCreateElement: (type, props, children) => {
useWarning(
!props["aria-label"] && !props["aria-labelledby"],
"You should provide either `aria-label` or `aria-labelledby` props.",
"See https://reakit.io/docs/combobox"
);
return useCreateElement(type, props, children);
},
});
export type unstable_ComboboxListOptions = BoxOptions &
Pick<Partial<unstable_ComboboxStateReturn>, "menuRole"> &
Pick<unstable_ComboboxStateReturn, "baseId">;
export type unstable_ComboboxListHTMLProps = BoxHTMLProps;
export type unstable_ComboboxListProps = unstable_ComboboxListOptions &
unstable_ComboboxListHTMLProps;

View File

@@ -0,0 +1,103 @@
import * as React from "react";
import {
SealedInitialState,
useSealedState,
} from "reakit-utils/useSealedState";
import { SetState } from "reakit-utils/types";
import {
unstable_useGridState as useGridState,
unstable_GridState as GridState,
unstable_GridActions as GridActions,
unstable_GridInitialState as GridInitialState,
} from "../Grid/GridState";
import {
useComboboxBaseState,
ComboboxBaseState,
ComboboxBaseActions,
ComboboxBaseInitialState,
} from "./__utils/ComboboxBaseState";
function chunk<T>(array: T[], size: number) {
const chunks: T[][] = [];
for (let i = 0, j = array.length; i < j; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
}
export function unstable_useComboboxListGridState(
initialState: SealedInitialState<unstable_ComboboxListGridInitialState> = {}
): unstable_ComboboxListGridStateReturn {
const {
columns: initialColumns = 1,
currentId = null,
loop = true,
...sealed
} = useSealedState(initialState);
const [columns, setColumns] = React.useState(initialColumns);
const grid = useGridState({
currentId,
loop,
...sealed,
unstable_virtual: true,
unstable_includesBaseElement: true,
});
const combobox = useComboboxBaseState(grid, sealed);
const matches = React.useMemo(() => chunk(combobox.matches, columns), [
combobox.matches,
columns,
]);
return {
...combobox,
menuRole: "grid",
columns,
matches,
setColumns,
};
}
export type unstable_ComboboxListGridState = Omit<
ComboboxBaseState<GridState>,
"matches"
> & {
/**
* Number of columns by which `values` will be splitted to generate the
* `matches` 2D array.
*/
columns: number;
/**
* Result of filtering `values` based on `inputValue`.
* @default []
* @example
* const combobox = useComboboxState({
* values: ["Red", "Green", "Blue"],
* columns: 2,
* });
* combobox.matches; // [["Red", "Green"], ["Blue"]]
* combobox.setInputValue("g");
* // On next render
* combobox.matches; // [["Green"]]
*/
matches: string[][];
};
export type unstable_ComboboxListGridActions = ComboboxBaseActions<GridActions> & {
/**
* Sets `columns`.
*/
setColumns: SetState<unstable_ComboboxListGridState["columns"]>;
};
export type unstable_ComboboxListGridInitialState = Omit<
GridInitialState,
"unstable_virtual" | "unstable_includesBaseElement"
> &
ComboboxBaseInitialState &
Pick<Partial<unstable_ComboboxListGridState>, "columns">;
export type unstable_ComboboxListGridStateReturn = unstable_ComboboxListGridState &
unstable_ComboboxListGridActions;

51
node_modules/reakit/src/Combobox/ComboboxListState.ts generated vendored Normal file
View File

@@ -0,0 +1,51 @@
import {
SealedInitialState,
useSealedState,
} from "reakit-utils/useSealedState";
import {
useCompositeState,
CompositeState,
CompositeActions,
CompositeInitialState,
} from "../Composite/CompositeState";
import {
ComboboxBaseState,
ComboboxBaseActions,
ComboboxBaseInitialState,
useComboboxBaseState,
} from "./__utils/ComboboxBaseState";
export function unstable_useComboboxListState(
initialState: SealedInitialState<unstable_ComboboxListInitialState> = {}
): unstable_ComboboxListStateReturn {
const {
currentId = null,
orientation = "vertical",
loop = true,
...sealed
} = useSealedState(initialState);
const composite = useCompositeState({
currentId,
orientation,
loop,
...sealed,
unstable_virtual: true,
unstable_includesBaseElement: true,
});
return useComboboxBaseState(composite, sealed);
}
export type unstable_ComboboxListState = ComboboxBaseState<CompositeState>;
export type unstable_ComboboxListActions = ComboboxBaseActions<CompositeActions>;
export type unstable_ComboboxListInitialState = Omit<
CompositeInitialState,
"unstable_virtual" | "unstable_includesBaseElement"
> &
ComboboxBaseInitialState;
export type unstable_ComboboxListStateReturn = unstable_ComboboxListState &
unstable_ComboboxListActions;

41
node_modules/reakit/src/Combobox/ComboboxOption.ts generated vendored Normal file
View File

@@ -0,0 +1,41 @@
import { createComponent } from "reakit-system/createComponent";
import { createHook } from "reakit-system/createHook";
import {
CompositeItemOptions,
CompositeItemHTMLProps,
useCompositeItem,
} from "../Composite/CompositeItem";
import { COMBOBOX_OPTION_KEYS } from "./__keys";
import {
unstable_ComboboxItemOptions as ComboboxItemOptions,
unstable_ComboboxItemHTMLProps as ComboboxItemHTMLProps,
unstable_useComboboxItem as useComboboxItem,
} from "./ComboboxItem";
export const unstable_useComboboxOption = createHook<
unstable_ComboboxOptionOptions,
unstable_ComboboxOptionHTMLProps
>({
name: "ComboboxOption",
compose: [useComboboxItem, useCompositeItem],
keys: COMBOBOX_OPTION_KEYS,
useProps(_, htmlProps) {
return { role: "option", ...htmlProps };
},
});
export const unstable_ComboboxOption = createComponent({
as: "div",
memo: true,
useHook: unstable_useComboboxOption,
});
export type unstable_ComboboxOptionOptions = CompositeItemOptions &
ComboboxItemOptions;
export type unstable_ComboboxOptionHTMLProps = CompositeItemHTMLProps &
ComboboxItemHTMLProps;
export type unstable_ComboboxOptionProps = unstable_ComboboxOptionOptions &
unstable_ComboboxOptionHTMLProps;

71
node_modules/reakit/src/Combobox/ComboboxPopover.ts generated vendored Normal file
View File

@@ -0,0 +1,71 @@
import { createComponent } from "reakit-system/createComponent";
import { createHook } from "reakit-system/createHook";
import { useWarning } from "reakit-warning";
import { useCreateElement } from "reakit-system/useCreateElement";
import {
PopoverOptions,
PopoverHTMLProps,
usePopover,
} from "../Popover/Popover";
import { COMBOBOX_POPOVER_KEYS } from "./__keys";
import {
unstable_ComboboxListOptions as ComboboxListOptions,
unstable_ComboboxListHTMLProps as ComboboxListHTMLProps,
unstable_useComboboxList as useComboboxList,
} from "./ComboboxList";
import { ComboboxPopoverStateReturn } from "./__utils/ComboboxPopoverState";
export const unstable_useComboboxPopover = createHook<
unstable_ComboboxPopoverOptions,
unstable_ComboboxPopoverHTMLProps
>({
name: "ComboboxPopover",
compose: [useComboboxList, usePopover],
keys: COMBOBOX_POPOVER_KEYS,
useOptions(options) {
return {
...options,
unstable_disclosureRef: options.unstable_referenceRef,
unstable_autoFocusOnShow: false,
unstable_autoFocusOnHide: false,
};
},
useComposeProps(options, { tabIndex, ...htmlProps }) {
htmlProps = useComboboxList(options, htmlProps, true);
htmlProps = usePopover(options, htmlProps, true);
return {
...htmlProps,
tabIndex: tabIndex ?? undefined,
};
},
});
export const unstable_ComboboxPopover = createComponent({
as: "div",
useHook: unstable_useComboboxPopover,
useCreateElement: (type, props, children) => {
useWarning(
!props["aria-label"] && !props["aria-labelledby"],
"You should provide either `aria-label` or `aria-labelledby` props.",
"See https://reakit.io/docs/combobox"
);
return useCreateElement(type, props, children);
},
});
export type unstable_ComboboxPopoverOptions = ComboboxListOptions &
Omit<
PopoverOptions,
| "unstable_disclosureRef"
| "unstable_autoFocusOnHide"
| "unstable_autoFocusOnShow"
> &
Pick<Partial<ComboboxPopoverStateReturn>, "unstable_referenceRef">;
export type unstable_ComboboxPopoverHTMLProps = PopoverHTMLProps &
ComboboxListHTMLProps;
export type unstable_ComboboxPopoverProps = unstable_ComboboxPopoverOptions &
unstable_ComboboxPopoverHTMLProps;

35
node_modules/reakit/src/Combobox/ComboboxState.ts generated vendored Normal file
View File

@@ -0,0 +1,35 @@
import {
SealedInitialState,
useSealedState,
} from "reakit-utils/useSealedState";
import {
unstable_ComboboxListState as ComboboxListState,
unstable_ComboboxListActions as ComboboxListActions,
unstable_ComboboxListInitialState as ComboboxListInitialState,
unstable_useComboboxListState as useComboboxListState,
} from "./ComboboxListState";
import {
ComboboxPopoverState,
ComboboxPopoverActions,
ComboboxPopoverInitialState,
useComboboxPopoverState,
} from "./__utils/ComboboxPopoverState";
export function unstable_useComboboxState(
initialState: SealedInitialState<unstable_ComboboxInitialState> = {}
): unstable_ComboboxStateReturn {
const sealed = useSealedState(initialState);
const combobox = useComboboxListState(sealed);
return useComboboxPopoverState(combobox, sealed);
}
export type unstable_ComboboxState = ComboboxPopoverState & ComboboxListState;
export type unstable_ComboboxActions = ComboboxPopoverActions &
ComboboxListActions;
export type unstable_ComboboxInitialState = ComboboxPopoverInitialState &
ComboboxListInitialState;
export type unstable_ComboboxStateReturn = unstable_ComboboxState &
unstable_ComboboxActions;

1573
node_modules/reakit/src/Combobox/README.md generated vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,236 @@
import * as React from "react";
import { render, press, click, type, screen } from "reakit-test-utils";
import AccessibleCombobox from "..";
test("open combobox popover on click", () => {
render(<AccessibleCombobox />);
expect(screen.getByLabelText("Fruits")).not.toBeVisible();
click(screen.getByLabelText("Fruit"));
expect(screen.getByLabelText("Fruits")).toBeVisible();
expect(screen.getByText("Apple")).not.toHaveFocus();
});
test("open combobox popover on arrow down", () => {
render(<AccessibleCombobox />);
press.Tab();
expect(screen.getByLabelText("Fruit")).toHaveFocus();
expect(screen.getByLabelText("Fruits")).not.toBeVisible();
press.ArrowDown();
expect(screen.getByLabelText("Fruits")).toBeVisible();
expect(screen.getByText("Apple")).not.toHaveFocus();
});
test("open combobox popover on arrow up", () => {
render(<AccessibleCombobox />);
press.Tab();
expect(screen.getByLabelText("Fruit")).toHaveFocus();
expect(screen.getByLabelText("Fruits")).not.toBeVisible();
press.ArrowUp();
expect(screen.getByLabelText("Fruits")).toBeVisible();
expect(screen.getByText("Banana")).not.toHaveFocus();
});
test("open combobox popover by typing on the combobox", () => {
render(<AccessibleCombobox />);
press.Tab();
expect(screen.getByLabelText("Fruit")).toHaveFocus();
expect(screen.getByLabelText("Fruits")).not.toBeVisible();
type("a");
expect(screen.getByLabelText("Fruits")).toBeVisible();
expect(screen.getByText("Apple")).not.toHaveFocus();
});
test("do not open combobox popover on arrow right/left", () => {
render(<AccessibleCombobox />);
press.Tab();
expect(screen.getByLabelText("Fruit")).toHaveFocus();
expect(screen.getByLabelText("Fruits")).not.toBeVisible();
press.ArrowLeft();
expect(screen.getByLabelText("Fruits")).not.toBeVisible();
press.ArrowRight();
expect(screen.getByLabelText("Fruits")).not.toBeVisible();
});
test("do not open combobox popover on backspace on empty input", () => {
render(<AccessibleCombobox />);
press.Tab();
expect(screen.getByLabelText("Fruit")).toHaveFocus();
expect(screen.getByLabelText("Fruits")).not.toBeVisible();
type("\b");
expect(screen.getByLabelText("Fruits")).not.toBeVisible();
});
test("close combobox popover by clicking outside", () => {
const { baseElement } = render(<AccessibleCombobox />);
expect(screen.getByLabelText("Fruits")).not.toBeVisible();
click(screen.getByLabelText("Fruit"));
expect(screen.getByLabelText("Fruits")).toBeVisible();
click(baseElement);
expect(screen.getByLabelText("Fruits")).not.toBeVisible();
});
test("close combobox popover by tabbing out", () => {
render(
<>
<AccessibleCombobox />
<button>button</button>
</>
);
expect(screen.getByLabelText("Fruits")).not.toBeVisible();
click(screen.getByLabelText("Fruit"));
expect(screen.getByLabelText("Fruits")).toBeVisible();
press.ArrowDown();
press.Tab();
expect(screen.getByLabelText("Fruits")).not.toBeVisible();
expect(screen.getByText("button")).toHaveFocus();
});
test("close combobox popover by pressing esc", () => {
render(<AccessibleCombobox />);
expect(screen.getByLabelText("Fruits")).not.toBeVisible();
click(screen.getByLabelText("Fruit"));
expect(screen.getByLabelText("Fruits")).toBeVisible();
press.Escape();
expect(screen.getByLabelText("Fruits")).not.toBeVisible();
expect(screen.getByLabelText("Fruit")).toHaveFocus();
});
test("open combobox popover after pressing esc", () => {
render(<AccessibleCombobox />);
expect(screen.getByLabelText("Fruits")).not.toBeVisible();
click(screen.getByLabelText("Fruit"));
expect(screen.getByLabelText("Fruits")).toBeVisible();
press.Escape();
expect(screen.getByLabelText("Fruits")).not.toBeVisible();
press.ArrowDown();
expect(screen.getByLabelText("Fruits")).toBeVisible();
press.Escape();
expect(screen.getByLabelText("Fruits")).not.toBeVisible();
press.ArrowUp();
expect(screen.getByLabelText("Fruits")).toBeVisible();
press.Escape();
expect(screen.getByLabelText("Fruits")).not.toBeVisible();
type("a");
expect(screen.getByLabelText("Fruits")).toBeVisible();
press.Escape();
expect(screen.getByLabelText("Fruits")).not.toBeVisible();
press.ArrowDown();
expect(screen.getByLabelText("Fruits")).toBeVisible();
press.Escape();
expect(screen.getByLabelText("Fruits")).not.toBeVisible();
press.ArrowDown();
expect(screen.getByLabelText("Fruits")).toBeVisible();
press.Escape();
expect(screen.getByLabelText("Fruits")).not.toBeVisible();
type("\b");
expect(screen.getByLabelText("Fruits")).toBeVisible();
});
test("open combobox popover after pressing esc twice", () => {
render(<AccessibleCombobox />);
expect(screen.getByLabelText("Fruits")).not.toBeVisible();
click(screen.getByLabelText("Fruit"));
expect(screen.getByLabelText("Fruits")).toBeVisible();
press.Escape();
press.Escape();
expect(screen.getByLabelText("Fruits")).not.toBeVisible();
press.ArrowDown();
expect(screen.getByLabelText("Fruits")).toBeVisible();
});
test("move through combobox options with keyboard", () => {
render(<AccessibleCombobox />);
click(screen.getByLabelText("Fruit"));
press.ArrowDown();
expect(screen.getByLabelText("Fruit")).toHaveFocus();
expect(screen.getByText("Apple")).toHaveFocus();
press.ArrowDown();
expect(screen.getByText("Orange")).toHaveFocus();
press.ArrowDown();
expect(screen.getByText("Banana")).toHaveFocus();
press.ArrowDown();
expect(screen.getByLabelText("Fruit")).toHaveFocus();
expect(screen.getByText("Apple")).not.toHaveFocus();
expect(screen.getByText("Orange")).not.toHaveFocus();
expect(screen.getByText("Banana")).not.toHaveFocus();
press.ArrowUp();
expect(screen.getByText("Banana")).toHaveFocus();
press.ArrowUp();
expect(screen.getByText("Orange")).toHaveFocus();
press.ArrowUp();
expect(screen.getByText("Apple")).toHaveFocus();
press.ArrowUp();
expect(screen.getByLabelText("Fruit")).toHaveFocus();
expect(screen.getByText("Apple")).not.toHaveFocus();
expect(screen.getByText("Orange")).not.toHaveFocus();
expect(screen.getByText("Banana")).not.toHaveFocus();
press.ArrowUp();
expect(screen.getByText("Banana")).toHaveFocus();
press.ArrowRight();
expect(screen.getByText("Banana")).toHaveFocus();
press.ArrowLeft();
expect(screen.getByText("Banana")).toHaveFocus();
press.Home();
expect(screen.getByText("Banana")).toHaveFocus();
press.End();
expect(screen.getByText("Banana")).toHaveFocus();
});
test("select combobox option by clicking on it", () => {
render(<AccessibleCombobox />);
click(screen.getByLabelText("Fruit"));
expect(screen.getByLabelText("Fruit")).toHaveValue("");
click(screen.getByText("Orange"));
expect(screen.getByLabelText("Fruit")).toHaveValue("Orange");
expect(screen.getByLabelText("Fruits")).not.toBeVisible();
type("\b\b\b\b\b\ba");
expect(screen.getByLabelText("Fruit")).toHaveValue("a");
click(screen.getByText("Apple"));
expect(screen.getByLabelText("Fruit")).toHaveValue("Apple");
});
test("select combobox option by pressing enter on it", () => {
render(<AccessibleCombobox />);
click(screen.getByLabelText("Fruit"));
expect(screen.getByLabelText("Fruit")).toHaveValue("");
press.ArrowUp();
press.Enter();
expect(screen.getByLabelText("Fruit")).toHaveValue("Banana");
expect(screen.getByLabelText("Fruits")).not.toBeVisible();
type("\b\b\b\b\b\ba");
expect(screen.getByLabelText("Fruit")).toHaveValue("a");
press.ArrowDown();
press.Enter();
expect(screen.getByLabelText("Fruit")).toHaveValue("Apple");
});
test("do not select combobox option by pressing space on it", () => {
render(<AccessibleCombobox />);
click(screen.getByLabelText("Fruit"));
expect(screen.getByLabelText("Fruit")).toHaveValue("");
press.ArrowDown();
press.Space();
expect(screen.getByLabelText("Fruit")).toHaveValue("");
expect(screen.getByLabelText("Fruits")).toBeVisible();
});
test("unselect combobox option when typing on the combobox", () => {
render(<AccessibleCombobox />);
click(screen.getByLabelText("Fruit"));
expect(screen.getByLabelText("Fruit")).toHaveValue("");
press.ArrowDown();
expect(screen.getByText("Apple")).toHaveFocus();
type("a");
expect(screen.getByText("Apple")).not.toHaveFocus();
press.ArrowDown();
expect(screen.getByText("Apple")).toHaveFocus();
});
test("clicking on combobox input unselects combobox option", () => {
render(<AccessibleCombobox />);
click(screen.getByLabelText("Fruit"));
press.ArrowDown();
expect(screen.getByText("Apple")).toHaveFocus();
click(screen.getByLabelText("Fruit"));
expect(screen.getByText("Apple")).not.toHaveFocus();
});

View File

@@ -0,0 +1,23 @@
import * as React from "react";
import {
unstable_useComboboxState as useComboboxState,
unstable_Combobox as Combobox,
unstable_ComboboxPopover as ComboboxPopover,
unstable_ComboboxOption as ComboboxOption,
} from "reakit/Combobox";
import "./style.css";
export default function AccessibleCombobox() {
const combobox = useComboboxState({ gutter: 8 });
return (
<>
<Combobox {...combobox} aria-label="Fruit" />
<ComboboxPopover {...combobox} aria-label="Fruits">
<ComboboxOption {...combobox} value="Apple" />
<ComboboxOption {...combobox} value="Orange" />
<ComboboxOption {...combobox} value="Banana" />
</ComboboxPopover>
</>
);
}

View File

@@ -0,0 +1,72 @@
:root {
--font-family: var(--font-family-body, sans-serif);
--font-size: var(--font-size-body, 16px);
--combobox-background: var(--color-background-50, #fff);
--combobox-color: var(--color-grayscale-700, #333);
--combobox-border: var(--color-alpha-500, rgba(0, 0, 0, 0.5));
--combobox-border-focus: var(--color-primary-700, #1976d2);
--listbox-background: var(--color-background-200, #fff);
--listbox-color: var(--color-grayscale-700, #333);
--listbox-shadow-50: var(--color-shadow-50, rgba(0, 0, 0, 0.05));
--listbox-shadow-100: var(--color-shadow-50, rgba(0, 0, 0, 0.1));
--option-background-hover: var(--color-primary-50, #e3f2fd);
--option-background-focus: var(--color-primary-100, #bbdefb);
}
[role="combobox"] {
font-family: var(--font-family);
font-size: var(--font-size);
background-color: var(--combobox-background);
color: var(--combobox-color);
border: 1px solid var(--combobox-border);
border-radius: 4px;
height: 2.5em;
padding: 0 1em;
outline: 0;
width: 250px;
box-sizing: border-box;
}
[role="combobox"]:focus {
border-color: var(--combobox-border-focus);
box-shadow: 0 0 0 1px var(--combobox-border-focus);
}
[role="listbox"] {
font-family: var(--font-family);
font-size: var(--font-size);
background-color: var(--listbox-background);
color: var(--listbox-color);
width: 250px;
z-index: 999;
padding: 1em;
box-sizing: border-box;
border-radius: 4px;
box-shadow:
0 0 8px var(--listbox-shadow-50),
0 10px 10px -5px var(--listbox-shadow-50),
0 20px 25px -5px var(--listbox-shadow-100);
}
[role="option"] {
cursor: default;
padding: 0.5em;
margin: 0 -0.5em;
border-radius: 4px;
}
[role="option"]:first-child {
margin-top: -0.5em;
}
[role="option"]:last-child {
margin-bottom: -0.5em;
}
[role="option"]:hover {
background-color: var(--option-background-hover);
}
[role="combobox"]:focus + [role="listbox"] [aria-selected="true"] {
background-color: var(--option-background-focus);
}

View File

@@ -0,0 +1,71 @@
import * as React from "react";
import { render, press, click, type, screen } from "reakit-test-utils";
import ComboboxAutoSelect from "..";
test("open combobox popover on click and do not auto select", () => {
render(<ComboboxAutoSelect />);
expect(screen.getByLabelText("Fruits")).not.toBeVisible();
click(screen.getByLabelText("Fruit"));
expect(screen.getByLabelText("Fruits")).toBeVisible();
expect(screen.getByText("Apple")).not.toHaveFocus();
});
test("open combobox popover on arrow down and do not auto select", () => {
render(<ComboboxAutoSelect />);
press.Tab();
expect(screen.getByLabelText("Fruits")).not.toBeVisible();
press.ArrowDown();
expect(screen.getByLabelText("Fruits")).toBeVisible();
expect(screen.getByText("Apple")).not.toHaveFocus();
});
test("open combobox popover on arrow up and do not auto select", () => {
render(<ComboboxAutoSelect />);
press.Tab();
expect(screen.getByLabelText("Fruits")).not.toBeVisible();
press.ArrowUp();
expect(screen.getByLabelText("Fruits")).toBeVisible();
expect(screen.getByText("Apple")).not.toHaveFocus();
});
test("open combobox popover on type and auto select", () => {
render(<ComboboxAutoSelect />);
press.Tab();
expect(screen.getByLabelText("Fruits")).not.toBeVisible();
type("a");
expect(screen.getByLabelText("Fruits")).toBeVisible();
expect(screen.getByText("Apple")).toHaveFocus();
type("\b");
expect(screen.getByText("Apple")).not.toHaveFocus();
});
test("open combobox popover after pressing esc", () => {
render(<ComboboxAutoSelect />);
expect(screen.getByLabelText("Fruits")).not.toBeVisible();
click(screen.getByLabelText("Fruit"));
expect(screen.getByLabelText("Fruits")).toBeVisible();
press.Escape();
expect(screen.getByLabelText("Fruits")).not.toBeVisible();
press.ArrowDown();
expect(screen.getByLabelText("Fruits")).toBeVisible();
press.Escape();
expect(screen.getByLabelText("Fruits")).not.toBeVisible();
press.ArrowUp();
expect(screen.getByLabelText("Fruits")).toBeVisible();
press.Escape();
expect(screen.getByLabelText("Fruits")).not.toBeVisible();
type("a");
expect(screen.getByLabelText("Fruits")).toBeVisible();
press.Escape();
expect(screen.getByLabelText("Fruits")).not.toBeVisible();
press.ArrowDown();
expect(screen.getByLabelText("Fruits")).toBeVisible();
press.Escape();
expect(screen.getByLabelText("Fruits")).not.toBeVisible();
press.ArrowDown();
expect(screen.getByLabelText("Fruits")).toBeVisible();
press.Escape();
expect(screen.getByLabelText("Fruits")).not.toBeVisible();
type("\b");
expect(screen.getByLabelText("Fruits")).toBeVisible();
});

View File

@@ -0,0 +1,23 @@
import * as React from "react";
import {
unstable_useComboboxState as useComboboxState,
unstable_Combobox as Combobox,
unstable_ComboboxPopover as ComboboxPopover,
unstable_ComboboxOption as ComboboxOption,
} from "reakit/Combobox";
import "./style.css";
export default function ComboboxAutoSelect() {
const combobox = useComboboxState({ autoSelect: true, gutter: 8 });
return (
<>
<Combobox {...combobox} aria-label="Fruit" />
<ComboboxPopover {...combobox} aria-label="Fruits">
<ComboboxOption {...combobox} value="Apple" />
<ComboboxOption {...combobox} value="Orange" />
<ComboboxOption {...combobox} value="Banana" />
</ComboboxPopover>
</>
);
}

View File

@@ -0,0 +1 @@
@import "../AccessibleCombobox/style.css";

View File

@@ -0,0 +1,50 @@
import * as React from "react";
import { render, press, type, screen } from "reakit-test-utils";
import ComboboxBothAutoSelect from "..";
function expectSelectionValue(element: Element, value: string) {
const input = element as HTMLInputElement;
const { selectionStart, selectionEnd } = input;
const selectionValue = input.value.slice(selectionStart!, selectionEnd!);
expect(selectionValue).toBe(value);
}
function getComboboxInput() {
return screen.getByLabelText("Fruit") as HTMLInputElement;
}
test("auto select combobox option", async () => {
render(<ComboboxBothAutoSelect />);
press.Tab();
type("car");
expect(getComboboxInput()).toHaveValue("carambola");
expectSelectionValue(getComboboxInput(), "ambola");
});
test("typing banana and then b", () => {
render(<ComboboxBothAutoSelect />);
press.Tab();
type("b");
expect(getComboboxInput()).toHaveValue("banana");
expectSelectionValue(getComboboxInput(), "anana");
getComboboxInput().setSelectionRange(0, 6);
expectSelectionValue(getComboboxInput(), "banana");
type("b");
expect(getComboboxInput()).toHaveValue("banana");
expectSelectionValue(getComboboxInput(), "anana");
});
test("typing g, moving to the third option and then replacing with b", () => {
render(<ComboboxBothAutoSelect />);
press.Tab();
type("g");
expect(getComboboxInput()).toHaveValue("gooseberries");
expectSelectionValue(getComboboxInput(), "ooseberries");
press.ArrowDown();
press.ArrowDown();
expect(getComboboxInput()).toHaveValue("Grapefruit");
getComboboxInput().setSelectionRange(0, 10);
type("b");
expect(getComboboxInput()).toHaveValue("banana");
expectSelectionValue(getComboboxInput(), "anana");
});

View File

@@ -0,0 +1,71 @@
export const fruits = [
"Acerola",
"Apple",
"Apricots",
"Avocado",
"Banana",
"Blackberries",
"Blackcurrant",
"Blueberries",
"Breadfruit",
"Cantaloupe",
"Carambola",
"Cherimoya",
"Cherries",
"Clementine",
"Coconut Meat",
"Cranberries",
"Custard-Apple",
"Date Fruit",
"Durian",
"Elderberries",
"Feijoa",
"Figs",
"Gooseberries",
"Grapefruit",
"Grapes",
"Guava",
"Honeydew Melon",
"Jackfruit",
"Java-Plum",
"Jujube Fruit",
"Kiwifruit",
"Kumquat",
"Lemon",
"Lime",
"Longan",
"Loquat",
"Lychee",
"Mandarin",
"Mango",
"Mangosteen",
"Mulberries",
"Nectarine",
"Olives",
"Orange",
"Papaya",
"Passion Fruit",
"Peaches",
"Pear",
"Persimmon",
"Pitaya",
"Pineapple",
"Pitanga",
"Plantain",
"Plums",
"Pomegranate",
"Prickly Pear",
"Prunes",
"Pummelo",
"Quince",
"Raspberries",
"Rhubarb",
"Rose-Apple",
"Sapodilla",
"Soursop",
"Strawberries",
"Sugar-Apple",
"Tamarind",
"Tangerine",
"Watermelon",
];

View File

@@ -0,0 +1,31 @@
import * as React from "react";
import {
unstable_useComboboxState as useComboboxState,
unstable_Combobox as Combobox,
unstable_ComboboxPopover as ComboboxPopover,
unstable_ComboboxOption as ComboboxOption,
} from "reakit/Combobox";
import { fruits } from "./fruits";
import "./style.css";
export default function ComboboxBothAutoSelect() {
const combobox = useComboboxState({
values: fruits,
inline: true,
autoSelect: true,
gutter: 8,
});
return (
<>
<Combobox {...combobox} aria-label="Fruit" placeholder="Enter a fruit" />
<ComboboxPopover {...combobox} aria-label="Fruits">
{combobox.matches.length
? combobox.matches.map((value) => (
<ComboboxOption {...combobox} key={value} value={value} />
))
: "No results found"}
</ComboboxPopover>
</>
);
}

View File

@@ -0,0 +1 @@
@import "../AccessibleCombobox/style.css";

View File

@@ -0,0 +1,42 @@
import * as React from "react";
import { render, press, focus, click, type, screen } from "reakit-test-utils";
import ComboboxBothAutocomplete from "..";
test("change combobox value by focusing on combobox options", () => {
render(<ComboboxBothAutocomplete />);
click(screen.getByLabelText("Fruit"));
expect(screen.getByLabelText("Fruit")).toHaveValue("");
focus(screen.getByText("Acerola"));
expect(screen.getByLabelText("Fruit")).toHaveValue("Acerola");
});
test("change combobox value by arrowing through combobox options", () => {
render(<ComboboxBothAutocomplete />);
click(screen.getByLabelText("Fruit"));
expect(screen.getByLabelText("Fruit")).toHaveValue("");
press.ArrowDown();
expect(screen.getByLabelText("Fruit")).toHaveValue("Acerola");
press.ArrowDown();
expect(screen.getByLabelText("Fruit")).toHaveValue("Apple");
press.ArrowDown();
expect(screen.getByLabelText("Fruit")).toHaveValue("Apricots");
press.ArrowUp();
press.ArrowUp();
press.ArrowUp();
expect(screen.getByLabelText("Fruit")).toHaveValue("");
});
test("filter combobox options by typing on the combobox", () => {
render(<ComboboxBothAutocomplete />);
press.Tab();
type("bla");
expect(screen.queryByText("Blackberries")).toBeInTheDocument();
expect(screen.queryByText("Blackcurrant")).toBeInTheDocument();
});
test("display no results when there is no option match", () => {
render(<ComboboxBothAutocomplete />);
press.Tab();
type("1");
expect(screen.queryByText("No results found")).toBeInTheDocument();
});

View File

@@ -0,0 +1,71 @@
export const fruits = [
"Acerola",
"Apple",
"Apricots",
"Avocado",
"Banana",
"Blackberries",
"Blackcurrant",
"Blueberries",
"Breadfruit",
"Cantaloupe",
"Carambola",
"Cherimoya",
"Cherries",
"Clementine",
"Coconut Meat",
"Cranberries",
"Custard-Apple",
"Date Fruit",
"Durian",
"Elderberries",
"Feijoa",
"Figs",
"Gooseberries",
"Grapefruit",
"Grapes",
"Guava",
"Honeydew Melon",
"Jackfruit",
"Java-Plum",
"Jujube Fruit",
"Kiwifruit",
"Kumquat",
"Lemon",
"Lime",
"Longan",
"Loquat",
"Lychee",
"Mandarin",
"Mango",
"Mangosteen",
"Mulberries",
"Nectarine",
"Olives",
"Orange",
"Papaya",
"Passion Fruit",
"Peaches",
"Pear",
"Persimmon",
"Pitaya",
"Pineapple",
"Pitanga",
"Plantain",
"Plums",
"Pomegranate",
"Prickly Pear",
"Prunes",
"Pummelo",
"Quince",
"Raspberries",
"Rhubarb",
"Rose-Apple",
"Sapodilla",
"Soursop",
"Strawberries",
"Sugar-Apple",
"Tamarind",
"Tangerine",
"Watermelon",
];

View File

@@ -0,0 +1,30 @@
import * as React from "react";
import {
unstable_useComboboxState as useComboboxState,
unstable_Combobox as Combobox,
unstable_ComboboxPopover as ComboboxPopover,
unstable_ComboboxOption as ComboboxOption,
} from "reakit/Combobox";
import { fruits } from "./fruits";
import "./style.css";
export default function ComboboxBothAutocomplete() {
const combobox = useComboboxState({
values: fruits,
inline: true,
gutter: 8,
});
return (
<>
<Combobox {...combobox} aria-label="Fruit" placeholder="Enter a fruit" />
<ComboboxPopover {...combobox} aria-label="Fruits">
{combobox.matches.length
? combobox.matches.map((value) => (
<ComboboxOption {...combobox} key={value} value={value} />
))
: "No results found"}
</ComboboxPopover>
</>
);
}

View File

@@ -0,0 +1 @@
@import "../AccessibleCombobox/style.css";

View File

@@ -0,0 +1,50 @@
import * as React from "react";
import { render, click, type, wait, screen } from "reakit-test-utils";
import ComboboxFetch from "..";
function getComboboxInput() {
return screen.getByLabelText("Fruit");
}
function expectSelectionValue(element: Element, value: string) {
const input = element as HTMLInputElement;
const { selectionStart, selectionEnd } = input;
const selectionValue = input.value.slice(selectionStart!, selectionEnd!);
expect(selectionValue).toBe(value);
}
test("open combobox popover on click", async () => {
render(<ComboboxFetch />);
expect(screen.getByLabelText("Fruits")).not.toBeVisible();
click(getComboboxInput());
expect(screen.getByLabelText("Fruits")).toBeVisible();
await wait(() => expect(screen.getByText("Acerola")).not.toHaveFocus());
});
test("type on combobox", async () => {
render(<ComboboxFetch />);
click(getComboboxInput());
await wait(() => expect(screen.getByText("Acerola")).not.toHaveFocus());
// value: ace[rola]
type("ace");
expect(screen.getByText("Acerola")).toHaveFocus();
expect(getComboboxInput()).toHaveValue("acerola");
expectSelectionValue(getComboboxInput(), "rola");
// value: ace
type("\b");
expect(screen.getByText("Acerola")).not.toHaveFocus();
expect(getComboboxInput()).toHaveValue("ace");
// Wait for fetch
await wait(expect(screen.queryByText("Aple")).not.toBeInTheDocument);
expect(screen.getByText("Acerola")).not.toHaveFocus();
expect(getComboboxInput()).toHaveValue("ace");
// value: bl[ackberries]
type("\b\b\bbl");
await wait(() => expect(getComboboxInput()).toHaveValue("blackberries"));
expectSelectionValue(getComboboxInput(), "ackberries");
expect(screen.getByText("Blackberries")).toHaveFocus();
// value: bl
type("\b");
await wait(() => expect(getComboboxInput()).toHaveValue("bl"));
await wait(() => expect(screen.getByText("Blackberries")).not.toHaveFocus());
});

View File

@@ -0,0 +1,87 @@
export function fetchFruits(value: string) {
const searchValue = new RegExp(escapeRegExp(value), "i");
return new Promise((resolve) => {
setTimeout(() => {
const matches = fruits
.filter((fruit) => fruit.search(searchValue) !== -1)
.slice(0, 10);
resolve(matches);
}, 250);
});
}
function escapeRegExp(string: string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
const fruits = [
"Acerola",
"Apple",
"Apricots",
"Avocado",
"Banana",
"Blackberries",
"Blackcurrant",
"Blueberries",
"Breadfruit",
"Cantaloupe",
"Carambola",
"Cherimoya",
"Cherries",
"Clementine",
"Coconut Meat",
"Cranberries",
"Custard-Apple",
"Date Fruit",
"Durian",
"Elderberries",
"Feijoa",
"Figs",
"Gooseberries",
"Grapefruit",
"Grapes",
"Guava",
"Honeydew Melon",
"Jackfruit",
"Java-Plum",
"Jujube Fruit",
"Kiwifruit",
"Kumquat",
"Lemon",
"Lime",
"Longan",
"Loquat",
"Lychee",
"Mandarin",
"Mango",
"Mangosteen",
"Mulberries",
"Nectarine",
"Olives",
"Orange",
"Papaya",
"Passion Fruit",
"Peaches",
"Pear",
"Persimmon",
"Pitaya",
"Pineapple",
"Pitanga",
"Plantain",
"Plums",
"Pomegranate",
"Prickly Pear",
"Prunes",
"Pummelo",
"Quince",
"Raspberries",
"Rhubarb",
"Rose-Apple",
"Sapodilla",
"Soursop",
"Strawberries",
"Sugar-Apple",
"Tamarind",
"Tangerine",
"Watermelon",
];

View File

@@ -0,0 +1,40 @@
import * as React from "react";
import {
unstable_useComboboxState as useComboboxState,
unstable_Combobox as Combobox,
unstable_ComboboxPopover as ComboboxPopover,
unstable_ComboboxOption as ComboboxOption,
} from "reakit/Combobox";
import { fetchFruits } from "./api";
import "./style.css";
export default function ComboboxFetch() {
const [matches, setMatches] = React.useState<string[]>([]);
const combobox = useComboboxState({
list: true,
inline: true,
autoSelect: true,
gutter: 8,
});
React.useEffect(() => {
const timeout = setTimeout(() => {
fetchFruits(combobox.inputValue).then(setMatches);
}, 250);
return () => clearTimeout(timeout);
}, [combobox.inputValue]);
return (
<>
<Combobox {...combobox} aria-label="Fruit" placeholder="Enter a fruit" />
<ComboboxPopover {...combobox} aria-label="Fruits">
{matches.length
? matches.map((value) => (
<ComboboxOption {...combobox} key={value} value={value} />
))
: "No results found"}
</ComboboxPopover>
</>
);
}

View File

@@ -0,0 +1 @@
@import "../AccessibleCombobox/style.css";

View File

@@ -0,0 +1,90 @@
import * as React from "react";
import {
render,
press,
focus,
click,
fireEvent,
screen,
} from "reakit-test-utils";
import ComboboxInline from "..";
test("change combobox value by focusing on combobox options", () => {
render(<ComboboxInline />);
click(screen.getByLabelText("Color"));
expect(screen.getByLabelText("Color")).toHaveValue("");
focus(screen.getByText("Red"));
expect(screen.getByLabelText("Color")).toHaveValue("Red");
});
test("change combobox value by arrowing through combobox options", () => {
render(<ComboboxInline />);
click(screen.getByLabelText("Color"));
expect(screen.getByLabelText("Color")).toHaveValue("");
press.ArrowDown();
expect(screen.getByLabelText("Color")).toHaveValue("Red");
press.ArrowDown();
expect(screen.getByLabelText("Color")).toHaveValue("Green");
press.ArrowDown();
expect(screen.getByLabelText("Color")).toHaveValue("Blue");
press.ArrowDown();
expect(screen.getByLabelText("Color")).toHaveValue("");
});
test("revert combobox value after closing combobox popover with esc", () => {
render(<ComboboxInline />);
click(screen.getByLabelText("Color"));
expect(screen.getByLabelText("Color")).toHaveValue("");
press.ArrowUp();
expect(screen.getByLabelText("Color")).toHaveValue("Blue");
press.Escape();
expect(screen.getByLabelText("Color")).toHaveValue("");
});
test("keep combobox value after closing combobox popover by tabbing out", () => {
render(
<>
<ComboboxInline />
<button>button</button>
</>
);
click(screen.getByLabelText("Color"));
expect(screen.getByLabelText("Color")).toHaveValue("");
press.ArrowDown();
expect(screen.getByLabelText("Color")).toHaveValue("Red");
press.Tab();
expect(screen.getByLabelText("Color")).toHaveValue("Red");
});
test("keep combobox value after closing combobox popover by clicking outside", () => {
const { baseElement } = render(<ComboboxInline />);
click(screen.getByLabelText("Color"));
expect(screen.getByLabelText("Color")).toHaveValue("");
press.ArrowUp();
expect(screen.getByLabelText("Color")).toHaveValue("Blue");
click(baseElement);
expect(screen.getByLabelText("Color")).toHaveValue("Blue");
});
test("unselect combobox option when cleaning combobox value", () => {
render(<ComboboxInline />);
click(screen.getByLabelText("Color"));
expect(screen.getByLabelText("Color")).toHaveValue("");
press.ArrowDown();
expect(screen.getByLabelText("Color")).toHaveValue("Red");
fireEvent.change(screen.getByLabelText("Color"), { target: { value: "" } });
expect(screen.getByText("Red")).not.toHaveFocus();
expect(screen.getByText("Green")).not.toHaveFocus();
expect(screen.getByText("Blue")).not.toHaveFocus();
expect(screen.getByLabelText("Color")).toHaveValue("");
});
test("clicking on combobox input does not revert current value", () => {
render(<ComboboxInline />);
click(screen.getByLabelText("Color"));
expect(screen.getByLabelText("Color")).toHaveValue("");
press.ArrowDown();
expect(screen.getByLabelText("Color")).toHaveValue("Red");
click(screen.getByLabelText("Color"));
expect(screen.getByLabelText("Color")).toHaveValue("Red");
});

View File

@@ -0,0 +1,23 @@
import * as React from "react";
import {
unstable_useComboboxState as useComboboxState,
unstable_Combobox as Combobox,
unstable_ComboboxPopover as ComboboxPopover,
unstable_ComboboxOption as ComboboxOption,
} from "reakit/Combobox";
import "./style.css";
export default function ComboboxInline() {
const combobox = useComboboxState({ inline: true, gutter: 8 });
return (
<>
<Combobox {...combobox} aria-label="Color" />
<ComboboxPopover {...combobox} aria-label="Colors">
<ComboboxOption {...combobox} value="Red" />
<ComboboxOption {...combobox} value="Green" />
<ComboboxOption {...combobox} value="Blue" />
</ComboboxPopover>
</>
);
}

View File

@@ -0,0 +1 @@
@import "../AccessibleCombobox/style.css";

View File

@@ -0,0 +1,94 @@
import * as React from "react";
import { render, press, click, type, screen } from "reakit-test-utils";
import ComboboxInlineAutoSelect from "..";
function expectSelectionValue(element: Element, value: string) {
const input = element as HTMLInputElement;
const { selectionStart, selectionEnd } = input;
const selectionValue = input.value.slice(selectionStart!, selectionEnd!);
expect(selectionValue).toBe(value);
}
test("open combobox popover on click and do not auto select", () => {
render(<ComboboxInlineAutoSelect />);
expect(screen.getByLabelText("Fruits")).not.toBeVisible();
click(screen.getByLabelText("Fruit"));
expect(screen.getByLabelText("Fruits")).toBeVisible();
expect(screen.getByText("Apple")).not.toHaveFocus();
});
test("open combobox popover on arrow down and do not auto select", () => {
render(<ComboboxInlineAutoSelect />);
press.Tab();
expect(screen.getByLabelText("Fruits")).not.toBeVisible();
press.ArrowDown();
expect(screen.getByLabelText("Fruits")).toBeVisible();
expect(screen.getByText("Apple")).not.toHaveFocus();
});
test("open combobox popover on arrow up and do not auto select", () => {
render(<ComboboxInlineAutoSelect />);
press.Tab();
expect(screen.getByLabelText("Fruits")).not.toBeVisible();
press.ArrowUp();
expect(screen.getByLabelText("Fruits")).toBeVisible();
expect(screen.getByText("Apple")).not.toHaveFocus();
});
test("open combobox popover on type and auto select", () => {
render(<ComboboxInlineAutoSelect />);
const combobox = screen.getByLabelText("Fruit");
press.Tab();
expect(screen.getByLabelText("Fruits")).not.toBeVisible();
type("a");
expect(screen.getByLabelText("Fruits")).toBeVisible();
expect(screen.getByText("Apple")).toHaveFocus();
expect(combobox).toHaveValue("apple");
expectSelectionValue(combobox, "pple");
type("pp");
expect(screen.getByText("Apple")).toHaveFocus();
expect(combobox).toHaveValue("apple");
expectSelectionValue(combobox, "le");
type("\b");
expect(screen.getByText("Apple")).not.toHaveFocus();
expect(combobox).toHaveValue("app");
expectSelectionValue(combobox, "");
type("\b");
expect(screen.getByText("Apple")).not.toHaveFocus();
expect(combobox).toHaveValue("ap");
expectSelectionValue(combobox, "");
type("p");
expect(screen.getByText("Apple")).toHaveFocus();
expect(combobox).toHaveValue("apple");
expectSelectionValue(combobox, "le");
});
test("keep combobox value after closing combobox popover by clicking outside", () => {
const { baseElement } = render(<ComboboxInlineAutoSelect />);
click(screen.getByLabelText("Fruit"));
type("a");
expect(screen.getByLabelText("Fruit")).toHaveValue("apple");
expectSelectionValue(screen.getByLabelText("Fruit"), "pple");
click(baseElement);
expect(screen.getByLabelText("Fruit")).toHaveValue("apple");
});
test("move through combobox options by pressing arrow keys", () => {
render(<ComboboxInlineAutoSelect />);
press.Tab();
type("a");
expect(screen.getByLabelText("Fruit")).toHaveValue("apple");
expectSelectionValue(screen.getByLabelText("Fruit"), "pple");
press.ArrowDown();
expect(screen.getByLabelText("Fruit")).toHaveValue("Orange");
expectSelectionValue(screen.getByLabelText("Fruit"), "");
press.ArrowDown();
expect(screen.getByLabelText("Fruit")).toHaveValue("Banana");
expectSelectionValue(screen.getByLabelText("Fruit"), "");
press.ArrowDown();
expect(screen.getByLabelText("Fruit")).toHaveValue("a");
expectSelectionValue(screen.getByLabelText("Fruit"), "");
press.ArrowDown();
expect(screen.getByLabelText("Fruit")).toHaveValue("apple");
expectSelectionValue(screen.getByLabelText("Fruit"), "pple");
});

View File

@@ -0,0 +1,27 @@
import * as React from "react";
import {
unstable_useComboboxState as useComboboxState,
unstable_Combobox as Combobox,
unstable_ComboboxPopover as ComboboxPopover,
unstable_ComboboxOption as ComboboxOption,
} from "reakit/Combobox";
import "./style.css";
export default function ComboboxInlineAutoSelect() {
const combobox = useComboboxState({
inline: true,
autoSelect: true,
gutter: 8,
});
return (
<>
<Combobox {...combobox} aria-label="Fruit" />
<ComboboxPopover {...combobox} aria-label="Fruits">
<ComboboxOption {...combobox} value="Apple" />
<ComboboxOption {...combobox} value="Orange" />
<ComboboxOption {...combobox} value="Banana" />
</ComboboxPopover>
</>
);
}

View File

@@ -0,0 +1 @@
@import "../AccessibleCombobox/style.css";

View File

@@ -0,0 +1,46 @@
import * as React from "react";
import { render, press, focus, click, type, screen } from "reakit-test-utils";
import ComboboxList from "..";
test("do not change combobox value by focusing on combobox options", () => {
render(<ComboboxList />);
click(screen.getByLabelText("Color"));
expect(screen.getByLabelText("Color")).toHaveValue("");
focus(screen.getByText("AliceBlue"));
expect(screen.getByLabelText("Color")).toHaveValue("");
});
test("do not change combobox value by arrowing through combobox options", () => {
render(<ComboboxList />);
click(screen.getByLabelText("Color"));
expect(screen.getByLabelText("Color")).toHaveValue("");
press.ArrowDown();
expect(screen.getByLabelText("Color")).toHaveValue("");
press.ArrowDown();
expect(screen.getByLabelText("Color")).toHaveValue("");
press.End();
expect(screen.getByLabelText("Color")).toHaveValue("");
});
test("change combobox value by clicking on the combobox option", () => {
render(<ComboboxList />);
click(screen.getByLabelText("Color"));
expect(screen.getByLabelText("Color")).toHaveValue("");
click(screen.getByText("AliceBlue"));
expect(screen.getByLabelText("Color")).toHaveValue("AliceBlue");
});
test("filter combobox options by typing on the combobox", () => {
render(<ComboboxList />);
press.Tab();
type("bla");
expect(screen.queryByText("Black")).toBeInTheDocument();
expect(screen.queryByText("BlanchedAlmond")).toBeInTheDocument();
});
test("display no results when there is no option match", () => {
render(<ComboboxList />);
press.Tab();
type("1");
expect(screen.queryByText("No results found")).toBeInTheDocument();
});

View File

@@ -0,0 +1,150 @@
export const colors = [
"AliceBlue",
"AntiqueWhite",
"Aqua",
"Aquamarine",
"Azure",
"Beige",
"Bisque",
"Black",
"BlanchedAlmond",
"Blue",
"BlueViolet",
"Brown",
"BurlyWood",
"CadetBlue",
"Chartreuse",
"Chocolate",
"Coral",
"CornflowerBlue",
"Cornsilk",
"Crimson",
"Cyan",
"DarkBlue",
"DarkCyan",
"DarkGoldenRod",
"DarkGray",
"DarkGrey",
"DarkGreen",
"DarkKhaki",
"DarkMagenta",
"DarkOliveGreen",
"DarkOrange",
"DarkOrchid",
"DarkRed",
"DarkSalmon",
"DarkSeaGreen",
"DarkSlateBlue",
"DarkSlateGray",
"DarkSlateGrey",
"DarkTurquoise",
"DarkViolet",
"DeepPink",
"DeepSkyBlue",
"DimGray",
"DimGrey",
"DodgerBlue",
"FireBrick",
"FloralWhite",
"ForestGreen",
"Fuchsia",
"Gainsboro",
"GhostWhite",
"Gold",
"GoldenRod",
"Gray",
"Grey",
"Green",
"GreenYellow",
"HoneyDew",
"HotPink",
"IndianRed",
"Indigo",
"Ivory",
"Khaki",
"Lavender",
"LavenderBlush",
"LawnGreen",
"LemonChiffon",
"LightBlue",
"LightCoral",
"LightCyan",
"LightGoldenRodYellow",
"LightGray",
"LightGrey",
"LightGreen",
"LightPink",
"LightSalmon",
"LightSeaGreen",
"LightSkyBlue",
"LightSlateGray",
"LightSlateGrey",
"LightSteelBlue",
"LightYellow",
"Lime",
"LimeGreen",
"Linen",
"Magenta",
"Maroon",
"MediumAquaMarine",
"MediumBlue",
"MediumOrchid",
"MediumPurple",
"MediumSeaGreen",
"MediumSlateBlue",
"MediumSpringGreen",
"MediumTurquoise",
"MediumVioletRed",
"MidnightBlue",
"MintCream",
"MistyRose",
"Moccasin",
"NavajoWhite",
"Navy",
"OldLace",
"Olive",
"OliveDrab",
"Orange",
"OrangeRed",
"Orchid",
"PaleGoldenRod",
"PaleGreen",
"PaleTurquoise",
"PaleVioletRed",
"PapayaWhip",
"PeachPuff",
"Peru",
"Pink",
"Plum",
"PowderBlue",
"Purple",
"RebeccaPurple",
"Red",
"RosyBrown",
"RoyalBlue",
"SaddleBrown",
"Salmon",
"SandyBrown",
"SeaGreen",
"SeaShell",
"Sienna",
"Silver",
"SkyBlue",
"SlateBlue",
"SlateGray",
"SlateGrey",
"Snow",
"SpringGreen",
"SteelBlue",
"Tan",
"Teal",
"Thistle",
"Tomato",
"Turquoise",
"Violet",
"Wheat",
"White",
"WhiteSmoke",
"Yellow",
"YellowGreen",
];

View File

@@ -0,0 +1,26 @@
import * as React from "react";
import {
unstable_useComboboxState as useComboboxState,
unstable_Combobox as Combobox,
unstable_ComboboxPopover as ComboboxPopover,
unstable_ComboboxOption as ComboboxOption,
} from "reakit/Combobox";
import { colors } from "./colors";
import "./style.css";
export default function ComboboxList() {
const combobox = useComboboxState({ values: colors, gutter: 8 });
return (
<>
<Combobox {...combobox} aria-label="Color" placeholder="Type a color" />
<ComboboxPopover {...combobox} aria-label="Colors">
{combobox.matches.length
? combobox.matches.map((value) => (
<ComboboxOption {...combobox} key={value} value={value} />
))
: "No results found"}
</ComboboxPopover>
</>
);
}

View File

@@ -0,0 +1 @@
@import "../AccessibleCombobox/style.css";

View File

@@ -0,0 +1,19 @@
import * as React from "react";
export default function ArrowDown() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
x="0px"
y="0px"
width="960px"
height="560px"
viewBox="0 0 960 560"
aria-hidden
focusable={false}
enableBackground="new 0 0 960 560"
>
<path d="M480,344.181L268.869,131.889c-15.756-15.859-41.3-15.859-57.054,0c-15.754,15.857-15.754,41.57,0,57.431l237.632,238.937 c8.395,8.451,19.562,12.254,30.553,11.698c10.993,0.556,22.159-3.247,30.555-11.698l237.631-238.937 c15.756-15.86,15.756-41.571,0-57.431s-41.299-15.859-57.051,0L480,344.181z" />
</svg>
);
}

View File

@@ -0,0 +1,22 @@
import * as React from "react";
type Props = React.HTMLAttributes<HTMLDivElement> & {
title: string;
icon?: JSX.Element;
isTabbable?: boolean;
};
function Block(
{ title, icon, isTabbable, ...props }: Props,
ref: React.Ref<HTMLDivElement>
) {
return (
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
<div {...props} ref={ref} tabIndex={isTabbable ? 0 : props.tabIndex}>
{icon}
{title}
</div>
);
}
export default React.forwardRef(Block);

View File

@@ -0,0 +1,70 @@
import * as React from "react";
import kebabCase from "lodash/kebabCase";
import {
unstable_useGridState as useGridState,
unstable_Grid as Grid,
unstable_GridRow as GridRow,
unstable_GridCell as GridCell,
} from "reakit/Grid";
import { data } from "./data";
import Block from "./Block";
type Props = React.HTMLAttributes<HTMLDivElement> & {
items: typeof data;
"aria-label": string;
};
function groupItemsByCategory(items: typeof data) {
const categories = [] as Array<{ title: string; items: typeof data }>;
for (const item of items) {
const category = categories.find((cat) => cat.title === item.category);
if (category) {
category.items = [...category.items, item];
} else {
categories.push({ title: item.category, items: [item] });
}
}
return categories;
}
function chunk<T>(array: T[], columns = 1) {
return Array.from({ length: Math.ceil(array.length / columns) }, (_, i) =>
array.slice(i * columns, i * columns + columns)
);
}
function getId(baseId: string, token: string) {
return `${baseId}-${kebabCase(token)}`;
}
export default function BlockGrid({ items, ...props }: Props) {
const grid = useGridState({ wrap: "horizontal", shift: true });
const categories = React.useMemo(() => groupItemsByCategory(items), [items]);
return (
<Grid {...grid} {...props}>
{categories.map((category) => {
const titleId = getId(grid.baseId, category.title);
return (
<div role="rowgroup" key={category.title} aria-labelledby={titleId}>
<h3 id={titleId}>{category.title}</h3>
{chunk(category.items, 3).map((row, i) => (
<GridRow {...grid} key={i}>
{row.map((item, j) => (
<GridCell
{...grid}
as={Block}
key={item.title}
id={getId(grid.baseId, item.title)}
title={item.title}
icon={item.icon}
isTabbable={i + j === 0}
/>
))}
</GridRow>
))}
</div>
);
})}
</Grid>
);
}

View File

@@ -0,0 +1,91 @@
import * as React from "react";
import {
unstable_useComboboxListGridState as useComboboxListGridState,
unstable_Combobox as Combobox,
unstable_ComboboxList as ComboboxList,
unstable_ComboboxGridRow as ComboboxGridRow,
unstable_ComboboxGridCell as ComboboxGridCell,
} from "reakit/Combobox";
import Block from "./Block";
import BlockGrid from "./BlockGrid";
import { data } from "./data";
type Props = Omit<React.InputHTMLAttributes<HTMLInputElement>, "list"> & {
initialItems: typeof data;
};
function getValues(items: typeof data) {
return items.map((item) => item.title);
}
function getIconFromValue(items: typeof data, value: string) {
return items.find((item) => item.title === value)?.icon;
}
function onOptionClick(event: React.MouseEvent) {
event.preventDefault();
}
export default function BlockList({ initialItems, ...props }: Props) {
const ref = React.useRef<HTMLInputElement>(null);
const initialValues = React.useMemo(() => getValues(initialItems), [
initialItems,
]);
const combobox = useComboboxListGridState({
values: initialValues,
limit: false,
columns: 3,
autoSelect: true,
wrap: "horizontal",
shift: true,
minValueLength: 1,
});
return (
<>
<Combobox
{...combobox}
{...props}
ref={ref}
aria-label="Block search"
placeholder="Search for a block"
/>
{combobox.inputValue ? (
combobox.matches.length ? (
<ComboboxList {...combobox} aria-label="Block suggestions">
{combobox.matches.map((values, i) => (
<ComboboxGridRow {...combobox} key={i}>
{values.map((val) => (
<ComboboxGridCell
{...combobox}
as={Block}
key={val}
value={val}
title={val}
icon={getIconFromValue(initialItems, val)}
onClick={onOptionClick}
/>
))}
</ComboboxGridRow>
))}
</ComboboxList>
) : (
<span>No results found</span>
)
) : (
<BlockGrid
aria-label="Blocks"
items={data}
onKeyDown={(event) => {
const { key } = event;
if (key && key.length === 1) {
ref.current?.focus();
}
}}
/>
)}
<footer>
💡 Move through items with arrow keys. Press any character to search.
</footer>
</>
);
}

View File

@@ -0,0 +1,95 @@
import * as React from "react";
import { render, press, click, type, screen } from "reakit-test-utils";
import ComboboxListGridWithPopover from "..";
function getPopoverDisclosure() {
return screen.getByText("Blocks");
}
function getPopover() {
return screen.queryByRole("dialog", { name: "Blocks" });
}
function getComboboxInput() {
return screen.queryByRole("combobox", {
name: "Block search",
}) as HTMLInputElement | null;
}
test("open popover on click", () => {
render(<ComboboxListGridWithPopover />);
expect(getPopover()).not.toBeInTheDocument();
click(getPopoverDisclosure());
expect(getPopover()).toBeVisible();
expect(getComboboxInput()).toHaveFocus();
});
test("keyboard navigation on grid", () => {
render(<ComboboxListGridWithPopover />);
click(getPopoverDisclosure());
expect(getComboboxInput()).toHaveFocus();
press.Tab();
expect(screen.getByText("Paragraph")).toHaveFocus();
press.Tab();
expect(screen.getByText("Image")).toHaveFocus();
press.Tab();
expect(screen.getByText("Shortcode")).toHaveFocus();
press.ShiftTab();
expect(screen.getByText("Image")).toHaveFocus();
press.ArrowRight();
press.ArrowRight();
press.ArrowRight();
expect(screen.getByText("Cover")).toHaveFocus();
press.ArrowRight();
press.ArrowRight();
expect(screen.getByText("Media & Text")).toHaveFocus();
press.ArrowDown();
expect(screen.getByText("Video")).toHaveFocus();
press.ArrowDown();
expect(screen.getByText("Shortcode")).toHaveFocus();
});
test("pressing character key while focusing grid item", () => {
render(<ComboboxListGridWithPopover />);
click(getPopoverDisclosure());
press.Tab();
expect(screen.getByText("Paragraph")).toHaveFocus();
type("l");
expect(getComboboxInput()).toHaveFocus();
type("a");
expect(getComboboxInput()).toHaveValue("la");
expect(screen.getByText("Latest Comments")).toHaveFocus();
getComboboxInput()?.setSelectionRange(1, 1);
type("\b");
expect(getComboboxInput()).toHaveValue("a");
getComboboxInput()?.setSelectionRange(1, 1);
type(" ");
expect(getComboboxInput()).toHaveValue("a ");
expect(screen.getByText("Media & Text")).toHaveFocus();
});
test("keyboard navigation on combobox grid", () => {
render(<ComboboxListGridWithPopover />);
click(getPopoverDisclosure());
type("r");
expect(screen.getByText("RSS")).toHaveFocus();
press.ArrowRight();
expect(screen.getByText("Paragraph")).toHaveFocus();
press.ArrowRight();
expect(screen.getByText("Gallery")).toHaveFocus();
press.ArrowRight();
expect(screen.getByText("Shortcode")).toHaveFocus();
press.End();
expect(screen.getByText("Calendar")).toHaveFocus();
press.PageDown();
expect(screen.getByText("Spacer")).toHaveFocus();
press.ArrowDown();
expect(screen.getByText("Verse")).toHaveFocus();
press.PageUp();
expect(screen.getByText("RSS")).toHaveFocus();
press.ArrowUp();
expect(screen.getByText("RSS")).not.toHaveFocus();
press.ArrowRight();
press.ArrowDown();
expect(screen.getByText("RSS")).toHaveFocus();
});

View File

@@ -0,0 +1,571 @@
import * as React from "react";
export const data = [
{
title: "Paragraph",
category: "Text",
description: "Start with the building block of all narrative.",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
aria-hidden="true"
focusable="false"
>
<path d="M18.3 4H9.9v-.1l-.9.2c-2.3.4-4 2.4-4 4.8s1.7 4.4 4 4.8l.7.1V20h1.5V5.5h2.9V20h1.5V5.5h2.7V4z" />
</svg>
),
},
{
title: "Image",
category: "Media",
description: "Insert an image to make a visual statement.",
icon: (
<svg
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
focusable="false"
>
<path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zM5 4.5h14c.3 0 .5.2.5.5v8.4l-3-2.9c-.3-.3-.8-.3-1 0L11.9 14 9 12c-.3-.2-.6-.2-.8 0l-3.6 2.6V5c-.1-.3.1-.5.4-.5zm14 15H5c-.3 0-.5-.2-.5-.5v-2.4l4.1-3 3 1.9c.3.2.7.2.9-.1L16 12l3.5 3.4V19c0 .3-.2.5-.5.5z" />
</svg>
),
},
{
title: "Heading",
category: "Text",
description:
"Introduce new sections and organize content to help visitors (and search engines) understand the structure of your content.",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
aria-hidden="true"
focusable="false"
>
<path d="M6.2 5.2v13.4l5.8-4.8 5.8 4.8V5.2z" />
</svg>
),
},
{
title: "Gallery",
category: "Media",
description: "Display multiple images in a rich gallery.",
icon: (
<svg
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
focusable="false"
>
<path d="M20.2 8v11c0 .7-.6 1.2-1.2 1.2H6v1.5h13c1.5 0 2.7-1.2 2.7-2.8V8h-1.5zM18 16.4V4.6c0-.9-.7-1.6-1.6-1.6H4.6C3.7 3 3 3.7 3 4.6v11.8c0 .9.7 1.6 1.6 1.6h11.8c.9 0 1.6-.7 1.6-1.6zM4.5 4.6c0-.1.1-.1.1-.1h11.8c.1 0 .1.1.1.1V12l-2.3-1.7c-.3-.2-.6-.2-.9 0l-2.9 2.1L8 11.3c-.2-.1-.5-.1-.7 0l-2.9 1.5V4.6zm0 11.8v-1.8l3.2-1.7 2.4 1.2c.2.1.5.1.8-.1l2.8-2 2.8 2v2.5c0 .1-.1.1-.1.1H4.6c0-.1-.1-.2-.1-.2z" />
</svg>
),
},
{
title: "List",
category: "Text",
description: "Create a bulleted or numbered list.",
icon: (
<svg
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
focusable="false"
>
<path d="M4 4v1.5h16V4H4zm8 8.5h8V11h-8v1.5zM4 20h16v-1.5H4V20zm4-8c0-1.1-.9-2-2-2s-2 .9-2 2 .9 2 2 2 2-.9 2-2z" />
</svg>
),
},
{
title: "Quote",
category: "Text",
description:
'Give quoted text visual emphasis. "In quoting others, we cite ourselves." — Julio Cortázar',
icon: (
<svg
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
focusable="false"
>
<path d="M13 6v6h5.2v4c0 .8-.2 1.4-.5 1.7-.6.6-1.6.6-2.5.5h-.3v1.5h.5c1 0 2.3-.1 3.3-1 .6-.6 1-1.6 1-2.8V6H13zm-9 6h5.2v4c0 .8-.2 1.4-.5 1.7-.6.6-1.6.6-2.5.5h-.3v1.5h.5c1 0 2.3-.1 3.3-1 .6-.6 1-1.6 1-2.8V6H4v6z" />
</svg>
),
},
{
title: "Shortcode",
category: "Widgets",
description:
"Insert additional custom elements with a WordPress shortcode.",
icon: (
<svg
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
focusable="false"
>
<path d="M16 4.2v1.5h2.5v12.5H16v1.5h4V4.2h-4zM4.2 19.8h4v-1.5H5.8V5.8h2.5V4.2h-4l-.1 15.6zm5.1-3.1l1.4.6 4-10-1.4-.6-4 10z" />
</svg>
),
},
{
title: "Archives",
category: "Widgets",
description: "Display a monthly archive of your posts.",
icon: (
<svg
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
focusable="false"
>
<path d="M19 6.2h-5.9l-.6-1.1c-.3-.7-1-1.1-1.8-1.1H5c-1.1 0-2 .9-2 2v11.8c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V8.2c0-1.1-.9-2-2-2zm.5 11.6c0 .3-.2.5-.5.5H5c-.3 0-.5-.2-.5-.5V6c0-.3.2-.5.5-.5h5.8c.2 0 .4.1.4.3l1 2H19c.3 0 .5.2.5.5v9.5zM8 12.8h8v-1.5H8v1.5zm0 3h8v-1.5H8v1.5z" />
</svg>
),
},
{
title: "Audio",
category: "Media",
description: "Embed a simple audio player.",
icon: (
<svg
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
focusable="false"
>
<path d="M17.7 4.3c-1.2 0-2.8 0-3.8 1-.6.6-.9 1.5-.9 2.6V14c-.6-.6-1.5-1-2.5-1C8.6 13 7 14.6 7 16.5S8.6 20 10.5 20c1.5 0 2.8-1 3.3-2.3.5-.8.7-1.8.7-2.5V7.9c0-.7.2-1.2.5-1.6.6-.6 1.8-.6 2.8-.6h.3V4.3h-.4z" />
</svg>
),
},
{
title: "Buttons",
category: "Design",
description:
"Prompt visitors to take action with a group of button-style links.",
icon: (
<svg
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
focusable="false"
>
<path d="M19 6.5H5c-1.1 0-2 .9-2 2v7c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2v-7c0-1.1-.9-2-2-2zm.5 9c0 .3-.2.5-.5.5H5c-.3 0-.5-.2-.5-.5v-7c0-.3.2-.5.5-.5h14c.3 0 .5.2.5.5v7zM8 13h8v-1.5H8V13z" />
</svg>
),
},
{
title: "Calendar",
category: "Widgets",
description: "A calendar of your sites posts.",
icon: (
<svg
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
focusable="false"
>
<path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm.5 16c0 .3-.2.5-.5.5H5c-.3 0-.5-.2-.5-.5V7h15v12zM9 10H7v2h2v-2zm0 4H7v2h2v-2zm4-4h-2v2h2v-2zm4 0h-2v2h2v-2zm-4 4h-2v2h2v-2zm4 0h-2v2h2v-2z" />
</svg>
),
},
{
title: "Categories",
category: "Widgets",
description: "Display a list of all categories.",
icon: (
<svg
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
focusable="false"
>
<path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm.5 16c0 .3-.2.5-.5.5H5c-.3 0-.5-.2-.5-.5V7h15v12zM9 10H7v2h2v-2zm0 4H7v2h2v-2zm4-4h-2v2h2v-2zm4 0h-2v2h2v-2zm-4 4h-2v2h2v-2zm4 0h-2v2h2v-2z" />
</svg>
),
},
{
title: "Code",
category: "Text",
description: "Display code snippets that respect your spacing and tabs.",
icon: (
<svg
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
focusable="false"
>
<path d="M20.8 10.7l-4.3-4.3-1.1 1.1 4.3 4.3c.1.1.1.3 0 .4l-4.3 4.3 1.1 1.1 4.3-4.3c.7-.8.7-1.9 0-2.6zM4.2 11.8l4.3-4.3-1-1-4.3 4.3c-.7.7-.7 1.8 0 2.5l4.3 4.3 1.1-1.1-4.3-4.3c-.2-.1-.2-.3-.1-.4z" />
</svg>
),
},
{
title: "Columns",
category: "Design",
description:
"Add a block that displays content in multiple columns, then add whatever content blocks youd like.",
icon: (
<svg
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
focusable="false"
>
<path d="M19 6H6c-1.1 0-2 .9-2 2v9c0 1.1.9 2 2 2h13c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zm-4.1 1.5v10H10v-10h4.9zM5.5 17V8c0-.3.2-.5.5-.5h2.5v10H6c-.3 0-.5-.2-.5-.5zm14 0c0 .3-.2.5-.5.5h-2.6v-10H19c.3 0 .5.2.5.5v9z" />
</svg>
),
},
{
title: "Cover",
category: "Media",
description:
"Add an image or video with a text overlay — great for headers.",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
aria-hidden="true"
focusable="false"
>
<path d="M18.7 3H5.3C4 3 3 4 3 5.3v13.4C3 20 4 21 5.3 21h13.4c1.3 0 2.3-1 2.3-2.3V5.3C21 4 20 3 18.7 3zm.8 15.7c0 .4-.4.8-.8.8H5.3c-.4 0-.8-.4-.8-.8V5.3c0-.4.4-.8.8-.8h6.2v8.9l2.5-3.1 2.5 3.1V4.5h2.2c.4 0 .8.4.8.8v13.4z" />
</svg>
),
},
{
title: "Embed",
category: "Embed",
description:
"Add a block that displays content pulled from other sites, like Twitter, Instagram or YouTube.",
icon: (
<svg
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
focusable="false"
>
<path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm.5 16c0 .3-.2.5-.5.5H5c-.3 0-.5-.2-.5-.5V9.8l4.7-5.3H19c.3 0 .5.2.5.5v14zm-6-9.5L16 12l-2.5 2.8 1.1 1L18 12l-3.5-3.5-1 1zm-3 0l-1-1L6 12l3.5 3.8 1.1-1L8 12l2.5-2.5z" />
</svg>
),
},
{
title: "File",
category: "Media",
description: "Add a link to a downloadable file.",
icon: (
<svg
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
focusable="false"
>
<path d="M19 6.2h-5.9l-.6-1.1c-.3-.7-1-1.1-1.8-1.1H5c-1.1 0-2 .9-2 2v11.8c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V8.2c0-1.1-.9-2-2-2zm.5 11.6c0 .3-.2.5-.5.5H5c-.3 0-.5-.2-.5-.5V6c0-.3.2-.5.5-.5h5.8c.2 0 .4.1.4.3l1 2H19c.3 0 .5.2.5.5v9.5z" />
</svg>
),
},
{
title: "Group",
category: "Design",
description: "Combine blocks into a group.",
icon: (
<svg
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
focusable="false"
>
<path d="M18 4h-7c-1.1 0-2 .9-2 2v3H6c-1.1 0-2 .9-2 2v7c0 1.1.9 2 2 2h7c1.1 0 2-.9 2-2v-3h3c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm-4.5 14c0 .3-.2.5-.5.5H6c-.3 0-.5-.2-.5-.5v-7c0-.3.2-.5.5-.5h3V13c0 1.1.9 2 2 2h2.5v3zm0-4.5H11c-.3 0-.5-.2-.5-.5v-2.5H13c.3 0 .5.2.5.5v2.5zm5-.5c0 .3-.2.5-.5.5h-3V11c0-1.1-.9-2-2-2h-2.5V6c0-.3.2-.5.5-.5h7c.3 0 .5.2.5.5v7z" />
</svg>
),
},
{
title: "Classic",
category: "Text",
description: "Use the classic WordPress editor.",
icon: (
<svg
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
focusable="false"
>
<path d="M20 6H4c-1.1 0-2 .9-2 2v9c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zm.5 11c0 .3-.2.5-.5.5H4c-.3 0-.5-.2-.5-.5V8c0-.3.2-.5.5-.5h16c.3 0 .5.2.5.5v9zM10 10H8v2h2v-2zm-5 2h2v-2H5v2zm8-2h-2v2h2v-2zm-5 6h8v-2H8v2zm6-4h2v-2h-2v2zm3 0h2v-2h-2v2zm0 4h2v-2h-2v2zM5 16h2v-2H5v2z" />
</svg>
),
},
{
title: "Custom HTML",
category: "Widgets",
description: "Add custom HTML code and preview it as you edit.",
icon: (
<svg
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
focusable="false"
>
<path d="M4.8 11.4H2.1V9H1v6h1.1v-2.6h2.7V15h1.1V9H4.8v2.4zm1.9-1.3h1.7V15h1.1v-4.9h1.7V9H6.7v1.1zM16.2 9l-1.5 2.7L13.3 9h-.9l-.8 6h1.1l.5-4 1.5 2.8 1.5-2.8.5 4h1.1L17 9h-.8zm3.8 5V9h-1.1v6h3.6v-1H20z" />
</svg>
),
},
{
title: "Media & Text",
category: "Media",
description: "Set media and words side-by-side for a richer layout.",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
aria-hidden="true"
focusable="false"
>
<path d="M4 17h7V6H4v11zm9-10v1.5h7V7h-7zm0 5.5h7V11h-7v1.5zm0 4h7V15h-7v1.5z" />
</svg>
),
},
{
title: "Latest Comments",
category: "Widgets",
description: "Display a list of your most recent comments.",
icon: (
<svg
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
focusable="false"
>
<path d="M18 4H6c-1.1 0-2 .9-2 2v12.9c0 .6.5 1.1 1.1 1.1.3 0 .5-.1.8-.3L8.5 17H18c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm.5 11c0 .3-.2.5-.5.5H7.9l-2.4 2.4V6c0-.3.2-.5.5-.5h12c.3 0 .5.2.5.5v9z" />
</svg>
),
},
{
title: "Latest Posts",
category: "Widgets",
description: "Display a list of your most recent posts.",
icon: (
<svg
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
focusable="false"
>
<path d="M18 4H6c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm.5 14c0 .3-.2.5-.5.5H6c-.3 0-.5-.2-.5-.5V6c0-.3.2-.5.5-.5h12c.3 0 .5.2.5.5v12zM7 11h2V9H7v2zm0 4h2v-2H7v2zm3-4h7V9h-7v2zm0 4h7v-2h-7v2z" />
</svg>
),
},
{
title: "More",
category: "Design",
description:
"Content before this block will be shown in the excerpt on your archives page.",
icon: (
<svg
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
focusable="false"
>
<path d="M4 9v1.5h16V9H4zm12 5.5h4V13h-4v1.5zm-6 0h4V13h-4v1.5zm-6 0h4V13H4v1.5z" />
</svg>
),
},
{
title: "Page Break",
category: "Design",
description: "Separate your content into a multi-page experience.",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
aria-hidden="true"
focusable="false"
>
<path d="M7.8 6c0-.7.6-1.2 1.2-1.2h6c.7 0 1.2.6 1.2 1.2v3h1.5V6c0-1.5-1.2-2.8-2.8-2.8H9C7.5 3.2 6.2 4.5 6.2 6v3h1.5V6zm8.4 11c0 .7-.6 1.2-1.2 1.2H9c-.7 0-1.2-.6-1.2-1.2v-3H6.2v3c0 1.5 1.2 2.8 2.8 2.8h6c1.5 0 2.8-1.2 2.8-2.8v-3h-1.5v3zM4 11v1h16v-1H4z" />
</svg>
),
},
{
title: "Preformatted",
category: "Text",
description:
"Add text that respects your spacing and tabs, and also allows styling.",
icon: (
<svg
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
focusable="false"
>
<path d="M18 4H6c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm.5 14c0 .3-.2.5-.5.5H6c-.3 0-.5-.2-.5-.5V6c0-.3.2-.5.5-.5h12c.3 0 .5.2.5.5v12zM7 16.5h6V15H7v1.5zm4-4h6V11h-6v1.5zM9 11H7v1.5h2V11zm6 5.5h2V15h-2v1.5z" />
</svg>
),
},
{
title: "Pullquote",
category: "Text",
description: "Give special visual emphasis to a quote from your text.",
icon: (
<svg
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
focusable="false"
>
<path d="M18 8H6c-1.1 0-2 .9-2 2v4c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2v-4c0-1.1-.9-2-2-2zm.5 6c0 .3-.2.5-.5.5H6c-.3 0-.5-.2-.5-.5v-4c0-.3.2-.5.5-.5h12c.3 0 .5.2.5.5v4zM4 4v1.5h16V4H4zm0 16h16v-1.5H4V20z" />
</svg>
),
},
{
title: "RSS",
category: "Widgets",
description: "Display entries from any RSS or Atom feed.",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
aria-hidden="true"
focusable="false"
>
<path d="M5 10.2h-.8v1.5H5c1.9 0 3.8.8 5.1 2.1 1.4 1.4 2.1 3.2 2.1 5.1v.8h1.5V19c0-2.3-.9-4.5-2.6-6.2-1.6-1.6-3.8-2.6-6.1-2.6zm10.4-1.6C12.6 5.8 8.9 4.2 5 4.2h-.8v1.5H5c3.5 0 6.9 1.4 9.4 3.9s3.9 5.8 3.9 9.4v.8h1.5V19c0-3.9-1.6-7.6-4.4-10.4zM4 20h3v-3H4v3z" />
</svg>
),
},
{
title: "Search",
category: "Widgets",
description: "Help visitors find your content.",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
aria-hidden="true"
focusable="false"
>
<path d="M13.5 6C10.5 6 8 8.5 8 11.5c0 1.1.3 2.1.9 3l-3.4 3 1 1.1 3.4-2.9c1 .9 2.2 1.4 3.6 1.4 3 0 5.5-2.5 5.5-5.5C19 8.5 16.5 6 13.5 6zm0 9.5c-2.2 0-4-1.8-4-4s1.8-4 4-4 4 1.8 4 4-1.8 4-4 4z" />
</svg>
),
},
{
title: "Separator",
category: "Design",
description:
"Create a break between ideas or sections with a horizontal separator.",
icon: (
<svg
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
focusable="false"
>
<path d="M20.2 7v4H3.8V7H2.2v9h1.6v-3.5h16.4V16h1.6V7z" />
</svg>
),
},
{
title: "Social Icons",
category: "Widgets",
description:
"Display icons linking to your social media profiles or websites.",
icon: (
<svg
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
focusable="false"
>
<path d="M9 11.8l6.1-4.5c.1.4.4.7.9.7h2c.6 0 1-.4 1-1V5c0-.6-.4-1-1-1h-2c-.6 0-1 .4-1 1v.4l-6.4 4.8c-.2-.1-.4-.2-.6-.2H6c-.6 0-1 .4-1 1v2c0 .6.4 1 1 1h2c.2 0 .4-.1.6-.2l6.4 4.8v.4c0 .6.4 1 1 1h2c.6 0 1-.4 1-1v-2c0-.6-.4-1-1-1h-2c-.5 0-.8.3-.9.7L9 12.2v-.4z" />
</svg>
),
},
{
title: "Spacer",
category: "Design",
description: "Add white space between blocks and customize its height.",
icon: (
<svg
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
focusable="false"
>
<path d="M12.5 4.2v1.6h4.7L5.8 17.2V12H4.2v7.8H12v-1.6H6.8L18.2 6.8v4.7h1.6V4.2z" />
</svg>
),
},
{
title: "Table",
category: "Text",
description: "Insert a table — perfect for sharing charts and data.",
icon: (
<svg
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
focusable="false"
>
<path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zM5 4.5h14c.3 0 .5.2.5.5v3.5h-15V5c0-.3.2-.5.5-.5zm8 5.5h6.5v3.5H13V10zm-1.5 3.5h-7V10h7v3.5zm-7 5.5v-4h7v4.5H5c-.3 0-.5-.2-.5-.5zm14.5.5h-6V15h6.5v4c0 .3-.2.5-.5.5z" />
</svg>
),
},
{
title: "Tag Cloud",
category: "Widgets",
description: "A cloud of your most used tags.",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
aria-hidden="true"
focusable="false"
>
<path d="M20.1 11.2l-6.7-6.7c-.1-.1-.3-.2-.5-.2H5c-.4-.1-.8.3-.8.7v7.8c0 .2.1.4.2.5l6.7 6.7c.2.2.5.4.7.5s.6.2.9.2c.3 0 .6-.1.9-.2.3-.1.5-.3.8-.5l5.6-5.6c.4-.4.7-1 .7-1.6.1-.6-.2-1.2-.6-1.6zM19 13.4L13.4 19c-.1.1-.2.1-.3.2-.2.1-.4.1-.6 0-.1 0-.2-.1-.3-.2l-6.5-6.5V5.8h6.8l6.5 6.5c.2.2.2.4.2.6 0 .1 0 .3-.2.5zM9 8c-.6 0-1 .4-1 1s.4 1 1 1 1-.4 1-1-.4-1-1-1z" />
</svg>
),
},
{
title: "Verse",
category: "Text",
description:
"Insert poetry. Use special spacing formats. Or quote song lyrics.",
icon: (
<svg
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
focusable="false"
>
<path d="M17.8 2l-.9.3c-.1 0-3.6 1-5.2 2.1C10 5.5 9.3 6.5 8.9 7.1c-.6.9-1.7 4.7-1.7 6.3l-.9 2.3c-.2.4 0 .8.4 1 .1 0 .2.1.3.1.3 0 .6-.2.7-.5l.6-1.5c.3 0 .7-.1 1.2-.2.7-.1 1.4-.3 2.2-.5.8-.2 1.6-.5 2.4-.8.7-.3 1.4-.7 1.9-1.2s.8-1.2 1-1.9c.2-.7.3-1.6.4-2.4.1-.8.1-1.7.2-2.5 0-.8.1-1.5.2-2.1V2zm-1.9 5.6c-.1.8-.2 1.5-.3 2.1-.2.6-.4 1-.6 1.3-.3.3-.8.6-1.4.9-.7.3-1.4.5-2.2.8-.6.2-1.3.3-1.8.4L15 7.5c.3-.3.6-.7 1-1.1 0 .4 0 .8-.1 1.2zM6 20h8v-1.5H6V20z" />
</svg>
),
},
{
title: "Video",
category: "Media",
description: "Embed a video from your media library or upload a new one.",
icon: (
<svg
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
focusable="false"
>
<path d="M18.7 3H5.3C4 3 3 4 3 5.3v13.4C3 20 4 21 5.3 21h13.4c1.3 0 2.3-1 2.3-2.3V5.3C21 4 20 3 18.7 3zm.8 15.7c0 .4-.4.8-.8.8H5.3c-.4 0-.8-.4-.8-.8V5.3c0-.4.4-.8.8-.8h13.4c.4 0 .8.4.8.8v13.4zM10 15l5-3-5-3v6z" />
</svg>
),
},
{
title: "Navigation",
category: "Design",
description: "Add a navigation block to your site.",
icon: (
<svg
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
focusable="false"
>
<path d="M12 4c-4.4 0-8 3.6-8 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm0 14.5c-3.6 0-6.5-2.9-6.5-6.5S8.4 5.5 12 5.5s6.5 2.9 6.5 6.5-2.9 6.5-6.5 6.5zM9 16l4.5-3L15 8.4l-4.5 3L9 16z" />
</svg>
),
},
];

View File

@@ -0,0 +1,20 @@
import * as React from "react";
import { usePopoverState, Popover, PopoverDisclosure } from "reakit/Popover";
import { data } from "./data";
import BlockList from "./BlockList";
import ArrowDown from "./ArrowDown";
import "./style.css";
export default function ComboboxListGridWithPopover() {
const popover = usePopoverState({ placement: "bottom-start", gutter: 8 });
return (
<>
<PopoverDisclosure {...popover}>
Blocks <ArrowDown />
</PopoverDisclosure>
<Popover {...popover} aria-label="Blocks">
{popover.visible && <BlockList initialItems={data} />}
</Popover>
</>
);
}

View File

@@ -0,0 +1,147 @@
:root {
--font-family: var(--font-family-body, sans-serif);
--font-size: var(--font-size-body, 16px);
--button-color: var(--color-grayscale-700, #333);
--button-background: transparent;
--button-background-hover: var(--color-primary-50, #e3f2fd);
--button-background-active: var(--color-primary-100, #bbdefb);
--button-border-focus: var(--color-primary-700, #1976d2);
--combobox-background: var(--color-grayscale-50, #eee);
--combobox-color: var(--color-grayscale-700, #333);
--combobox-border: transparent;
--combobox-border-focus: var(--color-primary-700, #1976d2);
--grid-background: var(--color-background-200, #fff);
--grid-color: var(--color-grayscale-700, #333);
--grid-shadow-50: var(--color-shadow-50, rgba(0, 0, 0, 0.05));
--grid-shadow-100: var(--color-shadow-50, rgba(0, 0, 0, 0.1));
--grid-title-color: var(--color-grayscale-400, #999);
--gridcell-background-hover: var(--color-primary-50, #e3f2fd);
--gridcell-background-focus: var(--color-primary-100, #bbdefb);
--footer-color: var(--color-grayscale-600, #666);
}
button {
display: inline-flex;
align-items: center;
font-family: var(--font-family);
background-color: var(--button-background);
color: var(--button-color);
font-size: var(--font-size);
position: relative;
border: 0;
padding: 0 1em;
line-height: 2.5em;
border-radius: 4px;
cursor: pointer;
outline: 0;
}
button svg {
fill: currentColor;
width: 1.5em;
height: 1.5em;
margin-left: 0.25em;
margin-right: -0.25em;
}
button:hover {
background-color: var(--button-background-hover);
}
button[aria-expanded="true"],
button:active {
background-color: var(--button-background-active);
}
button:focus {
box-shadow: 0 0 0 2px var(--button-border-focus);
}
[role="dialog"] {
display: flex;
flex-direction: column;
font-family: var(--font-family);
font-size: var(--font-size);
background-color: var(--grid-background);
color: var(--grid-color);
width: 400px;
max-height: calc(100% - 100px);
z-index: 999;
padding: 1em;
box-sizing: border-box;
border-radius: 4px;
outline: none;
box-shadow:
0 0 8px var(--grid-shadow-50),
0 10px 10px -5px var(--grid-shadow-50),
0 20px 25px -5px var(--grid-shadow-100);
}
[role="combobox"] {
flex: none;
font-family: var(--font-family);
font-size: var(--font-size);
background-color: var(--combobox-background);
color: var(--combobox-color);
border: 1px solid var(--combobox-border);
border-radius: 4px;
height: 2.5em;
padding: 0 1em;
outline: 0;
width: 100%;
box-sizing: border-box;
}
[role="combobox"]:focus {
border-color: var(--combobox-border-focus);
box-shadow: 0 0 0 1px var(--combobox-border-focus);
}
[role="grid"] {
flex: 1;
color: var(--grid-color);
margin: 1em -1em;
padding: 0 1em;
overflow-y: auto;
box-sizing: border-box;
}
[role="grid"] h3 {
font-size: 0.9em;
text-transform: uppercase;
font-weight: 500;
color: var(--grid-title-color);
}
[role="gridcell"] {
display: inline-flex;
flex-direction: column;
align-items: center;
cursor: default;
padding: 1.5em 1em;
border-radius: 4px;
font-size: 0.9em;
width: calc(100% / 3);
box-sizing: border-box;
text-align: center;
}
[role="gridcell"] svg {
width: 1.5em;
margin-bottom: 1em;
fill: currentColor;
}
[role="gridcell"]:hover {
background-color: var(--gridcell-background-hover);
}
[role="combobox"]:focus + [role="grid"] [aria-selected="true"] {
background-color: var(--gridcell-background-focus);
}
[role="dialog"] footer {
padding: 1em;
font-size: 0.85em;
color: var(--footer-color);
}

View File

@@ -0,0 +1,62 @@
import * as React from "react";
import { render, press, click, type, screen } from "reakit-test-utils";
import ComboboxMinValueLength from "..";
test("open combobox popover on click", () => {
render(<ComboboxMinValueLength />);
expect(screen.getByLabelText("Fruits")).not.toBeVisible();
click(screen.getByLabelText("Fruit"));
expect(screen.getByLabelText("Fruits")).not.toBeVisible();
type("a");
expect(screen.getByLabelText("Fruits")).not.toBeVisible();
click(screen.getByLabelText("Fruit"));
expect(screen.getByLabelText("Fruits")).not.toBeVisible();
type("a");
expect(screen.getByLabelText("Fruits")).toBeVisible();
expect(screen.getByText("Apple")).not.toHaveFocus();
press.Escape();
expect(screen.getByLabelText("Fruits")).not.toBeVisible();
click(screen.getByLabelText("Fruit"));
expect(screen.getByLabelText("Fruits")).toBeVisible();
expect(screen.getByText("Apple")).not.toHaveFocus();
});
test("open combobox popover on arrow down", () => {
render(<ComboboxMinValueLength />);
press.Tab();
expect(screen.getByLabelText("Fruits")).not.toBeVisible();
press.ArrowDown();
expect(screen.getByLabelText("Fruits")).not.toBeVisible();
type("a");
expect(screen.getByLabelText("Fruits")).not.toBeVisible();
press.ArrowDown();
expect(screen.getByLabelText("Fruits")).not.toBeVisible();
type("a");
expect(screen.getByLabelText("Fruits")).toBeVisible();
expect(screen.getByText("Apple")).not.toHaveFocus();
press.Escape();
expect(screen.getByLabelText("Fruits")).not.toBeVisible();
press.ArrowDown();
expect(screen.getByLabelText("Fruits")).toBeVisible();
expect(screen.getByText("Apple")).not.toHaveFocus();
});
test("open combobox popover on arrow up", () => {
render(<ComboboxMinValueLength />);
press.Tab();
expect(screen.getByLabelText("Fruits")).not.toBeVisible();
press.ArrowUp();
expect(screen.getByLabelText("Fruits")).not.toBeVisible();
type("a");
expect(screen.getByLabelText("Fruits")).not.toBeVisible();
press.ArrowUp();
expect(screen.getByLabelText("Fruits")).not.toBeVisible();
type("a");
expect(screen.getByLabelText("Fruits")).toBeVisible();
expect(screen.getByText("Apple")).not.toHaveFocus();
press.Escape();
expect(screen.getByLabelText("Fruits")).not.toBeVisible();
press.ArrowUp();
expect(screen.getByLabelText("Fruits")).toBeVisible();
expect(screen.getByText("Apple")).not.toHaveFocus();
});

View File

@@ -0,0 +1,23 @@
import * as React from "react";
import {
unstable_useComboboxState as useComboboxState,
unstable_Combobox as Combobox,
unstable_ComboboxPopover as ComboboxPopover,
unstable_ComboboxOption as ComboboxOption,
} from "reakit/Combobox";
import "./style.css";
export default function ComboboxMinValueLength() {
const combobox = useComboboxState({ minValueLength: 2, gutter: 8 });
return (
<>
<Combobox {...combobox} aria-label="Fruit" />
<ComboboxPopover {...combobox} aria-label="Fruits">
<ComboboxOption {...combobox} value="Apple" />
<ComboboxOption {...combobox} value="Orange" />
<ComboboxOption {...combobox} value="Banana" />
</ComboboxPopover>
</>
);
}

View File

@@ -0,0 +1 @@
@import "../AccessibleCombobox/style.css";

View File

@@ -0,0 +1,64 @@
import * as React from "react";
import { render, press, click, type, screen } from "reakit-test-utils";
import ComboboxVisible from "..";
test("combobox is visible by default", () => {
render(<ComboboxVisible />);
expect(screen.getByLabelText("Colors")).toBeVisible();
click(screen.getByLabelText("Color"));
expect(screen.getByLabelText("Colors")).toBeVisible();
expect(screen.getByText("Red")).not.toHaveFocus();
});
test("type on the combobox", () => {
render(<ComboboxVisible />);
press.Tab();
expect(screen.getByLabelText("Color")).toHaveFocus();
expect(screen.getByLabelText("Colors")).toBeVisible();
type("a");
expect(screen.getByLabelText("Colors")).toBeVisible();
expect(screen.getByText("Red")).not.toHaveFocus();
});
test("close combobox popover by clicking outside", () => {
const { baseElement } = render(<ComboboxVisible />);
expect(screen.getByLabelText("Colors")).toBeVisible();
click(baseElement);
expect(screen.getByLabelText("Colors")).not.toBeVisible();
});
test("close combobox popover by pressing esc", () => {
render(<ComboboxVisible />);
click(screen.getByLabelText("Color"));
press.Escape();
expect(screen.getByLabelText("Colors")).not.toBeVisible();
expect(screen.getByLabelText("Color")).toHaveFocus();
});
test("move through combobox options with keyboard", () => {
render(<ComboboxVisible />);
click(screen.getByLabelText("Color"));
press.ArrowDown();
expect(screen.getByText("Red")).toHaveFocus();
press.ArrowDown();
expect(screen.getByText("Green")).toHaveFocus();
press.ArrowDown();
expect(screen.getByText("Blue")).toHaveFocus();
press.Home();
expect(screen.getByText("Blue")).toHaveFocus();
press.End();
expect(screen.getByText("Blue")).toHaveFocus();
});
test("select combobox option by clicking on it", () => {
render(<ComboboxVisible />);
click(screen.getByLabelText("Color"));
expect(screen.getByLabelText("Color")).toHaveValue("");
click(screen.getByText("Green"));
expect(screen.getByLabelText("Color")).toHaveValue("Green");
expect(screen.getByLabelText("Colors")).not.toBeVisible();
type("\b\b\b\b\b\ba");
expect(screen.getByLabelText("Color")).toHaveValue("a");
click(screen.getByText("Red"));
expect(screen.getByLabelText("Color")).toHaveValue("Red");
});

View File

@@ -0,0 +1,23 @@
import * as React from "react";
import {
unstable_useComboboxState as useComboboxState,
unstable_Combobox as Combobox,
unstable_ComboboxPopover as ComboboxPopover,
unstable_ComboboxOption as ComboboxOption,
} from "reakit/Combobox";
import "./style.css";
export default function ComboboxVisible() {
const combobox = useComboboxState({ visible: true, gutter: 8 });
return (
<>
<Combobox {...combobox} aria-label="Color" />
<ComboboxPopover {...combobox} aria-label="Colors">
<ComboboxOption {...combobox} value="Red" />
<ComboboxOption {...combobox} value="Green" />
<ComboboxOption {...combobox} value="Blue" />
</ComboboxPopover>
</>
);
}

View File

@@ -0,0 +1 @@
@import "../AccessibleCombobox/style.css";

View File

@@ -0,0 +1,18 @@
import { unstable_Combobox as Combobox } from "../Combobox";
export { default as AccessibleCombobox } from "./AccessibleCombobox";
export { default as ComboboxAutoSelect } from "./ComboboxAutoSelect";
export { default as ComboboxBothAutocomplete } from "./ComboboxBothAutocomplete";
export { default as ComboboxBothAutoSelect } from "./ComboboxBothAutoSelect";
export { default as ComboboxFetch } from "./ComboboxFetch";
export { default as ComboboxInline } from "./ComboboxInline";
export { default as ComboboxInlineAutoSelect } from "./ComboboxInlineAutoSelect";
export { default as ComboboxList } from "./ComboboxList";
export { default as ComboboxListGridWithPopover } from "./ComboboxListGridWithPopover";
export { default as ComboboxMinValueLength } from "./ComboboxMinValueLength";
export { default as ComboboxVisible } from "./ComboboxVisible";
export default {
title: "Combobox",
component: Combobox,
};

100
node_modules/reakit/src/Combobox/__keys.ts generated vendored Normal file
View File

@@ -0,0 +1,100 @@
// Automatically generated
const COMBOBOX_LIST_STATE_KEYS = [
"baseId",
"unstable_idCountRef",
"unstable_virtual",
"rtl",
"orientation",
"groups",
"currentId",
"loop",
"wrap",
"shift",
"unstable_moves",
"unstable_hasActiveWidget",
"unstable_includesBaseElement",
"items",
"menuRole",
"inputValue",
"minValueLength",
"currentValue",
"values",
"limit",
"matches",
"list",
"inline",
"autoSelect",
"visible",
"setBaseId",
"unregisterItem",
"registerGroup",
"unregisterGroup",
"move",
"next",
"previous",
"up",
"down",
"first",
"last",
"sort",
"unstable_setVirtual",
"setRTL",
"setOrientation",
"setCurrentId",
"setLoop",
"setWrap",
"setShift",
"reset",
"unstable_setIncludesBaseElement",
"unstable_setHasActiveWidget",
"registerItem",
"setInputValue",
"setMinValueLength",
"setValues",
"setLimit",
"setList",
"setInline",
"setAutoSelect",
] as const;
const COMBOBOX_LIST_GRID_STATE_KEYS = [
...COMBOBOX_LIST_STATE_KEYS,
"columns",
"setColumns",
] as const;
const COMBOBOX_STATE_KEYS = [
...COMBOBOX_LIST_STATE_KEYS,
"animated",
"animating",
"modal",
"unstable_disclosureRef",
"unstable_referenceRef",
"unstable_popoverRef",
"unstable_arrowRef",
"unstable_popoverStyles",
"unstable_arrowStyles",
"unstable_originalPlacement",
"unstable_update",
"placement",
"show",
"hide",
"toggle",
"setVisible",
"setAnimated",
"stopAnimation",
"setModal",
"place",
] as const;
const COMBOBOX_GRID_STATE_KEYS = [
...COMBOBOX_LIST_GRID_STATE_KEYS,
...COMBOBOX_STATE_KEYS,
] as const;
export const COMBOBOX_KEYS = [
...COMBOBOX_GRID_STATE_KEYS,
"hideOnEsc",
] as const;
export const COMBOBOX_GRID_CELL_KEYS = COMBOBOX_GRID_STATE_KEYS;
export const COMBOBOX_GRID_ROW_KEYS = COMBOBOX_GRID_CELL_KEYS;
export const COMBOBOX_ITEM_KEYS = [...COMBOBOX_GRID_ROW_KEYS, "value"] as const;
export const COMBOBOX_LIST_KEYS = COMBOBOX_GRID_ROW_KEYS;
export const COMBOBOX_OPTION_KEYS = COMBOBOX_LIST_KEYS;
export const COMBOBOX_POPOVER_KEYS = COMBOBOX_OPTION_KEYS;

View File

@@ -0,0 +1,324 @@
import * as React from "react";
import { SetState } from "reakit-utils/types";
import {
CompositeStateReturn,
CompositeState,
CompositeActions,
} from "../../Composite/CompositeState";
import { Item } from "./types";
function escapeRegExp(string: string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function getMatches(
inputValue: ComboboxBaseState["inputValue"],
values: ComboboxBaseState["values"],
limit: ComboboxBaseState["limit"],
list: ComboboxBaseState["list"],
autoSelect: ComboboxBaseState["autoSelect"],
minValueLength: ComboboxBaseState["minValueLength"]
) {
if (limit === 0 || inputValue.length < minValueLength) {
// We don't want to populate combobox.matches if inputValue doesn't have
// enough characters.
return [];
}
const length = limit === false ? undefined : limit;
if (!list) {
// If list is false, this means that values aren't expected to be filtered.
return values.slice(0, length);
}
const regex = new RegExp(escapeRegExp(inputValue), "i");
const matches: string[] = [];
if (autoSelect) {
const match = values.find((value) => value.search(regex) === 0);
if (match) {
matches.push(match);
}
}
for (const value of values) {
if (length && matches.length >= length) {
break;
}
// Excludes first match, that can be auto selected
if (value !== matches[0] && value.search(regex) !== -1) {
matches.push(value);
}
}
return matches;
}
export function useComboboxBaseState<T extends CompositeStateReturn>(
composite: T,
{
inputValue: initialInputValue = "",
minValueLength: initialMinValueLength = 0,
values: initialValues = [],
limit: initialLimit = 10,
list: initialList = !!initialValues.length,
inline: initialInline = false,
autoSelect: initialAutoSelect = false,
}: ComboboxBaseInitialState = {}
): ComboboxBaseStateReturn<T> {
const valuesById = React.useRef<Record<string, string | undefined>>({});
const [inputValue, setInputValue] = React.useState(initialInputValue);
const [minValueLength, setMinValueLength] = React.useState(
initialMinValueLength
);
const [values, setValues] = React.useState(initialValues);
const [limit, setLimit] = React.useState(initialLimit);
const [list, setList] = React.useState(initialList);
const [inline, setInline] = React.useState(initialInline);
const [autoSelect, setAutoSelect] = React.useState(initialAutoSelect);
const matches = React.useMemo(
() =>
getMatches(inputValue, values, limit, list, autoSelect, minValueLength),
[inputValue, values, limit, list, autoSelect, minValueLength]
);
const currentValue = React.useMemo(
() =>
composite.currentId ? valuesById.current[composite.currentId] : undefined,
[valuesById, composite.currentId]
);
const items = React.useMemo(() => {
composite.items.forEach((item) => {
if (item.id) {
(item as Item).value = valuesById.current[item.id];
}
});
return composite.items;
}, [composite.items]);
const registerItem = React.useCallback(
(item: Item) => {
composite.registerItem(item);
if (item.id) {
valuesById.current[item.id] = item.value;
}
},
[composite.registerItem]
);
const unregisterItem = React.useCallback(
(id: string) => {
composite.unregisterItem(id);
delete valuesById.current[id];
},
[composite.unregisterItem]
);
return {
...composite,
menuRole: "listbox",
items,
registerItem,
unregisterItem,
visible: true,
inputValue,
minValueLength,
currentValue,
values,
limit,
matches,
list,
inline,
autoSelect,
setInputValue,
setMinValueLength,
setValues,
setLimit,
setList,
setInline,
setAutoSelect,
};
}
export type ComboboxBaseState<T extends CompositeState = CompositeState> = Omit<
T,
"items"
> & {
/**
* Lists all the combobox items with their `id`, DOM `ref`, `disabled` state,
* `value` and `groupId` if any. This state is automatically updated when
* `registerItem` and `unregisterItem` are called.
* @example
* const combobox = useComboboxState();
* combobox.items.forEach((item) => {
* console.log(item.value);
* });
*/
items: Item[];
/**
* Indicates the type of the suggestions popup.
*/
menuRole: "listbox" | "tree" | "grid" | "dialog";
/**
* Combobox input value that will be used to filter `values` and populate
* the `matches` property.
*/
inputValue: string;
/**
* How many characters are needed for opening the combobox popover and
* populating `matches` with filtered values.
* @default 0
* @example
* const combobox = useComboboxState({
* values: ["Red", "Green"],
* minValueLength: 2,
* });
* combobox.matches; // []
* combobox.setInputValue("g");
* // On next render
* combobox.matches; // []
* combobox.setInputValue("gr");
* // On next render
* combobox.matches; // ["Green"]
*/
minValueLength: number;
/**
* Value of the item that is currently selected.
*/
currentValue?: string;
/**
* Values that will be used to produce `matches`.
* @default []
* @example
* const combobox = useComboboxState({ values: ["Red", "Green"] });
* combobox.matches; // ["Red", "Green"]
* combobox.setInputValue("g");
* // On next render
* combobox.matches; // ["Green"]
*/
values: string[];
/**
* Maximum number of `matches`. If it's set to `false`, there will be no
* limit.
* @default 10
*/
limit: number | false;
/**
* Result of filtering `values` based on `inputValue`.
* @default []
* @example
* const combobox = useComboboxState({ values: ["Red", "Green"] });
* combobox.matches; // ["Red", "Green"]
* combobox.setInputValue("g");
* // On next render
* combobox.matches; // ["Green"]
*/
matches: string[];
/**
* Determines how the combobox options behave: dynamically or statically.
* By default, it's `true` if `values` are provided. Otherwise, it's `false`:
* - If it's `true` and `values` are provided, then they will be
* automatically filtered based on `inputValue` and will populate `matches`.
* - If it's `true` and `values` aren't provided, this means that you'll
* provide and filter values by yourself. `matches` will be empty.
* - If it's `false` and `values` are provided, then they won't be
* automatically filtered and `matches` will be the same as `values`.
* @example
* const withoutValues = useComboboxState();
* withValues.list; // false;
* const withValues = useComboboxState({ values: ["Red", "Green"] });
* withValues.list; // true;
* const withList = useComboboxState({ list: true });
* withValues.list; // true;
* <Combobox list={true} /> // <input aria-autocomplete="list">
*/
list: boolean;
/**
* Determines whether focusing on an option will temporarily change the value
* of the combobox. If it's `true`, focusing on an option will temporarily
* change the combobox value to the option's value.
* @default false
*/
inline: boolean;
/**
* Determines whether the first option will be automatically selected. When
* it's set to `true`, the exact behavior will depend on the value of
* `inline`:
* - If `inline` is `true`, the first option is automatically focused when
* the combobox popover opens and the input value changes to reflect this.
* The inline completion string will be highlighted and will have a selected
* state.
* - If `inline` is `false`, the first option is automatically focused when
* the combobox popover opens, but the input value remains the same.
* @default false
*/
autoSelect: boolean;
/**
* Whether the suggestions popup is visible or not.
*/
visible: boolean;
};
export type ComboboxBaseActions<
T extends CompositeActions = CompositeActions
> = Omit<T, "registerItem"> & {
/**
* Registers a combobox item.
* @example
* const ref = React.useRef();
* const combobox = useComboboxState();
* React.useEffect(() => {
* combobox.registerItem({ ref, id: "id" });
* return () => combobox.unregisterItem("id");
* });
*/
registerItem: (item: Item) => void;
/**
* Sets `inputValue`.
* @example
* const combobox = useComboboxState();
* combobox.setInputValue("new value");
*/
setInputValue: SetState<ComboboxBaseState["inputValue"]>;
/**
* Sets `minValueLength`.
*/
setMinValueLength: SetState<ComboboxBaseState["minValueLength"]>;
/**
* Sets `values`.
* @example
* const combobox = useComboboxState();
* combobox.setValues(["Red", "Green"]);
* combobox.setValues((prevValues) => [...prevValues, "Blue"]);
*/
setValues: SetState<ComboboxBaseState["values"]>;
/**
* Sets `limit`.
*/
setLimit: SetState<ComboboxBaseState["limit"]>;
/**
* Sets `list`.
*/
setList: SetState<ComboboxBaseState["list"]>;
/**
* Sets `inline`.
*/
setInline: SetState<ComboboxBaseState["inline"]>;
/**
* Sets `autoSelect`.
*/
setAutoSelect: SetState<ComboboxBaseState["autoSelect"]>;
};
export type ComboboxBaseInitialState = Pick<
Partial<ComboboxBaseState>,
| "inputValue"
| "minValueLength"
| "values"
| "limit"
| "list"
| "inline"
| "autoSelect"
>;
export type ComboboxBaseStateReturn<
T extends CompositeStateReturn
> = ComboboxBaseState<T> & ComboboxBaseActions<T>;

View File

@@ -0,0 +1,46 @@
import * as React from "react";
import {
PopoverState,
PopoverActions,
PopoverInitialState,
usePopoverState,
} from "../../Popover/PopoverState";
import { unstable_ComboboxListStateReturn as ComboboxListStateReturn } from "../ComboboxListState";
import { unstable_ComboboxListGridStateReturn as ComboboxListGridStateReturn } from "../ComboboxListGridState";
export function useComboboxPopoverState<
T extends ComboboxListStateReturn | ComboboxListGridStateReturn
>(
combobox: T,
{
gutter = 0,
placement = "bottom-start",
...initialState
}: ComboboxPopoverInitialState = {}
) {
const popover = usePopoverState({ gutter, placement, ...initialState });
const visible =
popover.visible && combobox.inputValue.length >= combobox.minValueLength;
React.useEffect(() => {
if (!visible) {
// We need to reset combobox.moves
combobox.reset();
}
}, [visible, combobox.reset]);
return {
...combobox,
...popover,
visible,
};
}
export type ComboboxPopoverState = PopoverState;
export type ComboboxPopoverActions = PopoverActions;
export type ComboboxPopoverInitialState = PopoverInitialState;
export type ComboboxPopoverStateReturn = ComboboxPopoverState &
ComboboxPopoverActions;

View File

@@ -0,0 +1,7 @@
function kebabCase(string: string) {
return string.toLowerCase().replace(/[^a-z0-9]/g, "-");
}
export function getItemId(baseId: string, value: string, id?: string) {
return id || `${baseId}-${kebabCase(value)}`;
}

View File

@@ -0,0 +1,3 @@
export function getMenuId(baseId: string) {
return `${baseId}-menu`;
}

12
node_modules/reakit/src/Combobox/__utils/types.ts generated vendored Normal file
View File

@@ -0,0 +1,12 @@
export type Group = {
id: string;
ref: React.RefObject<HTMLElement>;
};
export type Item = {
id: string | null;
ref: React.RefObject<HTMLElement>;
groupId?: Group["id"];
disabled?: boolean;
value?: string;
};

11
node_modules/reakit/src/Combobox/index.ts generated vendored Normal file
View File

@@ -0,0 +1,11 @@
export * from "./Combobox";
export * from "./ComboboxGridCell";
export * from "./ComboboxGridRow";
export * from "./ComboboxGridState";
export * from "./ComboboxItem";
export * from "./ComboboxList";
export * from "./ComboboxListGridState";
export * from "./ComboboxListState";
export * from "./ComboboxOption";
export * from "./ComboboxPopover";
export * from "./ComboboxState";

397
node_modules/reakit/src/Composite/Composite.ts generated vendored Normal file
View File

@@ -0,0 +1,397 @@
import * as React from "react";
import { createComponent } from "reakit-system/createComponent";
import { createHook } from "reakit-system/createHook";
import { useCreateElement } from "reakit-system/useCreateElement";
import { useForkRef } from "reakit-utils/useForkRef";
import { warning, useWarning } from "reakit-warning";
import { getDocument } from "reakit-utils/getDocument";
import { fireBlurEvent } from "reakit-utils/fireBlurEvent";
import { fireKeyboardEvent } from "reakit-utils/fireKeyboardEvent";
import { isSelfTarget } from "reakit-utils/isSelfTarget";
import { useLiveRef } from "reakit-utils/useLiveRef";
import { canUseDOM } from "reakit-utils/canUseDOM";
import { getNextActiveElementOnBlur } from "reakit-utils/getNextActiveElementOnBlur";
import { useTabbable, TabbableOptions, TabbableHTMLProps } from "../Tabbable";
import { useRole } from "../Role/Role";
import { CompositeStateReturn } from "./CompositeState";
import { Item } from "./__utils/types";
import { groupItems } from "./__utils/groupItems";
import { flatten } from "./__utils/flatten";
import { findFirstEnabledItem } from "./__utils/findFirstEnabledItem";
import { reverse } from "./__utils/reverse";
import { getCurrentId } from "./__utils/getCurrentId";
import { findEnabledItemById } from "./__utils/findEnabledItemById";
import { COMPOSITE_KEYS } from "./__keys";
import { userFocus } from "./__utils/userFocus";
export type CompositeOptions = TabbableOptions &
Pick<
Partial<CompositeStateReturn>,
| "baseId"
| "unstable_virtual"
| "currentId"
| "orientation"
| "unstable_moves"
| "wrap"
| "groups"
> &
Pick<
CompositeStateReturn,
"items" | "setCurrentId" | "first" | "last" | "move"
>;
export type CompositeHTMLProps = TabbableHTMLProps;
export type CompositeProps = CompositeOptions & CompositeHTMLProps;
const isIE11 = canUseDOM && "msCrypto" in window;
function canProxyKeyboardEvent(event: React.KeyboardEvent) {
if (!isSelfTarget(event)) return false;
if (event.metaKey) return false;
if (event.key === "Tab") return false;
return true;
}
function useKeyboardEventProxy(
virtual?: boolean,
currentItem?: Item,
htmlEventHandler?: React.KeyboardEventHandler
) {
const eventHandlerRef = useLiveRef(htmlEventHandler);
return React.useCallback(
(event: React.KeyboardEvent) => {
eventHandlerRef.current?.(event);
if (event.defaultPrevented) return;
if (virtual && canProxyKeyboardEvent(event)) {
const currentElement = currentItem?.ref.current;
if (currentElement) {
if (!fireKeyboardEvent(currentElement, event.type, event)) {
event.preventDefault();
}
// The event will be triggered on the composite item and then
// propagated up to this composite element again, so we can pretend
// that it wasn't called on this component in the first place.
if (event.currentTarget.contains(currentElement)) {
event.stopPropagation();
}
}
}
},
[virtual, currentItem]
);
}
// istanbul ignore next
function useActiveElementRef(elementRef: React.RefObject<HTMLElement>) {
const activeElementRef = React.useRef<HTMLElement | null>(null);
React.useEffect(() => {
const document = getDocument(elementRef.current);
const onFocus = (event: FocusEvent) => {
const target = event.target as HTMLElement;
activeElementRef.current = target;
};
document.addEventListener("focus", onFocus, true);
return () => {
document.removeEventListener("focus", onFocus, true);
};
}, []);
return activeElementRef;
}
function findFirstEnabledItemInTheLastRow(items: Item[]) {
return findFirstEnabledItem(flatten(reverse(groupItems(items))));
}
function isItem(items: Item[], element?: Element | EventTarget | null) {
return items?.some((item) => !!element && item.ref.current === element);
}
function useScheduleUserFocus(currentItem?: Item) {
const currentItemRef = useLiveRef(currentItem);
const [scheduled, schedule] = React.useReducer((n: number) => n + 1, 0);
React.useEffect(() => {
const currentElement = currentItemRef.current?.ref.current;
if (scheduled && currentElement) {
userFocus(currentElement);
}
}, [scheduled]);
return schedule;
}
export const useComposite = createHook<CompositeOptions, CompositeHTMLProps>({
name: "Composite",
compose: [useTabbable],
keys: COMPOSITE_KEYS,
useOptions(options) {
return { ...options, currentId: getCurrentId(options) };
},
useProps(
options,
{
ref: htmlRef,
onFocusCapture: htmlOnFocusCapture,
onFocus: htmlOnFocus,
onBlurCapture: htmlOnBlurCapture,
onKeyDown: htmlOnKeyDown,
onKeyDownCapture: htmlOnKeyDownCapture,
onKeyUpCapture: htmlOnKeyUpCapture,
...htmlProps
}
) {
const ref = React.useRef<HTMLElement>(null);
const currentItem = findEnabledItemById(options.items, options.currentId);
const previousElementRef = React.useRef<HTMLElement | null>(null);
const onFocusCaptureRef = useLiveRef(htmlOnFocusCapture);
const onFocusRef = useLiveRef(htmlOnFocus);
const onBlurCaptureRef = useLiveRef(htmlOnBlurCapture);
const onKeyDownRef = useLiveRef(htmlOnKeyDown);
const scheduleUserFocus = useScheduleUserFocus(currentItem);
// IE 11 doesn't support event.relatedTarget, so we use the active element
// ref instead.
const activeElementRef = isIE11 ? useActiveElementRef(ref) : undefined;
React.useEffect(() => {
const element = ref.current;
if (options.unstable_moves && !currentItem) {
warning(
!element,
"Can't focus composite component because `ref` wasn't passed to component.",
"See https://reakit.io/docs/composite"
);
// If composite.move(null) has been called, the composite container
// will receive focus.
element?.focus();
}
}, [options.unstable_moves, currentItem]);
const onKeyDownCapture = useKeyboardEventProxy(
options.unstable_virtual,
currentItem,
htmlOnKeyDownCapture
);
const onKeyUpCapture = useKeyboardEventProxy(
options.unstable_virtual,
currentItem,
htmlOnKeyUpCapture
);
const onFocusCapture = React.useCallback(
(event: React.FocusEvent) => {
onFocusCaptureRef.current?.(event);
if (event.defaultPrevented) return;
if (!options.unstable_virtual) return;
// IE11 doesn't support event.relatedTarget, so we use the active
// element ref instead.
const previousActiveElement =
activeElementRef?.current ||
(event.relatedTarget as HTMLElement | null);
const previousActiveElementWasItem = isItem(
options.items,
previousActiveElement
);
if (isSelfTarget(event) && previousActiveElementWasItem) {
// Composite has been focused as a result of an item receiving focus.
// The composite item will move focus back to the composite
// container. In this case, we don't want to propagate this
// additional event nor call the onFocus handler passed to
// <Composite onFocus={...} />.
event.stopPropagation();
// We keep track of the previous active item element so we can
// manually fire a blur event on it later when the focus is moved to
// another item on the onBlurCapture event below.
previousElementRef.current = previousActiveElement;
}
},
[options.unstable_virtual, options.items]
);
const onFocus = React.useCallback(
(event: React.FocusEvent) => {
onFocusRef.current?.(event);
if (event.defaultPrevented) return;
if (options.unstable_virtual) {
if (isSelfTarget(event)) {
// This means that the composite element has been focused while the
// composite item has not. For example, by clicking on the
// composite element without touching any item, or by tabbing into
// the composite element. In this case, we want to trigger focus on
// the item, just like it would happen with roving tabindex.
// When it receives focus, the composite item will put focus back
// on the composite element, in which case hasItemWithFocus will be
// true.
scheduleUserFocus();
}
} else if (isSelfTarget(event)) {
// When the roving tabindex composite gets intentionally focused (for
// example, by clicking directly on it, and not on an item), we make
// sure to set the current id to null (which means the composite
// itself is focused).
options.setCurrentId?.(null);
}
},
[options.unstable_virtual, options.setCurrentId]
);
const onBlurCapture = React.useCallback(
(event: React.FocusEvent) => {
onBlurCaptureRef.current?.(event);
if (event.defaultPrevented) return;
if (!options.unstable_virtual) return;
// When virtual is set to true, we move focus from the composite
// container (this component) to the composite item that is being
// selected. Then we move focus back to the composite container. This
// is so we can provide the same API as the roving tabindex method,
// which means people can attach onFocus/onBlur handlers on the
// CompositeItem component regardless of whether it's virtual or not.
// This sequence of blurring and focusing items and composite may be
// confusing, so we ignore intermediate focus and blurs by stopping its
// propagation and not calling the passed onBlur handler (htmlOnBlur).
const currentElement = currentItem?.ref.current || null;
const nextActiveElement = getNextActiveElementOnBlur(event);
const nextActiveElementIsItem = isItem(
options.items,
nextActiveElement
);
if (isSelfTarget(event) && nextActiveElementIsItem) {
// This is an intermediate blur event: blurring the composite
// container to focus an item (nextActiveElement).
if (nextActiveElement === currentElement) {
// The next active element will be the same as the current item in
// the state in two scenarios:
// - Moving focus with keyboard: the state is updated before the
// blur event is triggered, so here the current item is already
// pointing to the next active element.
// - Clicking on the current active item with a pointer: this
// will trigger blur on the composite element and then the next
// active element will be the same as the current item. Clicking on
// an item other than the current one doesn't end up here as the
// currentItem state will be updated only after it.
if (
previousElementRef.current &&
previousElementRef.current !== nextActiveElement
) {
// If there's a previous active item and it's not a click action,
// then we fire a blur event on it so it will work just like if
// it had DOM focus before (like when using roving tabindex).
fireBlurEvent(previousElementRef.current, event);
}
} else if (currentElement) {
// This will be true when the next active element is not the
// current element, but there's a current item. This will only
// happen when clicking with a pointer on a different item, when
// there's already an item selected, in which case currentElement
// is the item that is getting blurred, and nextActiveElement is
// the item that is being clicked.
fireBlurEvent(currentElement, event);
}
// We want to ignore intermediate blur events, so we stop its
// propagation and return early so onFocus will not be called.
event.stopPropagation();
} else {
const targetIsItem = isItem(options.items, event.target);
if (!targetIsItem && currentElement) {
// If target is not a composite item, it may be the composite
// element itself (isSelfTarget) or a tabbable element inside the
// composite widget. This may be triggered by clicking outside the
// composite widget or by tabbing out of it. In either cases we
// want to fire a blur event on the current item.
fireBlurEvent(currentElement, event);
}
}
},
[options.unstable_virtual, options.items, currentItem]
);
const onKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLElement>) => {
onKeyDownRef.current?.(event);
if (event.defaultPrevented) return;
if (options.currentId !== null) return;
if (!isSelfTarget(event)) return;
const isVertical = options.orientation !== "horizontal";
const isHorizontal = options.orientation !== "vertical";
const isGrid = !!options.groups?.length;
const up = () => {
if (isGrid) {
const item = findFirstEnabledItemInTheLastRow(options.items);
if (item?.id) {
options.move?.(item.id);
}
} else {
options.last?.();
}
};
const keyMap = {
ArrowUp: (isGrid || isVertical) && up,
ArrowRight: (isGrid || isHorizontal) && options.first,
ArrowDown: (isGrid || isVertical) && options.first,
ArrowLeft: (isGrid || isHorizontal) && options.last,
Home: options.first,
End: options.last,
PageUp: options.first,
PageDown: options.last,
};
const action = keyMap[event.key as keyof typeof keyMap];
if (action) {
event.preventDefault();
action();
}
},
[
options.currentId,
options.orientation,
options.groups,
options.items,
options.move,
options.last,
options.first,
]
);
return {
ref: useForkRef(ref, htmlRef),
id: options.baseId,
onFocus,
onFocusCapture,
onBlurCapture,
onKeyDownCapture,
onKeyDown,
onKeyUpCapture,
"aria-activedescendant": options.unstable_virtual
? currentItem?.id || undefined
: undefined,
...htmlProps,
};
},
useComposeProps(options, htmlProps) {
htmlProps = useRole(options, htmlProps, true);
const tabbableHTMLProps = useTabbable(options, htmlProps, true);
if (options.unstable_virtual || options.currentId === null) {
// Composite will only be tabbable by default if the focus is managed
// using aria-activedescendant, which requires DOM focus on the container
// element (the composite)
return { tabIndex: 0, ...tabbableHTMLProps };
}
return { ...htmlProps, ref: tabbableHTMLProps.ref };
},
});
export const Composite = createComponent({
as: "div",
useHook: useComposite,
useCreateElement: (type, props, children) => {
useWarning(
!props["aria-label"] && !props["aria-labelledby"],
"You should provide either `aria-label` or `aria-labelledby` props.",
"See https://reakit.io/docs/composite"
);
return useCreateElement(type, props, children);
},
});

Some files were not shown because too many files have changed in this diff Show More