Skip to content

Commit f45a49f

Browse files
committed
Add react-link tokens
1 parent aab6c81 commit f45a49f

File tree

10 files changed

+450
-0
lines changed

10 files changed

+450
-0
lines changed

packages/react-components/semantic-style-hooks-preview/library/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
},
2626
"dependencies": {
2727
"@fluentui/react-button": "^9.4.6",
28+
"@fluentui/react-link": "^9.6.7",
2829
"@fluentui/semantic-tokens": "^0.0.1",
2930
"@fluentui/react-icons": "^2.0.245",
3031
"@fluentui/react-provider": "^9.22.7",
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
'use client';
2+
3+
import { shorthands, makeStyles, mergeClasses } from '@griffel/react';
4+
import { createCustomFocusIndicatorStyle } from '@fluentui/react-tabster';
5+
import { tokens } from '@fluentui/react-theme';
6+
import * as semanticTokens from '@fluentui/semantic-tokens';
7+
import { linkClassNames, type LinkState } from '@fluentui/react-link';
8+
import { getSlotClassNameProp_unstable } from '@fluentui/react-utilities';
9+
10+
const useStyles = makeStyles({
11+
focusIndicator: createCustomFocusIndicatorStyle({
12+
textDecorationColor: semanticTokens.groupFocusInnerStroke,
13+
textDecorationLine: 'underline',
14+
textDecorationStyle: 'double',
15+
outlineStyle: 'none',
16+
}),
17+
// Common styles.
18+
root: {
19+
':focus-visible': {
20+
outlineStyle: 'none',
21+
},
22+
textDecoration: semanticTokens.groupLinkTextUnderlineStyle,
23+
backgroundColor: 'transparent',
24+
boxSizing: 'border-box',
25+
color: semanticTokens.groupLinkBrandTextForeground,
26+
cursor: 'pointer',
27+
display: 'inline',
28+
fontFamily: semanticTokens.groupLinkTextFontfamily,
29+
fontSize: semanticTokens.groupLinkMediumTextFontsize,
30+
lineHeight: semanticTokens.groupLinkMediumTextLineheight,
31+
fontWeight: semanticTokens.groupLinkMediumTextFontweight,
32+
margin: '0',
33+
padding: '0',
34+
overflow: 'inherit',
35+
textAlign: 'left',
36+
textDecorationLine: semanticTokens.groupLinkOnpageTextDecorationline,
37+
textDecorationThickness: semanticTokens.groupLinkTextUnderlineStrokewidth,
38+
textOverflow: 'inherit',
39+
userSelect: 'text',
40+
41+
':hover': {
42+
textDecorationLine: semanticTokens.groupLinkOnpageTextDecorationlineHover,
43+
color: semanticTokens.groupLinkBrandTextForegroundHover,
44+
},
45+
46+
':active': {
47+
textDecorationLine: semanticTokens.groupLinkOnpageTextDecorationlinePressed,
48+
color: semanticTokens.groupLinkBrandTextForegroundPressed,
49+
},
50+
},
51+
// Overrides when the Link renders as a button.
52+
button: {
53+
...shorthands.borderStyle('none'),
54+
},
55+
// Overrides when an href is present so the Link renders as an anchor.
56+
href: {
57+
fontSize: 'inherit',
58+
lineHeight: 'inherit',
59+
},
60+
// Overrides when the Link appears subtle.
61+
subtle: {
62+
color: semanticTokens.groupLinkNeutralTextForeground,
63+
64+
':hover': {
65+
color: semanticTokens.groupLinkNeutralTextForegroundHover,
66+
},
67+
68+
':active': {
69+
color: semanticTokens.groupLinkNeutralTextForegroundPressed,
70+
},
71+
},
72+
// Overrides when the Link is rendered inline within text.
73+
inline: {
74+
textDecorationLine: 'underline',
75+
':hover': {
76+
textDecorationLine: 'underline',
77+
},
78+
':active': {
79+
textDecorationLine: 'underline',
80+
},
81+
},
82+
// Overrides when the Link is disabled.
83+
disabled: {
84+
textDecorationLine: 'none',
85+
color: semanticTokens.groupLinkBrandTextForegroundDisabled,
86+
cursor: 'not-allowed',
87+
88+
':hover': {
89+
textDecorationLine: 'none',
90+
color: semanticTokens.groupLinkBrandTextForegroundDisabled,
91+
},
92+
93+
':active': {
94+
textDecorationLine: 'none',
95+
color: semanticTokens.groupLinkBrandTextForegroundDisabled,
96+
},
97+
},
98+
// Overrides when the Link is disabled.
99+
disabledSubtle: {
100+
textDecorationLine: 'none',
101+
color: semanticTokens.groupLinkNeutralTextForegroundDisabled,
102+
cursor: 'not-allowed',
103+
104+
':hover': {
105+
textDecorationLine: 'none',
106+
color: semanticTokens.groupLinkNeutralTextForegroundDisabled,
107+
},
108+
109+
':active': {
110+
textDecorationLine: 'none',
111+
color: semanticTokens.groupLinkNeutralTextForegroundDisabled,
112+
},
113+
},
114+
115+
// Semantic-tokens does not include inverted tokens, use legacy/inverted themes
116+
inverted: {
117+
color: tokens.colorBrandForegroundInverted,
118+
':hover': {
119+
color: tokens.colorBrandForegroundInvertedHover,
120+
},
121+
':active': {
122+
color: tokens.colorBrandForegroundInvertedPressed,
123+
},
124+
},
125+
});
126+
127+
export const useSemanticLinkStyles = (_state: unknown): LinkState => {
128+
'use no memo';
129+
130+
const state = _state as LinkState;
131+
132+
const styles = useStyles();
133+
const { appearance, disabled, inline, root, backgroundAppearance } = state;
134+
135+
state.root.className = mergeClasses(
136+
state.root.className,
137+
linkClassNames.root,
138+
styles.root,
139+
styles.focusIndicator,
140+
root.as === 'a' && root.href && styles.href,
141+
root.as === 'button' && styles.button,
142+
appearance === 'subtle' && styles.subtle,
143+
backgroundAppearance === 'inverted' && styles.inverted,
144+
inline && styles.inline,
145+
disabled && styles.disabled,
146+
appearance === 'subtle' && disabled && styles.disabledSubtle,
147+
getSlotClassNameProp_unstable(state.root),
148+
);
149+
150+
return state;
151+
};

packages/react-components/semantic-style-hooks-preview/library/src/component-styles/semanticStyleHooks.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
useSemanticSplitButtonStyles,
88
useSemanticToggleButtonStyles,
99
} from './Button';
10+
import { useSemanticLinkStyles } from './Link/useSemanticLinkStyles.styles';
1011

1112
export const SEMANTIC_STYLE_HOOKS: FluentProviderCustomStyleHooks = {
1213
// Button styles
@@ -15,4 +16,6 @@ export const SEMANTIC_STYLE_HOOKS: FluentProviderCustomStyleHooks = {
1516
useSplitButtonStyles_unstable: useSemanticSplitButtonStyles,
1617
useMenuButtonStyles_unstable: useSemanticMenuButtonStyles,
1718
useCompoundButtonStyles_unstable: useSemanticCompoundButtonStyles,
19+
// Link styles
20+
useLinkStyles_unstable: useSemanticLinkStyles,
1821
};

packages/semantic-tokens/etc/semantic-tokens.api.md

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -862,6 +862,114 @@ export const groupFocusOuterStroke = "var(--smtc-group-focus-outer-stroke, var(-
862862
// @public (undocumented)
863863
export const groupFocusOuterStrokewidth = "var(--smtc-group-focus-outer-strokewidth, var(--strokeWidthThick))";
864864

865+
// @public (undocumented)
866+
export const groupLinkBrandIconForeground = "var(--smtc-group-link-brand-icon-foreground)";
867+
868+
// @public (undocumented)
869+
export const groupLinkBrandIconForegroundDisabled = "var(--smtc-group-link-brand-icon-foreground-disabled)";
870+
871+
// @public (undocumented)
872+
export const groupLinkBrandIconForegroundHover = "var(--smtc-group-link-brand-icon-foreground-hover)";
873+
874+
// @public (undocumented)
875+
export const groupLinkBrandIconForegroundPressed = "var(--smtc-group-link-brand-icon-foreground-pressed)";
876+
877+
// @public (undocumented)
878+
export const groupLinkBrandTextForeground = "var(--smtc-group-link-brand-text-foreground, var(--colorBrandForegroundLink))";
879+
880+
// @public (undocumented)
881+
export const groupLinkBrandTextForegroundDisabled = "var(--smtc-group-link-brand-text-foreground-disabled, var(--colorNeutralForegroundDisabled))";
882+
883+
// @public (undocumented)
884+
export const groupLinkBrandTextForegroundHover = "var(--smtc-group-link-brand-text-foreground-hover, var(--colorBrandForegroundLinkHover))";
885+
886+
// @public (undocumented)
887+
export const groupLinkBrandTextForegroundPressed = "var(--smtc-group-link-brand-text-foreground-pressed, var(--colorBrandForegroundLinkPressed))";
888+
889+
// @public (undocumented)
890+
export const groupLinkGap = "var(--smtc-group-link-gap)";
891+
892+
// @public (undocumented)
893+
export const groupLinkLargeIconSize = "var(--smtc-group-link-large-icon-size)";
894+
895+
// @public (undocumented)
896+
export const groupLinkLargeTextFontsize = "var(--smtc-group-link-large-text-fontsize)";
897+
898+
// @public (undocumented)
899+
export const groupLinkLargeTextFontweight = "var(--smtc-group-link-large-text-fontweight)";
900+
901+
// @public (undocumented)
902+
export const groupLinkLargeTextLineheight = "var(--smtc-group-link-large-text-lineheight)";
903+
904+
// @public (undocumented)
905+
export const groupLinkMediumIconSize = "var(--smtc-group-link-medium-icon-size)";
906+
907+
// @public (undocumented)
908+
export const groupLinkMediumTextFontsize = "var(--smtc-group-link-medium-text-fontsize, var(--fontSizeBase300))";
909+
910+
// @public (undocumented)
911+
export const groupLinkMediumTextFontweight = "var(--smtc-group-link-medium-text-fontweight, var(--fontWeightRegular))";
912+
913+
// @public (undocumented)
914+
export const groupLinkMediumTextLineheight = "var(--smtc-group-link-medium-text-lineheight, var(--lineHeightBase300))";
915+
916+
// @public (undocumented)
917+
export const groupLinkNeutralIconForeground = "var(--smtc-group-link-neutral-icon-foreground)";
918+
919+
// @public (undocumented)
920+
export const groupLinkNeutralIconForegroundDisabled = "var(--smtc-group-link-neutral-icon-foreground-disabled)";
921+
922+
// @public (undocumented)
923+
export const groupLinkNeutralIconForegroundHover = "var(--smtc-group-link-neutral-icon-foreground-hover)";
924+
925+
// @public (undocumented)
926+
export const groupLinkNeutralIconForegroundPressed = "var(--smtc-group-link-neutral-icon-foreground-pressed)";
927+
928+
// @public (undocumented)
929+
export const groupLinkNeutralTextForeground = "var(--smtc-group-link-neutral-text-foreground, var(--colorNeutralForeground2))";
930+
931+
// @public (undocumented)
932+
export const groupLinkNeutralTextForegroundDisabled = "var(--smtc-group-link-neutral-text-foreground-disabled, var(--colorNeutralForegroundDisabled))";
933+
934+
// @public (undocumented)
935+
export const groupLinkNeutralTextForegroundHover = "var(--smtc-group-link-neutral-text-foreground-hover, var(--colorNeutralForeground2Hover))";
936+
937+
// @public (undocumented)
938+
export const groupLinkNeutralTextForegroundPressed = "var(--smtc-group-link-neutral-text-foreground-pressed, var(--colorNeutralForeground2Pressed))";
939+
940+
// @public (undocumented)
941+
export const groupLinkOnpageTextDecorationline = "var(--smtc-group-link-onpage-text-decorationline, none)";
942+
943+
// @public (undocumented)
944+
export const groupLinkOnpageTextDecorationlineDisabled = "var(--smtc-group-link-onpage-text-decorationline-disabled, none)";
945+
946+
// @public (undocumented)
947+
export const groupLinkOnpageTextDecorationlineHover = "var(--smtc-group-link-onpage-text-decorationline-hover, underline)";
948+
949+
// @public (undocumented)
950+
export const groupLinkOnpageTextDecorationlinePressed = "var(--smtc-group-link-onpage-text-decorationline-pressed, underline)";
951+
952+
// @public (undocumented)
953+
export const groupLinkSmallIconSize = "var(--smtc-group-link-small-icon-size)";
954+
955+
// @public (undocumented)
956+
export const groupLinkSmallTextFontsize = "var(--smtc-group-link-small-text-fontsize)";
957+
958+
// @public (undocumented)
959+
export const groupLinkSmallTextFontweight = "var(--smtc-group-link-small-text-fontweight)";
960+
961+
// @public (undocumented)
962+
export const groupLinkSmallTextLineheight = "var(--smtc-group-link-small-text-lineheight)";
963+
964+
// @public (undocumented)
965+
export const groupLinkTextFontfamily = "var(--smtc-group-link-text-fontfamily, var(--fontFamilyBase))";
966+
967+
// @public (undocumented)
968+
export const groupLinkTextUnderlineStrokewidth = "var(--smtc-group-link-text-underline-strokewidth, var(--strokeWidthThin))";
969+
970+
// @public (undocumented)
971+
export const groupLinkTextUnderlineStyle = "var(--smtc-group-link-text-underline-style, solid)";
972+
865973
// (No @packageDocumentation comment for this package)
866974

867975
```
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { GroupPart } from '../groups.types';
2+
3+
// Link tokens
4+
export const linkGroup: GroupPart = {
5+
components: ['link'],
6+
coreProperties: ['gap'],
7+
variants: ['brand', 'neutral'],
8+
states: ['', 'hover', 'pressed', 'disabled'],
9+
parts: {
10+
icon: {
11+
variants: ['brand', 'neutral'],
12+
states: ['', 'hover', 'pressed', 'disabled'],
13+
scales: ['small', 'medium', 'large'],
14+
scaleProperties: ['size'],
15+
variantStateProperties: ['foreground'],
16+
},
17+
text: {
18+
variants: ['brand', 'neutral'],
19+
coreProperties: ['fontfamily'],
20+
variantStateProperties: ['foreground'],
21+
scales: ['small', 'medium', 'large'],
22+
states: ['', 'hover', 'pressed', 'disabled'],
23+
scaleProperties: ['fontsize', 'lineheight', 'fontweight'],
24+
exceptions: [
25+
{
26+
// Onpage has full control over how to apply underline styles per state
27+
states: ['', 'hover', 'pressed', 'disabled'],
28+
variants: ['onpage'],
29+
variantStateProperties: ['decorationline'],
30+
},
31+
],
32+
},
33+
'text.underline': {
34+
coreProperties: ['strokewidth', 'style'],
35+
variants: ['brand', 'neutral'],
36+
states: ['', 'hover', 'pressed', 'disabled'],
37+
},
38+
},
39+
};
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { tokens } from '@fluentui/tokens';
2+
import { GroupFallback } from './fallbacks.types';
3+
4+
export const linkFallbacks: GroupFallback = {
5+
groupLinkTextUnderlineStrokewidth: {
6+
fluent: tokens.strokeWidthThin,
7+
},
8+
groupLinkTextUnderlineStyle: { fluent: 'solid' },
9+
groupLinkTextFontfamily: {
10+
fluent: tokens.fontFamilyBase,
11+
},
12+
groupLinkMediumTextFontsize: {
13+
fluent: tokens.fontSizeBase300,
14+
},
15+
groupLinkMediumTextFontweight: {
16+
fluent: tokens.fontWeightRegular,
17+
},
18+
groupLinkBrandTextForeground: {
19+
fluent: tokens.colorBrandForegroundLink,
20+
},
21+
groupLinkBrandTextForegroundHover: {
22+
fluent: tokens.colorBrandForegroundLinkHover,
23+
},
24+
groupLinkBrandTextForegroundPressed: {
25+
fluent: tokens.colorBrandForegroundLinkPressed,
26+
},
27+
groupLinkNeutralTextForeground: {
28+
fluent: tokens.colorNeutralForeground2,
29+
},
30+
groupLinkNeutralTextForegroundHover: {
31+
fluent: tokens.colorNeutralForeground2Hover,
32+
},
33+
groupLinkNeutralTextForegroundPressed: {
34+
fluent: tokens.colorNeutralForeground2Pressed,
35+
},
36+
groupLinkBrandTextForegroundDisabled: {
37+
fluent: tokens.colorNeutralForegroundDisabled,
38+
},
39+
groupLinkNeutralTextForegroundDisabled: {
40+
fluent: tokens.colorNeutralForegroundDisabled,
41+
},
42+
groupLinkMediumTextLineheight: {
43+
fluent: tokens.lineHeightBase300,
44+
},
45+
groupLinkOnpageTextDecorationline: {
46+
fluent: 'none',
47+
},
48+
groupLinkOnpageTextDecorationlineHover: {
49+
fluent: 'underline',
50+
},
51+
groupLinkOnpageTextDecorationlinePressed: {
52+
fluent: 'underline',
53+
},
54+
groupLinkOnpageTextDecorationlineDisabled: {
55+
fluent: 'none',
56+
},
57+
};
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { buttonFallbacks } from './fallbacks/button';
22
import { focusFallbacks } from './fallbacks/focus';
33
import { GroupFallbacks } from './fallbacks/fallbacks.types';
4+
import { linkFallbacks } from './fallbacks/link';
45

56
export const groupFallbacks: GroupFallbacks = {
67
focus: focusFallbacks,
78
button: buttonFallbacks,
9+
link: linkFallbacks,
810
};

0 commit comments

Comments
 (0)