@react-aria/utils 3.28.2 → 3.29.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,63 @@
1
+ /*
2
+ * Copyright 2024 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+
13
+ import type {AsyncLoadable, Collection, Node} from '@react-types/shared';
14
+ import {getScrollParent} from './getScrollParent';
15
+ import {RefObject, useRef} from 'react';
16
+ import {useEffectEvent} from './useEffectEvent';
17
+ import {useLayoutEffect} from './useLayoutEffect';
18
+
19
+ export interface LoadMoreSentinelProps extends Omit<AsyncLoadable, 'isLoading'> {
20
+ collection: Collection<Node<unknown>>,
21
+ /**
22
+ * The amount of offset from the bottom of your scrollable region that should trigger load more.
23
+ * Uses a percentage value relative to the scroll body's client height. Load more is then triggered
24
+ * when your current scroll position's distance from the bottom of the currently loaded list of items is less than
25
+ * or equal to the provided value. (e.g. 1 = 100% of the scroll region's height).
26
+ * @default 1
27
+ */
28
+ scrollOffset?: number
29
+ }
30
+
31
+ export function UNSTABLE_useLoadMoreSentinel(props: LoadMoreSentinelProps, ref: RefObject<HTMLElement | null>): void {
32
+ let {collection, onLoadMore, scrollOffset = 1} = props;
33
+
34
+ let sentinelObserver = useRef<IntersectionObserver>(null);
35
+
36
+ let triggerLoadMore = useEffectEvent((entries: IntersectionObserverEntry[]) => {
37
+ // Use "isIntersecting" over an equality check of 0 since it seems like there is cases where
38
+ // a intersection ratio of 0 can be reported when isIntersecting is actually true
39
+ for (let entry of entries) {
40
+ // Note that this will be called if the collection changes, even if onLoadMore was already called and is being processed.
41
+ // Up to user discretion as to how to handle these multiple onLoadMore calls
42
+ if (entry.isIntersecting && onLoadMore) {
43
+ onLoadMore();
44
+ }
45
+ }
46
+ });
47
+
48
+ useLayoutEffect(() => {
49
+ if (ref.current) {
50
+ // Tear down and set up a new IntersectionObserver when the collection changes so that we can properly trigger additional loadMores if there is room for more items
51
+ // Need to do this tear down and set up since using a large rootMargin will mean the observer's callback isn't called even when scrolling the item into view beause its visibility hasn't actually changed
52
+ // https://codesandbox.io/p/sandbox/magical-swanson-dhgp89?file=%2Fsrc%2FApp.js%3A21%2C21
53
+ sentinelObserver.current = new IntersectionObserver(triggerLoadMore, {root: getScrollParent(ref?.current) as HTMLElement, rootMargin: `0px ${100 * scrollOffset}% ${100 * scrollOffset}% ${100 * scrollOffset}%`});
54
+ sentinelObserver.current.observe(ref.current);
55
+ }
56
+
57
+ return () => {
58
+ if (sentinelObserver.current) {
59
+ sentinelObserver.current.disconnect();
60
+ }
61
+ };
62
+ }, [collection, triggerLoadMore, ref, scrollOffset]);
63
+ }
@@ -10,30 +10,60 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
 
13
- import {MutableRefObject, useMemo, useRef} from 'react';
13
+ import {MutableRefObject, useCallback, useMemo, useRef} from 'react';
14
14
 
15
15
  /**
16
16
  * Offers an object ref for a given callback ref or an object ref. Especially
17
17
  * helfpul when passing forwarded refs (created using `React.forwardRef`) to
18
18
  * React Aria hooks.
19
19
  *
20
- * @param forwardedRef The original ref intended to be used.
20
+ * @param ref The original ref intended to be used.
21
21
  * @returns An object ref that updates the given ref.
22
- * @see https://reactjs.org/docs/forwarding-refs.html
22
+ * @see https://react.dev/reference/react/forwardRef
23
23
  */
24
- export function useObjectRef<T>(forwardedRef?: ((instance: T | null) => void) | MutableRefObject<T | null> | null): MutableRefObject<T | null> {
24
+ export function useObjectRef<T>(ref?: ((instance: T | null) => (() => void) | void) | MutableRefObject<T | null> | null): MutableRefObject<T | null> {
25
25
  const objRef: MutableRefObject<T | null> = useRef<T>(null);
26
- return useMemo(() => ({
27
- get current() {
28
- return objRef.current;
26
+ const cleanupRef: MutableRefObject<(() => void) | void> = useRef(undefined);
27
+
28
+ const refEffect = useCallback(
29
+ (instance: T | null) => {
30
+ if (typeof ref === 'function') {
31
+ const refCallback = ref;
32
+ const refCleanup = refCallback(instance);
33
+ return () => {
34
+ if (typeof refCleanup === 'function') {
35
+ refCleanup();
36
+ } else {
37
+ refCallback(null);
38
+ }
39
+ };
40
+ } else if (ref) {
41
+ ref.current = instance;
42
+ return () => {
43
+ ref.current = null;
44
+ };
45
+ }
29
46
  },
30
- set current(value) {
31
- objRef.current = value;
32
- if (typeof forwardedRef === 'function') {
33
- forwardedRef(value);
34
- } else if (forwardedRef) {
35
- forwardedRef.current = value;
47
+ [ref]
48
+ );
49
+
50
+ return useMemo(
51
+ () => ({
52
+ get current() {
53
+ return objRef.current;
54
+ },
55
+ set current(value) {
56
+ objRef.current = value;
57
+ if (cleanupRef.current) {
58
+ cleanupRef.current();
59
+ cleanupRef.current = undefined;
60
+ }
61
+
62
+ if (value != null) {
63
+ cleanupRef.current = refEffect(value);
64
+ }
36
65
  }
37
- }
38
- }), [forwardedRef]);
66
+ }),
67
+ [refEffect]
68
+ );
39
69
  }