Skip to content

Commit d4f15eb

Browse files
committed
feat(link): adds asChild to Link and removes as prop
As to fix the usage with other link providers such as ReactRouter by using the simpler Radix asChild approach over the Stiches `as`. Also exposes the component parts so easier to make your own. BREAKING CHANGE: Users of Link `as` prop should change to `asChild` and nest the component with any of it's own props. fix #256
1 parent 484e1a7 commit d4f15eb

File tree

5 files changed

+113
-32
lines changed

5 files changed

+113
-32
lines changed

package-lock.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@
219219
"@radix-ui/react-select": "0.1.1",
220220
"@radix-ui/react-separator": "0.1.4",
221221
"@radix-ui/react-slider": "0.1.4",
222+
"@radix-ui/react-slot": "^0.1.2",
222223
"@radix-ui/react-switch": "0.1.5",
223224
"@radix-ui/react-tabs": "0.1.5",
224225
"@radix-ui/react-toast": "0.1.1",

src/components/Link/Link.stories.tsx

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { Meta, Story } from '@storybook/react'
22
import React from 'react'
3-
import { Link as RouterLink, MemoryRouter } from 'react-router-dom'
3+
import {
4+
Link as RouterLink,
5+
MemoryRouter,
6+
Route,
7+
Routes,
8+
} from 'react-router-dom'
49
import { Link } from '.'
510
import { Box, Column, Text } from '../'
611

@@ -20,7 +25,7 @@ export const Default: Story = () => {
2025
</Box>
2126
<Box>
2227
<Text font="monospace">
23-
<Link href="#"> font="monospace"</Link>
28+
<Link href="/"> font="monospace"</Link>
2429
</Text>
2530
</Box>
2631
<Box>
@@ -61,30 +66,48 @@ export const Styled: Story = () => (
6166
export const WithReactRouter: Story = () => {
6267
return (
6368
<MemoryRouter>
69+
<Routes>
70+
<Route index element={<div>Hello Router</div>} />
71+
<Route path="/one" element={<div>Route 1</div>} />
72+
<Route path="/two" element={<div>Route 2</div>} />
73+
</Routes>
6474
<Box>
6575
<Box>
66-
<RouterLink
67-
// FIXME component={Link}
68-
to="./example-route"
69-
// props for component={Link} are passed on despite the error
70-
// @ts-ignore
71-
css={{ color: '$text' }}
72-
>
73-
Click to change Router path
74-
</RouterLink>
76+
<RouterLink to="/one">Click to change Router path</RouterLink>
77+
</Box>
78+
<Box>
79+
<Link asChild>
80+
<RouterLink to="/two">Click to change Router path</RouterLink>
81+
</Link>
82+
</Box>
83+
<Box>
84+
<Link variant="styled" asChild>
85+
<RouterLink to="./one">Click to change Router path</RouterLink>
86+
</Link>
7587
</Box>
7688
</Box>
7789
</MemoryRouter>
7890
)
7991
}
8092

8193
/**
82-
* Links support substituting the rendered element for one supplied to the `as` prop.
94+
* Links support substituting the rendered element for one supplied to the `asChild` pattern prop.
8395
*/
8496
export const As: Story = () => (
8597
<ol>
86-
<Link as="li" href="test.com" css={{ color: '$text' }}>
87-
Test
98+
<Link asChild href="test.com" css={{ display: 'block' }}>
99+
<li>Test</li>
100+
</Link>
101+
<Link
102+
variant="styled"
103+
asChild
104+
href="test.com"
105+
css={{ display: 'inline-block' }}
106+
>
107+
<li>Test</li>
108+
</Link>
109+
<Link variant="clear" asChild href="test.com" css={{ display: 'block' }}>
110+
<li>Test</li>
88111
</Link>
89112
</ol>
90113
)

src/components/Link/Link.tsx

Lines changed: 71 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,66 @@
1-
import React, { ComponentProps, ElementRef, forwardRef } from 'react'
2-
import type { AsProps, CSSProps, VariantProps } from '../../stitches.config'
3-
import { styled } from '../../stitches.config'
1+
import { Slot } from '@radix-ui/react-slot'
2+
import React, { ElementRef, forwardRef } from 'react'
3+
import {
4+
AsChildProps,
5+
css,
6+
CSSProps,
7+
styled,
8+
VariantProps,
9+
} from '../../stitches.config'
410

511
const DEFAULT_TAG = 'a'
612

7-
const isExternal = (url: string | undefined): boolean =>
8-
!!(url && url.startsWith('http'))
9-
1013
/**
11-
* Link component
14+
* Can be used to test the href to determine if it is external to the application.
15+
*
16+
* Used inside `Link` but can be used separately to create custom links.
1217
*/
13-
export const StyledLink = styled(DEFAULT_TAG, {
18+
export function isExternalUrl(url: string | undefined): boolean {
19+
return !!(
20+
url &&
21+
/^((https?:|s?ftp:|file:\/|chrome:)?\/\/|mailto:|tel:)/.test(
22+
url.toLowerCase()
23+
)
24+
)
25+
}
26+
27+
type InternalLinkProps = React.ComponentPropsWithRef<typeof DEFAULT_TAG> &
28+
AsChildProps
29+
30+
const InternalLink: React.FC<InternalLinkProps> = ({ asChild, ...props }) => {
31+
const Comp = asChild ? Slot : DEFAULT_TAG
32+
return <Comp {...props} />
33+
}
34+
35+
/**
36+
* Used to provide link props derived from the href
37+
*
38+
* Used inside `Link` but can be used separately to create custom links.
39+
* */
40+
export function linkProps(
41+
href: string | undefined
42+
): {
43+
href?: string | undefined
44+
target?: string
45+
rel?: string
46+
external: boolean
47+
} {
48+
return isExternalUrl(href)
49+
? {
50+
href,
51+
target: '_blank',
52+
rel: 'noopener noreferrer',
53+
external: true,
54+
}
55+
: { href, external: false }
56+
}
57+
58+
/**
59+
* Used to style `Link` component
60+
*
61+
* Can be used separately to create custom links.
62+
* */
63+
export const linkStyles = css({
1464
$$linkBackground: '$colors$selection',
1565
// Reset
1666
lineHeight: '1',
@@ -58,20 +108,23 @@ export const StyledLink = styled(DEFAULT_TAG, {
58108
},
59109
})
60110

111+
export const StyledLink = styled(InternalLink, linkStyles)
112+
61113
type LinkVariants = VariantProps<typeof StyledLink>
62-
type AProps = ComponentProps<typeof DEFAULT_TAG>
63-
type LinkProps = Omit<LinkVariants, 'external'> & AProps & CSSProps & AsProps
114+
export type LinkProps = Omit<LinkVariants, 'external'> &
115+
InternalLinkProps &
116+
CSSProps &
117+
AsChildProps
64118

119+
/**
120+
* Link component
121+
*
122+
* Has standard anchor tag props. Uses the radix style `asChild` prop to render as the child component.
123+
* This can be used to wrap other link providers, say for routing. Other the styles and utils are also available to build your own: `linkStyles`, `linkProps` `isExternalUrl`.
124+
*/
65125
export const Link = forwardRef<ElementRef<typeof StyledLink>, LinkProps>(
66126
({ href, ...props }, forwardedRef) => {
67-
return (
68-
<StyledLink
69-
href={href}
70-
external={isExternal(href)}
71-
{...props}
72-
ref={forwardedRef}
73-
/>
74-
)
127+
return <StyledLink {...linkProps(href)} {...props} ref={forwardedRef} />
75128
}
76129
)
77130
Link.toString = () => `.${StyledLink.className}`

src/stitches.config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -594,6 +594,9 @@ export type As = React.ElementType
594594
export type AsProps = {
595595
as?: As
596596
}
597+
export type AsChildProps = {
598+
asChild?: boolean
599+
}
597600
export type ChildProps = {
598601
children?: ReactNode
599602
}

0 commit comments

Comments
 (0)