Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Accessibility engineering review for Link component. #3317

Merged
merged 44 commits into from
Jun 22, 2023
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
66e6403
Remove as prop from Link.
radglob May 17, 2023
4589960
Merge branch 'main' of github.com:primer/react into link-a11y-review
radglob May 23, 2023
9f40db6
Underline Link when inside of a paragraph tag, hover behavior should …
radglob May 23, 2023
d37e430
Create curly-otters-repeat.md
radglob May 23, 2023
a206550
Soft deprecate as prop for Link so we don't break compatibility.
radglob May 23, 2023
c1c1242
Fix issues with Link-dependent components when as prop is not accepted.
radglob May 25, 2023
c8bb536
Remove ReactRouterLikeLink function from NavList tests.
radglob May 25, 2023
9d40e3f
Merge branch 'main' of github.com:primer/react into link-a11y-review
radglob May 25, 2023
c518bb5
Merge branch 'main' into link-a11y-review
radglob May 25, 2023
df75238
Merge branch 'main' into link-a11y-review
radglob May 26, 2023
a90c33e
Merge branch 'main' into link-a11y-review
radglob May 26, 2023
e19891d
Remove references to as prop from Link docs.
radglob May 30, 2023
38695a4
Update generated/components.json
radglob May 30, 2023
42e3ceb
Remove reference to as prop in ActionList.LinkItem docs.
radglob May 30, 2023
20288a4
Update generated/components.json
radglob May 30, 2023
0ad1a40
Merge branch 'main' into link-a11y-review
radglob May 30, 2023
07f7e8b
Merge branch 'main' into link-a11y-review
radglob May 30, 2023
2176bc9
Merge branch 'main' into link-a11y-review
radglob May 31, 2023
8c262c3
Merge branch 'main' into link-a11y-review
radglob Jun 1, 2023
ed6256c
Merge branch 'main' into link-a11y-review
radglob Jun 1, 2023
ec88a54
Merge branch 'main' into link-a11y-review
radglob Jun 5, 2023
fed6236
Merge branch 'main' into link-a11y-review
radglob Jun 5, 2023
e9988c5
Merge branch 'main' into link-a11y-review
radglob Jun 5, 2023
bf6b5e8
Merge branch 'main' into link-a11y-review
radglob Jun 6, 2023
00cad18
Add link variant to button to match PVC implementation.
radglob Jun 6, 2023
e21b94e
Merge branch 'main' into link-a11y-review
radglob Jun 6, 2023
97ba614
Update generated/components.json
radglob Jun 6, 2023
d873d9e
Merge branch 'main' into link-a11y-review
radglob Jun 6, 2023
8de413b
Allow up to 4 levels of nesting for the NavList (#3343)
mperrotti Jun 6, 2023
48e36e2
Refactor(ActionList): ActionList.Item should render content as a butt…
radglob Jun 6, 2023
1a2b644
Merge branch 'main' of github.com:primer/react into link-a11y-review
radglob Jun 6, 2023
41fcb98
Merge branch 'main' into link-a11y-review
radglob Jun 6, 2023
b40594d
Merge branch 'main' into link-a11y-review
radglob Jun 7, 2023
70476d0
Merge branch 'main' into link-a11y-review
radglob Jun 7, 2023
1103d06
Merge branch 'main' into link-a11y-review
radglob Jun 8, 2023
6e4e560
Merge branch 'main' into link-a11y-review
radglob Jun 8, 2023
aed4851
Rollback attempt at underlying links inside other bits of text.
radglob Jun 15, 2023
6ad63d5
Merge branch 'main' of github.com:primer/react into link-a11y-review
radglob Jun 16, 2023
3fc73f8
Merge branch 'main' into link-a11y-review
radglob Jun 16, 2023
1e320d7
Merge branch 'main' into link-a11y-review
radglob Jun 16, 2023
9684a5c
Merge branch 'main' into link-a11y-review
radglob Jun 20, 2023
dbfe69d
Merge branch 'main' into link-a11y-review
radglob Jun 20, 2023
1e1cc7e
Merge branch 'main' into link-a11y-review
radglob Jun 21, 2023
4aba402
Update src/Button/styles.ts
radglob Jun 22, 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
7 changes: 7 additions & 0 deletions .changeset/curly-otters-repeat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@primer/react": minor
---

Link components no longer accept an `as` prop. If you need link-like styling, you should use a different component and style accordingly.

Links that are rendered within `<p>` tags will always have underlines to distinguish from the surrounding text.
4 changes: 0 additions & 4 deletions docs/content/Link.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,6 @@ import data from '../../src/Link/Link.docs.json'

The Link component styles anchor tags with default hyperlink color cues and hover text decoration. `Link` is used for destinations, or moving from one page to another.

In special cases where you'd like a `<button>` styled like a `Link`, use `<Link as='button'>`. Make sure to provide a click handler with `onClick`.

**Important:** When using the `as` prop, be sure to always render an accessible element type, like `a`, `button`, `input`, or `summary`.
radglob marked this conversation as resolved.
Show resolved Hide resolved

radglob marked this conversation as resolved.
Show resolved Hide resolved
## Examples

```jsx live
Expand Down
12 changes: 1 addition & 11 deletions generated/components.json
Original file line number Diff line number Diff line change
Expand Up @@ -215,11 +215,6 @@
"name": "ref",
"type": "React.RefObject<HTMLAnchorElement>"
},
{
"name": "as",
"type": "React.ElementType",
"defaultValue": "\"a\""
},
{
"name": "sx",
"type": "SystemStyleObject"
Expand Down Expand Up @@ -2048,7 +2043,7 @@
"name": "href",
"type": "string",
"defaultValue": "",
"description": "URL to be used for the Link. (The `href` is passed to the underlying `<a>` element. If `as` is specified, the link behavior may need different props)."
"description": "URL to be used for the Link. (The `href` is passed to the underlying `<a>` element)."
},
{
"name": "muted",
Expand All @@ -2072,11 +2067,6 @@
"name": "ref",
"type": "React.RefObject<HTMLAnchorElement>"
},
{
"name": "as",
"type": "React.ElementType",
"defaultValue": "\"a\""
},
{
"name": "sx",
"type": "SystemStyleObject"
Expand Down
5 changes: 0 additions & 5 deletions src/ActionList/ActionList.docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,6 @@
"name": "ref",
"type": "React.RefObject<HTMLAnchorElement>"
},
{
"name": "as",
"type": "React.ElementType",
"defaultValue": "\"a\""
},
{
"name": "sx",
"type": "SystemStyleObject"
Expand Down
14 changes: 5 additions & 9 deletions src/ActionList/LinkItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {ActionListItemProps} from './shared'
type LinkProps = {
download?: string
href?: string
to?: string
hrefLang?: string
media?: string
ping?: string
Expand All @@ -21,7 +22,7 @@ type LinkProps = {
// LinkItem does not support selected, variants, etc.
export type ActionListLinkItemProps = Pick<ActionListItemProps, 'active' | 'children' | 'sx'> & LinkProps

export const LinkItem = React.forwardRef(({sx = {}, active, as: Component, ...props}, forwardedRef) => {
export const LinkItem = React.forwardRef(({sx = {}, active, ...props}, forwardedRef) => {
const styles = {
// occupy full size of Item
paddingX: 2,
Expand All @@ -35,6 +36,8 @@ export const LinkItem = React.forwardRef(({sx = {}, active, as: Component, ...pr
'&:hover': {color: 'inherit', textDecoration: 'none'},
}

if (props.href === undefined && props.to !== undefined) props.href = props.to

return (
<Item
active={active}
Expand All @@ -45,14 +48,7 @@ export const LinkItem = React.forwardRef(({sx = {}, active, as: Component, ...pr
props.onClick && props.onClick(event as React.MouseEvent<HTMLAnchorElement>)
}
return (
<Link
as={Component}
sx={merge(styles, sx as SxProp)}
{...rest}
{...props}
onClick={clickHandler}
ref={forwardedRef}
>
<Link sx={merge(styles, sx as SxProp)} {...rest} {...props} onClick={clickHandler} ref={forwardedRef}>
{children}
</Link>
)
Expand Down
4 changes: 4 additions & 0 deletions src/BaseStyles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ const GlobalStyle = createGlobalStyle<{colorScheme?: 'light' | 'dark'}>`
details-dialog:focus:not(:focus-visible):not(.focus-visible) {
outline: none;
}

p a[class*=StyledLink] {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way you could test this in some of our React apps? I like the idea of it, but I feel like this could easily cause unexpected visual changes. Underlined in a body of text probably won't ruffle any feathers, but there's probably some cases where its not a body of text and more like one line of text as part of a UI.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd be happy to test it out, but I don't have a clue where to start with that.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with @langermank , but also have no idea where to start. @colebemis - maybe you can help?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if this is a blocker then we could wait until we receive more guidance on Link and underline usage across Primer. I know this is the case for the PVC Link accessibility fixes, so if this is required here too then I think it's okay to come back to this.

If we wanted to test this we'd probably have to query up the usage of PRC Link across GitHub and find the instances where this component is a descendant of a paragraph element, though I would love to know if there are better ways to go about this. From a very quick look, this seems like an uncommon pairing, so there might not be too much friction, but that's only from a small sample size.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree with @TylerJDev that this may be better solved more holistically through upcoming work to determine underline styles for links in https://github.com/github/primer/issues/2099.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rolled back the changes around styling links in text. This way, we can merge in the as restriction, and let the more holistic work handle this issue.

text-decoration: underline;
}
radglob marked this conversation as resolved.
Show resolved Hide resolved
`

const Base = styled.div<SystemTypographyProps & SystemCommonProps>`
Expand Down
9 changes: 2 additions & 7 deletions src/Link/Link.docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"name": "href",
"type": "string",
"defaultValue": "",
"description": "URL to be used for the Link. (The `href` is passed to the underlying `<a>` element. If `as` is specified, the link behavior may need different props)."
"description": "URL to be used for the Link. (The `href` is passed to the underlying `<a>` element)."
},
{
"name": "muted",
Expand All @@ -33,15 +33,10 @@
"name": "ref",
"type": "React.RefObject<HTMLAnchorElement>"
},
{
"name": "as",
"type": "React.ElementType",
"defaultValue": "\"a\""
},
{
"name": "sx",
"type": "SystemStyleObject"
}
],
"subcomponents": []
}
}
20 changes: 19 additions & 1 deletion src/Link/Link.features.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Link from '../Link'
import {Meta} from '@storybook/react'
import Box from '../Box'
import {Meta, StoryFn} from '@storybook/react'
import React from 'react'
import {ComponentProps} from '../utils/types'

Expand All @@ -19,3 +20,20 @@ export const Underline = () => (
Link
</Link>
)

export const WithinText: StoryFn<typeof Link> = args => (
<Box as="p">
This{' '}
<Link href="#" {...args}>
link
</Link>{' '}
is inside of other text.
</Box>
)

WithinText.args = {
muted: false,
underline: false,
}

WithinText.argTypes = {}
46 changes: 9 additions & 37 deletions src/Link/Link.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, {forwardRef, useEffect} from 'react'
import React, {forwardRef} from 'react'
import styled from 'styled-components'
import {system} from 'styled-system'
import {get} from '../constants'
Expand All @@ -23,55 +23,27 @@ const hoverColor = system({
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 {
&:hover,
&:focus {
text-decoration: ${props => (props.muted ? 'none' : 'underline')};
${props => (props.hoverColor ? hoverColor : props.muted ? `color: ${get('colors.accent.fg')(props)}` : '')};
}
&:is(button) {
display: inline-block;
padding: 0;
font-size: inherit;
white-space: nowrap;
cursor: pointer;
user-select: none;
background-color: transparent;
border: 0;
appearance: none;
}
${sx};
`

const Link = forwardRef(({as: Component = 'a', ...props}, forwardedRef) => {
const Link = forwardRef(({as, ...props}, forwardedRef) => {
radglob marked this conversation as resolved.
Show resolved Hide resolved
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])
if (as !== undefined) {
// eslint-disable-next-line no-console
console.warn(
'Links no longer accept an as prop. If you need to style another tag as a link, you should use a different component and apply appropriate styling.',
)
}

return (
<StyledLink
as={Component}
{...props}
// @ts-ignore shh
ref={innerRef}
Expand Down
15 changes: 1 addition & 14 deletions src/Link/__tests__/Link.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {render as HTMLRender} from '@testing-library/react'
import {axe} from 'jest-axe'

describe('Link', () => {
behavesAsComponent({Component: Link})
behavesAsComponent({Component: Link, options: {skipAs: true}})

checkExports('Link', {
default: Link,
Expand All @@ -29,24 +29,11 @@ describe('Link', () => {
expect(render(<Link sx={{fontStyle: 'italic'}} />)).toHaveStyleRule('font-style', 'italic')
})

it('applies button styles when rendering a button element', () => {
expect(render(<Link as="button" />)).toMatchSnapshot()
})

it('respects the "muted" prop', () => {
expect(render(<Link muted />)).toMatchSnapshot()
})

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()
})
})
Loading