1 |
|
2 | import * as React from "react";
|
3 | import {
|
4 | SealedInitialState,
|
5 | useSealedState,
|
6 | } from "reakit-utils/useSealedState";
|
7 | import {
|
8 | unstable_IdState,
|
9 | unstable_IdActions,
|
10 | unstable_IdInitialState,
|
11 | unstable_useIdState,
|
12 | } from "../Id/IdState";
|
13 |
|
14 | type Stop = {
|
15 | id: string;
|
16 | ref: React.RefObject<HTMLElement>;
|
17 | };
|
18 |
|
19 | export type RoverState = unstable_IdState & {
|
20 | |
21 |
|
22 |
|
23 | orientation?: "horizontal" | "vertical";
|
24 | |
25 |
|
26 |
|
27 | stops: Stop[];
|
28 | |
29 |
|
30 |
|
31 | currentId: Stop["id"] | null;
|
32 | |
33 |
|
34 |
|
35 |
|
36 | unstable_pastId: Stop["id"] | null;
|
37 | |
38 |
|
39 |
|
40 |
|
41 | unstable_moves: number;
|
42 | |
43 |
|
44 |
|
45 |
|
46 |
|
47 | loop: boolean;
|
48 | };
|
49 |
|
50 | export type RoverActions = unstable_IdActions & {
|
51 | |
52 |
|
53 |
|
54 | register: (id: Stop["id"], ref: Stop["ref"]) => void;
|
55 | |
56 |
|
57 |
|
58 | unregister: (id: Stop["id"]) => void;
|
59 | |
60 |
|
61 |
|
62 | move: (id: Stop["id"] | null, unstable_silent?: boolean) => void;
|
63 | |
64 |
|
65 |
|
66 | next: () => void;
|
67 | |
68 |
|
69 |
|
70 | previous: () => void;
|
71 | |
72 |
|
73 |
|
74 | first: () => void;
|
75 | |
76 |
|
77 |
|
78 | last: () => void;
|
79 | |
80 |
|
81 |
|
82 |
|
83 | unstable_reset: () => void;
|
84 | |
85 |
|
86 |
|
87 |
|
88 | unstable_orientate: (orientation: RoverState["orientation"]) => void;
|
89 | };
|
90 |
|
91 | export type RoverInitialState = unstable_IdInitialState &
|
92 | Partial<Pick<RoverState, "orientation" | "currentId" | "loop">>;
|
93 |
|
94 | export type RoverStateReturn = RoverState & RoverActions;
|
95 |
|
96 | type RoverAction =
|
97 | | { type: "register"; id: Stop["id"]; ref: Stop["ref"] }
|
98 | | { type: "unregister"; id: Stop["id"] }
|
99 | | { type: "move"; id: Stop["id"] | null; silent?: boolean }
|
100 | | { type: "next" }
|
101 | | { type: "previous" }
|
102 | | { type: "first" }
|
103 | | { type: "last" }
|
104 | | { type: "reset" }
|
105 | | {
|
106 | type: "orientate";
|
107 | orientation?: RoverState["orientation"];
|
108 | };
|
109 |
|
110 | type RoverReducerState = Omit<RoverState, keyof unstable_IdState>;
|
111 |
|
112 | function reducer(
|
113 | state: RoverReducerState,
|
114 | action: RoverAction
|
115 | ): RoverReducerState {
|
116 | const {
|
117 | stops,
|
118 | currentId,
|
119 | unstable_pastId: pastId,
|
120 | unstable_moves: moves,
|
121 | loop,
|
122 | } = state;
|
123 |
|
124 | switch (action.type) {
|
125 | case "register": {
|
126 | const { id, ref } = action;
|
127 | if (stops.length === 0) {
|
128 | return {
|
129 | ...state,
|
130 | stops: [{ id, ref }],
|
131 | };
|
132 | }
|
133 |
|
134 | const index = stops.findIndex((stop) => stop.id === id);
|
135 |
|
136 | if (index >= 0) {
|
137 | return state;
|
138 | }
|
139 |
|
140 | const indexToInsertAt = stops.findIndex((stop) => {
|
141 | if (!stop.ref.current || !ref.current) return false;
|
142 |
|
143 |
|
144 | return Boolean(
|
145 | stop.ref.current.compareDocumentPosition(ref.current) &
|
146 | Node.DOCUMENT_POSITION_PRECEDING
|
147 | );
|
148 | });
|
149 |
|
150 |
|
151 |
|
152 |
|
153 | if (indexToInsertAt === -1) {
|
154 | return {
|
155 | ...state,
|
156 | stops: [...stops, { id, ref }],
|
157 | };
|
158 | }
|
159 | return {
|
160 | ...state,
|
161 | stops: [
|
162 | ...stops.slice(0, indexToInsertAt),
|
163 | { id, ref },
|
164 | ...stops.slice(indexToInsertAt),
|
165 | ],
|
166 | };
|
167 | }
|
168 | case "unregister": {
|
169 | const { id } = action;
|
170 | const nextStops = stops.filter((stop) => stop.id !== id);
|
171 | if (nextStops.length === stops.length) {
|
172 | return state;
|
173 | }
|
174 |
|
175 | return {
|
176 | ...state,
|
177 | stops: nextStops,
|
178 | unstable_pastId: pastId && pastId === id ? null : pastId,
|
179 | currentId: currentId && currentId === id ? null : currentId,
|
180 | };
|
181 | }
|
182 | case "move": {
|
183 | const { id, silent } = action;
|
184 | const nextMoves = silent ? moves : moves + 1;
|
185 |
|
186 | if (id === null) {
|
187 | return {
|
188 | ...state,
|
189 | currentId: null,
|
190 | unstable_pastId: currentId,
|
191 | unstable_moves: nextMoves,
|
192 | };
|
193 | }
|
194 |
|
195 | const index = stops.findIndex((stop) => stop.id === id);
|
196 |
|
197 |
|
198 | if (index === -1) {
|
199 | return state;
|
200 | }
|
201 |
|
202 | if (stops[index].id === currentId) {
|
203 | return { ...state, unstable_moves: nextMoves };
|
204 | }
|
205 |
|
206 | return {
|
207 | ...state,
|
208 | currentId: stops[index].id,
|
209 | unstable_pastId: currentId,
|
210 | unstable_moves: nextMoves,
|
211 | };
|
212 | }
|
213 | case "next": {
|
214 | if (currentId == null) {
|
215 | return reducer(state, { type: "move", id: stops[0] && stops[0].id });
|
216 | }
|
217 | const index = stops.findIndex((stop) => stop.id === currentId);
|
218 |
|
219 |
|
220 |
|
221 | const reorderedStops = [
|
222 | ...stops.slice(index + 1),
|
223 | ...(loop ? stops.slice(0, index) : []),
|
224 | ];
|
225 |
|
226 | const nextIndex =
|
227 | reorderedStops.findIndex((stop) => stop.id === currentId) + 1;
|
228 |
|
229 | return reducer(state, {
|
230 | type: "move",
|
231 | id: reorderedStops[nextIndex] && reorderedStops[nextIndex].id,
|
232 | });
|
233 | }
|
234 | case "previous": {
|
235 | const { stops: _, ...nextState } = reducer(
|
236 | { ...state, stops: stops.slice().reverse() },
|
237 | { type: "next" }
|
238 | );
|
239 | return {
|
240 | ...state,
|
241 | ...nextState,
|
242 | };
|
243 | }
|
244 | case "first": {
|
245 | const stop = stops[0];
|
246 | return reducer(state, { type: "move", id: stop && stop.id });
|
247 | }
|
248 | case "last": {
|
249 | const stop = stops[stops.length - 1];
|
250 | return reducer(state, { type: "move", id: stop && stop.id });
|
251 | }
|
252 | case "reset": {
|
253 | return {
|
254 | ...state,
|
255 | currentId: null,
|
256 | unstable_pastId: null,
|
257 | };
|
258 | }
|
259 | case "orientate":
|
260 | return { ...state, orientation: action.orientation };
|
261 | default:
|
262 | throw new Error();
|
263 | }
|
264 | }
|
265 |
|
266 | export function useRoverState(
|
267 | initialState: SealedInitialState<RoverInitialState> = {}
|
268 | ): RoverStateReturn {
|
269 | const {
|
270 | orientation,
|
271 | currentId = null,
|
272 | loop = false,
|
273 | ...sealed
|
274 | } = useSealedState(initialState);
|
275 | const [state, dispatch] = React.useReducer(reducer, {
|
276 | orientation,
|
277 | stops: [],
|
278 | currentId,
|
279 | unstable_pastId: null,
|
280 | unstable_moves: 0,
|
281 | loop,
|
282 | });
|
283 |
|
284 | const idState = unstable_useIdState(sealed);
|
285 |
|
286 | return {
|
287 | ...idState,
|
288 | ...state,
|
289 | register: useAction((id, ref) => dispatch({ type: "register", id, ref })),
|
290 | unregister: useAction((id) => dispatch({ type: "unregister", id })),
|
291 | move: useAction((id, silent) => dispatch({ type: "move", id, silent })),
|
292 | next: useAction(() => dispatch({ type: "next" })),
|
293 | previous: useAction(() => dispatch({ type: "previous" })),
|
294 | first: useAction(() => dispatch({ type: "first" })),
|
295 | last: useAction(() => dispatch({ type: "last" })),
|
296 | unstable_reset: useAction(() => dispatch({ type: "reset" })),
|
297 | unstable_orientate: useAction((o) =>
|
298 | dispatch({ type: "orientate", orientation: o })
|
299 | ),
|
300 | };
|
301 | }
|
302 |
|
303 | function useAction<T extends (...args: any[]) => any>(fn: T) {
|
304 | return React.useCallback(fn, []);
|
305 | }
|