UNPKG

4.48 kBPlain TextView Raw
1import * as React from "react";
2import { createComponent } from "reakit-system/createComponent";
3import { createHook } from "reakit-system/createHook";
4import { createOnKeyDown } from "reakit-utils/createOnKeyDown";
5import { warning } from "reakit-warning";
6import { useForkRef } from "reakit-utils/useForkRef";
7import { hasFocusWithin } from "reakit-utils/hasFocusWithin";
8import { AnyFunction } from "reakit-utils/types";
9import {
10 ClickableOptions,
11 ClickableHTMLProps,
12 useClickable,
13} from "../Clickable/Clickable";
14import {
15 unstable_useId,
16 unstable_IdOptions,
17 unstable_IdHTMLProps,
18} from "../Id/Id";
19import { RoverStateReturn } from "./RoverState";
20import { ROVER_KEYS } from "./__keys";
21
22export type RoverOptions = ClickableOptions &
23 unstable_IdOptions &
24 Pick<Partial<RoverStateReturn>, "orientation" | "unstable_moves"> &
25 Pick<
26 RoverStateReturn,
27 | "stops"
28 | "currentId"
29 | "register"
30 | "unregister"
31 | "move"
32 | "next"
33 | "previous"
34 | "first"
35 | "last"
36 > & {
37 /**
38 * Element ID.
39 */
40 stopId?: string;
41 };
42
43export type RoverHTMLProps = ClickableHTMLProps & unstable_IdHTMLProps;
44
45export type RoverProps = RoverOptions & RoverHTMLProps;
46
47function useAllCallbacks(
48 ...callbacks: Array<AnyFunction | null | undefined>
49): AnyFunction {
50 return React.useCallback((...args: any[]) => {
51 const fns = callbacks.filter(Boolean) as Array<AnyFunction>;
52 for (const callback of fns) callback(...args);
53 }, callbacks);
54}
55
56export const useRover = createHook<RoverOptions, RoverHTMLProps>({
57 name: "Rover",
58 compose: [useClickable, unstable_useId],
59 keys: ROVER_KEYS,
60
61 useProps(
62 options,
63 {
64 ref: htmlRef,
65 tabIndex: htmlTabIndex = 0,
66 onFocus: htmlOnFocus,
67 onKeyDown: htmlOnKeyDown,
68 ...htmlProps
69 }
70 ) {
71 const ref = React.useRef<HTMLElement>(null);
72 const id = options.stopId || options.id;
73
74 const trulyDisabled = options.disabled && !options.focusable;
75 const noFocused = options.currentId == null;
76 const focused = options.currentId === id;
77 const isFirst = (options.stops || [])[0] && options.stops[0].id === id;
78 const shouldTabIndex = focused || (isFirst && noFocused);
79
80 React.useEffect(() => {
81 if (trulyDisabled || !id) return undefined;
82 options.register && options.register(id, ref);
83 return () => options.unregister && options.unregister(id);
84 }, [id, trulyDisabled, options.register, options.unregister]);
85
86 React.useEffect(() => {
87 const rover = ref.current;
88 if (!rover) {
89 warning(
90 true,
91 "Can't focus rover component because `ref` wasn't passed to component.",
92 "See https://reakit.io/docs/rover"
93 );
94 return;
95 }
96 if (options.unstable_moves && focused && !hasFocusWithin(rover)) {
97 rover.focus();
98 }
99 }, [focused, options.unstable_moves]);
100
101 const onFocus = React.useCallback(
102 (event: React.FocusEvent) => {
103 if (!id || !event.currentTarget.contains(event.target)) return;
104 // this is already focused, so we move silently
105 options.move(id, true);
106 },
107 [options.move, id]
108 );
109
110 const onKeyDown = React.useMemo(
111 () =>
112 createOnKeyDown({
113 onKeyDown: htmlOnKeyDown,
114 stopPropagation: true,
115 shouldKeyDown: (event) =>
116 // Ignore portals
117 // https://github.com/facebook/react/issues/11387
118 event.currentTarget.contains(event.target as Node),
119 keyMap: {
120 ArrowUp: options.orientation !== "horizontal" && options.previous,
121 ArrowRight: options.orientation !== "vertical" && options.next,
122 ArrowDown: options.orientation !== "horizontal" && options.next,
123 ArrowLeft: options.orientation !== "vertical" && options.previous,
124 Home: options.first,
125 End: options.last,
126 PageUp: options.first,
127 PageDown: options.last,
128 },
129 }),
130 [
131 htmlOnKeyDown,
132 options.orientation,
133 options.previous,
134 options.next,
135 options.first,
136 options.last,
137 ]
138 );
139
140 return {
141 id,
142 ref: useForkRef(ref, htmlRef),
143 tabIndex: shouldTabIndex ? htmlTabIndex : -1,
144 onFocus: useAllCallbacks(onFocus, htmlOnFocus),
145 onKeyDown,
146 ...htmlProps,
147 };
148 },
149});
150
151export const Rover = createComponent({
152 as: "button",
153 useHook: useRover,
154});