First pass at moving Link to React Aria

Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
Charles de Dreuille
2025-07-14 14:18:35 +02:00
parent 9dd33c42bb
commit 4690c00826
4 changed files with 180 additions and 69 deletions
@@ -18,7 +18,6 @@ import type { Meta, StoryObj } from '@storybook/react';
import { Link } from './Link';
import { Flex } from '../Flex';
import { Text } from '../Text';
import { Link as RouterLink, MemoryRouter } from 'react-router-dom';
const meta = {
title: 'Components/Link',
@@ -33,7 +32,7 @@ type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
to: 'https://ui.backstage.io',
href: 'https://ui.backstage.io',
children: 'Sign up for Backstage',
},
};
@@ -44,10 +43,14 @@ export const AllVariants: Story = {
},
render: args => (
<Flex gap="4" direction="column">
<Link href="https://ui.backstage.io" variant="subtitle" {...args} />
<Link href="https://ui.backstage.io" variant="body" {...args} />
<Link href="https://ui.backstage.io" variant="caption" {...args} />
<Link href="https://ui.backstage.io" variant="label" {...args} />
<Link href="https://ui.backstage.io" variant="title-large" {...args} />
<Link href="https://ui.backstage.io" variant="title-medium" {...args} />
<Link href="https://ui.backstage.io" variant="title-small" {...args} />
<Link href="https://ui.backstage.io" variant="title-x-small" {...args} />
<Link href="https://ui.backstage.io" variant="body-large" {...args} />
<Link href="https://ui.backstage.io" variant="body-medium" {...args} />
<Link href="https://ui.backstage.io" variant="body-small" {...args} />
<Link href="https://ui.backstage.io" variant="body-x-small" {...args} />
</Flex>
),
};
@@ -58,8 +61,102 @@ export const AllWeights: Story = {
},
render: args => (
<Flex gap="4" direction="column">
<Link weight="regular" style={{ maxWidth: '600px' }} {...args} />
<Link weight="bold" style={{ maxWidth: '600px' }} {...args} />
<Link
href="https://ui.backstage.io"
variant="title-large"
weight="regular"
{...args}
/>
<Link
href="https://ui.backstage.io"
variant="title-large"
weight="bold"
{...args}
/>
<Link
href="https://ui.backstage.io"
variant="title-medium"
weight="regular"
{...args}
/>
<Link
href="https://ui.backstage.io"
variant="title-medium"
weight="bold"
{...args}
/>
<Link
href="https://ui.backstage.io"
variant="title-small"
weight="regular"
{...args}
/>
<Link
href="https://ui.backstage.io"
variant="title-small"
weight="bold"
{...args}
/>
<Link
href="https://ui.backstage.io"
variant="title-x-small"
weight="regular"
{...args}
/>
<Link
href="https://ui.backstage.io"
variant="title-x-small"
weight="bold"
{...args}
/>
<Link
href="https://ui.backstage.io"
variant="body-large"
weight="regular"
{...args}
/>
<Link
href="https://ui.backstage.io"
variant="body-large"
weight="bold"
{...args}
/>
<Link
href="https://ui.backstage.io"
variant="body-medium"
weight="regular"
{...args}
/>
<Link
href="https://ui.backstage.io"
variant="body-medium"
weight="bold"
{...args}
/>
<Link
href="https://ui.backstage.io"
variant="body-small"
weight="regular"
{...args}
/>
<Link
href="https://ui.backstage.io"
variant="body-small"
weight="bold"
{...args}
/>
<Link
href="https://ui.backstage.io"
variant="body-x-small"
weight="regular"
{...args}
/>
<Link
href="https://ui.backstage.io"
variant="body-x-small"
weight="bold"
{...args}
/>
</Flex>
),
};
@@ -68,43 +165,32 @@ export const Responsive: Story = {
args: {
...Default.args,
variant: {
xs: 'label',
md: 'body',
xs: 'title-x-small',
md: 'body-x-small',
},
},
};
export const CustomRender: Story = {
render: () => {
return (
<Flex gap="4" direction="column" align="start">
<Link render={<RouterLink to="/catalog" />}>Go to Catalog</Link>
</Flex>
);
},
decorators: [
Story => (
<MemoryRouter>
<Story />
</MemoryRouter>
),
],
};
export const Playground: Story = {
args: {
...Default.args,
},
render: args => (
<Flex gap="4" direction="column">
<Text>Subtitle</Text>
<Link variant="subtitle" style={{ maxWidth: '600px' }} {...args} />
<Text>Body</Text>
<Link variant="body" style={{ maxWidth: '600px' }} {...args} />
<Text>Caption</Text>
<Link variant="caption" style={{ maxWidth: '600px' }} {...args} />
<Text>Label</Text>
<Link variant="label" style={{ maxWidth: '600px' }} {...args} />
<Text>Title X Small</Text>
<Link variant="title-x-small" style={{ maxWidth: '600px' }} {...args} />
<Text>Body X Small</Text>
<Link variant="body-x-small" style={{ maxWidth: '600px' }} {...args} />
<Text>Body Small</Text>
<Link variant="body-small" style={{ maxWidth: '600px' }} {...args} />
<Text>Body Medium</Text>
<Link variant="body-medium" style={{ maxWidth: '600px' }} {...args} />
<Text>Body Large</Text>
<Link variant="body-large" style={{ maxWidth: '600px' }} {...args} />
<Text>Title Small</Text>
<Link variant="title-small" style={{ maxWidth: '600px' }} {...args} />
<Text>Title Medium</Text>
<Link variant="title-medium" style={{ maxWidth: '600px' }} {...args} />
</Flex>
),
};
+11 -17
View File
@@ -14,37 +14,31 @@
* limitations under the License.
*/
import { useRef, forwardRef } from 'react';
import { useRender } from '@base-ui-components/react/use-render';
import { forwardRef } from 'react';
import { Link as AriaLink } from 'react-aria-components';
import clsx from 'clsx';
import { useStyles } from '../../hooks/useStyles';
import type { LinkProps } from './types';
/** @public */
export const Link = forwardRef<HTMLElement, LinkProps>((props, ref) => {
export const Link = forwardRef<HTMLAnchorElement, LinkProps>((props, ref) => {
const {
className,
variant = 'body',
weight = 'regular',
render = <a />,
...restProps
} = props;
const { classNames, dataAttributes } = useStyles('Link', { variant, weight });
const internalRef = useRef<HTMLElement | null>(null);
const { renderElement } = useRender({
render,
props: {
className: clsx(classNames.root, className),
...dataAttributes,
...restProps,
},
refs: [ref, internalRef],
});
return renderElement();
return (
<AriaLink
ref={ref}
className={clsx(classNames.root, className)}
{...dataAttributes}
{...restProps}
/>
);
});
Link.displayName = 'Link';
+25 -5
View File
@@ -36,22 +36,42 @@
}
}
.bui-Link[data-variant='body'] {
font-size: var(--bui-font-size-3);
.bui-Link[data-variant='title-large'] {
font-size: var(--bui-font-size-8);
line-height: 140%;
}
.bui-Link[data-variant='subtitle'] {
.bui-Link[data-variant='title-medium'] {
font-size: var(--bui-font-size-7);
line-height: 140%;
}
.bui-Link[data-variant='title-small'] {
font-size: var(--bui-font-size-6);
line-height: 140%;
}
.bui-Link[data-variant='title-x-small'] {
font-size: var(--bui-font-size-5);
line-height: 140%;
}
.bui-Link[data-variant='body-large'] {
font-size: var(--bui-font-size-4);
line-height: 140%;
}
.bui-Link[data-variant='caption'] {
.bui-Link[data-variant='body-medium'] {
font-size: var(--bui-font-size-3);
line-height: 140%;
}
.bui-Link[data-variant='body-small'] {
font-size: var(--bui-font-size-2);
line-height: 140%;
}
.bui-Link[data-variant='label'] {
.bui-Link[data-variant='body-x-small'] {
font-size: var(--bui-font-size-1);
line-height: 140%;
}
+23 -12
View File
@@ -14,21 +14,32 @@
* limitations under the License.
*/
import type { CSSProperties, ReactNode } from 'react';
import type { Breakpoint } from '../../types';
import type { useRender } from '@base-ui-components/react/use-render';
import type { LinkProps as AriaLinkProps } from 'react-aria-components';
/** @public */
export interface LinkProps extends useRender.ComponentProps<'a'> {
children: ReactNode;
to?: string;
export interface LinkProps extends AriaLinkProps {
variant?:
| 'subtitle'
| 'body'
| 'caption'
| 'label'
| Partial<Record<Breakpoint, 'subtitle' | 'body' | 'caption' | 'label'>>;
| 'title-large'
| 'title-medium'
| 'title-small'
| 'title-x-small'
| 'body-large'
| 'body-medium'
| 'body-small'
| 'body-x-small'
| Partial<
Record<
Breakpoint,
| 'title-large'
| 'title-medium'
| 'title-small'
| 'title-x-small'
| 'body-large'
| 'body-medium'
| 'body-small'
| 'body-x-small'
>
>;
weight?: 'regular' | 'bold' | Partial<Record<Breakpoint, 'regular' | 'bold'>>;
className?: string;
style?: CSSProperties;
}