Skip to content

Commit

Permalink
feat: Link now accepts a forwarded ref
Browse files Browse the repository at this point in the history
  • Loading branch information
jquense committed Mar 22, 2023
1 parent 8ba6d66 commit 47cda70
Showing 1 changed file with 104 additions and 93 deletions.
197 changes: 104 additions & 93 deletions src/Link.tsx
Original file line number Diff line number Diff line change
@@ -1,116 +1,127 @@
import useEventCallback from '@restart/hooks/useEventCallback';
import React from 'react';
import React, { forwardRef } from 'react';
import warning from 'tiny-warning';

import { LinkInjectedProps, LinkProps } from './typeUtils';
import useRouter from './useRouter';

// TODO: Try to type this & simplify those types in next breaking change.
function Link({
as: Component = 'a',
to,
activeClassName,
activeStyle,
activePropName,
match: propsMatch,
router: propsRouter,
exact = false,
onClick,
target,
children,
...props
}: any) {
const { router, match } = useRouter() || {
match: propsMatch,
router: propsRouter,
};

const handleClick = useEventCallback((event) => {
if (onClick) {
onClick(event);
}

// Don't do anything if the user's onClick handler prevented default.
// Otherwise, let the browser handle the link with the computed href if the
// event wasn't an unmodified left click, or if the link has a target other
// than _self.
if (
event.defaultPrevented ||
event.metaKey ||
event.altKey ||
event.ctrlKey ||
event.shiftKey ||
event.button !== 0 ||
(target && target !== '_self')
) {
return;
}

event.preventDefault();

// FIXME: When clicking on a link to the same location in the browser, the
// actual becomes a replace rather than a push. We may want the same
// handling – perhaps implemented in the Farce protocol.
router.push(to);
});
const Link = forwardRef(
(
{
as: Component = 'a',
to,
activeClassName,
activeStyle,
activePropName,
match: propsMatch,
router: propsRouter,
exact = false,
onClick,
target,
children,
...props
}: any,
ref,
) => {
const { router, match } = useRouter() || {
match: propsMatch,
router: propsRouter,
};

const handleClick = useEventCallback((event) => {
if (onClick) {
onClick(event);
}

if (__DEV__ && typeof Component !== 'function') {
for (const wrongPropName of ['component', 'Component'] as const) {
const wrongPropValue = (props as any)[wrongPropName];
if (!wrongPropValue) {
continue;
// Don't do anything if the user's onClick handler prevented default.
// Otherwise, let the browser handle the link with the computed href if the
// event wasn't an unmodified left click, or if the link has a target other
// than _self.
if (
event.defaultPrevented ||
event.metaKey ||
event.altKey ||
event.ctrlKey ||
event.shiftKey ||
event.button !== 0 ||
(target && target !== '_self')
) {
return;
}

warning(
false,
`Link to ${JSON.stringify(to)} with \`${wrongPropName}\` prop \`${
wrongPropValue.displayName || wrongPropValue.name || 'UNKNOWN'
}\` has an element type that is not a component. The expected prop for the link component is \`as\`.`,
);
event.preventDefault();

// FIXME: When clicking on a link to the same location in the browser, the
// actual becomes a replace rather than a push. We may want the same
// handling – perhaps implemented in the Farce protocol.
router.push(to);
});

if (__DEV__ && typeof Component !== 'function') {
for (const wrongPropName of ['component', 'Component'] as const) {
const wrongPropValue = (props as any)[wrongPropName];
if (!wrongPropValue) {
continue;
}

warning(
false,
`Link to ${JSON.stringify(to)} with \`${wrongPropName}\` prop \`${
wrongPropValue.displayName || wrongPropValue.name || 'UNKNOWN'
}\` has an element type that is not a component. The expected prop for the link component is \`as\`.`,
);
}
}
}

const href = router.createHref(to);
const childrenIsFunction = typeof children === 'function';
const href = router.createHref(to);
const childrenIsFunction = typeof children === 'function';

if (childrenIsFunction || activeClassName || activeStyle || activePropName) {
const toLocation = router.createLocation(to);
const active = router.isActive(match!, toLocation, { exact });
if (
childrenIsFunction ||
activeClassName ||
activeStyle ||
activePropName
) {
const toLocation = router.createLocation(to);
const active = router.isActive(match!, toLocation, { exact });

if (childrenIsFunction) {
const add = { href, active, onClick: handleClick };
return children(add);
}
if (childrenIsFunction) {
const add = { href, active, onClick: handleClick };
return children(add);
}

if (active) {
if (activeClassName) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
props.className = props.className
? `${props.className} ${activeClassName}`
: activeClassName;
if (active) {
if (activeClassName) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
props.className = props.className
? `${props.className} ${activeClassName}`
: activeClassName;
}

if (activeStyle) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
props.style = { ...props.style, ...activeStyle };
}
}

if (activeStyle) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
props.style = { ...props.style, ...activeStyle };
if (activePropName) {
props[activePropName] = active;
}
}

if (activePropName) {
props[activePropName] = active;
}
}

return (
<Component
{...props}
href={href}
onClick={handleClick} // This overrides props.onClick.
>
{children}
</Component>
);
}
return (
<Component
{...props}
href={href}
ref={ref}
onClick={handleClick} // This overrides props.onClick.
>
{children}
</Component>
);
},
);

// eslint-disable-next-line react/prefer-stateless-function
declare class LinkType<
Expand Down

0 comments on commit 47cda70

Please sign in to comment.