fix(ui): remove Router context requirement for Link, ButtonLink, Row
Introduced InternalLinkProvider component that conditionally wraps children in RouterProvider only when an internal href is present. This allows Link, ButtonLink, and Row components to render without requiring a Router context when used with external or no hrefs. Signed-off-by: Johan Persson <johanopersson@gmail.com>
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/ui': patch
|
||||
---
|
||||
|
||||
Fixed components to not require a Router context when rendering without internal links.
|
||||
|
||||
Affected components: Link, ButtonLink, Row
|
||||
@@ -15,51 +15,36 @@
|
||||
*/
|
||||
|
||||
import { forwardRef, Ref } from 'react';
|
||||
import { Link as RALink, RouterProvider } from 'react-aria-components';
|
||||
import { useNavigate, useHref } from 'react-router-dom';
|
||||
import { Link as RALink } from 'react-aria-components';
|
||||
import type { ButtonLinkProps } from './types';
|
||||
import { useDefinition } from '../../hooks/useDefinition';
|
||||
import { ButtonLinkDefinition } from './definition';
|
||||
import { isExternalLink } from '../../utils/isExternalLink';
|
||||
import { InternalLinkProvider } from '../InternalLinkProvider';
|
||||
|
||||
/** @public */
|
||||
export const ButtonLink = forwardRef(
|
||||
(props: ButtonLinkProps, ref: Ref<HTMLAnchorElement>) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { ownProps, restProps, dataAttributes } = useDefinition(
|
||||
ButtonLinkDefinition,
|
||||
props,
|
||||
);
|
||||
const { classes, iconStart, iconEnd, children } = ownProps;
|
||||
|
||||
const isExternal = isExternalLink(restProps.href);
|
||||
|
||||
const linkButton = (
|
||||
<RALink
|
||||
className={classes.root}
|
||||
ref={ref}
|
||||
{...dataAttributes}
|
||||
{...restProps}
|
||||
>
|
||||
<span className={classes.content}>
|
||||
{iconStart}
|
||||
{children}
|
||||
{iconEnd}
|
||||
</span>
|
||||
</RALink>
|
||||
);
|
||||
|
||||
// If it's an external link, render RALink without RouterProvider
|
||||
if (isExternal) {
|
||||
return linkButton;
|
||||
}
|
||||
|
||||
// For internal links, use RouterProvider
|
||||
return (
|
||||
<RouterProvider navigate={navigate} useHref={useHref}>
|
||||
{linkButton}
|
||||
</RouterProvider>
|
||||
<InternalLinkProvider href={restProps.href}>
|
||||
<RALink
|
||||
className={classes.root}
|
||||
ref={ref}
|
||||
{...dataAttributes}
|
||||
{...restProps}
|
||||
>
|
||||
<span className={classes.content}>
|
||||
{iconStart}
|
||||
{children}
|
||||
{iconEnd}
|
||||
</span>
|
||||
</RALink>
|
||||
</InternalLinkProvider>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ReactNode } from 'react';
|
||||
import { RouterProvider } from 'react-aria-components';
|
||||
import { useNavigate, useHref } from 'react-router-dom';
|
||||
import { isExternalLink } from '../../utils/isExternalLink';
|
||||
|
||||
/**
|
||||
* Inner component that uses router hooks.
|
||||
* Separated so hooks are only called when this component mounts.
|
||||
*/
|
||||
function InternalLinkProviderInner({ children }: { children: ReactNode }) {
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<RouterProvider navigate={navigate} useHref={useHref}>
|
||||
{children}
|
||||
</RouterProvider>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Conditionally wraps children in a RouterProvider for internal link navigation.
|
||||
* Only mounts the router hooks when `href` is an internal link, avoiding the
|
||||
* requirement for a Router context when rendering components without internal hrefs.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function InternalLinkProvider({
|
||||
href,
|
||||
children,
|
||||
}: {
|
||||
href: string | undefined;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const hasInternalHref = !!href && !isExternalLink(href);
|
||||
|
||||
if (!hasInternalHref) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
return <InternalLinkProviderInner>{children}</InternalLinkProviderInner>;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { InternalLinkProvider } from './InternalLinkProvider';
|
||||
@@ -16,13 +16,11 @@
|
||||
|
||||
import { forwardRef, useRef } from 'react';
|
||||
import { useLink } from 'react-aria';
|
||||
import { RouterProvider } from 'react-aria-components';
|
||||
import clsx from 'clsx';
|
||||
import { useStyles } from '../../hooks/useStyles';
|
||||
import { LinkDefinition } from './definition';
|
||||
import type { LinkProps } from './types';
|
||||
import { useNavigate, useHref } from 'react-router-dom';
|
||||
import { isExternalLink } from '../../utils/isExternalLink';
|
||||
import { InternalLinkProvider } from '../InternalLinkProvider';
|
||||
import styles from './Link.module.css';
|
||||
|
||||
const LinkInternal = forwardRef<HTMLAnchorElement, LinkProps>((props, ref) => {
|
||||
@@ -83,19 +81,10 @@ LinkInternal.displayName = 'LinkInternal';
|
||||
|
||||
/** @public */
|
||||
export const Link = forwardRef<HTMLAnchorElement, LinkProps>((props, ref) => {
|
||||
const navigate = useNavigate();
|
||||
const isExternal = isExternalLink(props.href);
|
||||
|
||||
// If it's an external link, render without RouterProvider
|
||||
if (isExternal) {
|
||||
return <LinkInternal {...props} ref={ref} />;
|
||||
}
|
||||
|
||||
// For internal links, wrap in RouterProvider so useLink can access the router
|
||||
return (
|
||||
<RouterProvider navigate={navigate} useHref={useHref}>
|
||||
<InternalLinkProvider href={props.href}>
|
||||
<LinkInternal {...props} ref={ref} />
|
||||
</RouterProvider>
|
||||
</InternalLinkProvider>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -20,14 +20,12 @@ import {
|
||||
useTableOptions,
|
||||
Cell as ReactAriaCell,
|
||||
Collection,
|
||||
RouterProvider,
|
||||
} from 'react-aria-components';
|
||||
import { Checkbox } from '../../Checkbox';
|
||||
import { useStyles } from '../../../hooks/useStyles';
|
||||
import { TableDefinition } from '../definition';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useHref } from 'react-router-dom';
|
||||
import { isExternalLink } from '../../../utils/isExternalLink';
|
||||
import { InternalLinkProvider } from '../../InternalLinkProvider';
|
||||
import styles from '../Table.module.css';
|
||||
import clsx from 'clsx';
|
||||
import { Flex } from '../../Flex';
|
||||
@@ -36,8 +34,7 @@ import { Flex } from '../../Flex';
|
||||
export function Row<T extends object>(props: RowProps<T>) {
|
||||
const { classNames, cleanedProps } = useStyles(TableDefinition, props);
|
||||
const { id, columns, children, href, ...rest } = cleanedProps;
|
||||
const navigate = useNavigate();
|
||||
const isExternal = isExternalLink(href);
|
||||
const hasInternalHref = !!href && !isExternalLink(href);
|
||||
|
||||
let { selectionBehavior, selectionMode } = useTableOptions();
|
||||
|
||||
@@ -62,30 +59,17 @@ export function Row<T extends object>(props: RowProps<T>) {
|
||||
</>
|
||||
);
|
||||
|
||||
if (!href || isExternal) {
|
||||
return (
|
||||
<ReactAriaRow
|
||||
id={id}
|
||||
href={href}
|
||||
className={clsx(classNames.row, styles[classNames.row])}
|
||||
{...rest}
|
||||
>
|
||||
{content}
|
||||
</ReactAriaRow>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<RouterProvider navigate={navigate} useHref={useHref}>
|
||||
<InternalLinkProvider href={href}>
|
||||
<ReactAriaRow
|
||||
id={id}
|
||||
href={href}
|
||||
className={clsx(classNames.row, styles[classNames.row])}
|
||||
data-react-aria-pressable="true"
|
||||
data-react-aria-pressable={hasInternalHref ? 'true' : undefined}
|
||||
{...rest}
|
||||
>
|
||||
{content}
|
||||
</ReactAriaRow>
|
||||
</RouterProvider>
|
||||
</InternalLinkProvider>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user