Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
77aaa4f
fix(Link): updated tests and revised logic to accomodate react-router
agreenberry Jan 20, 2023
ed43ef5
chore: added changeset and updated pkgs
agreenberry Jan 20, 2023
3f123b6
added package.json
agreenberry Jan 23, 2023
a44f94d
Merge remote-tracking branch 'origin/main' into 1573-accessibility-re…
agreenberry Jan 23, 2023
8c4c69c
not sure whats going on with packages on this branch
agreenberry Jan 23, 2023
193e944
trying again to fix packages
agreenberry Jan 23, 2023
8a9e7bf
chore(Link): whoops, fix variable name
agreenberry Jan 23, 2023
c9e332d
Merge remote-tracking branch 'origin/main' into 1573-accessibility-re…
agreenberry Jan 23, 2023
5dfd814
Merge branch 'main' into 1573-accessibility-request-primer-react-link…
agreenberry Jan 23, 2023
e434a9c
chore: get package lock in sync
agreenberry Jan 23, 2023
d4470e6
Merge remote-tracking branch 'origin/main' into 1573-accessibility-re…
agreenberry Jan 23, 2023
af51986
Merge remote-tracking branch 'origin/main' into 1573-accessibility-re…
agreenberry Jan 23, 2023
659dc00
chore: get package lock in sync
agreenberry Jan 23, 2023
8151c87
chore: adding changeset again
agreenberry Jan 23, 2023
1fef6a8
chore: updated snapshots and revised story wording
agreenberry Jan 23, 2023
10f041a
Delete gold-boats-mix.md
agreenberry Jan 24, 2023
567ca92
Merge branch 'main' into 1573-accessibility-request-primer-react-link…
agreenberry Jan 24, 2023
31e18b0
Update src/Link.tsx
agreenberry Jan 24, 2023
6b59edb
Merge branch 'main' into 1573-accessibility-request-primer-react-link…
agreenberry Jan 25, 2023
c1d6c7d
Merge branch 'main' into 1573-accessibility-request-primer-react-link…
agreenberry Jan 27, 2023
eb3edb6
chore(Link): fixed formatting
agreenberry Jan 27, 2023
d40219c
chore(Link): remove redundant, failing test
agreenberry Jan 27, 2023
d459e4b
Merge branch 'main' into 1573-accessibility-request-primer-react-link…
agreenberry Jan 27, 2023
340ef85
fix test to log error
agreenberry Jan 27, 2023
44453f8
Merge branch '1573-accessibility-request-primer-react-link-enforce-wh…
agreenberry Jan 27, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tidy-hornets-act.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@primer/react': minor
---

warn user if link `as` prop is not <a> or <button>
45 changes: 44 additions & 1 deletion src/Link.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import React, {forwardRef, useEffect} from 'react'
import styled from 'styled-components'
import {system} from 'styled-system'
import {get} from './constants'
import {useRefObjectAsForwardedRef} from './hooks'
import sx, {SxProp} from './sx'
import {ComponentProps} from './utils/types'
import {ForwardRefComponent as PolymorphicForwardRefComponent} from './utils/polymorphic'

type StyledLinkProps = {
hoverColor?: string
Expand All @@ -17,7 +20,7 @@ const hoverColor = system({
},
})

const Link = styled.a<StyledLinkProps>`
const StyledLink = styled.a<StyledLinkProps>`
color: ${props => (props.muted ? get('colors.fg.muted')(props) : get('colors.accent.fg')(props))};
text-decoration: ${props => (props.underline ? 'underline' : 'none')};
&:hover {
Expand All @@ -38,5 +41,45 @@ const Link = styled.a<StyledLinkProps>`
${sx};
`

const Link = forwardRef(({as: Component = 'a', ...props}, forwardedRef) => {
const innerRef = React.useRef<HTMLAnchorElement>(null)
useRefObjectAsForwardedRef(forwardedRef, innerRef)

if (__DEV__) {
/**
* The Linter yells because it thinks this conditionally calls an effect,
* but since this is a compile-time flag and not a runtime conditional
* this is safe, and ensures the entire effect is kept out of prod builds
* shaving precious bytes from the output, and avoiding mounting a noop effect
*/
// eslint-disable-next-line react-hooks/rules-of-hooks
useEffect(() => {
if (
innerRef.current &&
!(innerRef.current instanceof HTMLButtonElement) &&
!(innerRef.current instanceof HTMLAnchorElement)
) {
// eslint-disable-next-line no-console
console.error(
'Error: Found `Link` component that renders an inaccessible element',
innerRef.current,
'Please ensure `Link` always renders as <a> or <button>',
)
}
}, [innerRef])
}

return (
<StyledLink
as={Component}
{...props}
// @ts-ignore shh
ref={innerRef}
/>
)
}) as PolymorphicForwardRefComponent<'a', StyledLinkProps>

Link.displayName = 'Link'

export type LinkProps = ComponentProps<typeof Link>
export default Link
7 changes: 4 additions & 3 deletions src/PageHeader/PageHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import {useResponsiveValue, ResponsiveValue} from '../hooks/useResponsiveValue'
import {SxProp, merge, BetterSystemStyleObject} from '../sx'
import Heading from '../Heading'
import {ArrowLeftIcon} from '@primer/octicons-react'
import Link from '../Link'
import Link, {LinkProps as BaseLinkProps} from '../Link'

import {ForwardRefComponent as PolymorphicForwardRefComponent} from '../utils/polymorphic'
import {getBreakpointDeclarations} from '../utils/getBreakpointDeclarations'
const REGION_ORDER = {
Expand Down Expand Up @@ -92,8 +93,8 @@ const ContextArea: React.FC<React.PropsWithChildren<PageHeaderProps>> = ({
return <Box sx={merge<BetterSystemStyleObject>(contentNavStyles, sx)}>{children}</Box>
}
type LinkProps = Pick<
React.AnchorHTMLAttributes<HTMLAnchorElement>,
'download' | 'href' | 'hrefLang' | 'media' | 'ping' | 'rel' | 'target' | 'type' | 'referrerPolicy'
React.AnchorHTMLAttributes<HTMLAnchorElement> & BaseLinkProps,
'download' | 'href' | 'hrefLang' | 'media' | 'ping' | 'rel' | 'target' | 'type' | 'referrerPolicy' | 'as'
>
export type ParentLinkProps = React.PropsWithChildren<PageHeaderProps & LinkProps>

Expand Down
10 changes: 9 additions & 1 deletion src/__tests__/Link.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ describe('Link', () => {

it('respects the "sx" prop', () => {
expect(render(<Link sx={{fontStyle: 'italic'}} />)).toHaveStyleRule('font-style', 'italic')
expect(render(<Link as="i" sx={{fontStyle: 'normal'}} />)).toHaveStyleRule('font-style', 'normal')
})

it('applies button styles when rendering a button element', () => {
Expand All @@ -43,4 +42,13 @@ describe('Link', () => {
it('respects the "sx" prop when "muted" prop is also passed', () => {
expect(render(<Link muted sx={{color: 'fg.onEmphasis'}} />)).toMatchSnapshot()
})

it('logs a warning when trying to render invalid "as" prop', () => {
const consoleSpy = jest.spyOn(global.console, 'error').mockImplementation()

HTMLRender(<Link as="i" />)
expect(consoleSpy).toHaveBeenCalled()

consoleSpy.mockRestore()
})
})
47 changes: 25 additions & 22 deletions src/__tests__/__snapshots__/SideNav.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,6 @@ exports[`SideNav SideNav.Link renders consistently 1`] = `
color: #0969da;
-webkit-text-decoration: none;
text-decoration: none;
position: relative;
display: block;
width: 100%;
text-align: left;
font-size: 14px;
}

.c0:hover {
Expand All @@ -34,29 +29,37 @@ exports[`SideNav SideNav.Link renders consistently 1`] = `
appearance: none;
}

.c0 > .c1 {
.c1 {
position: relative;
display: block;
width: 100%;
text-align: left;
font-size: 14px;
}

.c1 > .c2 {
border-bottom: none;
}

.c1.variant-normal > .c0 {
.c2.variant-normal > .c1 {
color: #24292f;
padding: 16px;
border: 0;
border-top: 1px solid hsla(210,18%,87%,1);
}

.c1.variant-normal > .c0:first-child {
.c2.variant-normal > .c1:first-child {
border-top: 0;
border-top-right-radius: 6px;
border-top-left-radius: 6px;
}

.c1.variant-normal > .c0:last-child {
.c2.variant-normal > .c1:last-child {
border-bottom-right-radius: 6px;
border-bottom-left-radius: 6px;
}

.c1.variant-normal > .c0::before {
.c2.variant-normal > .c1::before {
position: absolute;
top: 0;
bottom: 0;
Expand All @@ -67,57 +70,57 @@ exports[`SideNav SideNav.Link renders consistently 1`] = `
content: '';
}

.c1.variant-normal > .c0:hover {
.c2.variant-normal > .c1:hover {
background-color: rgba(234,238,242,0.5);
-webkit-text-decoration: none;
text-decoration: none;
}

.c1.variant-normal > .c0:focus {
.c2.variant-normal > .c1:focus {
background-color: rgba(234,238,242,0.5);
-webkit-text-decoration: none;
text-decoration: none;
outline: solid 2px #0969da;
z-index: 1;
}

.c1.variant-normal > .c0[aria-current='page'],
.c1.variant-normal > .c0[aria-selected='true'] {
.c2.variant-normal > .c1[aria-current='page'],
.c2.variant-normal > .c1[aria-selected='true'] {
background-color: #ffffff;
}

.c1.variant-normal > .c0[aria-current='page']::before,
.c1.variant-normal > .c0[aria-selected='true']::before {
.c2.variant-normal > .c1[aria-current='page']::before,
.c2.variant-normal > .c1[aria-selected='true']::before {
background-color: #fd8c73;
}

.c1.variant-lightweight > .c0 {
.c2.variant-lightweight > .c1 {
padding: 4px 0;
color: #0969da;
}

.c1.variant-lightweight > .c0:hover {
.c2.variant-lightweight > .c1:hover {
color: #24292f;
-webkit-text-decoration: none;
text-decoration: none;
}

.c1.variant-lightweight > .c0:focus {
.c2.variant-lightweight > .c1:focus {
color: #24292f;
-webkit-text-decoration: none;
text-decoration: none;
outline: solid 1px #0969da;
z-index: 1;
}

.c1.variant-lightweight > .c0[aria-current='page'],
.c1.variant-lightweight > .c0[aria-selected='true'] {
.c2.variant-lightweight > .c1[aria-current='page'],
.c2.variant-lightweight > .c1[aria-selected='true'] {
color: #24292f;
font-weight: 500;
}

<a
className="c0"
className="c0 c1"
/>
`;

Expand Down
22 changes: 22 additions & 0 deletions src/stories/Link.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import Link from '../Link'
import {Meta} from '@storybook/react'
import React from 'react'
import {ThemeProvider} from '..'

const meta: Meta = {
title: 'Components/Link',
component: Link,
decorators: [
(Story: React.ComponentType<React.PropsWithChildren<unknown>>): JSX.Element => (
<ThemeProvider>
<Story />
</ThemeProvider>
),
],
}
export default meta

export function LinkStory(): JSX.Element {
return <Link as="i">{`Link with <i> as prop`}</Link>
}
LinkStory.storyName = 'Link'