@toptal/picasso-link 100.0.2-alpha-pf-react-19-improvements-970580a42.0 → 100.0.2
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/dist-package/src/Link/Link.d.ts +3 -1
- package/dist-package/src/Link/Link.d.ts.map +1 -1
- package/dist-package/src/Link/Link.js +26 -4
- package/dist-package/src/Link/Link.js.map +1 -1
- package/package.json +10 -10
- package/src/Link/Link.tsx +39 -2
- package/src/Link/__snapshots__/test.tsx.snap +4 -4
- package/src/Link/test.tsx +81 -2
|
@@ -21,7 +21,7 @@ export type Props = BaseProps & AnchorHTMLAttributes<HTMLAnchorElement> & {
|
|
|
21
21
|
variant?: VariantType;
|
|
22
22
|
/** Controls color of the link */
|
|
23
23
|
color?: ColorType;
|
|
24
|
-
/** Indicates the order of receiving focus.
|
|
24
|
+
/** Indicates the order of receiving focus. Defaults to 0 for a link with `onClick` and no `href` */
|
|
25
25
|
tabIndex?: number;
|
|
26
26
|
/** Indicates that the user cannot interact with the Link or its children */
|
|
27
27
|
disabled?: boolean;
|
|
@@ -42,6 +42,8 @@ type ViewModel = {
|
|
|
42
42
|
weight: 'inherit' | 'semibold';
|
|
43
43
|
style?: React.CSSProperties;
|
|
44
44
|
tabIndex?: number;
|
|
45
|
+
role?: React.AriaRole;
|
|
46
|
+
onKeyDown?: React.KeyboardEventHandler<HTMLAnchorElement>;
|
|
45
47
|
as: Props['as'];
|
|
46
48
|
ariaDisabled?: boolean;
|
|
47
49
|
nativeHTMLAttributes: Omit<Props, keyof BaseProps>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Link.d.ts","sourceRoot":"","sources":["../../../src/Link/Link.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,oBAAoB,EAAE,WAAW,EAAE,MAAM,OAAO,CAAA;AACzE,OAAO,KAAqB,MAAM,OAAO,CAAA;AAEzC,OAAO,KAAK,EAAE,SAAS,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAA;AAG7E,QAAA,MAAM,eAAe,4BAA6B,CAAA;AAClD,QAAA,MAAM,iBAAiB,+BAAgC,CAAA;AAEvD,KAAK,WAAW,GAAG,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM,CAAC,CAAA;AACrD,KAAK,SAAS,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,CAAA;
|
|
1
|
+
{"version":3,"file":"Link.d.ts","sourceRoot":"","sources":["../../../src/Link/Link.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,oBAAoB,EAAE,WAAW,EAAE,MAAM,OAAO,CAAA;AACzE,OAAO,KAAqB,MAAM,OAAO,CAAA;AAEzC,OAAO,KAAK,EAAE,SAAS,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAA;AAG7E,QAAA,MAAM,eAAe,4BAA6B,CAAA;AAClD,QAAA,MAAM,iBAAiB,+BAAgC,CAAA;AAEvD,KAAK,WAAW,GAAG,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM,CAAC,CAAA;AACrD,KAAK,SAAS,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,CAAA;AAkDjD,MAAM,MAAM,KAAK,GAAG,SAAS,GAC3B,oBAAoB,CAAC,iBAAiB,CAAC,GAAG;IACxC,+BAA+B;IAC/B,QAAQ,CAAC,EAAE,SAAS,CAAA;IACpB,qCAAqC;IACrC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,iDAAiD;IACjD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,UAAU,CAAC,iBAAiB,CAAC,KAAK,IAAI,CAAA;IAC9D;;;OAGG;IACH,EAAE,CAAC,EAAE,WAAW,CAAA;IAChB,qDAAqD;IACrD,OAAO,CAAC,EAAE,WAAW,CAAA;IACrB,iCAAiC;IACjC,KAAK,CAAC,EAAE,SAAS,CAAA;IACjB,oGAAoG;IACpG,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,4EAA4E;IAC5E,QAAQ,CAAC,EAAE,OAAO,CAAA;IAElB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB;;OAEG;IACH,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,eAAe,CAAC,EAAE,OAAO,CAAA;CAC1B,CAAA;AAKH,KAAK,SAAS,GAAG;IACf,SAAS,EAAE,MAAM,CAAA;IACjB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,KAAK,CAAC,EAAE,SAAS,CAAA;IACjB,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,UAAU,CAAC,iBAAiB,CAAC,KAAK,IAAI,CAAA;IAC9D,MAAM,EAAE,SAAS,GAAG,UAAU,CAAA;IAC9B,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAA;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,IAAI,CAAC,EAAE,KAAK,CAAC,QAAQ,CAAA;IACrB,SAAS,CAAC,EAAE,KAAK,CAAC,oBAAoB,CAAC,iBAAiB,CAAC,CAAA;IACzD,EAAE,EAAE,KAAK,CAAC,IAAI,CAAC,CAAA;IACf,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,oBAAoB,EAAE,IAAI,CAAC,KAAK,EAAE,MAAM,SAAS,CAAC,CAAA;CACnD,CAAA;AAkBD,eAAO,MAAM,kBAAkB,UAAW,KAAK,KAAG,SA+DjD,CAAA;AAED,eAAO,MAAM,IAAI,EAAE,oBAAoB,CAAC,KAAK,CA2C3C,CAAA;AAIF,eAAe,IAAI,CAAA"}
|
|
@@ -46,19 +46,37 @@ const COLOR_DISABLED_MAP = {
|
|
|
46
46
|
},
|
|
47
47
|
},
|
|
48
48
|
};
|
|
49
|
+
// One shadow class and no radius, so a component rendered `as={Link}` keeps
|
|
50
|
+
// its own focus ring and corners
|
|
51
|
+
const FOCUS_RING_MAP = {
|
|
52
|
+
blue: 'focus-visible:shadow-[0_0_0_0.1875rem_color-mix(in_srgb,theme(colors.blue.500)_48%,transparent)]',
|
|
53
|
+
white: 'focus-visible:shadow-[0_0_0_0.1875rem_color-mix(in_srgb,theme(colors.white)_48%,transparent)]',
|
|
54
|
+
};
|
|
49
55
|
const defaultColor = 'blue';
|
|
50
56
|
const defaultVariant = 'anchor';
|
|
57
|
+
// Enter and Space click it, as they do a button
|
|
58
|
+
const createActionKeyDownHandler = (onKeyDown) => (event) => {
|
|
59
|
+
onKeyDown === null || onKeyDown === void 0 ? void 0 : onKeyDown(event);
|
|
60
|
+
if (!event.defaultPrevented &&
|
|
61
|
+
(event.key === 'Enter' || event.key === ' ')) {
|
|
62
|
+
event.preventDefault();
|
|
63
|
+
event.currentTarget.click();
|
|
64
|
+
}
|
|
65
|
+
};
|
|
51
66
|
/* eslint-disable complexity */
|
|
52
67
|
export const calculateViewModel = (props) => {
|
|
53
|
-
const { href, onClick, className, as = 'a', color: inputColor = 'blue', style, variant: inputVariant = 'anchor', tabIndex, target, rel, disabled, visited = false, noUnderline, 'aria-disabled': ariaDisabled } = props, nativeHTMLAttributes = __rest(props, ["href", "onClick", "className", "as", "color", "style", "variant", "tabIndex", "target", "rel", "disabled", "visited", "noUnderline", 'aria-disabled']);
|
|
68
|
+
const { href, onClick, className, as = 'a', color: inputColor = 'blue', style, variant: inputVariant = 'anchor', tabIndex, target, rel, disabled, visited = false, noUnderline, 'aria-disabled': ariaDisabled, role, onKeyDown } = props, nativeHTMLAttributes = __rest(props, ["href", "onClick", "className", "as", "color", "style", "variant", "tabIndex", "target", "rel", "disabled", "visited", "noUnderline", 'aria-disabled', "role", "onKeyDown"]);
|
|
54
69
|
const sanitizedRel = sanitizeRel(rel, target);
|
|
70
|
+
// An `<a>` without `href` can't be focused or operated by keyboard
|
|
71
|
+
const isAction = as === 'a' && !href && Boolean(onClick);
|
|
72
|
+
const isEnabledAction = isAction && !disabled;
|
|
55
73
|
// When Link is used as={Link}, TypeScript can't ensure the input to the Link is compatible with its Props type.
|
|
56
74
|
const color = supportedColors.includes(inputColor) ? inputColor : defaultColor;
|
|
57
75
|
const variant = supportedVariants.includes(inputVariant)
|
|
58
76
|
? inputVariant
|
|
59
77
|
: defaultVariant;
|
|
60
78
|
return {
|
|
61
|
-
className: twMerge('focus:outline-hidden hover:underline leading-[inherit]', COLOR_DISABLED_MAP[color][variant][disabled ? 'disabled' : 'normal'], disabled ? 'cursor-not-allowed' : '', noUnderline ? 'no-underline!' : '', visited
|
|
79
|
+
className: twMerge('focus:outline-hidden hover:underline leading-[inherit]', FOCUS_RING_MAP[color], COLOR_DISABLED_MAP[color][variant][disabled ? 'disabled' : 'normal'], disabled ? 'cursor-not-allowed' : '', noUnderline ? 'no-underline!' : '', visited
|
|
62
80
|
? color === 'blue'
|
|
63
81
|
? 'visited text-purple-500'
|
|
64
82
|
: 'visited text-gray-500'
|
|
@@ -70,7 +88,11 @@ export const calculateViewModel = (props) => {
|
|
|
70
88
|
weight: variant === 'action' ? 'semibold' : 'inherit',
|
|
71
89
|
color: 'inherit',
|
|
72
90
|
style,
|
|
73
|
-
tabIndex,
|
|
91
|
+
tabIndex: isEnabledAction ? tabIndex !== null && tabIndex !== void 0 ? tabIndex : 0 : tabIndex,
|
|
92
|
+
role: isAction ? role !== null && role !== void 0 ? role : 'button' : role,
|
|
93
|
+
onKeyDown: isEnabledAction
|
|
94
|
+
? createActionKeyDownHandler(onKeyDown)
|
|
95
|
+
: onKeyDown,
|
|
74
96
|
as,
|
|
75
97
|
ariaDisabled: disabled || ariaDisabled,
|
|
76
98
|
nativeHTMLAttributes,
|
|
@@ -84,7 +106,7 @@ export const Link = forwardRef(function Link(_a, ref) {
|
|
|
84
106
|
noUnderline }));
|
|
85
107
|
return (React.createElement(Typography, Object.assign({}, viewModel.nativeHTMLAttributes, { ref: ref, as: viewModel.as,
|
|
86
108
|
// @ts-expect-error Typography is incompatible with href prop
|
|
87
|
-
href: viewModel.href, target: viewModel.target, rel: viewModel.rel, onClick: viewModel.onClick, color: viewModel.color, weight: viewModel.weight, className: viewModel.className, style: viewModel.style, tabIndex: viewModel.tabIndex, "aria-disabled": viewModel.ariaDisabled }), props.children));
|
|
109
|
+
href: viewModel.href, target: viewModel.target, rel: viewModel.rel, onClick: viewModel.onClick, color: viewModel.color, weight: viewModel.weight, className: viewModel.className, style: viewModel.style, tabIndex: viewModel.tabIndex, role: viewModel.role, onKeyDown: viewModel.onKeyDown, "aria-disabled": viewModel.ariaDisabled }), props.children));
|
|
88
110
|
});
|
|
89
111
|
Link.displayName = 'Link';
|
|
90
112
|
export default Link;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Link.js","sourceRoot":"","sources":["../../../src/Link/Link.tsx"],"names":[],"mappings":";;;;;;;;;;;AACA,OAAO,KAAK,EAAE,EAAE,UAAU,EAAE,MAAM,OAAO,CAAA;AACzC,OAAO,EAAE,OAAO,EAAE,MAAM,gCAAgC,CAAA;AAExD,OAAO,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAA;AAEvD,MAAM,eAAe,GAAG,CAAC,OAAO,EAAE,MAAM,CAAU,CAAA;AAClD,MAAM,iBAAiB,GAAG,CAAC,QAAQ,EAAE,QAAQ,CAAU,CAAA;AAKvD,MAAM,WAAW,GAAG,CAAC,GAAuB,EAAE,MAA0B,EAAE,EAAE;IAC1E,IAAI,MAAM,KAAK,QAAQ,EAAE,CAAC;QACxB,OAAO,GAAG,CAAA;IACZ,CAAC;IAED,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,OAAO,UAAU,CAAA;IACnB,CAAC;IAED,MAAM,SAAS,GAAG,GAAG,CAAC,QAAQ,CAAC,YAAY,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAA;IAExE,OAAO,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,WAAW,CAAC,CAAA;AAClD,CAAC,CAAA;AAED,MAAM,kBAAkB,GAGpB;IACF,IAAI,EAAE;QACJ,MAAM,EAAE;YACN,QAAQ,EAAE,2DAA2D;YACrE,MAAM,EAAE,oDAAoD;SAC7D;QACD,MAAM,EAAE;YACN,QAAQ,EAAE,yBAAyB;YACnC,MAAM,EAAE,oDAAoD;SAC7D;KACF;IACD,KAAK,EAAE;QACL,MAAM,EAAE;YACN,QAAQ,EAAE,eAAe;YACzB,MAAM,EAAE,SAAS;SAClB;QACD,MAAM,EAAE;YACN,QAAQ,EAAE,+CAA+C;YACzD,MAAM,EAAE,oDAAoD;SAC7D;KACF;CACF,CAAA;AAgCD,MAAM,YAAY,GAAG,MAAM,CAAA;AAC3B,MAAM,cAAc,GAAG,QAAQ,CAAA;
|
|
1
|
+
{"version":3,"file":"Link.js","sourceRoot":"","sources":["../../../src/Link/Link.tsx"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@toptal/picasso-link",
|
|
3
|
-
"version": "100.0.2
|
|
3
|
+
"version": "100.0.2",
|
|
4
4
|
"description": "Toptal UI components library - Link",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -18,26 +18,26 @@
|
|
|
18
18
|
},
|
|
19
19
|
"homepage": "https://github.com/toptal/picasso/tree/master/packages/picasso#readme",
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@toptal/picasso-utils": "100.0.
|
|
22
|
-
"@toptal/picasso-typography": "100.0.
|
|
21
|
+
"@toptal/picasso-utils": "100.0.1",
|
|
22
|
+
"@toptal/picasso-typography": "100.0.1"
|
|
23
23
|
},
|
|
24
24
|
"sideEffects": [
|
|
25
25
|
"**/styles.ts",
|
|
26
26
|
"**/styles.js"
|
|
27
27
|
],
|
|
28
28
|
"peerDependencies": {
|
|
29
|
-
"react": "
|
|
30
|
-
"@toptal/picasso-tailwind-merge": "^100.0.
|
|
31
|
-
"@toptal/picasso-
|
|
32
|
-
"@toptal/picasso-
|
|
29
|
+
"react": ">=17.0.0 < 19.0.0",
|
|
30
|
+
"@toptal/picasso-tailwind-merge": "^100.0.0",
|
|
31
|
+
"@toptal/picasso-tailwind": "^100.0.0",
|
|
32
|
+
"@toptal/picasso-provider": "^100.2.0"
|
|
33
33
|
},
|
|
34
34
|
"exports": {
|
|
35
35
|
".": "./dist-package/src/index.js"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"@toptal/picasso-
|
|
39
|
-
"@toptal/picasso-
|
|
40
|
-
"@toptal/picasso-test-utils": "100.1.
|
|
38
|
+
"@toptal/picasso-tailwind-merge": "100.0.0",
|
|
39
|
+
"@toptal/picasso-provider": "100.2.0",
|
|
40
|
+
"@toptal/picasso-test-utils": "100.1.0"
|
|
41
41
|
},
|
|
42
42
|
"files": [
|
|
43
43
|
"dist-package/**",
|
package/src/Link/Link.tsx
CHANGED
|
@@ -50,6 +50,14 @@ const COLOR_DISABLED_MAP: Record<
|
|
|
50
50
|
},
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
// One shadow class and no radius, so a component rendered `as={Link}` keeps
|
|
54
|
+
// its own focus ring and corners
|
|
55
|
+
const FOCUS_RING_MAP: Record<ColorType, string> = {
|
|
56
|
+
blue: 'focus-visible:shadow-[0_0_0_0.1875rem_color-mix(in_srgb,theme(colors.blue.500)_48%,transparent)]',
|
|
57
|
+
white:
|
|
58
|
+
'focus-visible:shadow-[0_0_0_0.1875rem_color-mix(in_srgb,theme(colors.white)_48%,transparent)]',
|
|
59
|
+
}
|
|
60
|
+
|
|
53
61
|
export type Props = BaseProps &
|
|
54
62
|
AnchorHTMLAttributes<HTMLAnchorElement> & {
|
|
55
63
|
/** Content of the component */
|
|
@@ -67,7 +75,7 @@ export type Props = BaseProps &
|
|
|
67
75
|
variant?: VariantType
|
|
68
76
|
/** Controls color of the link */
|
|
69
77
|
color?: ColorType
|
|
70
|
-
/** Indicates the order of receiving focus.
|
|
78
|
+
/** Indicates the order of receiving focus. Defaults to 0 for a link with `onClick` and no `href` */
|
|
71
79
|
tabIndex?: number
|
|
72
80
|
/** Indicates that the user cannot interact with the Link or its children */
|
|
73
81
|
disabled?: boolean
|
|
@@ -93,11 +101,28 @@ type ViewModel = {
|
|
|
93
101
|
weight: 'inherit' | 'semibold'
|
|
94
102
|
style?: React.CSSProperties
|
|
95
103
|
tabIndex?: number
|
|
104
|
+
role?: React.AriaRole
|
|
105
|
+
onKeyDown?: React.KeyboardEventHandler<HTMLAnchorElement>
|
|
96
106
|
as: Props['as']
|
|
97
107
|
ariaDisabled?: boolean
|
|
98
108
|
nativeHTMLAttributes: Omit<Props, keyof BaseProps>
|
|
99
109
|
}
|
|
100
110
|
|
|
111
|
+
// Enter and Space click it, as they do a button
|
|
112
|
+
const createActionKeyDownHandler =
|
|
113
|
+
(onKeyDown?: React.KeyboardEventHandler<HTMLAnchorElement>) =>
|
|
114
|
+
(event: React.KeyboardEvent<HTMLAnchorElement>) => {
|
|
115
|
+
onKeyDown?.(event)
|
|
116
|
+
|
|
117
|
+
if (
|
|
118
|
+
!event.defaultPrevented &&
|
|
119
|
+
(event.key === 'Enter' || event.key === ' ')
|
|
120
|
+
) {
|
|
121
|
+
event.preventDefault()
|
|
122
|
+
event.currentTarget.click()
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
101
126
|
/* eslint-disable complexity */
|
|
102
127
|
export const calculateViewModel = (props: Props): ViewModel => {
|
|
103
128
|
const {
|
|
@@ -115,10 +140,15 @@ export const calculateViewModel = (props: Props): ViewModel => {
|
|
|
115
140
|
visited = false,
|
|
116
141
|
noUnderline,
|
|
117
142
|
'aria-disabled': ariaDisabled,
|
|
143
|
+
role,
|
|
144
|
+
onKeyDown,
|
|
118
145
|
...nativeHTMLAttributes
|
|
119
146
|
} = props
|
|
120
147
|
|
|
121
148
|
const sanitizedRel = sanitizeRel(rel, target)
|
|
149
|
+
// An `<a>` without `href` can't be focused or operated by keyboard
|
|
150
|
+
const isAction = as === 'a' && !href && Boolean(onClick)
|
|
151
|
+
const isEnabledAction = isAction && !disabled
|
|
122
152
|
|
|
123
153
|
// When Link is used as={Link}, TypeScript can't ensure the input to the Link is compatible with its Props type.
|
|
124
154
|
const color = supportedColors.includes(inputColor) ? inputColor : defaultColor
|
|
@@ -129,6 +159,7 @@ export const calculateViewModel = (props: Props): ViewModel => {
|
|
|
129
159
|
return {
|
|
130
160
|
className: twMerge(
|
|
131
161
|
'focus:outline-hidden hover:underline leading-[inherit]',
|
|
162
|
+
FOCUS_RING_MAP[color],
|
|
132
163
|
COLOR_DISABLED_MAP[color][variant][disabled ? 'disabled' : 'normal'],
|
|
133
164
|
disabled ? 'cursor-not-allowed' : '',
|
|
134
165
|
noUnderline ? 'no-underline!' : '',
|
|
@@ -147,7 +178,11 @@ export const calculateViewModel = (props: Props): ViewModel => {
|
|
|
147
178
|
weight: variant === 'action' ? 'semibold' : 'inherit',
|
|
148
179
|
color: 'inherit',
|
|
149
180
|
style,
|
|
150
|
-
tabIndex,
|
|
181
|
+
tabIndex: isEnabledAction ? tabIndex ?? 0 : tabIndex,
|
|
182
|
+
role: isAction ? role ?? 'button' : role,
|
|
183
|
+
onKeyDown: isEnabledAction
|
|
184
|
+
? createActionKeyDownHandler(onKeyDown)
|
|
185
|
+
: onKeyDown,
|
|
151
186
|
as,
|
|
152
187
|
ariaDisabled: disabled || ariaDisabled,
|
|
153
188
|
nativeHTMLAttributes,
|
|
@@ -190,6 +225,8 @@ export const Link: OverridableComponent<Props> = forwardRef<
|
|
|
190
225
|
className={viewModel.className}
|
|
191
226
|
style={viewModel.style}
|
|
192
227
|
tabIndex={viewModel.tabIndex}
|
|
228
|
+
role={viewModel.role}
|
|
229
|
+
onKeyDown={viewModel.onKeyDown}
|
|
193
230
|
aria-disabled={viewModel.ariaDisabled}
|
|
194
231
|
>
|
|
195
232
|
{props.children}
|
|
@@ -6,7 +6,7 @@ exports[`Link renders 1`] = `
|
|
|
6
6
|
class="flex-1 box-border [:where(&)_*]:font-sans [&_*]:[box-sizing:inherit] [&_*::before]:[box-sizing:inherit] [&_*::after]:[box-sizing:inherit]"
|
|
7
7
|
>
|
|
8
8
|
<a
|
|
9
|
-
class="m-0 font-inherit font-inherit focus:outline-hidden hover:underline leading-[inherit] text-blue visited:text-purple no-underline"
|
|
9
|
+
class="m-0 font-inherit font-inherit focus:outline-hidden hover:underline leading-[inherit] focus-visible:shadow-[0_0_0_0.1875rem_color-mix(in_srgb,theme(colors.blue.500)_48%,transparent)] text-blue visited:text-purple no-underline"
|
|
10
10
|
>
|
|
11
11
|
Please verify your email
|
|
12
12
|
</a>
|
|
@@ -21,7 +21,7 @@ exports[`Link when disabled renders disabled link 1`] = `
|
|
|
21
21
|
>
|
|
22
22
|
<a
|
|
23
23
|
aria-disabled="true"
|
|
24
|
-
class="m-0 font-inherit font-inherit focus:outline-hidden hover:underline leading-[inherit] text-gray underline cursor-pointer"
|
|
24
|
+
class="m-0 font-inherit font-inherit focus:outline-hidden hover:underline leading-[inherit] focus-visible:shadow-[0_0_0_0.1875rem_color-mix(in_srgb,theme(colors.blue.500)_48%,transparent)] text-gray underline cursor-pointer"
|
|
25
25
|
download="filename"
|
|
26
26
|
rel="noopener"
|
|
27
27
|
>
|
|
@@ -37,7 +37,7 @@ exports[`Link when native attributes are provided renders native attributes 1`]
|
|
|
37
37
|
class="flex-1 box-border [:where(&)_*]:font-sans [&_*]:[box-sizing:inherit] [&_*::before]:[box-sizing:inherit] [&_*::after]:[box-sizing:inherit]"
|
|
38
38
|
>
|
|
39
39
|
<a
|
|
40
|
-
class="m-0 font-inherit font-inherit focus:outline-hidden hover:underline leading-[inherit] text-blue visited:text-purple no-underline cursor-pointer"
|
|
40
|
+
class="m-0 font-inherit font-inherit focus:outline-hidden hover:underline leading-[inherit] focus-visible:shadow-[0_0_0_0.1875rem_color-mix(in_srgb,theme(colors.blue.500)_48%,transparent)] text-blue visited:text-purple no-underline cursor-pointer"
|
|
41
41
|
download="filename"
|
|
42
42
|
href="https://toptal.com/filename.txt"
|
|
43
43
|
rel="noopener"
|
|
@@ -56,7 +56,7 @@ exports[`Link when using react-router Link renders a Link from react-router 1`]
|
|
|
56
56
|
>
|
|
57
57
|
<div>
|
|
58
58
|
<a
|
|
59
|
-
class="m-0 font-inherit font-inherit focus:outline-hidden hover:underline leading-[inherit] text-blue visited:text-purple no-underline"
|
|
59
|
+
class="m-0 font-inherit font-inherit focus:outline-hidden hover:underline leading-[inherit] focus-visible:shadow-[0_0_0_0.1875rem_color-mix(in_srgb,theme(colors.blue.500)_48%,transparent)] text-blue visited:text-purple no-underline"
|
|
60
60
|
href="/"
|
|
61
61
|
>
|
|
62
62
|
Please verify your email
|
package/src/Link/test.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
-
import { render, fireEvent } from '@toptal/picasso-test-utils'
|
|
2
|
+
import { render, fireEvent, screen } from '@toptal/picasso-test-utils'
|
|
3
3
|
import { MemoryRouter, Link as RouterLink } from 'react-router-dom'
|
|
4
4
|
|
|
5
5
|
import { calculateViewModel } from './Link'
|
|
@@ -110,6 +110,85 @@ describe('Link', () => {
|
|
|
110
110
|
expect(document.querySelector('a')).not.toHaveAttribute('rel', 'noopener')
|
|
111
111
|
})
|
|
112
112
|
})
|
|
113
|
+
|
|
114
|
+
describe('when focused with the keyboard', () => {
|
|
115
|
+
it.each([
|
|
116
|
+
['blue', 'blue.500'],
|
|
117
|
+
['white', 'white'],
|
|
118
|
+
] as const)('shows a %s focus ring', (color, ringColor) => {
|
|
119
|
+
render(
|
|
120
|
+
<Link href='#' color={color}>
|
|
121
|
+
Link
|
|
122
|
+
</Link>
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
expect(screen.getByRole('link')).toHaveClass(
|
|
126
|
+
`focus-visible:shadow-[0_0_0_0.1875rem_color-mix(in_srgb,theme(colors.${ringColor})_48%,transparent)]`
|
|
127
|
+
)
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
describe('when it has onClick and no href', () => {
|
|
132
|
+
it('renders a focusable button', () => {
|
|
133
|
+
render(<Link onClick={jest.fn()}>Action</Link>)
|
|
134
|
+
|
|
135
|
+
expect(screen.getByRole('button', { name: 'Action' })).toHaveAttribute(
|
|
136
|
+
'tabindex',
|
|
137
|
+
'0'
|
|
138
|
+
)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it.each(['Enter', ' '])('calls onClick on "%s"', key => {
|
|
142
|
+
const handleClick = jest.fn()
|
|
143
|
+
|
|
144
|
+
render(<Link onClick={handleClick}>Action</Link>)
|
|
145
|
+
fireEvent.keyDown(screen.getByRole('button'), { key })
|
|
146
|
+
|
|
147
|
+
expect(handleClick).toHaveBeenCalledTimes(1)
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('lets onKeyDown prevent the click', () => {
|
|
151
|
+
const handleClick = jest.fn()
|
|
152
|
+
|
|
153
|
+
render(
|
|
154
|
+
<Link onClick={handleClick} onKeyDown={event => event.preventDefault()}>
|
|
155
|
+
Action
|
|
156
|
+
</Link>
|
|
157
|
+
)
|
|
158
|
+
fireEvent.keyDown(screen.getByRole('button'), { key: 'Enter' })
|
|
159
|
+
|
|
160
|
+
expect(handleClick).not.toHaveBeenCalled()
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('is not focusable or operable when disabled', () => {
|
|
164
|
+
const handleClick = jest.fn()
|
|
165
|
+
|
|
166
|
+
render(
|
|
167
|
+
<Link onClick={handleClick} disabled>
|
|
168
|
+
Action
|
|
169
|
+
</Link>
|
|
170
|
+
)
|
|
171
|
+
const action = screen.getByRole('button')
|
|
172
|
+
|
|
173
|
+
fireEvent.keyDown(action, { key: 'Enter' })
|
|
174
|
+
|
|
175
|
+
expect(action).not.toHaveAttribute('tabindex')
|
|
176
|
+
expect(action).toHaveAttribute('aria-disabled', 'true')
|
|
177
|
+
expect(handleClick).not.toHaveBeenCalled()
|
|
178
|
+
})
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
describe('when it has href and onClick', () => {
|
|
182
|
+
it('stays a link', () => {
|
|
183
|
+
render(
|
|
184
|
+
<Link href='#' onClick={jest.fn()}>
|
|
185
|
+
Link
|
|
186
|
+
</Link>
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
expect(screen.getByRole('link')).not.toHaveAttribute('tabindex')
|
|
190
|
+
})
|
|
191
|
+
})
|
|
113
192
|
})
|
|
114
193
|
|
|
115
194
|
describe('calculateViewModel', () => {
|
|
@@ -186,7 +265,7 @@ describe('calculateViewModel', () => {
|
|
|
186
265
|
const whiteResult = calculateViewModel(whiteProps)
|
|
187
266
|
|
|
188
267
|
expect(blueResult.className).toContain(
|
|
189
|
-
'
|
|
268
|
+
'text-gray-600 underline cursor-not-allowed'
|
|
190
269
|
)
|
|
191
270
|
expect(whiteResult.className).toContain('text-gray-600')
|
|
192
271
|
})
|