@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.
@@ -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. If not set will not receive 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;AA0CjD,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,iFAAiF;IACjF,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,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;AAGD,eAAO,MAAM,kBAAkB,UAAW,KAAK,KAAG,SAqDjD,CAAA;AAED,eAAO,MAAM,IAAI,EAAE,oBAAoB,CAAC,KAAK,CAyC3C,CAAA;AAIF,eAAe,IAAI,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":"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"}
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-alpha-pf-react-19-improvements-970580a42.0",
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.2-alpha-pf-react-19-improvements-970580a42.0",
22
- "@toptal/picasso-typography": "100.0.2-alpha-pf-react-19-improvements-970580a42.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": "^17.0.0 || ^18.0.0 || ^19.0.0",
30
- "@toptal/picasso-tailwind-merge": "^100.0.1-alpha-pf-react-19-improvements-970580a42.0",
31
- "@toptal/picasso-provider": "^100.2.1-alpha-pf-react-19-improvements-970580a42.0",
32
- "@toptal/picasso-tailwind": "^100.0.1-alpha-pf-react-19-improvements-970580a42.0"
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-provider": "100.2.1-alpha-pf-react-19-improvements-970580a42.0",
39
- "@toptal/picasso-tailwind-merge": "100.0.1-alpha-pf-react-19-improvements-970580a42.0",
40
- "@toptal/picasso-test-utils": "100.1.1-alpha-pf-react-19-improvements-970580a42.0"
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. If not set will not receive 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
- 'focus:outline-hidden hover:underline leading-[inherit] text-gray-600 underline cursor-not-allowed'
268
+ 'text-gray-600 underline cursor-not-allowed'
190
269
  )
191
270
  expect(whiteResult.className).toContain('text-gray-600')
192
271
  })