@octanejs/aria 0.0.1
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.
- package/LICENSE +21 -0
- package/README.md +26 -0
- package/package.json +60 -0
- package/src/index.ts +52 -0
- package/src/interactions/PressResponder.ts +54 -0
- package/src/interactions/Pressable.ts +37 -0
- package/src/interactions/context.ts +20 -0
- package/src/interactions/createEventHandler.ts +94 -0
- package/src/interactions/focusSafely.ts +40 -0
- package/src/interactions/textSelection.ts +96 -0
- package/src/interactions/useFocus.ts +114 -0
- package/src/interactions/useFocusVisible.ts +456 -0
- package/src/interactions/useFocusWithin.ts +165 -0
- package/src/interactions/useFocusable.ts +205 -0
- package/src/interactions/useHover.ts +268 -0
- package/src/interactions/useInteractOutside.ts +161 -0
- package/src/interactions/useKeyboard.ts +50 -0
- package/src/interactions/useLongPress.ts +159 -0
- package/src/interactions/useMove.ts +281 -0
- package/src/interactions/usePress.ts +1249 -0
- package/src/interactions/useScrollWheel.ts +55 -0
- package/src/interactions/utils.ts +212 -0
- package/src/internal.ts +57 -0
- package/src/ssr/SSRProvider.ts +72 -0
- package/src/stately/flags.ts +21 -0
- package/src/stately/index.ts +7 -0
- package/src/stately/utils/number.ts +70 -0
- package/src/stately/utils/useControlledState.ts +75 -0
- package/src/utils/chain.ts +14 -0
- package/src/utils/constants.ts +5 -0
- package/src/utils/domHelpers.ts +36 -0
- package/src/utils/focusWithoutScrolling.ts +83 -0
- package/src/utils/getNonce.ts +53 -0
- package/src/utils/isElementVisible.ts +66 -0
- package/src/utils/isFocusable.ts +57 -0
- package/src/utils/isScrollable.ts +21 -0
- package/src/utils/isVirtualEvent.ts +47 -0
- package/src/utils/mergeProps.ts +76 -0
- package/src/utils/mergeRefs.ts +48 -0
- package/src/utils/openLink.ts +231 -0
- package/src/utils/platform.ts +74 -0
- package/src/utils/runAfterTransition.ts +114 -0
- package/src/utils/shadowdom/DOMFunctions.ts +100 -0
- package/src/utils/shadowdom/ShadowTreeWalker.ts +303 -0
- package/src/utils/useDescription.ts +62 -0
- package/src/utils/useEffectEvent.ts +34 -0
- package/src/utils/useEvent.ts +55 -0
- package/src/utils/useGlobalListeners.ts +91 -0
- package/src/utils/useId.ts +150 -0
- package/src/utils/useLayoutEffect.ts +7 -0
- package/src/utils/useObjectRef.ts +88 -0
- package/src/utils/useSyncRef.ts +39 -0
- package/src/utils/useValueEffect.ts +81 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Ported from react-aria (source: .react-spectrum/packages/react-aria/src/utils/useEvent.ts).
|
|
2
|
+
import type { RefObject } from '@react-types/shared';
|
|
3
|
+
import { useEffect } from 'octane';
|
|
4
|
+
|
|
5
|
+
import { S, splitSlot, subSlot } from '../internal';
|
|
6
|
+
import { useEffectEvent } from './useEffectEvent';
|
|
7
|
+
|
|
8
|
+
export function useEvent<K extends keyof GlobalEventHandlersEventMap>(
|
|
9
|
+
ref: RefObject<EventTarget | null>,
|
|
10
|
+
event: K | (string & {}),
|
|
11
|
+
handler?: (this: Document, ev: GlobalEventHandlersEventMap[K]) => any,
|
|
12
|
+
options?: boolean | AddEventListenerOptions,
|
|
13
|
+
): void;
|
|
14
|
+
// Slot-threading form: sibling ported hooks pass their derived sub-slot as the trailing arg
|
|
15
|
+
// (the optional `options` user arg, when present, comes before it).
|
|
16
|
+
export function useEvent<K extends keyof GlobalEventHandlersEventMap>(
|
|
17
|
+
ref: RefObject<EventTarget | null>,
|
|
18
|
+
event: K | (string & {}),
|
|
19
|
+
handler: ((this: Document, ev: GlobalEventHandlersEventMap[K]) => any) | undefined,
|
|
20
|
+
slot: symbol | undefined,
|
|
21
|
+
): void;
|
|
22
|
+
export function useEvent<K extends keyof GlobalEventHandlersEventMap>(
|
|
23
|
+
ref: RefObject<EventTarget | null>,
|
|
24
|
+
event: K | (string & {}),
|
|
25
|
+
handler: ((this: Document, ev: GlobalEventHandlersEventMap[K]) => any) | undefined,
|
|
26
|
+
options: boolean | AddEventListenerOptions | undefined,
|
|
27
|
+
slot: symbol | undefined,
|
|
28
|
+
): void;
|
|
29
|
+
export function useEvent(...args: any[]): void {
|
|
30
|
+
const [user, slotArg] = splitSlot(args);
|
|
31
|
+
const slot = slotArg ?? S('useEvent');
|
|
32
|
+
const ref = user[0] as { current: EventTarget | null };
|
|
33
|
+
const event = user[1] as string;
|
|
34
|
+
const handler = user[2] as EventListener | undefined;
|
|
35
|
+
const options = user[3] as boolean | AddEventListenerOptions | undefined;
|
|
36
|
+
|
|
37
|
+
let handleEvent = useEffectEvent(handler, subSlot(slot, 'handler'));
|
|
38
|
+
let isDisabled = handler == null;
|
|
39
|
+
|
|
40
|
+
useEffect(
|
|
41
|
+
() => {
|
|
42
|
+
if (isDisabled || !ref.current) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let element = ref.current;
|
|
47
|
+
element.addEventListener(event, handleEvent as EventListener, options);
|
|
48
|
+
return () => {
|
|
49
|
+
element.removeEventListener(event, handleEvent as EventListener, options);
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
[ref, event, options, isDisabled],
|
|
53
|
+
subSlot(slot, 'listen'),
|
|
54
|
+
);
|
|
55
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// Ported from react-aria (source: .react-spectrum/packages/react-aria/src/utils/useGlobalListeners.ts).
|
|
2
|
+
import { useCallback, useEffect, useRef } from 'octane';
|
|
3
|
+
|
|
4
|
+
import { S, splitSlot, subSlot } from '../internal';
|
|
5
|
+
|
|
6
|
+
export interface GlobalListeners {
|
|
7
|
+
addGlobalListener<K extends keyof WindowEventMap>(
|
|
8
|
+
el: Window,
|
|
9
|
+
type: K,
|
|
10
|
+
listener: (this: Document, ev: WindowEventMap[K]) => any,
|
|
11
|
+
options?: boolean | AddEventListenerOptions,
|
|
12
|
+
): void;
|
|
13
|
+
addGlobalListener<K extends keyof DocumentEventMap>(
|
|
14
|
+
el: EventTarget,
|
|
15
|
+
type: K,
|
|
16
|
+
listener: (this: Document, ev: DocumentEventMap[K]) => any,
|
|
17
|
+
options?: boolean | AddEventListenerOptions,
|
|
18
|
+
): void;
|
|
19
|
+
addGlobalListener(
|
|
20
|
+
el: EventTarget,
|
|
21
|
+
type: string,
|
|
22
|
+
listener: EventListenerOrEventListenerObject,
|
|
23
|
+
options?: boolean | AddEventListenerOptions,
|
|
24
|
+
): void;
|
|
25
|
+
removeGlobalListener<K extends keyof DocumentEventMap>(
|
|
26
|
+
el: EventTarget,
|
|
27
|
+
type: K,
|
|
28
|
+
listener: (this: Document, ev: DocumentEventMap[K]) => any,
|
|
29
|
+
options?: boolean | EventListenerOptions,
|
|
30
|
+
): void;
|
|
31
|
+
removeGlobalListener(
|
|
32
|
+
el: EventTarget,
|
|
33
|
+
type: string,
|
|
34
|
+
listener: EventListenerOrEventListenerObject,
|
|
35
|
+
options?: boolean | EventListenerOptions,
|
|
36
|
+
): void;
|
|
37
|
+
removeAllGlobalListeners(): void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function useGlobalListeners(): GlobalListeners;
|
|
41
|
+
// Slot-threading form: sibling ported hooks pass their derived sub-slot as the trailing arg.
|
|
42
|
+
export function useGlobalListeners(slot: symbol | undefined): GlobalListeners;
|
|
43
|
+
export function useGlobalListeners(...args: any[]): GlobalListeners {
|
|
44
|
+
const [, slotArg] = splitSlot(args);
|
|
45
|
+
const slot = slotArg ?? S('useGlobalListeners');
|
|
46
|
+
|
|
47
|
+
let globalListeners = useRef(new Map(), subSlot(slot, 'map'));
|
|
48
|
+
let addGlobalListener = useCallback(
|
|
49
|
+
(eventTarget: any, type: any, listener: any, options: any) => {
|
|
50
|
+
// Make sure we remove the listener after it is called with the `once` option.
|
|
51
|
+
let fn = options?.once
|
|
52
|
+
? (...args: any[]) => {
|
|
53
|
+
globalListeners.current.delete(listener);
|
|
54
|
+
listener(...args);
|
|
55
|
+
}
|
|
56
|
+
: listener;
|
|
57
|
+
globalListeners.current.set(listener, { type, eventTarget, fn, options });
|
|
58
|
+
eventTarget.addEventListener(type, fn, options);
|
|
59
|
+
},
|
|
60
|
+
[],
|
|
61
|
+
subSlot(slot, 'add'),
|
|
62
|
+
);
|
|
63
|
+
let removeGlobalListener = useCallback(
|
|
64
|
+
(eventTarget: any, type: any, listener: any, options: any) => {
|
|
65
|
+
let fn = globalListeners.current.get(listener)?.fn || listener;
|
|
66
|
+
eventTarget.removeEventListener(type, fn, options);
|
|
67
|
+
globalListeners.current.delete(listener);
|
|
68
|
+
},
|
|
69
|
+
[],
|
|
70
|
+
subSlot(slot, 'remove'),
|
|
71
|
+
);
|
|
72
|
+
let removeAllGlobalListeners = useCallback(
|
|
73
|
+
() => {
|
|
74
|
+
globalListeners.current.forEach((value: any, key: any) => {
|
|
75
|
+
removeGlobalListener(value.eventTarget, value.type, key, value.options);
|
|
76
|
+
});
|
|
77
|
+
},
|
|
78
|
+
[removeGlobalListener],
|
|
79
|
+
subSlot(slot, 'removeAll'),
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
useEffect(
|
|
83
|
+
() => {
|
|
84
|
+
return removeAllGlobalListeners;
|
|
85
|
+
},
|
|
86
|
+
[removeAllGlobalListeners],
|
|
87
|
+
subSlot(slot, 'teardown'),
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
return { addGlobalListener, removeGlobalListener, removeAllGlobalListeners } as GlobalListeners;
|
|
91
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// Ported from react-aria (source: .react-spectrum/packages/react-aria/src/utils/useId.ts).
|
|
2
|
+
// The full id machinery ports as-is: `mergeIds` retroactively rewrites the id of every
|
|
3
|
+
// registered `useId` consumer (via the `idsUpdaterMap` ref registry + the run-every-render
|
|
4
|
+
// effect below), so two hooks that each generated an id converge on one after merging.
|
|
5
|
+
import { useEffect, useRef, useState, useCallback } from 'octane';
|
|
6
|
+
|
|
7
|
+
import { S, splitSlot, subSlot } from '../internal';
|
|
8
|
+
import { useSSRSafeId } from '../ssr/SSRProvider';
|
|
9
|
+
import { useLayoutEffect } from './useLayoutEffect';
|
|
10
|
+
import { useValueEffect } from './useValueEffect';
|
|
11
|
+
|
|
12
|
+
let canUseDOM = Boolean(
|
|
13
|
+
typeof window !== 'undefined' && window.document && window.document.createElement,
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
export let idsUpdaterMap: Map<string, { current: string | null }[]> = new Map();
|
|
17
|
+
// This allows us to clean up the idsUpdaterMap when the id is no longer used.
|
|
18
|
+
// Map is a strong reference, so unused ids wouldn't be cleaned up otherwise.
|
|
19
|
+
// This can happen in suspended components where mount/unmount is not called.
|
|
20
|
+
let registry: FinalizationRegistry<string> | undefined;
|
|
21
|
+
if (typeof FinalizationRegistry !== 'undefined') {
|
|
22
|
+
registry = new FinalizationRegistry<string>((heldValue) => {
|
|
23
|
+
idsUpdaterMap.delete(heldValue);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* If a default is not provided, generate an id.
|
|
29
|
+
*
|
|
30
|
+
* @param defaultId - Default component id.
|
|
31
|
+
*/
|
|
32
|
+
export function useId(defaultId?: string): string;
|
|
33
|
+
// Slot-threading form: sibling ported hooks pass their derived sub-slot as the trailing arg.
|
|
34
|
+
export function useId(slot: symbol | undefined): string;
|
|
35
|
+
export function useId(defaultId: string | undefined, slot: symbol | undefined): string;
|
|
36
|
+
export function useId(...args: any[]): string {
|
|
37
|
+
const [user, slotArg] = splitSlot(args);
|
|
38
|
+
const slot = slotArg ?? S('useId');
|
|
39
|
+
const defaultId = user[0] as string | undefined;
|
|
40
|
+
|
|
41
|
+
let [value, setValue] = useState(defaultId, subSlot(slot, 'value'));
|
|
42
|
+
let nextId = useRef<string | null>(null, subSlot(slot, 'nextId'));
|
|
43
|
+
|
|
44
|
+
let res = useSSRSafeId(value, subSlot(slot, 'ssrId'));
|
|
45
|
+
let cleanupRef = useRef(null, subSlot(slot, 'cleanupToken'));
|
|
46
|
+
|
|
47
|
+
if (registry) {
|
|
48
|
+
registry.register(cleanupRef, res);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (canUseDOM) {
|
|
52
|
+
const cacheIdRef = idsUpdaterMap.get(res);
|
|
53
|
+
if (cacheIdRef && !cacheIdRef.includes(nextId)) {
|
|
54
|
+
cacheIdRef.push(nextId);
|
|
55
|
+
} else {
|
|
56
|
+
idsUpdaterMap.set(res, [nextId]);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
useLayoutEffect(
|
|
61
|
+
() => {
|
|
62
|
+
let r = res;
|
|
63
|
+
return () => {
|
|
64
|
+
// In Suspense, the cleanup function may be not called
|
|
65
|
+
// when it is though, also remove it from the finalization registry.
|
|
66
|
+
if (registry) {
|
|
67
|
+
registry.unregister(cleanupRef);
|
|
68
|
+
}
|
|
69
|
+
idsUpdaterMap.delete(r);
|
|
70
|
+
};
|
|
71
|
+
},
|
|
72
|
+
[res],
|
|
73
|
+
subSlot(slot, 'unregister'),
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
// This cannot cause an infinite loop because the ref is always cleaned up.
|
|
77
|
+
useEffect(
|
|
78
|
+
() => {
|
|
79
|
+
let newId = nextId.current;
|
|
80
|
+
if (newId) {
|
|
81
|
+
setValue(newId);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return () => {
|
|
85
|
+
if (newId) {
|
|
86
|
+
nextId.current = null;
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
},
|
|
90
|
+
null,
|
|
91
|
+
subSlot(slot, 'apply'),
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
return res;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Merges two ids.
|
|
99
|
+
* Different ids will trigger a side-effect and re-render components hooked up with `useId`.
|
|
100
|
+
*/
|
|
101
|
+
export function mergeIds(idA: string, idB: string): string {
|
|
102
|
+
if (idA === idB) {
|
|
103
|
+
return idA;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let setIdsA = idsUpdaterMap.get(idA);
|
|
107
|
+
if (setIdsA) {
|
|
108
|
+
setIdsA.forEach((ref) => (ref.current = idB));
|
|
109
|
+
return idB;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let setIdsB = idsUpdaterMap.get(idB);
|
|
113
|
+
if (setIdsB) {
|
|
114
|
+
setIdsB.forEach((ref) => (ref.current = idA));
|
|
115
|
+
return idA;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return idB;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Used to generate an id, and after render, check if that id is rendered so we know
|
|
123
|
+
* if we can use it in places such as labelledby.
|
|
124
|
+
*
|
|
125
|
+
* @param depArray - When to recalculate if the id is in the DOM.
|
|
126
|
+
*/
|
|
127
|
+
export function useSlotId(depArray?: ReadonlyArray<any>): string;
|
|
128
|
+
export function useSlotId(...args: any[]): string {
|
|
129
|
+
const [user, slotArg] = splitSlot(args);
|
|
130
|
+
const slot = slotArg ?? S('useSlotId');
|
|
131
|
+
const depArray = (user[0] as ReadonlyArray<any> | undefined) ?? [];
|
|
132
|
+
|
|
133
|
+
let id = useId(subSlot(slot, 'id'));
|
|
134
|
+
let [resolvedId, setResolvedId] = useValueEffect(id, subSlot(slot, 'resolved'));
|
|
135
|
+
let updateId = useCallback(
|
|
136
|
+
() => {
|
|
137
|
+
setResolvedId(function* () {
|
|
138
|
+
yield id;
|
|
139
|
+
|
|
140
|
+
yield document.getElementById(id) ? id : undefined;
|
|
141
|
+
});
|
|
142
|
+
},
|
|
143
|
+
[id, setResolvedId],
|
|
144
|
+
subSlot(slot, 'update'),
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
useLayoutEffect(updateId, [id, updateId, ...depArray], subSlot(slot, 'run'));
|
|
148
|
+
|
|
149
|
+
return resolvedId as string;
|
|
150
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Ported from react-aria (source: .react-spectrum/packages/react-aria/src/utils/useLayoutEffect.ts).
|
|
2
|
+
// Layout effects never run on the server; the document guard keeps server bundles
|
|
3
|
+
// (where `octane` resolves to the server runtime) from scheduling one at all.
|
|
4
|
+
import { useLayoutEffect as octaneUseLayoutEffect } from 'octane';
|
|
5
|
+
|
|
6
|
+
export const useLayoutEffect: typeof octaneUseLayoutEffect =
|
|
7
|
+
typeof document !== 'undefined' ? octaneUseLayoutEffect : ((() => {}) as any);
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Ported from react-aria (source: .react-spectrum/packages/react-aria/src/utils/useObjectRef.ts).
|
|
2
|
+
// React's `forwardRef` becomes octane's ref-as-prop; this hook adapts whatever ref shape a
|
|
3
|
+
// caller passed (callback ref with optional cleanup, or object ref) into an object ref the
|
|
4
|
+
// aria hooks can read, updating the original on assignment.
|
|
5
|
+
import { useCallback, useMemo, useRef } from 'octane';
|
|
6
|
+
|
|
7
|
+
import { S, splitSlot, subSlot } from '../internal';
|
|
8
|
+
|
|
9
|
+
type MutableRefObject<T> = { current: T };
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Offers an object ref for a given callback ref or an object ref. Especially
|
|
13
|
+
* helpful when passing forwarded refs to aria hooks.
|
|
14
|
+
*
|
|
15
|
+
* @param ref The original ref intended to be used.
|
|
16
|
+
* @returns An object ref that updates the given ref.
|
|
17
|
+
*/
|
|
18
|
+
export function useObjectRef<T>(
|
|
19
|
+
ref?: ((instance: T | null) => (() => void) | void) | MutableRefObject<T | null> | null,
|
|
20
|
+
): MutableRefObject<T | null>;
|
|
21
|
+
// Slot-threading form: sibling ported hooks pass their derived sub-slot as the trailing arg.
|
|
22
|
+
export function useObjectRef<T>(
|
|
23
|
+
ref:
|
|
24
|
+
| ((instance: T | null) => (() => void) | void)
|
|
25
|
+
| MutableRefObject<T | null>
|
|
26
|
+
| null
|
|
27
|
+
| undefined,
|
|
28
|
+
slot: symbol | undefined,
|
|
29
|
+
): MutableRefObject<T | null>;
|
|
30
|
+
export function useObjectRef(...args: any[]): any {
|
|
31
|
+
const [user, slotArg] = splitSlot(args);
|
|
32
|
+
const slot = slotArg ?? S('useObjectRef');
|
|
33
|
+
const ref = user[0] as
|
|
34
|
+
| ((instance: any) => (() => void) | void)
|
|
35
|
+
| MutableRefObject<any>
|
|
36
|
+
| null
|
|
37
|
+
| undefined;
|
|
38
|
+
|
|
39
|
+
const objRef: MutableRefObject<any> = useRef(null, subSlot(slot, 'obj'));
|
|
40
|
+
const cleanupRef: MutableRefObject<(() => void) | void> = useRef(
|
|
41
|
+
undefined,
|
|
42
|
+
subSlot(slot, 'cleanup'),
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const refEffect = useCallback(
|
|
46
|
+
(instance: any) => {
|
|
47
|
+
if (typeof ref === 'function') {
|
|
48
|
+
const refCallback = ref;
|
|
49
|
+
const refCleanup = refCallback(instance);
|
|
50
|
+
return () => {
|
|
51
|
+
if (typeof refCleanup === 'function') {
|
|
52
|
+
refCleanup();
|
|
53
|
+
} else {
|
|
54
|
+
refCallback(null);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
} else if (ref) {
|
|
58
|
+
ref.current = instance;
|
|
59
|
+
return () => {
|
|
60
|
+
ref.current = null;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
[ref],
|
|
65
|
+
subSlot(slot, 'refEffect'),
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
return useMemo(
|
|
69
|
+
() => ({
|
|
70
|
+
get current() {
|
|
71
|
+
return objRef.current;
|
|
72
|
+
},
|
|
73
|
+
set current(value) {
|
|
74
|
+
objRef.current = value;
|
|
75
|
+
if (cleanupRef.current) {
|
|
76
|
+
cleanupRef.current();
|
|
77
|
+
cleanupRef.current = undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (value != null) {
|
|
81
|
+
cleanupRef.current = refEffect(value);
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
}),
|
|
85
|
+
[refEffect],
|
|
86
|
+
subSlot(slot, 'result'),
|
|
87
|
+
);
|
|
88
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Ported from react-aria (source: .react-spectrum/packages/react-aria/src/utils/useSyncRef.ts).
|
|
2
|
+
import type { RefObject } from '@react-types/shared';
|
|
3
|
+
|
|
4
|
+
import { S, splitSlot, subSlot } from '../internal';
|
|
5
|
+
import { useLayoutEffect } from './useLayoutEffect';
|
|
6
|
+
|
|
7
|
+
interface ContextValue<T> {
|
|
8
|
+
ref?: { current: T | null };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Syncs ref from context with ref passed to hook
|
|
12
|
+
export function useSyncRef<T>(context?: ContextValue<T> | null, ref?: RefObject<T | null>): void;
|
|
13
|
+
// Slot-threading form: sibling ported hooks pass their derived sub-slot as the trailing arg.
|
|
14
|
+
export function useSyncRef<T>(
|
|
15
|
+
context: ContextValue<T> | null | undefined,
|
|
16
|
+
ref: RefObject<T | null> | undefined,
|
|
17
|
+
slot: symbol | undefined,
|
|
18
|
+
): void;
|
|
19
|
+
export function useSyncRef(...args: any[]): void {
|
|
20
|
+
const [user, slotArg] = splitSlot(args);
|
|
21
|
+
const slot = slotArg ?? S('useSyncRef');
|
|
22
|
+
const context = user[0] as ContextValue<any> | null | undefined;
|
|
23
|
+
const ref = user[1] as { current: any } | undefined;
|
|
24
|
+
|
|
25
|
+
useLayoutEffect(
|
|
26
|
+
() => {
|
|
27
|
+
if (context && context.ref && ref) {
|
|
28
|
+
context.ref.current = ref.current;
|
|
29
|
+
return () => {
|
|
30
|
+
if (context.ref) {
|
|
31
|
+
context.ref.current = null;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
null,
|
|
37
|
+
subSlot(slot, 'sync'),
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Ported from react-aria (source: .react-spectrum/packages/react-aria/src/utils/useValueEffect.ts).
|
|
2
|
+
import { useCallback, useRef, useState } from 'octane';
|
|
3
|
+
|
|
4
|
+
import { S, splitSlot, subSlot } from '../internal';
|
|
5
|
+
import { useLayoutEffect } from './useLayoutEffect';
|
|
6
|
+
|
|
7
|
+
type SetValueAction<S> = (prev: S) => Generator<any, void, unknown>;
|
|
8
|
+
type Dispatch<A> = (action: A) => void;
|
|
9
|
+
|
|
10
|
+
// This hook works like `useState`, but when setting the value, you pass a generator function
|
|
11
|
+
// that can yield multiple values. Each yielded value updates the state and waits for the next
|
|
12
|
+
// layout effect, then continues the generator. This allows sequential updates to state to be
|
|
13
|
+
// written linearly.
|
|
14
|
+
export function useValueEffect<S>(defaultValue: S | (() => S)): [S, Dispatch<SetValueAction<S>>];
|
|
15
|
+
// Slot-threading form: sibling ported hooks pass their derived sub-slot as the trailing arg.
|
|
16
|
+
export function useValueEffect<S>(
|
|
17
|
+
defaultValue: S | (() => S),
|
|
18
|
+
slot: symbol | undefined,
|
|
19
|
+
): [S, Dispatch<SetValueAction<S>>];
|
|
20
|
+
export function useValueEffect(...args: any[]): any {
|
|
21
|
+
const [user, slotArg] = splitSlot(args);
|
|
22
|
+
const slot = slotArg ?? S('useValueEffect');
|
|
23
|
+
const defaultValue = user[0];
|
|
24
|
+
|
|
25
|
+
let [value, setValue] = useState(defaultValue, subSlot(slot, 'value'));
|
|
26
|
+
// Keep an up to date copy of value in a ref so we can access the current value in the generator.
|
|
27
|
+
// This allows us to maintain a stable queue function.
|
|
28
|
+
let currValue = useRef(value, subSlot(slot, 'curr'));
|
|
29
|
+
let effect = useRef<Generator<any> | null>(null, subSlot(slot, 'effect'));
|
|
30
|
+
|
|
31
|
+
// Store the function in a ref so we can always access the current version
|
|
32
|
+
// which has the proper `value` in scope.
|
|
33
|
+
let nextRef = useRef(
|
|
34
|
+
() => {
|
|
35
|
+
if (!effect.current) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
// Run the generator to the next yield.
|
|
39
|
+
let newValue = effect.current.next();
|
|
40
|
+
|
|
41
|
+
// If the generator is done, reset the effect.
|
|
42
|
+
if (newValue.done) {
|
|
43
|
+
effect.current = null;
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// If the value is the same as the current value,
|
|
48
|
+
// then continue to the next yield. Otherwise,
|
|
49
|
+
// set the value in state and wait for the next layout effect.
|
|
50
|
+
if (currValue.current === newValue.value) {
|
|
51
|
+
nextRef.current();
|
|
52
|
+
} else {
|
|
53
|
+
setValue(newValue.value);
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
subSlot(slot, 'next'),
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
useLayoutEffect(
|
|
60
|
+
() => {
|
|
61
|
+
currValue.current = value;
|
|
62
|
+
// If there is an effect currently running, continue to the next yield.
|
|
63
|
+
if (effect.current) {
|
|
64
|
+
nextRef.current();
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
null,
|
|
68
|
+
subSlot(slot, 'advance'),
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
let queue = useCallback(
|
|
72
|
+
(fn: any) => {
|
|
73
|
+
effect.current = fn(currValue.current);
|
|
74
|
+
nextRef.current();
|
|
75
|
+
},
|
|
76
|
+
[nextRef],
|
|
77
|
+
subSlot(slot, 'queue'),
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
return [value, queue];
|
|
81
|
+
}
|