@react-aria/utils 3.24.1 → 3.25.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.
Files changed (48) hide show
  1. package/dist/import.mjs +4 -2
  2. package/dist/main.js +4 -1
  3. package/dist/main.js.map +1 -1
  4. package/dist/mergeRefs.main.js +1 -1
  5. package/dist/mergeRefs.main.js.map +1 -1
  6. package/dist/mergeRefs.mjs +1 -1
  7. package/dist/mergeRefs.module.js +1 -1
  8. package/dist/mergeRefs.module.js.map +1 -1
  9. package/dist/module.js +4 -2
  10. package/dist/module.js.map +1 -1
  11. package/dist/openLink.main.js +4 -3
  12. package/dist/openLink.main.js.map +1 -1
  13. package/dist/openLink.mjs +4 -3
  14. package/dist/openLink.module.js +4 -3
  15. package/dist/openLink.module.js.map +1 -1
  16. package/dist/platform.main.js +25 -18
  17. package/dist/platform.main.js.map +1 -1
  18. package/dist/platform.mjs +25 -18
  19. package/dist/platform.module.js +25 -18
  20. package/dist/platform.module.js.map +1 -1
  21. package/dist/types.d.ts +35 -17
  22. package/dist/types.d.ts.map +1 -1
  23. package/dist/useEvent.main.js.map +1 -1
  24. package/dist/useEvent.module.js.map +1 -1
  25. package/dist/useFormReset.main.js.map +1 -1
  26. package/dist/useFormReset.module.js.map +1 -1
  27. package/dist/useLoadMore.main.js +72 -0
  28. package/dist/useLoadMore.main.js.map +1 -0
  29. package/dist/useLoadMore.mjs +67 -0
  30. package/dist/useLoadMore.module.js +67 -0
  31. package/dist/useLoadMore.module.js.map +1 -0
  32. package/dist/useResizeObserver.main.js +6 -3
  33. package/dist/useResizeObserver.main.js.map +1 -1
  34. package/dist/useResizeObserver.mjs +6 -3
  35. package/dist/useResizeObserver.module.js +6 -3
  36. package/dist/useResizeObserver.module.js.map +1 -1
  37. package/dist/useSyncRef.main.js.map +1 -1
  38. package/dist/useSyncRef.module.js.map +1 -1
  39. package/package.json +6 -6
  40. package/src/index.ts +2 -1
  41. package/src/mergeRefs.ts +2 -2
  42. package/src/openLink.tsx +3 -2
  43. package/src/platform.ts +32 -18
  44. package/src/useEvent.ts +4 -3
  45. package/src/useFormReset.ts +4 -2
  46. package/src/useLoadMore.ts +82 -0
  47. package/src/useResizeObserver.ts +8 -5
  48. package/src/useSyncRef.ts +3 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@react-aria/utils",
3
- "version": "3.24.1",
3
+ "version": "3.25.0",
4
4
  "description": "Spectrum UI components in React",
5
5
  "license": "Apache-2.0",
6
6
  "main": "dist/main.js",
@@ -22,17 +22,17 @@
22
22
  "url": "https://github.com/adobe/react-spectrum"
23
23
  },
24
24
  "dependencies": {
25
- "@react-aria/ssr": "^3.9.4",
26
- "@react-stately/utils": "^3.10.1",
27
- "@react-types/shared": "^3.23.1",
25
+ "@react-aria/ssr": "^3.9.5",
26
+ "@react-stately/utils": "^3.10.2",
27
+ "@react-types/shared": "^3.24.0",
28
28
  "@swc/helpers": "^0.5.0",
29
29
  "clsx": "^2.0.0"
30
30
  },
31
31
  "peerDependencies": {
32
- "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
32
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
33
33
  },
34
34
  "publishConfig": {
35
35
  "access": "public"
36
36
  },
37
- "gitHead": "b77d7d594dff4dcfb5359bffbcfd18142b146433"
37
+ "gitHead": "86d80e3216bc32e75108831cf3a5a720bc849206"
38
38
  }
package/src/index.ts CHANGED
@@ -17,7 +17,7 @@ export {mergeRefs} from './mergeRefs';
17
17
  export {filterDOMProps} from './filterDOMProps';
18
18
  export {focusWithoutScrolling} from './focusWithoutScrolling';
19
19
  export {getOffset} from './getOffset';
20
- export {openLink, getSyntheticLinkProps, RouterProvider, shouldClientNavigate, useRouter, useLinkProps} from './openLink';
20
+ export {openLink, useSyntheticLinkProps, RouterProvider, shouldClientNavigate, useRouter, useLinkProps} from './openLink';
21
21
  export {runAfterTransition} from './runAfterTransition';
22
22
  export {useDrag1D} from './useDrag1D';
23
23
  export {useGlobalListeners} from './useGlobalListeners';
@@ -41,3 +41,4 @@ export {isVirtualClick, isVirtualPointerEvent} from './isVirtualEvent';
41
41
  export {useEffectEvent} from './useEffectEvent';
42
42
  export {useDeepMemo} from './useDeepMemo';
43
43
  export {useFormReset} from './useFormReset';
44
+ export {useLoadMore} from './useLoadMore';
package/src/mergeRefs.ts CHANGED
@@ -15,8 +15,8 @@ import {ForwardedRef, MutableRefObject} from 'react';
15
15
  /**
16
16
  * Merges multiple refs into one. Works with either callback or object refs.
17
17
  */
18
- export function mergeRefs<T>(...refs: Array<ForwardedRef<T> | MutableRefObject<T>>): ForwardedRef<T> {
19
- if (refs.length === 1) {
18
+ export function mergeRefs<T>(...refs: Array<ForwardedRef<T> | MutableRefObject<T> | null | undefined>): ForwardedRef<T> {
19
+ if (refs.length === 1 && refs[0]) {
20
20
  return refs[0];
21
21
  }
22
22
 
package/src/openLink.tsx CHANGED
@@ -146,9 +146,10 @@ function openSyntheticLink(target: Element, modifiers: Modifiers) {
146
146
  getSyntheticLink(target, link => openLink(link, modifiers));
147
147
  }
148
148
 
149
- export function getSyntheticLinkProps(props: LinkDOMProps) {
149
+ export function useSyntheticLinkProps(props: LinkDOMProps) {
150
+ let router = useRouter();
150
151
  return {
151
- 'data-href': props.href,
152
+ 'data-href': props.href ? router.useHref(props.href) : undefined,
152
153
  'data-target': props.target,
153
154
  'data-rel': props.rel,
154
155
  'data-download': props.download,
package/src/platform.ts CHANGED
@@ -26,40 +26,54 @@ function testPlatform(re: RegExp) {
26
26
  : false;
27
27
  }
28
28
 
29
- export function isMac() {
30
- return testPlatform(/^Mac/i);
29
+ function cached(fn: () => boolean) {
30
+ if (process.env.NODE_ENV === 'test') {
31
+ return fn;
32
+ }
33
+
34
+ let res: boolean | null = null;
35
+ return () => {
36
+ if (res == null) {
37
+ res = fn();
38
+ }
39
+ return res;
40
+ };
31
41
  }
32
42
 
33
- export function isIPhone() {
43
+ export const isMac = cached(function () {
44
+ return testPlatform(/^Mac/i);
45
+ });
46
+
47
+ export const isIPhone = cached(function () {
34
48
  return testPlatform(/^iPhone/i);
35
- }
49
+ });
36
50
 
37
- export function isIPad() {
51
+ export const isIPad = cached(function () {
38
52
  return testPlatform(/^iPad/i) ||
39
53
  // iPadOS 13 lies and says it's a Mac, but we can distinguish by detecting touch support.
40
54
  (isMac() && navigator.maxTouchPoints > 1);
41
- }
55
+ });
42
56
 
43
- export function isIOS() {
57
+ export const isIOS = cached(function () {
44
58
  return isIPhone() || isIPad();
45
- }
59
+ });
46
60
 
47
- export function isAppleDevice() {
61
+ export const isAppleDevice = cached(function () {
48
62
  return isMac() || isIOS();
49
- }
63
+ });
50
64
 
51
- export function isWebKit() {
65
+ export const isWebKit = cached(function () {
52
66
  return testUserAgent(/AppleWebKit/i) && !isChrome();
53
- }
67
+ });
54
68
 
55
- export function isChrome() {
69
+ export const isChrome = cached(function () {
56
70
  return testUserAgent(/Chrome/i);
57
- }
71
+ });
58
72
 
59
- export function isAndroid() {
73
+ export const isAndroid = cached(function () {
60
74
  return testUserAgent(/Android/i);
61
- }
75
+ });
62
76
 
63
- export function isFirefox() {
77
+ export const isFirefox = cached(function () {
64
78
  return testUserAgent(/Firefox/i);
65
- }
79
+ });
package/src/useEvent.ts CHANGED
@@ -10,12 +10,13 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
 
13
- import {RefObject, useEffect} from 'react';
13
+ import {RefObject} from '@react-types/shared';
14
+ import {useEffect} from 'react';
14
15
  import {useEffectEvent} from './useEffectEvent';
15
16
 
16
17
  export function useEvent<K extends keyof GlobalEventHandlersEventMap>(
17
- ref: RefObject<EventTarget>,
18
- event: K,
18
+ ref: RefObject<EventTarget | null>,
19
+ event: K | (string & {}),
19
20
  handler?: (this: Document, ev: GlobalEventHandlersEventMap[K]) => any,
20
21
  options?: boolean | AddEventListenerOptions
21
22
  ) {
@@ -9,11 +9,13 @@
9
9
  * OF ANY KIND, either express or implied. See the License for the specific language
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
- import {RefObject, useEffect, useRef} from 'react';
12
+
13
+ import {RefObject} from '@react-types/shared';
14
+ import {useEffect, useRef} from 'react';
13
15
  import {useEffectEvent} from './useEffectEvent';
14
16
 
15
17
  export function useFormReset<T>(
16
- ref: RefObject<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>,
18
+ ref: RefObject<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement | null>,
17
19
  initialValue: T,
18
20
  onReset: (value: T) => void
19
21
  ) {
@@ -0,0 +1,82 @@
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 {RefObject, useCallback, useRef} from 'react';
14
+ import {useEvent} from './useEvent';
15
+ // eslint-disable-next-line rulesdir/useLayoutEffectRule
16
+ import {useLayoutEffect} from './useLayoutEffect';
17
+
18
+ export interface LoadMoreProps {
19
+ /** Whether data is currently being loaded. */
20
+ isLoading?: boolean,
21
+ /** Handler that is called when more items should be loaded, e.g. while scrolling near the bottom. */
22
+ onLoadMore?: () => void,
23
+ /**
24
+ * The amount of offset from the bottom of your scrollable region that should trigger load more.
25
+ * Uses a percentage value relative to the scroll body's client height. Load more is then triggered
26
+ * when your current scroll position's distance from the bottom of the currently loaded list of items is less than
27
+ * or equal to the provided value. (e.g. 1 = 100% of the scroll region's height).
28
+ * @default 1
29
+ */
30
+ scrollOffset?: number,
31
+ /** The data currently loaded. */
32
+ items?: any[]
33
+ }
34
+
35
+ export function useLoadMore(props: LoadMoreProps, ref: RefObject<HTMLElement | null>) {
36
+ let {isLoading, onLoadMore, scrollOffset = 1, items} = props;
37
+
38
+ // Handle scrolling, and call onLoadMore when nearing the bottom.
39
+ let isLoadingRef = useRef(isLoading);
40
+ let prevProps = useRef(props);
41
+ let onScroll = useCallback(() => {
42
+ if (ref.current && !isLoadingRef.current && onLoadMore) {
43
+ let shouldLoadMore = ref.current.scrollHeight - ref.current.scrollTop - ref.current.clientHeight < ref.current.clientHeight * scrollOffset;
44
+
45
+ if (shouldLoadMore) {
46
+ isLoadingRef.current = true;
47
+ onLoadMore();
48
+ }
49
+ }
50
+ }, [onLoadMore, ref, scrollOffset]);
51
+
52
+ let lastItems = useRef(items);
53
+ useLayoutEffect(() => {
54
+ // Only update isLoadingRef if props object actually changed,
55
+ // not if a local state change occurred.
56
+ if (props !== prevProps.current) {
57
+ isLoadingRef.current = isLoading;
58
+ prevProps.current = props;
59
+ }
60
+
61
+ // TODO: Eventually this hook will move back into RAC during which we will accept the collection as a option to this hook.
62
+ // We will only load more if the collection has changed after the last load to prevent multiple onLoadMore from being called
63
+ // while the data from the last onLoadMore is being processed by RAC collection.
64
+ let shouldLoadMore = ref?.current
65
+ && !isLoadingRef.current
66
+ && onLoadMore
67
+ && (!items || items !== lastItems.current)
68
+ && ref.current.clientHeight === ref.current.scrollHeight;
69
+
70
+ if (shouldLoadMore) {
71
+ isLoadingRef.current = true;
72
+ onLoadMore?.();
73
+ }
74
+
75
+ lastItems.current = items;
76
+ }, [isLoading, onLoadMore, props, ref]);
77
+
78
+ // TODO: maybe this should still just return scroll props?
79
+ // Test against case where the ref isn't defined when this is called
80
+ // Think this was a problem when trying to attach to the scrollable body of the table in OnLoadMoreTableBodyScroll
81
+ useEvent(ref, 'scroll', onScroll);
82
+ }
@@ -1,16 +1,19 @@
1
- import {RefObject, useEffect} from 'react';
1
+
2
+ import {RefObject} from '@react-types/shared';
3
+ import {useEffect} from 'react';
2
4
 
3
5
  function hasResizeObserver() {
4
6
  return typeof window.ResizeObserver !== 'undefined';
5
7
  }
6
8
 
7
9
  type useResizeObserverOptionsType<T> = {
8
- ref: RefObject<T | undefined> | undefined,
10
+ ref: RefObject<T | undefined | null> | undefined,
11
+ box?: ResizeObserverBoxOptions,
9
12
  onResize: () => void
10
13
  }
11
14
 
12
15
  export function useResizeObserver<T extends Element>(options: useResizeObserverOptionsType<T>) {
13
- const {ref, onResize} = options;
16
+ const {ref, box, onResize} = options;
14
17
 
15
18
  useEffect(() => {
16
19
  let element = ref?.current;
@@ -32,7 +35,7 @@ export function useResizeObserver<T extends Element>(options: useResizeObserverO
32
35
 
33
36
  onResize();
34
37
  });
35
- resizeObserverInstance.observe(element);
38
+ resizeObserverInstance.observe(element, {box});
36
39
 
37
40
  return () => {
38
41
  if (element) {
@@ -41,5 +44,5 @@ export function useResizeObserver<T extends Element>(options: useResizeObserverO
41
44
  };
42
45
  }
43
46
 
44
- }, [onResize, ref]);
47
+ }, [onResize, ref, box]);
45
48
  }
package/src/useSyncRef.ts CHANGED
@@ -10,7 +10,8 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
 
13
- import {MutableRefObject, RefObject} from 'react';
13
+ import {MutableRefObject} from 'react';
14
+ import {RefObject} from '@react-types/shared';
14
15
  import {useLayoutEffect} from './';
15
16
 
16
17
  interface ContextValue<T> {
@@ -18,7 +19,7 @@ interface ContextValue<T> {
18
19
  }
19
20
 
20
21
  // Syncs ref from context with ref passed to hook
21
- export function useSyncRef<T>(context?: ContextValue<T> | null, ref?: RefObject<T>) {
22
+ export function useSyncRef<T>(context?: ContextValue<T> | null, ref?: RefObject<T | null>) {
22
23
  useLayoutEffect(() => {
23
24
  if (context && context.ref && ref) {
24
25
  context.ref.current = ref.current;