@hrnec06/react_utils 1.4.1 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,261 @@
1
+ import { disposables, mapRecord, Nullable } from "@hrnec06/util";
2
+ import { useLayoutEffect, useRef, useState } from "react";
3
+ import useFlags from "./useFlags";
4
+ import useDisposables from "./useDisposables";
5
+
6
+ enum TransitionState {
7
+ None = 0,
8
+
9
+ Closed = 1 << 0,
10
+
11
+ Enter = 1 << 1,
12
+ Leave = 1 << 2,
13
+ }
14
+
15
+ export interface TransitionStates {
16
+ enter?: boolean,
17
+ leave?: boolean,
18
+ closed?: boolean,
19
+ transition?: boolean
20
+ }
21
+
22
+ export function transitionDataAttributes(data: TransitionStates)
23
+ {
24
+ return mapRecord(data, (key, value) => [
25
+ `data-${key}`,
26
+ value === true ? "true" : undefined
27
+ ]);
28
+ }
29
+
30
+ export default function useTransition(
31
+ enabled: boolean,
32
+ element: Nullable<HTMLElement>,
33
+ show: boolean
34
+ ): [visible: boolean, data: TransitionStates]
35
+ {
36
+ const [visible, setVisible] = useState(show);
37
+
38
+ const flags = useFlags(
39
+ (enabled && visible) ? TransitionState.Enter | TransitionState.Closed : TransitionState.None
40
+ );
41
+
42
+ const inFlight = useRef(false);
43
+ const cancelledRef = useRef(false);
44
+
45
+ const d = useDisposables();
46
+
47
+ useLayoutEffect(() => {
48
+ if (!enabled) return;
49
+
50
+ if (show)
51
+ {
52
+ setVisible(true);
53
+ }
54
+
55
+ if (!element)
56
+ {
57
+ if (show)
58
+ {
59
+ flags.addFlag(TransitionState.Enter | TransitionState.Closed);
60
+ }
61
+
62
+ return;
63
+ }
64
+
65
+ return transition(
66
+ element,
67
+ {
68
+ inFlight: inFlight,
69
+ prepare: () => {
70
+ if (cancelledRef.current)
71
+ {
72
+ cancelledRef.current = false;
73
+ }
74
+ else
75
+ {
76
+ cancelledRef.current = inFlight.current;
77
+ }
78
+
79
+ inFlight.current = true;
80
+
81
+ if (cancelledRef.current) return;
82
+
83
+ if (show)
84
+ {
85
+ flags.addFlag(TransitionState.Enter | TransitionState.Closed);
86
+ flags.removeFlag(TransitionState.Leave);
87
+ }
88
+ else
89
+ {
90
+ flags.addFlag(TransitionState.Leave);
91
+ flags.removeFlag(TransitionState.Enter);
92
+ }
93
+ },
94
+ run: () => {
95
+ if (cancelledRef.current)
96
+ {
97
+ if (show)
98
+ {
99
+ flags.addFlag(TransitionState.Leave);
100
+ flags.removeFlag(TransitionState.Enter | TransitionState.Closed);
101
+ }
102
+ else
103
+ {
104
+ flags.addFlag(TransitionState.Enter | TransitionState.Closed);
105
+ flags.removeFlag(TransitionState.Leave);
106
+ }
107
+ }
108
+ else
109
+ {
110
+ if (show)
111
+ {
112
+ flags.removeFlag(TransitionState.Closed);
113
+ }
114
+ else
115
+ {
116
+ flags.addFlag(TransitionState.Closed);
117
+ }
118
+ }
119
+ },
120
+ done: () => {
121
+ if (cancelledRef.current)
122
+ {
123
+ if (hasPendingTransitions(element))
124
+ {
125
+ return;
126
+ }
127
+ }
128
+
129
+ inFlight.current = false;
130
+
131
+ flags.removeFlag(TransitionState.Enter | TransitionState.Leave | TransitionState.Closed);
132
+
133
+ if (!show)
134
+ {
135
+ setVisible(false);
136
+ }
137
+ }
138
+ }
139
+ );
140
+ }, [enabled, show, element, d]);
141
+
142
+ if (!enabled)
143
+ {
144
+ return [
145
+ show,
146
+ {
147
+ closed: undefined,
148
+ enter: undefined,
149
+ leave: undefined,
150
+ transition: undefined
151
+ }
152
+ ] as const;
153
+ }
154
+
155
+ return [
156
+ visible,
157
+ {
158
+ closed: flags.hasFlag(TransitionState.Closed),
159
+ enter: flags.hasFlag(TransitionState.Enter),
160
+ leave: flags.hasFlag(TransitionState.Leave),
161
+ transition: flags.hasFlag(TransitionState.Enter) || flags.hasFlag(TransitionState.Leave)
162
+ }
163
+ ] as const;
164
+ }
165
+
166
+ function transition(
167
+ node: HTMLElement,
168
+ {
169
+ prepare,
170
+ run,
171
+ done,
172
+ inFlight
173
+ }: {
174
+ prepare: () => void,
175
+ run: () => void,
176
+ done: () => void,
177
+ inFlight: React.RefObject<boolean>
178
+ }
179
+ )
180
+ {
181
+ const d = disposables();
182
+
183
+ prepareTransition(node, {
184
+ prepare,
185
+ inFlight
186
+ });
187
+
188
+ d.nextFrame(() => {
189
+ run();
190
+
191
+ d.requestAnimationFrame(() => {
192
+ d.add(waitForTransition(node, done));
193
+ });
194
+ });
195
+
196
+ return d.dispose;
197
+ }
198
+
199
+ function waitForTransition(node: Nullable<HTMLElement>, done: () => void)
200
+ {
201
+ const d = disposables();
202
+
203
+ if (!node) return d.dispose;
204
+
205
+ let cancelled = false;
206
+ d.add(() => {
207
+ cancelled = true;
208
+ });
209
+
210
+ const transitions = node.getAnimations?.().filter((animation) => animation instanceof CSSTransition) ?? [];
211
+
212
+ if (transitions.length === 0)
213
+ {
214
+ done();
215
+
216
+ return d.dispose;
217
+ }
218
+
219
+ Promise.allSettled(transitions.map((transition) => transition.finished))
220
+ .then(() => {
221
+ if (!cancelled)
222
+ {
223
+ done();
224
+ }
225
+ });
226
+
227
+ return d.dispose;
228
+ }
229
+
230
+ function prepareTransition(
231
+ node: HTMLElement,
232
+ { inFlight, prepare }: { inFlight?: React.RefObject<boolean>, prepare: () => void }
233
+ )
234
+ {
235
+ if (inFlight?.current)
236
+ {
237
+ prepare();
238
+ return;
239
+ }
240
+
241
+ const previous = node.style.transition;
242
+
243
+ node.style.transition = "none";
244
+
245
+ prepare();
246
+
247
+ // Trigger a reflow, flushing the CSS changes
248
+ node.offsetHeight;
249
+
250
+ // Reset the transition to what it was before
251
+ node.style.transition = previous;
252
+ }
253
+
254
+ function hasPendingTransitions(node: HTMLElement): boolean
255
+ {
256
+ const animations = node.getAnimations?.() ?? []
257
+
258
+ return animations.some((animation) => {
259
+ return animation instanceof CSSTransition && animation.playState !== 'finished'
260
+ })
261
+ }
package/src/index.ts CHANGED
@@ -6,17 +6,20 @@ import useWindowSize from "./hooks/useWindowSize";
6
6
  import useEfficientRef from "./hooks/useEfficientRef";
7
7
  import useEfficientState from "./hooks/useEfficientState";
8
8
  import useUUID from "./hooks/useUUID";
9
+ import useFlags from "./hooks/useFlags";
9
10
 
10
11
  import useSignal, { Signal } from "./hooks/useSignal";
11
-
12
12
  import useLazySignal, { LazySignal } from "./hooks/useLazySignal";
13
13
 
14
- import Debugger from './components/Debugger/Debugger';
14
+ import * as Debugger from './components/Debugger/Debugger';
15
+
15
16
  import ResizeableBox from "./components/ResizeableBox/ResizeableBox";
17
+ import Dialog from "./components/Dialog/Dialog";
16
18
 
17
19
  import * as util from './lib/utils';
18
20
 
19
21
  export {
22
+ // Hooks
20
23
  useKeyListener,
21
24
  useListener,
22
25
  useUpdatedRef,
@@ -25,15 +28,19 @@ export {
25
28
  useEfficientRef,
26
29
  useEfficientState,
27
30
  useUUID,
31
+ useFlags,
28
32
 
33
+ // Signals
29
34
  useSignal,
30
35
  Signal,
31
-
32
36
  useLazySignal,
33
37
  LazySignal,
34
38
 
39
+ // Components
35
40
  Debugger,
36
41
  ResizeableBox,
42
+ Dialog,
37
43
 
38
- util
44
+ // Utilities
45
+ util,
39
46
  };