Skip to content

Commit 991892e

Browse files
authored
feat: Implementation of the Link component Style API (#3699)
1 parent a499eab commit 991892e

File tree

10 files changed

+352
-12
lines changed

10 files changed

+352
-12
lines changed
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
import React, { useRef } from 'react';
4+
5+
import { useCurrentMode } from '@cloudscape-design/component-toolkit/internal';
6+
7+
import { Link as CloudscapeLink, SpaceBetween } from '~components';
8+
9+
import { palette } from '../app/themes/style-api';
10+
import ScreenshotArea from '../utils/screenshot-area';
11+
12+
export default function CustomLinkTypes() {
13+
return (
14+
<ScreenshotArea>
15+
<h1>Custom Link Types</h1>
16+
<SpaceBetween direction="vertical" size="xl">
17+
<CustomLink colorTheme="secondary">Secondary</CustomLink>
18+
<CustomLink colorTheme="primary">Primary</CustomLink>
19+
<CustomLink colorTheme="external">External</CustomLink>
20+
<CustomLink colorTheme="button">Button</CustomLink>
21+
<CustomLink colorTheme="info">Info</CustomLink>
22+
</SpaceBetween>
23+
</ScreenshotArea>
24+
);
25+
}
26+
27+
interface CustomLinkProps {
28+
children?: React.ReactNode;
29+
colorTheme: 'secondary' | 'primary' | 'external' | 'button' | 'info';
30+
}
31+
32+
function CustomLink({ children, colorTheme }: CustomLinkProps) {
33+
const mode = useCurrentMode(useRef(document.body));
34+
const color = colors[mode][colorTheme];
35+
const focusRing = focusRings[mode];
36+
const linkProps = getLinkProps(colorTheme);
37+
38+
return (
39+
<CloudscapeLink
40+
{...linkProps}
41+
style={{
42+
root: {
43+
color,
44+
focusRing,
45+
},
46+
}}
47+
>
48+
{children}
49+
</CloudscapeLink>
50+
);
51+
}
52+
53+
const colors = {
54+
light: {
55+
secondary: {
56+
active: palette.blue100,
57+
default: palette.blue80,
58+
hover: palette.green90,
59+
},
60+
primary: {
61+
active: palette.blue100,
62+
default: palette.blue80,
63+
hover: palette.blue90,
64+
},
65+
external: {
66+
active: palette.red80,
67+
default: palette.red60,
68+
hover: palette.red60,
69+
},
70+
button: {
71+
active: palette.green100,
72+
default: palette.green80,
73+
hover: palette.green90,
74+
},
75+
info: {
76+
active: palette.teal100,
77+
default: palette.teal90,
78+
hover: palette.teal90,
79+
},
80+
},
81+
dark: {
82+
secondary: {
83+
active: palette.blue20,
84+
default: palette.blue40,
85+
hover: palette.blue60,
86+
},
87+
primary: {
88+
active: palette.blue20,
89+
default: palette.blue40,
90+
hover: palette.blue40,
91+
},
92+
external: {
93+
active: palette.red20,
94+
default: palette.red30,
95+
hover: palette.red30,
96+
},
97+
button: {
98+
active: palette.green10,
99+
default: palette.green20,
100+
hover: palette.green60,
101+
},
102+
info: {
103+
active: palette.teal10,
104+
default: palette.teal20,
105+
hover: palette.teal20,
106+
},
107+
},
108+
};
109+
110+
const focusRings = {
111+
light: {
112+
borderColor: palette.blue80,
113+
borderRadius: '4px',
114+
borderWidth: '2px',
115+
},
116+
dark: {
117+
borderColor: palette.red60,
118+
borderRadius: '4px',
119+
borderWidth: '2px',
120+
},
121+
};
122+
123+
function getLinkProps(colorTheme: string) {
124+
const baseProps = {
125+
href: '#',
126+
ariaLabel: `${colorTheme} link example`,
127+
};
128+
129+
switch (colorTheme) {
130+
case 'primary':
131+
return { ...baseProps, variant: 'primary' as const };
132+
case 'secondary':
133+
return { ...baseProps, variant: 'secondary' as const };
134+
case 'external':
135+
return { ...baseProps, variant: 'secondary' as const, external: true, target: '_blank' };
136+
case 'button':
137+
return {
138+
...baseProps,
139+
variant: 'primary' as const,
140+
onFollow: () => alert('You clicked the button link!'),
141+
};
142+
case 'info':
143+
return { ...baseProps, variant: 'info' as const };
144+
default:
145+
return baseProps;
146+
}
147+
}

src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14818,6 +14818,89 @@ By default, the component sets the \`rel\` attribute to "noopener noreferrer" wh
1481814818
"optional": true,
1481914819
"type": "string",
1482014820
},
14821+
{
14822+
"description": "Specifies an object of selectors and properties that are used to apply custom styles.
14823+
14824+
- \`root.color\` {active, default, hover} (string) - (Optional) Text color for link.
14825+
- \`root.focusRing.borderColor\` (string) - (Optional) Focus ring border color.
14826+
- \`root.focusRing.borderRadius\` (string) - (Optional) Focus ring border radius.
14827+
- \`root.focusRing.borderWidth\` (string) - (Optional) Focus ring border width.",
14828+
"inlineType": {
14829+
"name": "LinkProps.Style",
14830+
"properties": [
14831+
{
14832+
"inlineType": {
14833+
"name": "object",
14834+
"properties": [
14835+
{
14836+
"inlineType": {
14837+
"name": "{ active?: string | undefined; default?: string | undefined; hover?: string | undefined; }",
14838+
"properties": [
14839+
{
14840+
"name": "active",
14841+
"optional": true,
14842+
"type": "string",
14843+
},
14844+
{
14845+
"name": "default",
14846+
"optional": true,
14847+
"type": "string",
14848+
},
14849+
{
14850+
"name": "hover",
14851+
"optional": true,
14852+
"type": "string",
14853+
},
14854+
],
14855+
"type": "object",
14856+
},
14857+
"name": "color",
14858+
"optional": true,
14859+
"type": "{ active?: string | undefined; default?: string | undefined; hover?: string | undefined; }",
14860+
},
14861+
{
14862+
"inlineType": {
14863+
"name": "object",
14864+
"properties": [
14865+
{
14866+
"name": "borderColor",
14867+
"optional": true,
14868+
"type": "string",
14869+
},
14870+
{
14871+
"name": "borderRadius",
14872+
"optional": true,
14873+
"type": "string",
14874+
},
14875+
{
14876+
"name": "borderWidth",
14877+
"optional": true,
14878+
"type": "string",
14879+
},
14880+
],
14881+
"type": "object",
14882+
},
14883+
"name": "focusRing",
14884+
"optional": true,
14885+
"type": "{ borderColor?: string | undefined; borderRadius?: string | undefined; borderWidth?: string | undefined; }",
14886+
},
14887+
],
14888+
"type": "object",
14889+
},
14890+
"name": "root",
14891+
"optional": true,
14892+
"type": "{ color?: { active?: string | undefined; default?: string | undefined; hover?: string | undefined; } | undefined; focusRing?: { borderColor?: string | undefined; borderRadius?: string | undefined; borderWidth?: string | undefined; } | undefined; }",
14893+
},
14894+
],
14895+
"type": "object",
14896+
},
14897+
"name": "style",
14898+
"optional": true,
14899+
"systemTags": [
14900+
"core",
14901+
],
14902+
"type": "LinkProps.Style",
14903+
},
1482114904
{
1482214905
"description": "Specifies where to open the linked URL. Set this to \`_blank\` to open the URL
1482314906
in a new tab. If you set this property to \`_blank\`, the component

src/internal/styles/forms/mixins.scss

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,22 @@
1111
@use '../utils' as utils;
1212
@use './constants' as constants;
1313

14-
@mixin link-focus {
14+
@mixin link-focus(
15+
$border-color: awsui.$color-border-item-focused,
16+
$border-radius: awsui.$border-radius-control-default-focus-ring,
17+
$box-shadow: 0 0 0 awsui.$border-link-focus-ring-shadow-spread awsui.$color-border-item-focused
18+
) {
1519
// For classic
1620
outline: thin dotted;
1721
outline: awsui.$border-link-focus-ring-outline;
1822
outline-offset: 2px;
19-
outline-color: awsui.$color-border-item-focused;
23+
outline-color: $border-color;
2024
// For visual refresh
21-
border-start-start-radius: awsui.$border-radius-control-default-focus-ring;
22-
border-start-end-radius: awsui.$border-radius-control-default-focus-ring;
23-
border-end-start-radius: awsui.$border-radius-control-default-focus-ring;
24-
border-end-end-radius: awsui.$border-radius-control-default-focus-ring;
25-
box-shadow: 0 0 0 awsui.$border-link-focus-ring-shadow-spread awsui.$color-border-item-focused;
25+
border-start-start-radius: $border-radius;
26+
border-start-end-radius: $border-radius;
27+
border-end-start-radius: $border-radius;
28+
border-end-end-radius: $border-radius;
29+
box-shadow: $box-shadow;
2630
}
2731

2832
@mixin container-focus($border-radius: awsui.$border-radius-container) {

src/internal/styles/links.scss

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@
1010
@use './forms/' as mixins;
1111
@use '@cloudscape-design/component-toolkit/internal/focus-visible' as focus-visible;
1212
@use '../../link/constants.scss' as constants;
13+
@use '../generated/custom-css-properties/index.scss' as custom-props;
1314

1415
@mixin link-variant-style($variant) {
15-
color: map.get($variant, 'text-color-default');
16+
color: var(#{custom-props.$styleColorDefault}, map.get($variant, 'text-color-default'));
1617
font-weight: map.get($variant, 'font-weight');
1718
letter-spacing: map.get($variant, 'letter-spacing');
1819
text-decoration-line: map.get($variant, 'decoration-line');
@@ -25,15 +26,15 @@
2526

2627
&:hover {
2728
cursor: pointer;
28-
color: map.get($variant, 'text-color-hover');
29+
color: var(#{custom-props.$styleColorHover}, map.get($variant, 'text-color-hover'));
2930
}
3031

3132
&:focus {
3233
outline: none;
3334
}
3435

3536
&:active {
36-
color: map.get($variant, 'text-color-active');
37+
color: var(#{custom-props.$styleColorActive}, map.get($variant, 'text-color-active'));
3738
}
3839

3940
&:active,

src/link/__tests__/index.test.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import createWrapper from '../../../lib/components/test-utils/dom';
1515
import { linkRelExpectations, linkTargetExpectations } from '../../__tests__/target-rel-test-helper';
1616
import { mockedFunnelInteractionId, mockFunnelMetrics } from '../../internal/analytics/__tests__/mocks';
1717
import { renderWithSingleTabStopNavigation } from '../../internal/context/__tests__/utils';
18+
import customCssProps from '../../internal/generated/custom-css-properties';
1819

1920
import styles from '../../../lib/components/link/styles.css.js';
2021

@@ -377,3 +378,38 @@ describe('"onClick" event', () => {
377378
);
378379
});
379380
});
381+
382+
describe('Style API', () => {
383+
test('all style properties', () => {
384+
const { container } = render(
385+
<Link
386+
style={{
387+
root: {
388+
color: {
389+
active: 'rgb(163, 15, 15)',
390+
default: 'rgb(15, 77, 163)',
391+
hover: 'rgb(22, 104, 9)',
392+
},
393+
focusRing: {
394+
borderColor: 'rgb(157, 18, 10)',
395+
borderRadius: '6px',
396+
borderWidth: '4px',
397+
},
398+
},
399+
}}
400+
>
401+
Link
402+
</Link>
403+
);
404+
405+
const link = createWrapper(container).findLink()!.getElement();
406+
407+
expect(getComputedStyle(link).getPropertyValue(customCssProps.styleColorActive)).toBe('rgb(163, 15, 15)');
408+
expect(getComputedStyle(link).getPropertyValue(customCssProps.styleColorDefault)).toBe('rgb(15, 77, 163)');
409+
expect(getComputedStyle(link).getPropertyValue(customCssProps.styleColorHover)).toBe('rgb(22, 104, 9)');
410+
411+
expect(getComputedStyle(link).getPropertyValue(customCssProps.styleFocusRingBorderColor)).toBe('rgb(157, 18, 10)');
412+
expect(getComputedStyle(link).getPropertyValue(customCssProps.styleFocusRingBorderRadius)).toBe('6px');
413+
expect(getComputedStyle(link).getPropertyValue(customCssProps.styleFocusRingBorderWidth)).toBe('4px');
414+
});
415+
});

src/link/index.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ import InternalLink from './internal';
1414
export { LinkProps };
1515

1616
const Link = React.forwardRef(
17-
({ fontSize = 'body-m', color = 'normal', external = false, ...props }: LinkProps, ref: React.Ref<LinkProps.Ref>) => {
17+
(
18+
{ fontSize = 'body-m', color = 'normal', external = false, style, ...props }: LinkProps,
19+
ref: React.Ref<LinkProps.Ref>
20+
) => {
1821
const baseComponentProps = useBaseComponent('Link', {
1922
props: { color, external, fontSize, rel: props.rel, target: props.target, variant: props.variant },
2023
});
@@ -45,6 +48,7 @@ const Link = React.forwardRef(
4548
{...baseComponentProps}
4649
ref={ref}
4750
{...getAnalyticsMetadataAttribute(analyticsMetadata)}
51+
style={style}
4852
/>
4953
);
5054
}

0 commit comments

Comments
 (0)