1 | import * as React from "react";
|
2 | import { createComponent } from "reakit-system/createComponent";
|
3 | import { createHook } from "reakit-system/createHook";
|
4 | import { createOnKeyDown } from "reakit-utils/createOnKeyDown";
|
5 | import { warning } from "reakit-warning";
|
6 | import { useForkRef } from "reakit-utils/useForkRef";
|
7 | import { hasFocusWithin } from "reakit-utils/hasFocusWithin";
|
8 | import { AnyFunction } from "reakit-utils/types";
|
9 | import {
|
10 | ClickableOptions,
|
11 | ClickableHTMLProps,
|
12 | useClickable,
|
13 | } from "../Clickable/Clickable";
|
14 | import {
|
15 | unstable_useId,
|
16 | unstable_IdOptions,
|
17 | unstable_IdHTMLProps,
|
18 | } from "../Id/Id";
|
19 | import { RoverStateReturn } from "./RoverState";
|
20 | import { ROVER_KEYS } from "./__keys";
|
21 |
|
22 | export 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 |
|
39 |
|
40 | stopId?: string;
|
41 | };
|
42 |
|
43 | export type RoverHTMLProps = ClickableHTMLProps & unstable_IdHTMLProps;
|
44 |
|
45 | export type RoverProps = RoverOptions & RoverHTMLProps;
|
46 |
|
47 | function 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 |
|
56 | export 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 |
|
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 |
|
117 |
|
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 |
|
151 | export const Rover = createComponent({
|
152 | as: "button",
|
153 | useHook: useRover,
|
154 | });
|