diff --git a/.changeset/selfish-beans-cheat.md b/.changeset/selfish-beans-cheat.md new file mode 100644 index 00000000000..4732db0fbb2 --- /dev/null +++ b/.changeset/selfish-beans-cheat.md @@ -0,0 +1,5 @@ +--- +"@primer/react": patch +--- + +Link: Add `inline` prop to tag links inside a text block, underlined with accessibility setting `[data-a11y-link-underlines]` diff --git a/.playwright/snapshots/components/Link.test.ts-snapshots/Link-Inline-dark-colorblind-linux.png b/.playwright/snapshots/components/Link.test.ts-snapshots/Link-Inline-dark-colorblind-linux.png new file mode 100644 index 00000000000..1d58f8b46ef Binary files /dev/null and b/.playwright/snapshots/components/Link.test.ts-snapshots/Link-Inline-dark-colorblind-linux.png differ diff --git a/.playwright/snapshots/components/Link.test.ts-snapshots/Link-Inline-dark-dimmed-linux.png b/.playwright/snapshots/components/Link.test.ts-snapshots/Link-Inline-dark-dimmed-linux.png new file mode 100644 index 00000000000..c3d64ca0481 Binary files /dev/null and b/.playwright/snapshots/components/Link.test.ts-snapshots/Link-Inline-dark-dimmed-linux.png differ diff --git a/.playwright/snapshots/components/Link.test.ts-snapshots/Link-Inline-dark-high-contrast-linux.png b/.playwright/snapshots/components/Link.test.ts-snapshots/Link-Inline-dark-high-contrast-linux.png new file mode 100644 index 00000000000..5ddd221f8c2 Binary files /dev/null and b/.playwright/snapshots/components/Link.test.ts-snapshots/Link-Inline-dark-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/Link.test.ts-snapshots/Link-Inline-dark-linux.png b/.playwright/snapshots/components/Link.test.ts-snapshots/Link-Inline-dark-linux.png new file mode 100644 index 00000000000..5f560bacc07 Binary files /dev/null and b/.playwright/snapshots/components/Link.test.ts-snapshots/Link-Inline-dark-linux.png differ diff --git a/.playwright/snapshots/components/Link.test.ts-snapshots/Link-Inline-dark-tritanopia-linux.png b/.playwright/snapshots/components/Link.test.ts-snapshots/Link-Inline-dark-tritanopia-linux.png new file mode 100644 index 00000000000..1d58f8b46ef Binary files /dev/null and b/.playwright/snapshots/components/Link.test.ts-snapshots/Link-Inline-dark-tritanopia-linux.png differ diff --git a/.playwright/snapshots/components/Link.test.ts-snapshots/Link-Inline-light-colorblind-linux.png b/.playwright/snapshots/components/Link.test.ts-snapshots/Link-Inline-light-colorblind-linux.png new file mode 100644 index 00000000000..bc86c4f604b Binary files /dev/null and b/.playwright/snapshots/components/Link.test.ts-snapshots/Link-Inline-light-colorblind-linux.png differ diff --git a/.playwright/snapshots/components/Link.test.ts-snapshots/Link-Inline-light-high-contrast-linux.png b/.playwright/snapshots/components/Link.test.ts-snapshots/Link-Inline-light-high-contrast-linux.png new file mode 100644 index 00000000000..f6e998b8d32 Binary files /dev/null and b/.playwright/snapshots/components/Link.test.ts-snapshots/Link-Inline-light-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/Link.test.ts-snapshots/Link-Inline-light-linux.png b/.playwright/snapshots/components/Link.test.ts-snapshots/Link-Inline-light-linux.png new file mode 100644 index 00000000000..7bb2f6efae2 Binary files /dev/null and b/.playwright/snapshots/components/Link.test.ts-snapshots/Link-Inline-light-linux.png differ diff --git a/.playwright/snapshots/components/Link.test.ts-snapshots/Link-Inline-light-tritanopia-linux.png b/.playwright/snapshots/components/Link.test.ts-snapshots/Link-Inline-light-tritanopia-linux.png new file mode 100644 index 00000000000..bc86c4f604b Binary files /dev/null and b/.playwright/snapshots/components/Link.test.ts-snapshots/Link-Inline-light-tritanopia-linux.png differ diff --git a/e2e/components/Link.test.ts b/e2e/components/Link.test.ts index 75259a13e91..099286a4368 100644 --- a/e2e/components/Link.test.ts +++ b/e2e/components/Link.test.ts @@ -128,4 +128,38 @@ test.describe('Link', () => { }) } }) + + test.describe('Dev: Inline', () => { + for (const theme of themes) { + test.describe(theme, () => { + test('default @vrt', async ({page}) => { + await visit(page, { + id: 'components-link-devonly--inline', + globals: { + colorScheme: theme, + }, + }) + + // Default state + expect(await page.screenshot()).toMatchSnapshot(`Link.Inline.${theme}.png`) + }) + + test('axe @aat', async ({page}) => { + await visit(page, { + id: 'components-link-devonly--inline', + globals: { + colorScheme: theme, + }, + }) + await expect(page).toHaveNoViolations({ + rules: { + 'color-contrast': { + enabled: theme !== 'dark_dimmed', + }, + }, + }) + }) + }) + } + }) }) diff --git a/src/Link/Link.dev.stories.tsx b/src/Link/Link.dev.stories.tsx new file mode 100644 index 00000000000..1e49676165e --- /dev/null +++ b/src/Link/Link.dev.stories.tsx @@ -0,0 +1,83 @@ +import Link from '.' +import {Meta} from '@storybook/react' +import React from 'react' +import {ComponentProps} from '../utils/types' + +export default { + title: 'Components/Link/DevOnly', + component: Link, +} as Meta> + +export const Inline = () => ( +
+
+ [data-a11y-link-underlines=true] (inline always underlines) + inline: undefined, underline: undefined + + inline: undefined, underline: true + + + inline: undefined, underline: false + +
+ + inline: true, underline: undefined + + + inline: false, underline: undefined + +
+ + inline: true, underline: true + + + inline: true, underline: false + + + inline: false, underline: true + + + inline: false, underline: false + +
+ + inline: true, muted: true + +
+
+
+ [data-a11y-link-underlines=false] (inline does nothing) + inline: undefined, underline: undefined + + inline: undefined, underline: true + + + inline: undefined, underline: false + +
+ + inline: true, underline: undefined + + + inline: false, underline: undefined + +
+ + inline: true, underline: true + + + inline: true, underline: false + + + inline: false, underline: true + + + inline: false, underline: false + +
+ + inline: true, muted: true + +
+
+) diff --git a/src/Link/Link.docs.json b/src/Link/Link.docs.json index 3563203ff67..1b7f4b0a0b9 100644 --- a/src/Link/Link.docs.json +++ b/src/Link/Link.docs.json @@ -17,6 +17,12 @@ "defaultValue": "false", "description": "Uses a less prominent shade for Link color, and the default link shade on hover" }, + { + "name": "inline", + "type": "boolean", + "defaultValue": "false", + "description": "Tag link inside a text block" + }, { "name": "underline", "type": "boolean", @@ -44,4 +50,4 @@ } ], "subcomponents": [] -} \ No newline at end of file +} diff --git a/src/Link/Link.features.stories.tsx b/src/Link/Link.features.stories.tsx index 22fe440c73b..be1705bd37b 100644 --- a/src/Link/Link.features.stories.tsx +++ b/src/Link/Link.features.stories.tsx @@ -19,3 +19,11 @@ export const Underline = () => ( Link ) + +export const Inline = () => ( +
+ + Link + +
+) diff --git a/src/Link/Link.stories.tsx b/src/Link/Link.stories.tsx index 88136c4e3e5..0812405edbd 100644 --- a/src/Link/Link.stories.tsx +++ b/src/Link/Link.stories.tsx @@ -14,6 +14,7 @@ Playground.args = { href: '#', muted: false, underline: false, + inline: false, } export const Default = () => Link diff --git a/src/Link/Link.tsx b/src/Link/Link.tsx index 735c1674466..fd7e6ba8e58 100644 --- a/src/Link/Link.tsx +++ b/src/Link/Link.tsx @@ -11,6 +11,8 @@ type StyledLinkProps = { hoverColor?: string muted?: boolean underline?: boolean + // Link inside a text block + inline?: boolean } & SxProp const hoverColor = system({ @@ -22,7 +24,19 @@ const hoverColor = system({ const StyledLink = styled.a` color: ${props => (props.muted ? get('colors.fg.muted')(props) : get('colors.accent.fg')(props))}; - text-decoration: ${props => (props.underline ? 'underline' : 'none')}; + + /* By default, Link does not have underline */ + text-decoration: none; + + /* You can add one by setting underline={true} */ + text-decoration: ${props => (props.underline ? 'underline' : undefined)}; + + /* Inline links (inside a text block), however, should have underline based on accessibility setting set in data-attribute */ + /* Note: setting underline={false} does not override this */ + [data-a11y-link-underlines='true'] &[data-inline='true'] { + text-decoration: underline; + } + &:hover { text-decoration: ${props => (props.muted ? 'none' : 'underline')}; ${props => (props.hoverColor ? hoverColor : props.muted ? `color: ${get('colors.accent.fg')(props)}` : '')}; @@ -72,6 +86,7 @@ const Link = forwardRef(({as: Component = 'a', ...props}, forwardedRef) => { return ( `; exports[`Link passes href down to link element 1`] = ` -.c0 { +.c1 { color: #0969da; -webkit-text-decoration: none; text-decoration: none; } -.c0:hover { +[data-a11y-link-underlines='true'] .c0[data-inline='true'] { + -webkit-text-decoration: underline; + text-decoration: underline; +} + +.c1:hover { -webkit-text-decoration: underline; text-decoration: underline; } -.c0:is(button) { +.c1:is(button) { display: inline-block; padding: 0; font-size: inherit; @@ -64,24 +74,29 @@ exports[`Link passes href down to link element 1`] = ` } `; exports[`Link renders consistently 1`] = ` -.c0 { +.c1 { color: #0969da; -webkit-text-decoration: none; text-decoration: none; } -.c0:hover { +[data-a11y-link-underlines='true'] .c0[data-inline='true'] { + -webkit-text-decoration: underline; + text-decoration: underline; +} + +.c1:hover { -webkit-text-decoration: underline; text-decoration: underline; } -.c0:is(button) { +.c1:is(button) { display: inline-block; padding: 0; font-size: inherit; @@ -99,24 +114,29 @@ exports[`Link renders consistently 1`] = ` } `; exports[`Link respects hoverColor prop 1`] = ` -.c0 { +.c1 { color: #0969da; -webkit-text-decoration: none; text-decoration: none; } -.c0:hover { +[data-a11y-link-underlines='true'] .c0[data-inline='true'] { + -webkit-text-decoration: underline; + text-decoration: underline; +} + +.c1:hover { -webkit-text-decoration: underline; text-decoration: underline; color: #0969da; } -.c0:is(button) { +.c1:is(button) { display: inline-block; padding: 0; font-size: inherit; @@ -134,25 +154,30 @@ exports[`Link respects hoverColor prop 1`] = ` } `; exports[`Link respects the "sx" prop when "muted" prop is also passed 1`] = ` -.c0 { +.c1 { color: #656d76; -webkit-text-decoration: none; text-decoration: none; color: #ffffff; } -.c0:hover { +[data-a11y-link-underlines='true'] .c0[data-inline='true'] { + -webkit-text-decoration: underline; + text-decoration: underline; +} + +.c1:hover { -webkit-text-decoration: none; text-decoration: none; color: #0969da; } -.c0:is(button) { +.c1:is(button) { display: inline-block; padding: 0; font-size: inherit; @@ -170,25 +195,30 @@ exports[`Link respects the "sx" prop when "muted" prop is also passed 1`] = ` } `; exports[`Link respects the "muted" prop 1`] = ` -.c0 { +.c1 { color: #656d76; -webkit-text-decoration: none; text-decoration: none; } -.c0:hover { +[data-a11y-link-underlines='true'] .c0[data-inline='true'] { + -webkit-text-decoration: underline; + text-decoration: underline; +} + +.c1:hover { -webkit-text-decoration: none; text-decoration: none; color: #0969da; } -.c0:is(button) { +.c1:is(button) { display: inline-block; padding: 0; font-size: inherit; @@ -206,7 +236,7 @@ exports[`Link respects the "muted" prop 1`] = ` } `; diff --git a/src/NavList/__snapshots__/NavList.test.tsx.snap b/src/NavList/__snapshots__/NavList.test.tsx.snap index eebaf1c1e46..fbbcd726f81 100644 --- a/src/NavList/__snapshots__/NavList.test.tsx.snap +++ b/src/NavList/__snapshots__/NavList.test.tsx.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`NavList renders a simple list 1`] = ` -.c4 { +.c5 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -16,7 +16,7 @@ exports[`NavList renders a simple list 1`] = ` min-width: 0; } -.c5 { +.c6 { -webkit-box-flex: 1; -webkit-flex-grow: 1; -ms-flex-positive: 1; @@ -120,7 +120,7 @@ exports[`NavList renders a simple list 1`] = ` border-radius: 6px; } -.c6 { +.c7 { position: relative; display: -webkit-box; display: -webkit-flex; @@ -152,21 +152,21 @@ exports[`NavList renders a simple list 1`] = ` margin-bottom: unset; } -.c6[aria-disabled] { +.c7[aria-disabled] { cursor: not-allowed; } -.c6[aria-disabled] [data-component="ActionList.Checkbox"] { +.c7[aria-disabled] [data-component="ActionList.Checkbox"] { cursor: not-allowed; background-color: var(--color-input-disabled-bg,rgba(175,184,193,0.2)); border-color: var(--color-input-disabled-bg,rgba(175,184,193,0.2)); } -.c6 [data-component="ActionList.Item--DividerContainer"] { +.c7 [data-component="ActionList.Item--DividerContainer"] { position: relative; } -.c6 [data-component="ActionList.Item--DividerContainer"]::before { +.c7 [data-component="ActionList.Item--DividerContainer"]::before { content: " "; display: block; position: absolute; @@ -177,7 +177,7 @@ exports[`NavList renders a simple list 1`] = ` border-color: var(--divider-color,transparent); } -.c6:not(:first-of-type) { +.c7:not(:first-of-type) { --divider-color: rgba(208,215,222,0.48); } @@ -185,18 +185,18 @@ exports[`NavList renders a simple list 1`] = ` --divider-color: transparent !important; } -.c6:hover:not([aria-disabled]), -.c6:focus:not([aria-disabled]), -.c6[data-focus-visible-added]:not([aria-disabled]) { +.c7:hover:not([aria-disabled]), +.c7:focus:not([aria-disabled]), +.c7[data-focus-visible-added]:not([aria-disabled]) { --divider-color: transparent; } -.c6:hover:not([aria-disabled]) + .c1, -.c6[data-focus-visible-added] + li { +.c7:hover:not([aria-disabled]) + .c1, +.c7[data-focus-visible-added] + li { --divider-color: transparent; } -.c3 { +.c4 { color: #0969da; -webkit-text-decoration: none; text-decoration: none; @@ -216,12 +216,17 @@ exports[`NavList renders a simple list 1`] = ` color: inherit; } -.c3:hover { +[data-a11y-link-underlines='true'] .c3[data-inline='true'] { -webkit-text-decoration: underline; text-decoration: underline; } -.c3:is(button) { +.c4:hover { + -webkit-text-decoration: underline; + text-decoration: underline; +} + +.c4:is(button) { display: inline-block; padding: 0; font-size: inherit; @@ -238,7 +243,7 @@ exports[`NavList renders a simple list 1`] = ` appearance: none; } -.c3:hover { +.c4:hover { color: inherit; -webkit-text-decoration: none; text-decoration: none; @@ -271,27 +276,27 @@ exports[`NavList renders a simple list 1`] = ` } @media (hover:hover) and (pointer:fine) { - .c6:hover:not([aria-disabled]) { + .c7:hover:not([aria-disabled]) { background-color: rgba(208,215,222,0.32); color: #1F2328; box-shadow: inset 0 0 0 max(1px,0.0625rem) rgba(0,0,0,0); } - .c6:focus-visible, - .c6 > a:focus-visible { + .c7:focus-visible, + .c7 > a:focus-visible { outline: none; border: 2 solid; box-shadow: 0 0 0 2px #0969da; } - .c6:active:not([aria-disabled]) { + .c7:active:not([aria-disabled]) { background-color: rgba(208,215,222,0.48); color: #1F2328; } } @media (forced-colors:active) { - .c6:focus { + .c7:focus { outline: solid 1px transparent !important; } } @@ -309,17 +314,17 @@ exports[`NavList renders a simple list 1`] = `
Home @@ -328,21 +333,21 @@ exports[`NavList renders a simple list 1`] = `
  • Contact @@ -403,7 +408,7 @@ exports[`NavList renders with groups 1`] = ` padding-inline-start: 0; } -.c8 { +.c9 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -418,7 +423,7 @@ exports[`NavList renders with groups 1`] = ` min-width: 0; } -.c9 { +.c10 { -webkit-box-flex: 1; -webkit-flex-grow: 1; -ms-flex-positive: 1; @@ -535,7 +540,7 @@ exports[`NavList renders with groups 1`] = ` border-radius: 6px; } -.c10 { +.c11 { position: relative; display: -webkit-box; display: -webkit-flex; @@ -567,21 +572,21 @@ exports[`NavList renders with groups 1`] = ` margin-bottom: unset; } -.c10[aria-disabled] { +.c11[aria-disabled] { cursor: not-allowed; } -.c10[aria-disabled] [data-component="ActionList.Checkbox"] { +.c11[aria-disabled] [data-component="ActionList.Checkbox"] { cursor: not-allowed; background-color: var(--color-input-disabled-bg,rgba(175,184,193,0.2)); border-color: var(--color-input-disabled-bg,rgba(175,184,193,0.2)); } -.c10 [data-component="ActionList.Item--DividerContainer"] { +.c11 [data-component="ActionList.Item--DividerContainer"] { position: relative; } -.c10 [data-component="ActionList.Item--DividerContainer"]::before { +.c11 [data-component="ActionList.Item--DividerContainer"]::before { content: " "; display: block; position: absolute; @@ -592,7 +597,7 @@ exports[`NavList renders with groups 1`] = ` border-color: var(--divider-color,transparent); } -.c10:not(:first-of-type) { +.c11:not(:first-of-type) { --divider-color: rgba(208,215,222,0.48); } @@ -600,18 +605,18 @@ exports[`NavList renders with groups 1`] = ` --divider-color: transparent !important; } -.c10:hover:not([aria-disabled]), -.c10:focus:not([aria-disabled]), -.c10[data-focus-visible-added]:not([aria-disabled]) { +.c11:hover:not([aria-disabled]), +.c11:focus:not([aria-disabled]), +.c11[data-focus-visible-added]:not([aria-disabled]) { --divider-color: transparent; } -.c10:hover:not([aria-disabled]) + .c5, -.c10[data-focus-visible-added] + li { +.c11:hover:not([aria-disabled]) + .c5, +.c11[data-focus-visible-added] + li { --divider-color: transparent; } -.c7 { +.c8 { color: #0969da; -webkit-text-decoration: none; text-decoration: none; @@ -631,12 +636,17 @@ exports[`NavList renders with groups 1`] = ` color: inherit; } -.c7:hover { +[data-a11y-link-underlines='true'] .c7[data-inline='true'] { -webkit-text-decoration: underline; text-decoration: underline; } -.c7:is(button) { +.c8:hover { + -webkit-text-decoration: underline; + text-decoration: underline; +} + +.c8:is(button) { display: inline-block; padding: 0; font-size: inherit; @@ -653,7 +663,7 @@ exports[`NavList renders with groups 1`] = ` appearance: none; } -.c7:hover { +.c8:hover { color: inherit; -webkit-text-decoration: none; text-decoration: none; @@ -686,27 +696,27 @@ exports[`NavList renders with groups 1`] = ` } @media (hover:hover) and (pointer:fine) { - .c10:hover:not([aria-disabled]) { + .c11:hover:not([aria-disabled]) { background-color: rgba(208,215,222,0.32); color: #1F2328; box-shadow: inset 0 0 0 max(1px,0.0625rem) rgba(0,0,0,0); } - .c10:focus-visible, - .c10 > a:focus-visible { + .c11:focus-visible, + .c11 > a:focus-visible { outline: none; border: 2 solid; box-shadow: 0 0 0 2px #0969da; } - .c10:active:not([aria-disabled]) { + .c11:active:not([aria-disabled]) { background-color: rgba(208,215,222,0.48); color: #1F2328; } } @media (forced-colors:active) { - .c10:focus { + .c11:focus { outline: solid 1px transparent !important; } } @@ -742,17 +752,17 @@ exports[`NavList renders with groups 1`] = `
    Getting started @@ -781,21 +791,21 @@ exports[`NavList renders with groups 1`] = ` class="c4" >