Skip to content

Commit

Permalink
feat: Add experimental Id module
Browse files Browse the repository at this point in the history
  • Loading branch information
diegohaz committed Nov 14, 2019
1 parent 98c59f3 commit 3f20ea5
Show file tree
Hide file tree
Showing 13 changed files with 886 additions and 0 deletions.
5 changes: 5 additions & 0 deletions packages/reakit-playground/src/__deps/reakit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ export default {
"reakit/Menu/MenuBar": require("reakit/Menu/MenuBar"),
"reakit/Menu/MenuArrow": require("reakit/Menu/MenuArrow"),
"reakit/Menu/Menu": require("reakit/Menu/Menu"),
"reakit/Id": require("reakit/Id"),
"reakit/Id/IdState": require("reakit/Id/IdState"),
"reakit/Id/IdProvider": require("reakit/Id/IdProvider"),
"reakit/Id/IdGroup": require("reakit/Id/IdGroup"),
"reakit/Id/Id": require("reakit/Id/Id"),
"reakit/Hidden": require("reakit/Hidden"),
"reakit/Hidden/HiddenState": require("reakit/Hidden/HiddenState"),
"reakit/Hidden/HiddenDisclosure": require("reakit/Hidden/HiddenDisclosure"),
Expand Down
1 change: 1 addition & 0 deletions packages/reakit/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
/Portal
/Popover
/Menu
/Id
/Hidden
/Group
/Form
Expand Down
67 changes: 67 additions & 0 deletions packages/reakit/src/Id/Id.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import * as React from "react";
import { createComponent } from "reakit-system/createComponent";
import { createHook } from "reakit-system/createHook";
import { BoxOptions, BoxHTMLProps, useBox } from "../Box/Box";
import { unstable_IdStateReturn, unstable_useIdState } from "./IdState";
import { unstable_IdContext } from "./IdProvider";

export type unstable_IdOptions = BoxOptions &
Pick<Partial<unstable_IdStateReturn>, "baseId" | "unstable_idCountRef"> & {
/**
* Same as the HTML attribute.
*/
id?: string;
};

export type unstable_IdHTMLProps = BoxHTMLProps;

export type unstable_IdProps = unstable_IdOptions & unstable_IdHTMLProps;

export const unstable_useId = createHook<
unstable_IdOptions,
unstable_IdHTMLProps
>({
name: "Id",
compose: useBox,
useState: unstable_useIdState,

useOptions(options, htmlProps) {
const generateId = React.useContext(unstable_IdContext);

const [suffix] = React.useState(() => {
// This comes from useIdState
if (options.unstable_idCountRef) {
options.unstable_idCountRef.current += 1;
return `-${options.unstable_idCountRef.current}`;
}
// If there's no useIdState, we check if `baseId` was passed (as a prop,
// not from useIdState).
if (options.baseId) {
return `-${generateId("")}`;
}
return "";
});

// `baseId` will be the prop passed directly as a prop or via useIdState.
// If there's neither, then it'll fallback to Context's generateId.
// This generateId can result in a sequential ID (if there's a Provider)
// or a random string (without Provider).
const baseId = React.useMemo(() => options.baseId || generateId(), [
options.baseId,
generateId
]);

const id = htmlProps.id || options.id || `${baseId}${suffix}`;

return { ...options, id };
},

useProps(options, { id, ...htmlProps }) {
return { id: id || options.id, ...htmlProps };
}
});

export const unstable_Id = createComponent({
as: "div",
useHook: unstable_useId
});
51 changes: 51 additions & 0 deletions packages/reakit/src/Id/IdGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import * as React from "react";
import { createComponent } from "reakit-system/createComponent";
import { createHook } from "reakit-system/createHook";
import { BoxOptions, BoxHTMLProps, useBox } from "../Box/Box";
import { unstable_IdStateReturn, unstable_useIdState } from "./IdState";
import { unstable_IdContext } from "./IdProvider";

export type unstable_IdGroupOptions = BoxOptions &
Pick<
Partial<unstable_IdStateReturn>,
"baseId" | "unstable_setBaseId" | "unstable_idCountRef"
> & {
/**
* Same as the HTML attribute.
*/
id?: string;
};

export type unstable_IdGroupHTMLProps = BoxHTMLProps;

export type unstable_IdGroupProps = unstable_IdGroupOptions &
unstable_IdGroupHTMLProps;

export const unstable_useIdGroup = createHook<
unstable_IdGroupOptions,
unstable_IdGroupHTMLProps
>({
name: "IdGroup",
compose: useBox,
useState: unstable_useIdState,

useOptions(options, htmlProps) {
const generateId = React.useContext(unstable_IdContext);
const [baseId] = React.useState(
() => htmlProps.id || options.id || options.baseId || generateId()
);

// If there's useIdState and IdGroup has received a different id, then set
// the baseId on the state.
if (options.unstable_setBaseId && baseId !== options.baseId) {
options.unstable_setBaseId(baseId);
}

return { ...options, baseId };
}
});

export const unstable_IdGroup = createComponent({
as: "div",
useHook: unstable_useIdGroup
});
27 changes: 27 additions & 0 deletions packages/reakit/src/Id/IdProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import * as React from "react";
import { generateRandomString, defaultPrefix } from "./__utils";

export type unstable_IdProviderProps = {
children: React.ReactNode;
prefix?: string;
};

export const unstable_IdContext = React.createContext(generateRandomString);

export function unstable_IdProvider({
children,
prefix = defaultPrefix
}: unstable_IdProviderProps) {
const count = React.useRef(0);

const generateId = React.useCallback(
(localPrefix: string = prefix) =>
`${localPrefix ? `${localPrefix}-` : ""}${++count.current}`,
[]
);
return (
<unstable_IdContext.Provider value={generateId}>
{children}
</unstable_IdContext.Provider>
);
}
53 changes: 53 additions & 0 deletions packages/reakit/src/Id/IdState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import * as React from "react";
import {
useSealedState,
SealedInitialState
} from "reakit-utils/useSealedState";
import { unstable_IdContext } from "./IdProvider";

export type unstable_IdState = {
/**
* @private
*/
baseId: string;
/**
* @private
*/
unstable_idCountRef: React.MutableRefObject<number>;
};

export type unstable_IdActions = {
/**
* @private
*/
unstable_setBaseId: React.Dispatch<React.SetStateAction<string>>;
};

export type unstable_IdInitialState = Partial<Pick<unstable_IdState, "baseId">>;

export type unstable_IdStateReturn = unstable_IdState & unstable_IdActions;

export function unstable_useIdState(
initialState: SealedInitialState<unstable_IdInitialState> = {}
): unstable_IdStateReturn {
const { baseId: initialBaseId } = useSealedState(initialState);
const generateId = React.useContext(unstable_IdContext);
const idCountRef = React.useRef(0);
const [baseId, setBaseId] = React.useState(
() => initialBaseId || generateId()
);

return {
baseId,
unstable_setBaseId: setBaseId,
unstable_idCountRef: idCountRef
};
}

const keys: Array<keyof unstable_IdStateReturn> = [
"baseId",
"unstable_setBaseId",
"unstable_idCountRef"
];

unstable_useIdState.__keys = keys;
149 changes: 149 additions & 0 deletions packages/reakit/src/Id/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
---
path: /docs/id/
---

# Id

<blockquote experimental="true">
<strong>This is experimental</strong> and may have breaking changes in minor or patch version updates. Issues for this module will have lower priority. Even so, if you use it, feel free to <a href="https://github.com/reakit/reakit/issues/new/choose" target="_blank">give us feedback</a>.
</blockquote>

`Id` is a component that renders an element with an automatically generated `id` attribute that is consistent across server and client. It's used by several other components.

<carbon-ad></carbon-ad>

## Installation

```sh
npm install reakit
```

Learn more in [Get started](/docs/get-started/).

## Usage

```jsx
import {
unstable_IdProvider as IdProvider,
unstable_Id as Id
} from "reakit/Id";

function Example() {
return (
<IdProvider>
<Id>{props => <div {...props}>{props.id}</div>}</Id>
<Id>{props => <div {...props}>{props.id}</div>}</Id>
</IdProvider>
);
}
```

### `useIdState`

```jsx
import {
unstable_useIdState as useIdState,
unstable_Id as Id
} from "reakit/Id";

function Example() {
const id = useIdState({ baseId: "a" });
return (
<>
<Id {...id}>{props => <div {...props}>{props.id}</div>}</Id>
<Id {...id}>{props => <div {...props}>{props.id}</div>}</Id>
<Id {...id} id="different-id">
{props => <div {...props}>{props.id}</div>}
</Id>
<Id {...id}>{props => <div {...props}>{props.id}</div>}</Id>
</>
);
}
```

### `IdGroup`

```jsx
import {
unstable_useIdState as useIdState,
unstable_IdGroup as IdGroup,
unstable_Id as Id
} from "reakit/Id";

function Example() {
const id = useIdState();
return (
<IdGroup {...id} id="a">
<Id {...id}>{props => <div {...props}>{props.id}</div>}</Id>
<Id {...id}>{props => <div {...props}>{props.id}</div>}</Id>
<Id {...id} id="different-id">
{props => <div {...props}>{props.id}</div>}
</Id>
<Id {...id}>{props => <div {...props}>{props.id}</div>}</Id>
</IdGroup>
);
}
```

### `useId`

```jsx
import {
unstable_IdProvider as IdProvider,
unstable_useId as useId
} from "reakit/Id";

function Item(props) {
const { id } = useId(props);
return (
<div {...props} id={id}>
{id}
</div>
);
}

function Example() {
return (
<IdProvider prefix="a">
<Item />
<Item />
<Item id="different-id" />
<Item />
</IdProvider>
);
}
```

## Accessibility

`Id` renders unique and consistent `id` attributes so they can be used in several `aria-` props.

Learn more in [Accessibility](/docs/accessibility/).

## Composition

- `Id` uses [Box](/docs/box/).

Learn more in [Composition](/docs/composition/#props-hooks).

## Props

<!-- Automatically generated -->

### `useIdState`

No props to show

### `Id`

- **`id`**
<code>string | undefined</code>

Same as the HTML attribute.

### `IdGroup`

- **`id`**
<code>string | undefined</code>

Same as the HTML attribute.
21 changes: 21 additions & 0 deletions packages/reakit/src/Id/__tests__/Id-test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import * as React from "react";
import { render } from "@testing-library/react";
import { unstable_Id as Id } from "../Id";
import { unstable_IdProvider as IdProvider } from "../IdProvider";

test("render", () => {
const { baseElement } = render(
<IdProvider>
<Id />
</IdProvider>
);
expect(baseElement).toMatchInlineSnapshot(`
<body>
<div>
<div
id="id-1"
/>
</div>
</body>
`);
});
Loading

0 comments on commit 3f20ea5

Please sign in to comment.