Skip to content

Commit

Permalink
feat(reakit-system): Replace useCompose by useComposeOptions on `…
Browse files Browse the repository at this point in the history
…createHook` (#493)
  • Loading branch information
diegohaz committed Nov 14, 2019
1 parent 5d87e99 commit 50fd7df
Show file tree
Hide file tree
Showing 6 changed files with 286 additions and 39 deletions.
218 changes: 218 additions & 0 deletions packages/reakit-system/src/__tests__/createHook-test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
import * as React from "react";
import { render } from "@testing-library/react";
import { createHook } from "../createHook";
import { SystemProvider } from "../SystemProvider";

type Options = {
a: string;
};

test("useProps", () => {
const useHook = createHook<Options, React.HTMLAttributes<any>>({
useProps(options, htmlProps) {
return {
...htmlProps,
"data-a": options.a
};
}
});
expect(useHook({ a: "a" }, { id: "a" })).toMatchInlineSnapshot(`
Object {
"data-a": "a",
"id": "a",
}
`);
});

test("compose useProps", () => {
const useHook = createHook<Options, React.HTMLAttributes<any>>({
useProps(options, htmlProps) {
return {
...htmlProps,
"data-a": options.a
};
}
});
type Options2 = Options & {
b: string;
};
const useHook2 = createHook<Options2, React.HTMLAttributes<any>>({
compose: [useHook],
useProps(options, htmlProps) {
return {
...htmlProps,
"data-b": options.b
};
}
});
expect(useHook2({ a: "a", b: "b" }, { id: "a" })).toMatchInlineSnapshot(`
Object {
"data-a": "a",
"data-b": "b",
"id": "a",
}
`);
});

test("useOptions", () => {
const useHook = createHook<Options, React.HTMLAttributes<any>>({
useOptions(options) {
return {
...options,
a: "a"
};
},
useProps(options, htmlProps) {
return {
...htmlProps,
id: options.a
};
}
});
expect(useHook()).toEqual({ id: "a" });
});

test("compose useOptions", () => {
const useHook = createHook<Options, React.HTMLAttributes<any>>({
useOptions(options) {
return {
...options,
a: `${options.a}b`
};
}
});
const useHook2 = createHook<Options, React.HTMLAttributes<any>>({
compose: [useHook],
useOptions(options) {
return {
...options,
a: "a"
};
},
useProps(options, htmlProps) {
return {
...htmlProps,
id: options.a
};
}
});
expect(useHook2()).toEqual({ id: "ab" });
});

test("useComposeOptions", () => {
const useHook = createHook<Options, React.HTMLAttributes<any>>({
useProps(options, htmlProps) {
return {
...htmlProps,
id: `${options.a}b`
};
}
});
const useHook2 = createHook<Options, React.HTMLAttributes<any>>({
compose: [useHook],
useComposeOptions(options) {
return { ...options, a: "a" };
}
});
expect(useHook2()).toEqual({ id: "ab" });
});

test("name", () => {
const useHook = createHook<Options, React.HTMLAttributes<any>>({ name: "A" });
expect(useHook.name).toBe("useA");
});

test("name and context", () => {
const useHook = createHook<Options, React.HTMLAttributes<any>>({ name: "A" });
const Test = () => {
return <div {...useHook()} />;
};
const system = {
useAProps: (_: Options, htmlProps: React.HTMLAttributes<any>) => ({
...htmlProps,
id: "a"
})
};
const { baseElement } = render(
<SystemProvider unstable_system={system}>
<Test />
</SystemProvider>
);
expect(baseElement).toMatchInlineSnapshot(`
<body>
<div>
<div
id="a"
/>
</div>
</body>
`);
});

test("name and context with useProps", () => {
const useHook = createHook<Options, React.HTMLAttributes<any>>({
name: "A",
useProps(_, htmlProps) {
return { className: "a", ...htmlProps };
}
});
const Test = () => {
return <div {...useHook()} />;
};
const system = {
useAProps: (_: Options, htmlProps: React.HTMLAttributes<any>) => ({
...htmlProps,
id: "a"
})
};
const { baseElement } = render(
<SystemProvider unstable_system={system}>
<Test />
</SystemProvider>
);
expect(baseElement).toMatchInlineSnapshot(`
<body>
<div>
<div
class="a"
id="a"
/>
</div>
</body>
`);
});

test("name and context with useOptions", () => {
const useHook = createHook<Options, React.HTMLAttributes<any>>({
name: "A",
useOptions(options) {
return {
...options,
a: "a"
};
}
});
const Test = () => {
return <div {...useHook()} />;
};
const system = {
useAProps: (options: Options, htmlProps: React.HTMLAttributes<any>) => ({
...htmlProps,
id: options.a
})
};
const { baseElement } = render(
<SystemProvider unstable_system={system}>
<Test />
</SystemProvider>
);
expect(baseElement).toMatchInlineSnapshot(`
<body>
<div>
<div
id="a"
/>
</div>
</body>
`);
});
56 changes: 45 additions & 11 deletions packages/reakit-system/src/createHook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,48 +6,82 @@ import { useProps } from "./useProps";
type Hook<O = any, P = any> = {
(options?: O, htmlProps?: P): P;
__keys: ReadonlyArray<any>;
__useOptions: (options: O, htmlProps: P) => O;
__propsAreEqual?: (prev: O & P, next: O & P) => boolean;
};

type CreateHookOptions<O, P> = {
name: string;
name?: string;
compose?: Hook | Hook[];
useState?: { (): any; __keys: ReadonlyArray<any> };
useOptions?: (options: O, htmlProps: P) => O;
useProps?: (options: O, htmlProps: P) => P;
useCompose?: (options: O, htmlProps: P) => P;
useComposeOptions?: (options: O, htmlProps: P) => O;
propsAreEqual?: (prev: O & P, next: O & P) => boolean | undefined | null;
keys?: ReadonlyArray<keyof O>;
};

export function createHook<O, P>(options: CreateHookOptions<O, P>) {
const composedHooks = toArray(options.compose) as Hook[];

const useHook: Hook<O, P> = (hookOptions = {} as O, htmlProps = {} as P) => {
const __useOptions = (hookOptions: O, htmlProps: P) => {
// Call the current hook's useOptions first
if (options.useOptions) {
hookOptions = options.useOptions(hookOptions, htmlProps);
}
hookOptions = useOptions(options.name, hookOptions, htmlProps);
// If there's name, call useOptions from the system context
if (options.name) {
hookOptions = useOptions(options.name, hookOptions, htmlProps);
}
return hookOptions;
};

const useHook: Hook<O, P> = (
hookOptions = {} as O,
htmlProps = {} as P,
unstable_ignoreUseOptions = false
) => {
// This won't execute when useHook was called from within another useHook
if (!unstable_ignoreUseOptions) {
hookOptions = __useOptions(hookOptions, htmlProps);
}
// We're already calling composed useOptions here
// That's why we ignoreUseOptions for composed hooks
if (options.compose) {
composedHooks.forEach(hook => {
hookOptions = hook.__useOptions(hookOptions, htmlProps);
});
}
// Call the current hook's useProps
if (options.useProps) {
htmlProps = options.useProps(hookOptions, htmlProps);
}
htmlProps = useProps(options.name, hookOptions, htmlProps) as P;
if (options.useCompose) {
htmlProps = options.useCompose(hookOptions, htmlProps);
} else if (options.compose) {
// If there's name, call useProps from the system context
if (options.name) {
htmlProps = useProps(options.name, hookOptions, htmlProps) as P;
}

if (options.compose) {
if (options.useComposeOptions) {
hookOptions = options.useComposeOptions(hookOptions, htmlProps);
}
composedHooks.forEach(hook => {
htmlProps = hook(hookOptions, htmlProps);
// @ts-ignore The third option is only used internally
htmlProps = hook(hookOptions, htmlProps, true);
});
}
return htmlProps;
};

if (process.env.NODE_ENV !== "production") {
if (process.env.NODE_ENV !== "production" && options.name) {
Object.defineProperty(useHook, "name", {
value: options.name
value: `use${options.name}`
});
}

useHook.__useOptions = __useOptions;

// It's used by createComponent to split option props (keys) and html props
useHook.__keys = [
...composedHooks.reduce((allKeys, hook) => {
allKeys.push(...(hook.__keys || []));
Expand Down
12 changes: 6 additions & 6 deletions packages/reakit/src/Form/FormCheckbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ export const unstable_useFormCheckbox = createHook<
useState: unstable_useFormState,
keys: ["name", "value"],

useOptions(options) {
const state = unstable_getIn(options.values, options.name);
const setState = (value: any) => options.update(options.name, value);
return { ...options, state, setState };
},

useProps(options, { onBlur: htmlOnBlur, ...htmlProps }) {
const isBoolean = typeof options.value === "undefined";

Expand All @@ -74,12 +80,6 @@ export const unstable_useFormCheckbox = createHook<
: {}),
...htmlProps
};
},

useCompose(options, htmlProps) {
const state = unstable_getIn(options.values, options.name);
const setState = (value: any) => options.update(options.name, value);
return useCheckbox({ ...options, state, setState }, htmlProps);
}
}) as <V, P extends DeepPath<V, P>>(
options: unstable_FormCheckboxOptions<V, P>,
Expand Down
19 changes: 8 additions & 11 deletions packages/reakit/src/Form/FormInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@ export const unstable_useFormInput = createHook<
useState: unstable_useFormState,
keys: ["name"],

useOptions(options) {
return {
...options,
unstable_clickOnEnter: false,
unstable_clickOnSpace: false
};
},

useProps(
options,
{ onChange: htmlOnChange, onBlur: htmlOnBlur, ...htmlProps }
Expand All @@ -74,17 +82,6 @@ export const unstable_useFormInput = createHook<
"aria-invalid": shouldShowError(options, options.name),
...htmlProps
};
},

useCompose(options, htmlProps) {
return useTabbable(
{
...options,
unstable_clickOnEnter: false,
unstable_clickOnSpace: false
},
htmlProps
);
}
}) as <V, P extends DeepPath<V, P>>(
options: unstable_FormInputOptions<V, P>,
Expand Down
11 changes: 3 additions & 8 deletions packages/reakit/src/Menu/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ export const useMenu = createHook<MenuOptions, MenuHTMLProps>({
unstable_autoFocusOnShow: !parent,
unstable_autoFocusOnHide: !parentIsMenuBar,
modal: false,
...options
...options,
// will be handled differently from usePopover/useDialog
hideOnEsc: false
};
},

Expand Down Expand Up @@ -121,13 +123,6 @@ export const useMenu = createHook<MenuOptions, MenuHTMLProps>({
onKeyDown: useAllCallbacks(rovingBindings, parentBindings, htmlOnKeyDown),
...htmlProps
};
},

// Need to useCompose instead of useProps to overwrite `hideOnEsc`
// because Menu prop types don't include `hideOnEsc`
useCompose(options, htmlProps) {
htmlProps = useMenuBar(options, htmlProps);
return usePopover({ ...options, hideOnEsc: false }, htmlProps);
}
});

Expand Down
Loading

0 comments on commit 50fd7df

Please sign in to comment.