@automattic/vip-design-system 2.4.5 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/system/Breadcrumbs/Breadcrumbs.d.ts +1 -0
- package/build/system/Breadcrumbs/Breadcrumbs.js +75 -20
- package/build/system/Breadcrumbs/Breadcrumbs.stories.d.ts +2 -0
- package/build/system/Breadcrumbs/Breadcrumbs.stories.js +47 -7
- package/build/system/Breadcrumbs/Breadcrumbs.test.js +72 -0
- package/build/system/Breadcrumbs/styles.d.ts +2 -0
- package/build/system/Breadcrumbs/styles.js +8 -2
- package/build/system/Link/Link.d.ts +11 -1
- package/build/system/Link/Link.js +16 -1
- package/build/system/Link/Link.stories.d.ts +14 -1
- package/build/system/Link/Link.stories.js +16 -3
- package/build/system/Nav/styles.js +2 -1
- package/build/system/theme/index.d.ts +863 -3
- package/build/system/theme/index.js +5 -6
- package/build/system/utils/stories/CustomLink.d.ts +1 -0
- package/build/system/utils/stories/CustomLink.js +7 -1
- package/package.json +1 -1
- package/src/system/Breadcrumbs/Breadcrumbs.stories.tsx +32 -3
- package/src/system/Breadcrumbs/Breadcrumbs.test.tsx +60 -0
- package/src/system/Breadcrumbs/Breadcrumbs.tsx +100 -29
- package/src/system/Breadcrumbs/styles.ts +11 -0
- package/src/system/Link/Link.stories.tsx +42 -1
- package/src/system/Link/Link.tsx +17 -8
- package/src/system/Nav/styles.ts +1 -0
- package/src/system/theme/index.js +5 -6
- package/src/system/utils/stories/CustomLink.tsx +6 -0
|
@@ -6,6 +6,7 @@ import ColorBuilder from './colors';
|
|
|
6
6
|
import ValetDark from './generated/valet-theme-dark.json';
|
|
7
7
|
import Valet from './generated/valet-theme-light.json';
|
|
8
8
|
import ThemeBuilder from './getPropValue';
|
|
9
|
+
import { linkUnderlineProperties } from '../Link/Link';
|
|
9
10
|
|
|
10
11
|
// Light
|
|
11
12
|
const { getPropValue, getVariants, ValetTheme, getHeadingStyles } = ThemeBuilder( Valet );
|
|
@@ -402,21 +403,19 @@ export default {
|
|
|
402
403
|
|
|
403
404
|
links: {
|
|
404
405
|
primary: {
|
|
406
|
+
...linkUnderlineProperties,
|
|
407
|
+
|
|
405
408
|
color: 'link',
|
|
406
409
|
'&:visited': {
|
|
407
410
|
color: 'links.visited',
|
|
408
411
|
},
|
|
409
412
|
'&:hover': {
|
|
410
413
|
color: 'links.hover',
|
|
411
|
-
|
|
412
|
-
textDecorationThickness: '0.125rem',
|
|
414
|
+
textDecorationThickness: '0.15rem',
|
|
413
415
|
},
|
|
414
416
|
'&:active': {
|
|
415
417
|
color: 'links.active',
|
|
416
418
|
},
|
|
417
|
-
|
|
418
|
-
textDecorationThickness: '0.125rem',
|
|
419
|
-
textUnderlineOffset: '0.250rem',
|
|
420
419
|
},
|
|
421
420
|
'button-primary': {
|
|
422
421
|
variant: 'buttons.primary',
|
|
@@ -531,7 +530,7 @@ export default {
|
|
|
531
530
|
a: {
|
|
532
531
|
'&:hover': {
|
|
533
532
|
textDecorationLine: 'underline',
|
|
534
|
-
textDecorationThickness: '0.
|
|
533
|
+
textDecorationThickness: '0.1rem',
|
|
535
534
|
textUnderlineOffset: '0.250rem',
|
|
536
535
|
},
|
|
537
536
|
},
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
exports.__esModule = true;
|
|
4
|
-
exports.CustomLink = void 0;
|
|
4
|
+
exports.CustomLinkComponentized = exports.CustomLink = void 0;
|
|
5
5
|
var _react = _interopRequireWildcard(require("react"));
|
|
6
|
+
var _Link = require("../../Link/Link");
|
|
6
7
|
var _jsxRuntime = require("theme-ui/jsx-runtime");
|
|
7
8
|
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(e) { return e ? t : r; })(e); }
|
|
8
9
|
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { "default": e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n["default"] = e, t && t.set(e, n), n; }
|
|
@@ -13,4 +14,9 @@ function (props, ref) {
|
|
|
13
14
|
return (0, _jsxRuntime.jsx)("a", _extends({}, props, {
|
|
14
15
|
ref: ref
|
|
15
16
|
}));
|
|
17
|
+
});
|
|
18
|
+
var CustomLinkComponentized = exports.CustomLinkComponentized = /*#__PURE__*/(0, _react.forwardRef)(function (props, ref) {
|
|
19
|
+
return (0, _jsxRuntime.jsx)(_Link.Link, _extends({}, props, {
|
|
20
|
+
ref: ref
|
|
21
|
+
}));
|
|
16
22
|
});
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
+
/** @jsxImportSource theme-ui */
|
|
2
|
+
|
|
1
3
|
import React from 'react';
|
|
2
4
|
|
|
3
5
|
import { Breadcrumbs as Breadcrumbs } from './Breadcrumbs';
|
|
6
|
+
import { Box } from '../Box';
|
|
7
|
+
import { CustomLink, CustomLinkComponentized } from '../utils/stories/CustomLink';
|
|
4
8
|
|
|
5
9
|
import type { StoryObj } from '@storybook/react';
|
|
6
10
|
|
|
7
|
-
// eslint-disable-next-line jsx-a11y/anchor-has-content
|
|
8
|
-
const CustomLink = props => <a { ...props } />;
|
|
9
|
-
|
|
10
11
|
export default {
|
|
11
12
|
title: 'Navigation/Breadcrumbs',
|
|
12
13
|
component: Breadcrumbs,
|
|
@@ -78,3 +79,31 @@ export const Default: Story = {
|
|
|
78
79
|
/>
|
|
79
80
|
),
|
|
80
81
|
};
|
|
82
|
+
|
|
83
|
+
export const Collapsible: Story = {
|
|
84
|
+
render: () => (
|
|
85
|
+
<Box sx={ { display: 'flex', flexDirection: 'column', gap: 4 } }>
|
|
86
|
+
<p>
|
|
87
|
+
When entering Mobile views, the first and the last link will appear. A button with a … will
|
|
88
|
+
also be visible. Once pressed, the rest of the links become available, and the focus is
|
|
89
|
+
moved to the next link.
|
|
90
|
+
</p>
|
|
91
|
+
|
|
92
|
+
<hr sx={ { width: '100%', my: 4 } } />
|
|
93
|
+
|
|
94
|
+
<Breadcrumbs
|
|
95
|
+
wrapMode="collapsible"
|
|
96
|
+
LinkComponent={ CustomLinkComponentized }
|
|
97
|
+
label="Nav Breadcrumbs"
|
|
98
|
+
links={ [
|
|
99
|
+
{ href: '/', label: 'Home' },
|
|
100
|
+
{ href: 'https://datadog.com/', label: 'Data dog' },
|
|
101
|
+
{ href: 'https://newrelic.com/', label: 'New Relic' },
|
|
102
|
+
{ href: 'https://rollbar.com/', label: 'Rollbar' },
|
|
103
|
+
{ href: 'https://areallylong.com/', label: 'A really long name' },
|
|
104
|
+
{ href: 'https://google.com/', label: 'I am the last item' },
|
|
105
|
+
] }
|
|
106
|
+
/>
|
|
107
|
+
</Box>
|
|
108
|
+
),
|
|
109
|
+
};
|
|
@@ -2,12 +2,17 @@
|
|
|
2
2
|
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
|
3
3
|
// @ts-nocheck
|
|
4
4
|
import { render, screen } from '@testing-library/react';
|
|
5
|
+
import userEvent from '@testing-library/user-event';
|
|
6
|
+
import * as matchMedia from '@theme-ui/match-media';
|
|
5
7
|
import { axe } from 'jest-axe';
|
|
6
8
|
import { ThemeUIProvider } from 'theme-ui';
|
|
7
9
|
|
|
8
10
|
import { Breadcrumbs } from './Breadcrumbs';
|
|
9
11
|
import { theme } from '../';
|
|
10
12
|
|
|
13
|
+
jest.mock( '@theme-ui/match-media' );
|
|
14
|
+
const mockBreakpointIndex = matchMedia.useBreakpointIndex;
|
|
15
|
+
|
|
11
16
|
// eslint-disable-next-line jsx-a11y/anchor-has-content
|
|
12
17
|
const CustomLink = props => <a { ...props } />;
|
|
13
18
|
|
|
@@ -39,6 +44,7 @@ describe( '<Breadcrumbs />', () => {
|
|
|
39
44
|
} ),
|
|
40
45
|
} );
|
|
41
46
|
} );
|
|
47
|
+
|
|
42
48
|
it( 'renders the Breadcrumbs component', async () => {
|
|
43
49
|
const { container } = renderComponent();
|
|
44
50
|
|
|
@@ -53,4 +59,58 @@ describe( '<Breadcrumbs />', () => {
|
|
|
53
59
|
// Check for accessibility issues
|
|
54
60
|
expect( await axe( container ) ).toHaveNoViolations();
|
|
55
61
|
} );
|
|
62
|
+
|
|
63
|
+
describe( 'wrapMode tests', () => {
|
|
64
|
+
beforeEach( () => {
|
|
65
|
+
mockBreakpointIndex.mockReset();
|
|
66
|
+
} );
|
|
67
|
+
|
|
68
|
+
it( 'expands the breadcrumb items when clicking in the collapsible link', async () => {
|
|
69
|
+
mockBreakpointIndex.mockReturnValue( 0 );
|
|
70
|
+
|
|
71
|
+
const links = [
|
|
72
|
+
{ label: 'Home', href: '/' },
|
|
73
|
+
{ label: 'Applications', href: '/apps' },
|
|
74
|
+
{ label: 'Applications 3', href: '/apps/3' },
|
|
75
|
+
{ label: 'Applications 4', href: '/apps/4' },
|
|
76
|
+
{ label: 'The last' },
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
const user = userEvent.setup();
|
|
80
|
+
|
|
81
|
+
const { container } = renderWithTheme(
|
|
82
|
+
<Breadcrumbs
|
|
83
|
+
wrapMode="collapsible"
|
|
84
|
+
label="Main Collapsible Breadcrumb"
|
|
85
|
+
LinkComponent={ CustomLink }
|
|
86
|
+
links={ links }
|
|
87
|
+
/>
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
// Should find the nav label
|
|
91
|
+
const navEl = screen.getByLabelText( 'Main Collapsible Breadcrumb' );
|
|
92
|
+
|
|
93
|
+
expect( navEl ).toBeInTheDocument();
|
|
94
|
+
|
|
95
|
+
// Contract should have
|
|
96
|
+
expect( navEl.querySelectorAll( 'li' ) ).toHaveLength( 3 );
|
|
97
|
+
|
|
98
|
+
const pressToShowButton = screen.getByRole( 'button', {
|
|
99
|
+
name: 'Press to show more breadcrumbs',
|
|
100
|
+
} );
|
|
101
|
+
|
|
102
|
+
// Should find all links
|
|
103
|
+
expect( screen.getByText( 'Home' ) ).toBeInTheDocument();
|
|
104
|
+
expect( pressToShowButton ).toBeInTheDocument();
|
|
105
|
+
expect( screen.getByText( 'The last' ) ).toHaveAttribute( 'aria-current', 'page' );
|
|
106
|
+
|
|
107
|
+
// Click to expand the breadcrumbs
|
|
108
|
+
await user.click( pressToShowButton );
|
|
109
|
+
|
|
110
|
+
expect( navEl.querySelectorAll( 'li' ) ).toHaveLength( 5 );
|
|
111
|
+
|
|
112
|
+
// Check for accessibility issues
|
|
113
|
+
expect( await axe( container ) ).toHaveNoViolations();
|
|
114
|
+
} );
|
|
115
|
+
} );
|
|
56
116
|
} );
|
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
import * as NavigationMenu from '@radix-ui/react-navigation-menu';
|
|
4
4
|
import { useBreakpointIndex } from '@theme-ui/match-media';
|
|
5
5
|
import classNames from 'classnames';
|
|
6
|
-
import
|
|
6
|
+
import { useTranslate } from 'i18n-calypso';
|
|
7
|
+
import React, { forwardRef, useEffect, useRef, useState } from 'react';
|
|
7
8
|
import { ThemeUIStyleObject } from 'theme-ui';
|
|
8
9
|
|
|
9
10
|
export const VIP_BREACRUMBS = 'vip-breadcrumbs-component';
|
|
10
11
|
|
|
11
|
-
import { smallestScreenItemStyles } from './styles';
|
|
12
|
+
import { collapsibleSeparatorStyles, smallestScreenItemStyles } from './styles';
|
|
12
13
|
import { ItemBreadcrumb, NavItemProps, NavRawLink } from '../Nav/NavItem';
|
|
13
14
|
import { navItemStyles, navMenuListStyles } from '../Nav/styles';
|
|
14
15
|
|
|
@@ -22,37 +23,39 @@ export type BreadcrumbsLinkProps = {
|
|
|
22
23
|
export interface BreacrumbsProps extends NavigationMenu.NavigationMenuProps {
|
|
23
24
|
className?: string;
|
|
24
25
|
label?: string;
|
|
26
|
+
wrapMode?: 'collapsible' | 'lastItem';
|
|
25
27
|
LinkComponent: NavItemProps[ 'as' ];
|
|
26
28
|
links?: BreadcrumbsLinkProps[];
|
|
27
29
|
}
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
31
|
+
const breadcrumbLinks = (
|
|
32
|
+
links: BreadcrumbsLinkProps[],
|
|
33
|
+
isSmallestScreen: boolean = false,
|
|
34
|
+
wrapMode: BreacrumbsProps[ 'wrapMode' ],
|
|
35
|
+
showAllItems: boolean = false
|
|
36
|
+
): {
|
|
37
|
+
separatorLink: boolean;
|
|
38
|
+
lastLink: BreadcrumbsLinkProps | null;
|
|
39
|
+
otherLinks: BreadcrumbsLinkProps[];
|
|
40
|
+
} => {
|
|
41
|
+
let separatorLink: boolean = false;
|
|
42
|
+
let lastLink: BreadcrumbsLinkProps | null = null;
|
|
43
|
+
let otherLinks: BreadcrumbsLinkProps[] = [];
|
|
44
|
+
|
|
45
|
+
const totalLinks = links?.length;
|
|
46
|
+
|
|
47
|
+
if ( totalLinks === 1 ) {
|
|
48
|
+
lastLink = links?.[ 0 ];
|
|
49
|
+
otherLinks = [];
|
|
50
|
+
}
|
|
48
51
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
52
|
+
if ( totalLinks > 1 ) {
|
|
53
|
+
const otherLinksRaw = links?.slice( 0, totalLinks - 1 );
|
|
54
|
+
lastLink = links?.[ totalLinks - 1 ];
|
|
53
55
|
|
|
54
|
-
if (
|
|
55
|
-
penultimateLink = links?.[ totalLinks - 2 ];
|
|
56
|
+
if ( wrapMode === 'lastItem' ) {
|
|
57
|
+
const penultimateLink = links?.[ totalLinks - 2 ];
|
|
58
|
+
lastLink = isSmallestScreen ? null : links?.[ totalLinks - 1 ];
|
|
56
59
|
|
|
57
60
|
otherLinks = isSmallestScreen
|
|
58
61
|
? [
|
|
@@ -62,11 +65,62 @@ export const Breadcrumbs = forwardRef< HTMLElement, BreacrumbsProps >(
|
|
|
62
65
|
sx: smallestScreenItemStyles,
|
|
63
66
|
},
|
|
64
67
|
]
|
|
65
|
-
:
|
|
68
|
+
: otherLinksRaw;
|
|
69
|
+
} else if ( wrapMode === 'collapsible' ) {
|
|
70
|
+
separatorLink = isSmallestScreen && ! showAllItems && totalLinks > 2;
|
|
71
|
+
otherLinks = isSmallestScreen && ! showAllItems ? [ links?.[ 0 ] ] : otherLinksRaw;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
66
74
|
|
|
67
|
-
|
|
75
|
+
return { separatorLink, lastLink, otherLinks };
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export const Breadcrumbs = forwardRef< HTMLElement, BreacrumbsProps >(
|
|
79
|
+
(
|
|
80
|
+
{
|
|
81
|
+
className,
|
|
82
|
+
links = [],
|
|
83
|
+
label = 'Breadcrumbs',
|
|
84
|
+
LinkComponent = NavRawLink,
|
|
85
|
+
wrapMode = 'lastItem',
|
|
86
|
+
}: BreacrumbsProps,
|
|
87
|
+
ref
|
|
88
|
+
) => {
|
|
89
|
+
const breadcrumbsListRef = useRef< HTMLOListElement >( null );
|
|
90
|
+
const [ showAllItems, setShowAllItems ] = useState( false );
|
|
91
|
+
const translate = useTranslate();
|
|
92
|
+
|
|
93
|
+
// Focus on the next link when the collapsible separator is true
|
|
94
|
+
useEffect( () => {
|
|
95
|
+
if ( wrapMode !== 'collapsible' ) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const breadcrumbList = breadcrumbsListRef?.current;
|
|
100
|
+
|
|
101
|
+
if ( showAllItems && breadcrumbList ) {
|
|
102
|
+
const nextActiveLink: HTMLAnchorElement | null =
|
|
103
|
+
breadcrumbList.querySelector( 'li:nth-child(2) a' );
|
|
104
|
+
|
|
105
|
+
nextActiveLink?.focus();
|
|
106
|
+
}
|
|
107
|
+
}, [ showAllItems, wrapMode ] );
|
|
108
|
+
|
|
109
|
+
// The breadcrumb shrinks on smaller screens (mobile) and we need to hide some links
|
|
110
|
+
const bpIndex = useBreakpointIndex( { defaultIndex: 1 } );
|
|
111
|
+
const isSmallestScreen = bpIndex < 3;
|
|
112
|
+
|
|
113
|
+
if ( links?.length === 0 ) {
|
|
114
|
+
return null;
|
|
68
115
|
}
|
|
69
116
|
|
|
117
|
+
const { separatorLink, lastLink, otherLinks } = breadcrumbLinks(
|
|
118
|
+
links,
|
|
119
|
+
isSmallestScreen,
|
|
120
|
+
wrapMode,
|
|
121
|
+
showAllItems
|
|
122
|
+
);
|
|
123
|
+
|
|
70
124
|
return (
|
|
71
125
|
<NavigationMenu.Root
|
|
72
126
|
aria-label={ label }
|
|
@@ -77,6 +131,7 @@ export const Breadcrumbs = forwardRef< HTMLElement, BreacrumbsProps >(
|
|
|
77
131
|
<NavigationMenu.List
|
|
78
132
|
className={ classNames( `${ VIP_BREACRUMBS }-list` ) }
|
|
79
133
|
sx={ navMenuListStyles( 'horizontal' ) }
|
|
134
|
+
ref={ breadcrumbsListRef }
|
|
80
135
|
asChild
|
|
81
136
|
>
|
|
82
137
|
<ol>
|
|
@@ -92,6 +147,22 @@ export const Breadcrumbs = forwardRef< HTMLElement, BreacrumbsProps >(
|
|
|
92
147
|
</ItemBreadcrumb>
|
|
93
148
|
) ) }
|
|
94
149
|
|
|
150
|
+
{ separatorLink && (
|
|
151
|
+
<li
|
|
152
|
+
sx={ {
|
|
153
|
+
...navItemStyles( 'horizontal', 'breadcrumbs' ),
|
|
154
|
+
} }
|
|
155
|
+
>
|
|
156
|
+
<button
|
|
157
|
+
sx={ collapsibleSeparatorStyles }
|
|
158
|
+
aria-label={ translate( 'Press to show more breadcrumbs' ) }
|
|
159
|
+
onClick={ () => setShowAllItems( true ) }
|
|
160
|
+
>
|
|
161
|
+
…
|
|
162
|
+
</button>
|
|
163
|
+
</li>
|
|
164
|
+
) }
|
|
165
|
+
|
|
95
166
|
{ lastLink && (
|
|
96
167
|
<li
|
|
97
168
|
sx={ {
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import { ThemeUIStyleObject } from 'theme-ui';
|
|
2
|
+
|
|
3
|
+
import { linkUnderlineProperties } from '../Link/Link';
|
|
4
|
+
import { breadcrumbsLinkStyles } from '../Nav/styles/variants/breadcrumbs';
|
|
5
|
+
|
|
1
6
|
export const smallestScreenItemStyles = {
|
|
2
7
|
'&::before': {
|
|
3
8
|
display: 'inline-block',
|
|
@@ -10,3 +15,9 @@ export const smallestScreenItemStyles = {
|
|
|
10
15
|
content: "'←'",
|
|
11
16
|
},
|
|
12
17
|
};
|
|
18
|
+
|
|
19
|
+
export const collapsibleSeparatorStyles: ThemeUIStyleObject = {
|
|
20
|
+
all: 'unset',
|
|
21
|
+
...breadcrumbsLinkStyles,
|
|
22
|
+
...linkUnderlineProperties,
|
|
23
|
+
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* External dependencies
|
|
3
3
|
*/
|
|
4
|
-
import { Link } from '
|
|
4
|
+
import { Link, LinkVariant } from './Link';
|
|
5
5
|
import { Flex } from '../Flex/Flex';
|
|
6
6
|
|
|
7
7
|
import type { LinkProps } from './Link';
|
|
@@ -14,6 +14,47 @@ import type { StoryObj } from '@storybook/react';
|
|
|
14
14
|
export default {
|
|
15
15
|
title: 'Navigation/Link',
|
|
16
16
|
component: Link,
|
|
17
|
+
argTypes: {
|
|
18
|
+
variant: {
|
|
19
|
+
type: 'select',
|
|
20
|
+
options: Object.values( LinkVariant ),
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
parameters: {
|
|
24
|
+
docs: {
|
|
25
|
+
description: {
|
|
26
|
+
component: `
|
|
27
|
+
|
|
28
|
+
Links are navigational elements that direct visitors to other locations, either on the same page or to a different page or site. They can be inline or separate from the text flow. Since every link is a potential user interaction, too many links can be overwhelming.
|
|
29
|
+
|
|
30
|
+
## Guidance
|
|
31
|
+
|
|
32
|
+
### When to use the link component
|
|
33
|
+
|
|
34
|
+
- When you want to direct users to another page or site.
|
|
35
|
+
- When you want to direct users to another section of the same page (anchors).
|
|
36
|
+
|
|
37
|
+
### When to consider something else
|
|
38
|
+
|
|
39
|
+
- When you want to trigger an action (use a button instead).
|
|
40
|
+
- When you want to display a message (use a notification instead).
|
|
41
|
+
|
|
42
|
+
### Accessibility
|
|
43
|
+
|
|
44
|
+
- Don’t rely on only color to distinguish links
|
|
45
|
+
- Don’t block external links with disruptive notifications
|
|
46
|
+
- Icons can be helpful. Consider adding an icon to signal specific actions (Download, Open in a new window, etc).
|
|
47
|
+
- Don’t use the same link text for different URLs on the same page. This can confuse users and make it difficult for them to understand where the link will take them.
|
|
48
|
+
|
|
49
|
+
-------
|
|
50
|
+
|
|
51
|
+
This documentation is heavily inspired by the [U.S Web Design System (USWDS)](https://designsystem.digital.gov/components/link/). We use USWDS as trusted source of truth for accessibility and usability best practices.
|
|
52
|
+
|
|
53
|
+
## Component Properties
|
|
54
|
+
`,
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
},
|
|
17
58
|
};
|
|
18
59
|
|
|
19
60
|
type Story = StoryObj< typeof Link >;
|
package/src/system/Link/Link.tsx
CHANGED
|
@@ -14,17 +14,26 @@ interface LinkTheme extends Theme {
|
|
|
14
14
|
outline?: Record< string, string >;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
export enum LinkVariant {
|
|
18
|
+
'primary',
|
|
19
|
+
'button-primary',
|
|
20
|
+
'button-secondary',
|
|
21
|
+
'button-tertiary',
|
|
22
|
+
'button-ghost',
|
|
23
|
+
'button-display',
|
|
24
|
+
'button-danger',
|
|
25
|
+
}
|
|
26
|
+
|
|
17
27
|
export interface LinkProps extends ThemeLinkProps {
|
|
18
|
-
variant?:
|
|
19
|
-
| 'primary'
|
|
20
|
-
| 'button-primary'
|
|
21
|
-
| 'button-secondary'
|
|
22
|
-
| 'button-tertiary'
|
|
23
|
-
| 'button-ghost'
|
|
24
|
-
| 'button-display'
|
|
25
|
-
| 'button-danger';
|
|
28
|
+
variant?: keyof typeof LinkVariant;
|
|
26
29
|
}
|
|
27
30
|
|
|
31
|
+
export const linkUnderlineProperties: ThemeUIStyleObject = {
|
|
32
|
+
textDecorationLine: 'underline',
|
|
33
|
+
textDecorationThickness: '0.07rem',
|
|
34
|
+
textUnderlineOffset: '0.250rem',
|
|
35
|
+
};
|
|
36
|
+
|
|
28
37
|
export const defaultLinkComponentStyle: ThemeUIStyleObject = {
|
|
29
38
|
'&:focus-visible': ( theme: LinkTheme ) => theme.outline,
|
|
30
39
|
};
|
package/src/system/Nav/styles.ts
CHANGED
|
@@ -6,6 +6,7 @@ import ColorBuilder from './colors';
|
|
|
6
6
|
import ValetDark from './generated/valet-theme-dark.json';
|
|
7
7
|
import Valet from './generated/valet-theme-light.json';
|
|
8
8
|
import ThemeBuilder from './getPropValue';
|
|
9
|
+
import { linkUnderlineProperties } from '../Link/Link';
|
|
9
10
|
|
|
10
11
|
// Light
|
|
11
12
|
const { getPropValue, getVariants, ValetTheme, getHeadingStyles } = ThemeBuilder( Valet );
|
|
@@ -402,21 +403,19 @@ export default {
|
|
|
402
403
|
|
|
403
404
|
links: {
|
|
404
405
|
primary: {
|
|
406
|
+
...linkUnderlineProperties,
|
|
407
|
+
|
|
405
408
|
color: 'link',
|
|
406
409
|
'&:visited': {
|
|
407
410
|
color: 'links.visited',
|
|
408
411
|
},
|
|
409
412
|
'&:hover': {
|
|
410
413
|
color: 'links.hover',
|
|
411
|
-
|
|
412
|
-
textDecorationThickness: '0.125rem',
|
|
414
|
+
textDecorationThickness: '0.15rem',
|
|
413
415
|
},
|
|
414
416
|
'&:active': {
|
|
415
417
|
color: 'links.active',
|
|
416
418
|
},
|
|
417
|
-
|
|
418
|
-
textDecorationThickness: '0.125rem',
|
|
419
|
-
textUnderlineOffset: '0.250rem',
|
|
420
419
|
},
|
|
421
420
|
'button-primary': {
|
|
422
421
|
variant: 'buttons.primary',
|
|
@@ -531,7 +530,7 @@ export default {
|
|
|
531
530
|
a: {
|
|
532
531
|
'&:hover': {
|
|
533
532
|
textDecorationLine: 'underline',
|
|
534
|
-
textDecorationThickness: '0.
|
|
533
|
+
textDecorationThickness: '0.1rem',
|
|
535
534
|
textUnderlineOffset: '0.250rem',
|
|
536
535
|
},
|
|
537
536
|
},
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import React, { Ref, forwardRef } from 'react';
|
|
2
2
|
|
|
3
|
+
import { Link } from '../../Link/Link';
|
|
4
|
+
|
|
3
5
|
export const CustomLink = forwardRef< HTMLAnchorElement >(
|
|
4
6
|
// eslint-disable-next-line jsx-a11y/anchor-has-content
|
|
5
7
|
( props, ref: Ref< HTMLAnchorElement > ) => <a { ...props } ref={ ref } />
|
|
6
8
|
);
|
|
9
|
+
|
|
10
|
+
export const CustomLinkComponentized = forwardRef< HTMLAnchorElement >(
|
|
11
|
+
( props, ref: Ref< HTMLAnchorElement > ) => <Link { ...props } ref={ ref } />
|
|
12
|
+
);
|