diff --git a/.changeset/quiet-doors-double.md b/.changeset/quiet-doors-double.md new file mode 100644 index 0000000000..78d6a534bc --- /dev/null +++ b/.changeset/quiet-doors-double.md @@ -0,0 +1,7 @@ +--- +'@tanstack/react-router': patch +'@tanstack/solid-router': patch +'@tanstack/vue-router': patch +--- + +clean intersection observer options in link component diff --git a/packages/react-router/src/link.tsx b/packages/react-router/src/link.tsx index eddc0ac0b3..5347a52680 100644 --- a/packages/react-router/src/link.tsx +++ b/packages/react-router/src/link.tsx @@ -586,7 +586,7 @@ export function useLinkProps< innerRef, preloadViewportIoCallback, intersectionObserverOptions, - { disabled: !!disabled || !(preload === 'viewport') }, + !!disabled || preload !== 'viewport', ) // eslint-disable-next-line react-hooks/rules-of-hooks diff --git a/packages/react-router/src/utils.ts b/packages/react-router/src/utils.ts index ca0653e350..60808bceb1 100644 --- a/packages/react-router/src/utils.ts +++ b/packages/react-router/src/utils.ts @@ -67,7 +67,7 @@ export function usePrevious(value: T): T | null { * * @param ref - The ref to observe * @param intersectionObserverOptions - The options to pass to the IntersectionObserver - * @param options - The options to pass to the hook + * @param disabled - Whether observation is disabled * @param callback - The callback to call when the intersection changes * @returns The IntersectionObserver instance * @example @@ -78,7 +78,7 @@ export function usePrevious(value: T): T | null { * ref, * (entry) => { doSomething(entry) }, * { rootMargin: '10px' }, - * { disabled: false } + * false * ) * return
* ``` @@ -87,12 +87,12 @@ export function useIntersectionObserver( ref: React.RefObject, callback: (entry: IntersectionObserverEntry | undefined) => void, intersectionObserverOptions: IntersectionObserverInit = {}, - options: { disabled?: boolean } = {}, + disabled?: boolean, ) { React.useEffect(() => { if ( !ref.current || - options.disabled || + disabled || typeof IntersectionObserver !== 'function' ) { return @@ -107,7 +107,7 @@ export function useIntersectionObserver( return () => { observer.disconnect() } - }, [callback, intersectionObserverOptions, options.disabled, ref]) + }, [callback, disabled, intersectionObserverOptions, ref]) } /** diff --git a/packages/solid-router/src/link.tsx b/packages/solid-router/src/link.tsx index 935d0a3d2b..0b2d101d40 100644 --- a/packages/solid-router/src/link.tsx +++ b/packages/solid-router/src/link.tsx @@ -269,7 +269,7 @@ export function useLinkProps< ref, preloadViewportIoCallback, { rootMargin: '100px' }, - { disabled: !!local.disabled || !(preload() === 'viewport') }, + !!local.disabled || preload() !== 'viewport', ) Solid.createEffect(() => { diff --git a/packages/solid-router/src/utils.ts b/packages/solid-router/src/utils.ts index 8b07ab2919..cac0e28aaa 100644 --- a/packages/solid-router/src/utils.ts +++ b/packages/solid-router/src/utils.ts @@ -9,7 +9,7 @@ import * as Solid from 'solid-js' * * @param ref - The ref to observe * @param intersectionObserverOptions - The options to pass to the IntersectionObserver - * @param options - The options to pass to the hook + * @param disabled - Whether observation is disabled * @param callback - The callback to call when the intersection changes * @returns The IntersectionObserver instance * @example @@ -20,7 +20,7 @@ import * as Solid from 'solid-js' * ref, * (entry) => { doSomething(entry) }, * { rootMargin: '10px' }, - * { disabled: false } + * false * ) * return
* ``` @@ -29,7 +29,7 @@ export function useIntersectionObserver( ref: Solid.Accessor, callback: (entry: IntersectionObserverEntry | undefined) => void, intersectionObserverOptions: IntersectionObserverInit = {}, - options: { disabled?: boolean } = {}, + disabled?: boolean, ): Solid.Accessor { const isIntersectionObserverAvailable = typeof IntersectionObserver === 'function' @@ -37,7 +37,7 @@ export function useIntersectionObserver( Solid.createEffect(() => { const r = ref() - if (!r || !isIntersectionObserverAvailable || options.disabled) { + if (!r || !isIntersectionObserverAvailable || disabled) { return } diff --git a/packages/vue-router/src/link.tsx b/packages/vue-router/src/link.tsx index 3a5a9875a8..32bb37e3c7 100644 --- a/packages/vue-router/src/link.tsx +++ b/packages/vue-router/src/link.tsx @@ -270,7 +270,7 @@ export function useLinkProps< ref, preloadViewportIoCallback, { rootMargin: '100px' }, - { disabled: () => !!options.disabled || !(preload.value === 'viewport') }, + () => !!options.disabled || preload.value !== 'viewport', ) Vue.effect(() => { diff --git a/packages/vue-router/src/utils.ts b/packages/vue-router/src/utils.ts index 609c4558a8..0633847db6 100644 --- a/packages/vue-router/src/utils.ts +++ b/packages/vue-router/src/utils.ts @@ -32,7 +32,7 @@ export const usePrevious = (fn: () => boolean) => { * * @param ref - The ref to observe * @param intersectionObserverOptions - The options to pass to the IntersectionObserver - * @param options - The options to pass to the hook + * @param disabled - Whether observation is disabled * @param callback - The callback to call when the intersection changes * @returns The IntersectionObserver instance * @example @@ -43,7 +43,7 @@ export const usePrevious = (fn: () => boolean) => { * ref, * (entry) => { doSomething(entry) }, * { rootMargin: '10px' }, - * { disabled: false } + * () => false * ) * return
* ``` @@ -52,7 +52,7 @@ export function useIntersectionObserver( ref: Vue.Ref, callback: (entry: IntersectionObserverEntry | undefined) => void, intersectionObserverOptions: IntersectionObserverInit = {}, - options: { disabled?: boolean | (() => boolean) } = {}, + disabled: () => boolean, ): Vue.Ref { const isIntersectionObserverAvailable = typeof IntersectionObserver === 'function' @@ -61,12 +61,7 @@ export function useIntersectionObserver( // Use watchEffect with cleanup to properly manage the observer lifecycle Vue.watchEffect((onCleanup) => { const r = ref.value - // Support both static boolean and function for disabled check - const isDisabled = - typeof options.disabled === 'function' - ? options.disabled() - : options.disabled - if (!r || !isIntersectionObserverAvailable || isDisabled) { + if (!r || !isIntersectionObserverAvailable || disabled()) { return } diff --git a/packages/vue-router/tests/link.test.tsx b/packages/vue-router/tests/link.test.tsx index be5e9bf70b..8d9e4a67ac 100644 --- a/packages/vue-router/tests/link.test.tsx +++ b/packages/vue-router/tests/link.test.tsx @@ -33,6 +33,7 @@ import { useRouteContext, useSearch, } from '../src' +import { useIntersectionObserver } from '../src/utils' import { getIntersectionObserverMock, getSearchParamsFromURI, @@ -5053,6 +5054,70 @@ describe('Link', () => { expect(ioDisconnectMock).not.toHaveBeenCalled() // it should not disconnect again }) + test('Link.disabled should disable viewport observation', async () => { + const rootRoute = createRootRoute() + const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/', + component: () => ( + <> +

Index Heading

+ + Index Link + + + ), + }) + + const router = createRouter({ + routeTree: rootRoute.addChildren([indexRoute]), + defaultPreload: 'viewport', + }) + + render() + + const indexLink = await screen.findByRole('link', { name: 'Index Link' }) + expect(indexLink).toBeInTheDocument() + expect(indexLink).toHaveAttribute('aria-disabled', 'true') + expect(ioObserveMock).not.toHaveBeenCalled() + }) + + test('useIntersectionObserver should react to its disabled getter', async () => { + const TestComponent = Vue.defineComponent({ + name: 'TestComponent', + setup() { + const element = Vue.ref(null) + const disabled = Vue.ref(true) + + useIntersectionObserver( + element, + () => {}, + {}, + () => disabled.value, + ) + + return () => ( + <> + +
+ + ) + }, + }) + + render() + expect(ioObserveMock).not.toHaveBeenCalled() + + const toggle = screen.getByRole('button', { name: 'Toggle disabled' }) + await fireEvent.click(toggle) + await waitFor(() => expect(ioObserveMock).toHaveBeenCalledOnce()) + + await fireEvent.click(toggle) + await waitFor(() => expect(ioDisconnectMock).toHaveBeenCalledOnce()) + }) + test("Router.preload='render', should trigger the route loader on render", async () => { const mock = vi.fn()