Skip to content

Commit

Permalink
fix(react-router): correctly pass activeProps and inactiveProps t…
Browse files Browse the repository at this point in the history
…o custom link created through `createLink`

fixes #2079
  • Loading branch information
schiller-manuel committed Aug 6, 2024
1 parent 6ae3c70 commit 250da61
Show file tree
Hide file tree
Showing 2 changed files with 125 additions and 37 deletions.
73 changes: 36 additions & 37 deletions packages/react-router/src/link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -557,23 +557,24 @@ type LinkCurrentTargetElement = {
const preloadWarning = 'Error preloading route! ☝️'

export function useLinkProps<
TComp extends React.ElementType = 'a',
TRouter extends AnyRouter = RegisteredRouter,
TFrom extends RoutePaths<TRouter['routeTree']> | string = string,
TTo extends string = '',
TMaskFrom extends RoutePaths<TRouter['routeTree']> | string = TFrom,
TMaskTo extends string = '',
>(
options: UseLinkPropsOptions<TRouter, TFrom, TTo, TMaskFrom, TMaskTo>,
forwardedRef?: React.ForwardedRef<Element>,
): React.ComponentPropsWithRef<'a'> {
options: UseLinkPropsOptions<TComp, TRouter, TFrom, TTo, TMaskFrom, TMaskTo>,
forwardedRef?: React.ForwardedRef<TComp>,
): React.ComponentPropsWithRef<TComp> {
const router = useRouter()
const [isTransitioning, setIsTransitioning] = React.useState(false)
const innerRef = useForwardedRef(forwardedRef)
const {
// custom props
activeProps = () => ({ className: 'active' }),
inactiveProps = () => ({}),
inactiveProps,
activeOptions,
hash,
search,
Expand All @@ -587,6 +588,7 @@ export function useLinkProps<
startTransition,
resetScroll,
viewTransition,
ignoreBlocker,
// element props
children,
target,
Expand All @@ -598,13 +600,9 @@ export function useLinkProps<
onMouseEnter,
onMouseLeave,
onTouchStart,
ignoreBlocker,
...rest
} = options
// If this link simply reloads the current route,
// make sure it has a new key so it will trigger a data refresh
// If this `to` is a valid external URL, return
// null for LinkUtils
Expand Down Expand Up @@ -684,7 +682,7 @@ export function useLinkProps<
if (type === 'external') {
return {
...rest,
ref: innerRef as React.ComponentPropsWithRef<'a'>['ref'],
ref: innerRef,
type,
href: to,
...(children && { children }),
Expand Down Expand Up @@ -779,38 +777,37 @@ export function useLinkProps<
}

// Get the active props
const resolvedActiveProps: React.HTMLAttributes<HTMLAnchorElement> = isActive
? (functionalUpdate(activeProps as any, {}) ?? {})
: {}
const resolvedActiveProps = isActive ? functionalUpdate(activeProps, {}) : {}

// Get the inactive props
const resolvedInactiveProps: React.HTMLAttributes<HTMLAnchorElement> =
isActive ? {} : functionalUpdate(inactiveProps, {})
const resolvedInactiveProps = isActive
? {}
: functionalUpdate(inactiveProps, {})

const resolvedClassName = [
className,
resolvedActiveProps.className,
resolvedInactiveProps.className,
(resolvedInactiveProps as any).className,
]
.filter(Boolean)
.join(' ')

const resolvedStyle = {
...style,
...resolvedActiveProps.style,
...resolvedInactiveProps.style,
...(resolvedInactiveProps as any).style,
}

return {
...resolvedActiveProps,
...resolvedInactiveProps,
...rest,
...resolvedInactiveProps,
...resolvedActiveProps,
href: disabled
? undefined
: next.maskedLocation
? router.history.createHref(next.maskedLocation.href)
: router.history.createHref(next.href),
ref: innerRef as React.ComponentPropsWithRef<'a'>['ref'],
ref: innerRef,
onClick: composeHandlers([onClick, handleClick]),
onFocus: composeHandlers([onFocus, handleFocus]),
onMouseEnter: composeHandlers([onMouseEnter, handleEnter]),
Expand All @@ -830,49 +827,53 @@ export function useLinkProps<
}

export type UseLinkPropsOptions<
TComp = 'a',
TRouter extends AnyRouter = RegisteredRouter,
TFrom extends RoutePaths<TRouter['routeTree']> | string = string,
TTo extends string = '',
TMaskFrom extends RoutePaths<TRouter['routeTree']> | string = TFrom,
TMaskTo extends string = '',
> = ActiveLinkOptions<TRouter, TFrom, TTo, TMaskFrom, TMaskTo> &
React.AnchorHTMLAttributes<HTMLAnchorElement>
> = ActiveLinkOptions<TComp, TRouter, TFrom, TTo, TMaskFrom, TMaskTo> &
LinkComponentReactProps<TComp>
export type ActiveLinkOptions<
TComp,
TRouter extends AnyRouter = RegisteredRouter,
TFrom extends string = string,
TTo extends string = '',
TMaskFrom extends string = TFrom,
TMaskTo extends string = '',
> = LinkOptions<TRouter, TFrom, TTo, TMaskFrom, TMaskTo> & ActiveLinkOptionProps
type ActiveLinkAnchorProps = Omit<
React.AnchorHTMLAttributes<HTMLAnchorElement> & {
[key: `data-${string}`]: unknown
},
'children'
>
> = LinkOptions<TRouter, TFrom, TTo, TMaskFrom, TMaskTo> &
ActiveLinkOptionProps<TComp>
export interface ActiveLinkOptionProps {
type DataAttribute = {
[key: `data-${string}`]: unknown
}
export interface ActiveLinkOptionProps<
TComp,
TProps = Partial<Omit<LinkComponentReactProps<TComp>, 'children'>> &
DataAttribute,
> {
/**
* A function that returns additional props for the `active` state of this link.
* These props override other props passed to the link (`style`'s are merged, `className`'s are concatenated)
*/
activeProps?: ActiveLinkAnchorProps | (() => ActiveLinkAnchorProps)
activeProps?: TProps | (() => TProps)
/**
* A function that returns additional props for the `inactive` state of this link.
* These props override other props passed to the link (`style`'s are merged, `className`'s are concatenated)
*/
inactiveProps?: ActiveLinkAnchorProps | (() => ActiveLinkAnchorProps)
inactiveProps?: TProps | (() => TProps)
}
export type LinkProps<
TComp = 'a',
TRouter extends AnyRouter = RegisteredRouter,
TFrom extends string = string,
TTo extends string = '',
TMaskFrom extends string = TFrom,
TMaskTo extends string = '',
> = ActiveLinkOptions<TRouter, TFrom, TTo, TMaskFrom, TMaskTo> &
> = ActiveLinkOptions<TComp, TRouter, TFrom, TTo, TMaskFrom, TMaskTo> &
LinkPropsChildren
export interface LinkPropsChildren {
Expand Down Expand Up @@ -910,7 +911,7 @@ export type LinkComponentProps<
TMaskFrom extends string = TFrom,
TMaskTo extends string = '',
> = LinkComponentReactProps<TComp> &
LinkProps<TRouter, TFrom, TTo, TMaskFrom, TMaskTo>
LinkProps<TComp, TRouter, TFrom, TTo, TMaskFrom, TMaskTo>
export type LinkComponent<TComp> = <
TRouter extends RegisteredRouter = RegisteredRouter,
Expand All @@ -936,13 +937,11 @@ export const Link: LinkComponent<'a'> = React.forwardRef<Element, any>(
const children =
typeof rest.children === 'function'
? rest.children({
isActive: (linkProps as any)['data-status'] === 'active',
isActive: linkProps['data-status'] === 'active',
})
: rest.children
if (typeof _asChild === 'undefined') {
// the ReturnType of useLinkProps returns the correct type for a <a> element, not a general component that has a delete prop
// @ts-expect-error
delete linkProps.disabled
}
Expand Down
89 changes: 89 additions & 0 deletions packages/react-router/tests/link.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3518,4 +3518,93 @@ describe('createLink', () => {
expect(customElement.hasAttribute('foo')).toBe(true)
expect(customElement.getAttribute('foo')).toBe('bar')
})

it('should pass activeProps and inactiveProps to the custom link', async () => {
const Button: React.FC<
React.PropsWithChildren<{
active?: boolean
foo?: boolean
overrideMeIfYouWant: string
}>
> = ({ active, foo, children, ...props }) => (
<button {...props}>
active: {active ? 'yes' : 'no'} - foo: {foo ? 'yes' : 'no'} - {children}
</button>
)

const ButtonLink = createLink(Button)

const rootRoute = createRootRoute()
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
component: () => (
<>
<ButtonLink
to="/"
overrideMeIfYouWant="Button1"
activeProps={{
active: true,
'data-hello': 'world',
overrideMeIfYouWant: 'overridden-by-activeProps',
}}
inactiveProps={{ foo: true }}
>
Button1
</ButtonLink>
<ButtonLink
to="/posts"
overrideMeIfYouWant="Button2"
activeProps={{
active: false,
'data-hello': 'world',
}}
inactiveProps={{
foo: true,
'data-hello': 'void',
overrideMeIfYouWant: 'overridden-by-inactiveProps',
}}
>
Button2
</ButtonLink>
<ButtonLink
to="/posts"
overrideMeIfYouWant="Button3"
activeProps={{
active: false,
}}
inactiveProps={{
active: false,
}}
>
Button3
</ButtonLink>
</>
),
})
const postsRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/posts',
})
const router = createRouter({
routeTree: rootRoute.addChildren([indexRoute, postsRoute]),
})

render(<RouterProvider router={router} />)

const button1 = await screen.findByText('active: yes - foo: no - Button1')
expect(button1.getAttribute('data-hello')).toBe('world')
expect(button1.getAttribute('overrideMeIfYouWant')).toBe(
'overridden-by-activeProps',
)

const button2 = await screen.findByText('active: no - foo: yes - Button2')
expect(button2.getAttribute('data-hello')).toBe('void')
expect(button2.getAttribute('overrideMeIfYouWant')).toBe(
'overridden-by-inactiveProps',
)

const button3 = await screen.findByText('active: no - foo: no - Button3')
expect(button3.getAttribute('overrideMeIfYouWant')).toBe('Button3')
})
})

0 comments on commit 250da61

Please sign in to comment.