diff --git a/.changeset/tasty-eagles-draw.md b/.changeset/tasty-eagles-draw.md new file mode 100644 index 00000000000..95a768f410e --- /dev/null +++ b/.changeset/tasty-eagles-draw.md @@ -0,0 +1,31 @@ +--- +"@spectrum-css/link": major +--- + +### Link S2 migration + +The link component is updated with S2 specifications, colors, and typography. There is a new inline variant which uses the `.spectrum-Link--inline` modifier class and the default is the standalone variant. + +#### Note + +- Quiet styling does not apply to the inline variant so that it is distinguishable from the surrounding text and the underline indicator is clear +- `--mod-spectrum-link-font-weight` can be used to adjust inline variant to match surrounding text. Apply font weights to this mod `inherit` does not apply. + +## New tokens + +### Color + +`spectrum-link-focus-indicator-color` +`spectrum-link-focus-indicator-thickness` +`spectrum-link-focus-indicator-gap` +`spectrum-link-corner-radius` + +### Typography + +`spectrum-link-line-height` +`spectrum-link-line-height-cjk-100` +`spectrum-link-font-size` +`spectrum-link-font-style` +`spectrum-link-font-weight` +`spectrum-link-text-underline-thickness` +`spectrum-link-text-underline-gap` diff --git a/components/link/dist/metadata.json b/components/link/dist/metadata.json index ad90df33840..5bebb76f6d0 100644 --- a/components/link/dist/metadata.json +++ b/components/link/dist/metadata.json @@ -2,6 +2,7 @@ "sourceFile": "index.css", "selectors": [ ".spectrum-Link", + ".spectrum-Link--inline", ".spectrum-Link--quiet", ".spectrum-Link--quiet:hover", ".spectrum-Link--secondary", @@ -9,26 +10,41 @@ ".spectrum-Link--staticWhite", ".spectrum-Link:active", ".spectrum-Link:focus-visible", - ".spectrum-Link:hover" + ".spectrum-Link:hover", + ".spectrum-Link:lang(ja)", + ".spectrum-Link:lang(ko)", + ".spectrum-Link:lang(zh)" ], "modifiers": [ "--mod-link-animation-duration", + "--mod-link-inline-font-weight", + "--mod-link-line-height-cjk", "--mod-link-text-color", "--mod-link-text-color-active", "--mod-link-text-color-black", "--mod-link-text-color-focus", "--mod-link-text-color-hover", - "--mod-link-text-color-primary-active", - "--mod-link-text-color-primary-default", - "--mod-link-text-color-primary-focus", - "--mod-link-text-color-primary-hover", "--mod-link-text-color-secondary-active", "--mod-link-text-color-secondary-default", "--mod-link-text-color-secondary-focus", "--mod-link-text-color-secondary-hover", "--mod-link-text-color-white" ], - "component": [], + "component": [ + "--spectrum-link-corner-radius", + "--spectrum-link-default-font-family", + "--spectrum-link-focus-indicator-color", + "--spectrum-link-focus-indicator-gap", + "--spectrum-link-focus-indicator-thickness", + "--spectrum-link-font-size", + "--spectrum-link-font-style", + "--spectrum-link-font-weight", + "--spectrum-link-inline-font-weight", + "--spectrum-link-line-height", + "--spectrum-link-line-height-cjk", + "--spectrum-link-text-underline-gap", + "--spectrum-link-text-underline-thickness" + ], "global": [ "--spectrum-accent-content-color-default", "--spectrum-accent-content-color-down", @@ -36,12 +52,28 @@ "--spectrum-accent-content-color-key-focus", "--spectrum-animation-duration-100", "--spectrum-black", + "--spectrum-cjk-line-height-100", + "--spectrum-corner-radius-100", + "--spectrum-default-font-style", + "--spectrum-focus-indicator-color", + "--spectrum-focus-indicator-gap", + "--spectrum-focus-indicator-thickness", + "--spectrum-font-size-100", + "--spectrum-line-height-100", + "--spectrum-medium-font-weight", "--spectrum-neutral-content-color-default", "--spectrum-neutral-content-color-down", "--spectrum-neutral-content-color-hover", "--spectrum-neutral-content-color-key-focus", + "--spectrum-regular-font-weight", + "--spectrum-sans-font-family-stack", + "--spectrum-text-underline-gap", + "--spectrum-text-underline-thickness", "--spectrum-white" ], "passthroughs": [], - "high-contrast": ["--highcontrast-link-text-color"] + "high-contrast": [ + "--highcontrast-link-focus-indicator-color", + "--highcontrast-link-text-color" + ] } diff --git a/components/link/index.css b/components/link/index.css index 97a5f291228..0cd89349e43 100644 --- a/components/link/index.css +++ b/components/link/index.css @@ -11,21 +11,28 @@ * governing permissions and limitations under the License. */ -/* Windows high contrast mode */ -@media (forced-colors: active) { - .spectrum-Link { - --highcontrast-link-text-color: LinkText; - } -} +.spectrum-Link { + /* Focus state */ + --spectrum-link-focus-indicator-color: var(--spectrum-focus-indicator-color); + --spectrum-link-focus-indicator-thickness: var(--spectrum-focus-indicator-thickness); + --spectrum-link-focus-indicator-gap: var(--spectrum-focus-indicator-gap); + --spectrum-link-corner-radius: var(--spectrum-corner-radius-100); -.spectrum-Link--secondary { - --mod-link-text-color: var(--mod-link-text-color-secondary-default, var(--spectrum-neutral-content-color-default)); - --mod-link-text-color-hover: var(--mod-link-text-color-secondary-hover, var(--spectrum-neutral-content-color-hover)); - --mod-link-text-color-active: var(--mod-link-text-color-secondary-active, var(--spectrum-neutral-content-color-down)); - --mod-link-text-color-focus: var(--mod-link-text-color-secondary-focus, var(--spectrum-neutral-content-color-key-focus)); -} + /* Typography */ + --spectrum-link-default-font-family: var(--spectrum-sans-font-family-stack); + + /* Standalone */ + --spectrum-link-line-height: var(--spectrum-line-height-100); + --spectrum-link-line-height-cjk: var(--spectrum-cjk-line-height-100); + --spectrum-link-font-size: var(--spectrum-font-size-100); + --spectrum-link-font-style: var(--spectrum-default-font-style); + --spectrum-link-font-weight: var(--spectrum-medium-font-weight); + --spectrum-link-text-underline-thickness: var(--spectrum-text-underline-thickness); + --spectrum-link-text-underline-gap: var(--spectrum-text-underline-gap); + + /* Inline */ + --spectrum-link-inline-font-weight: var(--spectrum-regular-font-weight); -.spectrum-Link { /* Remove the gray background on active links in IE 10. */ background-color: transparent; @@ -36,29 +43,45 @@ outline: none; cursor: pointer; - /* @deprecated --mod-link-text-color-primary-default in favor of --mod-link-text-color */ - color: var(--highcontrast-link-text-color, var(--mod-link-text-color, var(--mod-link-text-color-primary-default, var(--spectrum-accent-content-color-default)))); + color: var(--highcontrast-link-text-color, var(--mod-link-text-color, var(--spectrum-accent-content-color-default))); + font-family: var(--spectrum-link-default-font-family); + font-weight: var(--spectrum-link-font-weight); + font-size: var(--spectrum-link-font-size); + font-style: var(--spectrum-link-font-style); + line-height: var(--spectrum-link-line-height); + text-decoration-thickness: var(--spectrum-link-text-underline-thickness); + text-underline-offset: var(--spectrum-link-text-underline-gap); &:hover { - /* @deprecated --mod-link-text-color-primary-hover in favor of --mod-link-text-color-hover */ - --mod-link-text-color: var(--mod-link-text-color-hover, var(--mod-link-text-color-primary-hover, var(--spectrum-accent-content-color-hover))); + --mod-link-text-color: var(--mod-link-text-color-hover, var(--spectrum-accent-content-color-hover)); } &:active { - /* @deprecated --mod-link-text-color-primary-active in favor of --mod-link-text-color-active */ - --mod-link-text-color: var(--mod-link-text-color-active, var(--mod-link-text-color-primary-active, var(--spectrum-accent-content-color-down))); + --mod-link-text-color: var(--mod-link-text-color-active, var(--spectrum-accent-content-color-down)); } &:focus-visible { - /* @deprecated --mod-link-text-color-primary-focus in favor of --mod-link-text-color-focus */ - --mod-link-text-color: var(--mod-link-text-color-focus, var(--mod-link-text-color-primary-focus, var(--spectrum-accent-content-color-key-focus))); + --mod-link-text-color: var(--mod-link-text-color-focus, var(--spectrum-accent-content-color-key-focus)); - text-decoration: underline; - text-decoration-style: double; - text-decoration-color: inherit; + outline: var(--spectrum-link-focus-indicator-thickness) solid var(--highcontrast-link-focus-indicator-color, var(--spectrum-link-focus-indicator-color)); + padding: var(--spectrum-link-focus-indicator-gap); + border-radius: var(--spectrum-link-corner-radius); + } + + &:lang(ja), + &:lang(zh), + &:lang(ko) { + line-height: var(--mod-link-line-height-cjk, var(--spectrum-link-line-height-cjk)); } } +.spectrum-Link--secondary { + --mod-link-text-color: var(--mod-link-text-color-secondary-default, var(--spectrum-neutral-content-color-default)); + --mod-link-text-color-hover: var(--mod-link-text-color-secondary-hover, var(--spectrum-neutral-content-color-hover)); + --mod-link-text-color-active: var(--mod-link-text-color-secondary-active, var(--spectrum-neutral-content-color-down)); + --mod-link-text-color-focus: var(--mod-link-text-color-secondary-focus, var(--spectrum-neutral-content-color-key-focus)); +} + .spectrum-Link--quiet { text-decoration: none; @@ -67,6 +90,10 @@ } } +.spectrum-Link--inline { + --spectrum-link-font-weight: var(--mod-link-inline-font-weight, var(--spectrum-link-inline-font-weight)); +} + .spectrum-Link--staticWhite { --mod-link-text-color: var(--mod-link-text-color-white, var(--spectrum-white)); --mod-link-text-color-hover: var(--mod-link-text-color-white, var(--spectrum-white)); @@ -80,3 +107,11 @@ --mod-link-text-color-active: var(--mod-link-text-color-black, var(--spectrum-black)); --mod-link-text-color-focus: var(--mod-link-text-color-black, var(--spectrum-black)); } + +/* Windows high contrast mode */ +@media (forced-colors: active) { + .spectrum-Link { + --highcontrast-link-text-color: LinkText; + --highcontrast-link-focus-indicator-color: LinkText; + } +} diff --git a/components/link/stories/link.stories.js b/components/link/stories/link.stories.js index 53c71bfacb2..bdb276051ce 100644 --- a/components/link/stories/link.stories.js +++ b/components/link/stories/link.stories.js @@ -3,7 +3,7 @@ import { isActive, isFocused, isHovered, isQuiet, staticColor } from "@spectrum- import metadata from "../dist/metadata.json"; import packageJson from "../package.json"; import { LinkGroup } from "./link.test.js"; -import { TemplateWithFillerText } from "./template.js"; +import { LinkGroupText, MultilineText, TemplateWithFillerText } from "./template.js"; /** * A link allows users to navigate to a different location. They can be presented in-line inside a paragraph or as standalone text. @@ -54,8 +54,20 @@ export default { }, control: "boolean", }, + isInline: { + name: "Inline", + type: { name: "boolean" }, + table: { + type: { summary: "boolean" }, + category: "Component", + }, + control: "boolean", + }, staticColor, - isQuiet, + isQuiet: { + ...isQuiet, + if: { arg: "isInline", neq: true }, + }, }, args: { rootClass: "spectrum-Link", @@ -65,6 +77,7 @@ export default { isActive: false, isFocused: false, isVisited: false, + isInline: false, }, parameters: { actions: { @@ -76,7 +89,7 @@ export default { }, packageJson, metadata, - }, + } }; export const Default = LinkGroup.bind({}); @@ -88,106 +101,83 @@ Default.tags = ["!autodocs"]; // ********* DOCS ONLY ********* // /** - * The primary link is the default variant and is blue. This should be used to call attention to the link and for when the blue color won’t feel too overwhelming in the experience. + * The standalone link is the default variant with the primary style which appears blue. This should be used to call attention to the link and for when the blue color won’t feel too overwhelming in the experience. + * All links can have a quiet style, without an underline. This style should only be used when the placement and context of the link is explicit enough that a visible underline isn’t necessary. */ -export const Primary = TemplateWithFillerText.bind({}); -Primary.args = { - ...Default.args, - text: "link using spectrum-Link" -}; +export const Primary = LinkGroupText.bind({}); +Primary.args = {}; Primary.tags = ["!dev"]; Primary.parameters = { chromatic: { disableSnapshot: true }, }; +Primary.storyName = "Standalone, primary"; /** * The secondary variant is the same gray color as the paragraph text. Its subdued appearance is optimal for when the primary variant is too overwhelming, such as in blocks of text with several references linked throughout. + * Link is using the class `spectrum-Link--secondary`. */ -export const Secondary = TemplateWithFillerText.bind({}); +export const Secondary = LinkGroupText.bind({}); Secondary.args = { variant: "secondary", - text: "link using spectrum-Link--secondary", }; Secondary.parameters = { chromatic: { disableSnapshot: true }, }; Secondary.tags = ["!dev"]; - -/** - * All links can have a quiet style, without an underline. This style should only be used when the placement and context of the link is explicit enough that a visible underline isn’t necessary. - */ -export const QuietPrimary = TemplateWithFillerText.bind({}); -QuietPrimary.storyName = "Primary (quiet)"; -QuietPrimary.args = { - isQuiet: true, - text: "link using spectrum-Link--quiet", -}; -QuietPrimary.parameters = { - chromatic: { disableSnapshot: true }, -}; -QuietPrimary.tags = ["!dev"]; - -export const QuietSecondary = TemplateWithFillerText.bind({}); -QuietSecondary.storyName = "Secondary (quiet)"; -QuietSecondary.args = { - isQuiet: true, - variant: "secondary", - text: "link using spectrum-Link--quiet and spectrum-Link--secondary", -}; -QuietSecondary.parameters = { - chromatic: { disableSnapshot: true }, -}; -QuietSecondary.tags = ["!dev"]; +Secondary.storyName = "Standalone, secondary"; /** * Use static white on dark color or image backgrounds, regardless of color theme. Make sure that the background and the link color meet the minimum color contrast ratio. + * Link is using the class `spectrum-Link--staticWhite`. */ -export const StaticWhite = Default.bind({}); +export const StaticWhite = LinkGroupText.bind({}); StaticWhite.storyName = "Static white"; StaticWhite.args = { staticColor: "white", - text: "Link using spectrum-Link--staticWhite", }; StaticWhite.tags = ["!dev"]; StaticWhite.parameters = { chromatic: { disableSnapshot: true }, }; +StaticWhite.storyName = "Standalone, static white"; /** * Use static black on light color or image backgrounds, regardless of color theme. Make sure that the background and the link color meet the minimum color contrast ratio. + * Link using is the class `spectrum-Link--staticBlack`. */ -export const StaticBlack = Default.bind({}); +export const StaticBlack = LinkGroupText.bind({}); StaticBlack.storyName = "Static black"; StaticBlack.args = { staticColor: "black", - text: "Link using spectrum-Link--staticBlack", }; StaticBlack.tags = ["!dev"]; StaticBlack.parameters = { chromatic: { disableSnapshot: true }, }; +StaticBlack.storyName = "Standalone, static black"; -export const QuietStaticWhite = Default.bind({}); -QuietStaticWhite.storyName = "Static white (quiet)"; -QuietStaticWhite.args = { - isQuiet: true, - staticColor: "white", - text: "Link using spectrum-Link--staticWhite and spectrum-Link--quiet", -}; -QuietStaticWhite.tags = ["!dev"]; -QuietStaticWhite.parameters = { +/** + * The focus ring wraps when the link component breaks on multiple rows. + */ +export const Multiline = MultilineText.bind({}); +Multiline.args = {}; +Multiline.tags = ["!dev"]; +Multiline.parameters = { chromatic: { disableSnapshot: true }, }; +Multiline.storyName = "Multiline, focused"; -export const QuietStaticBlack = Default.bind({}); -QuietStaticBlack.storyName = "Static black (quiet)"; -QuietStaticBlack.args = { - isQuiet: true, - staticColor: "black", - text: "Link using spectrum-Link--staticBlack and spectrum-Link--quiet", +/** + * Inline links are used within a paragraph of text. They are styled differently from standalone links in which the font weight is lighter and can be adjusted to match the paragraph text. + * Inline links do not have a quiet variant style so that it is distinguishable from the surrounding text and the underline indicator is clear. + */ +export const Inline = TemplateWithFillerText.bind({}); +Inline.args = { + isInline: true, + text: "Inline link", }; -QuietStaticBlack.tags = ["!dev"]; -QuietStaticBlack.parameters = { +Inline.tags = ["!dev"]; +Inline.parameters = { chromatic: { disableSnapshot: true }, }; diff --git a/components/link/stories/link.test.js b/components/link/stories/link.test.js index 04b33e7abe7..3fdfa9576fe 100644 --- a/components/link/stories/link.test.js +++ b/components/link/stories/link.test.js @@ -11,6 +11,15 @@ export const LinkGroup = Variants({ testHeading: "Secondary", variant: "secondary", }, + { + testHeading: "Inline", + isInline: true, + }, + { + testHeading: "Inline - secondary", + isInline: true, + variant: "secondary", + }, { testHeading: "Quiet", isQuiet: true, @@ -47,7 +56,7 @@ export const LinkGroup = Variants({ testHeading: "Static white - quiet", staticColor: "white", isQuiet: true, - }, + } ], stateData: [ { diff --git a/components/link/stories/template.js b/components/link/stories/template.js index a4faba05967..798f0da7323 100644 --- a/components/link/stories/template.js +++ b/components/link/stories/template.js @@ -1,4 +1,4 @@ -import { getRandomId } from "@spectrum-css/preview/decorators"; +import { Container, getRandomId } from "@spectrum-css/preview/decorators"; import { html } from "lit"; import { classMap } from "lit/directives/class-map.js"; import { ifDefined } from "lit/directives/if-defined.js"; @@ -17,6 +17,7 @@ export const Template = ({ isActive = false, isFocused = false, isVisited = false, + isInline = false, id = getRandomId("link"), customClasses = [], } = {}) => { @@ -25,6 +26,7 @@ export const Template = ({ class=${classMap({ [rootClass]: true, [`${rootClass}--quiet`]: isQuiet, + [`${rootClass}--inline`]: isInline, [`${rootClass}--${variant}`]: typeof variant !== "undefined", [`${rootClass}--static${capitalize(staticColor)}`]: typeof staticColor !== "undefined", @@ -43,9 +45,48 @@ export const Template = ({ }; export const TemplateWithFillerText = (args, context) => html` -
Hello, this is a ${Template(args, context)} . This is just filler text, but if you keep reading maybe something good will happen. -
+ I like focus styles. They are very important for accessibility. They help users know where they are on the page. + ${Template({ + ...args, + text: "This is a link that spans multiple lines", + isFocused: true, + isInline: true, + }, context)} + They are also very pretty. +
+`; + +export const LinkGroupText = (args, context) => Container({ + withBorder: false, + direction: "row", + content: [ + Container({ + withBorder: false, + direction: "column", + heading: "Default", + content: Template({ + ...args, + text: "Learn more", + }, context) + }, context), + Container({ + withBorder: false, + direction: "column", + heading: "Quiet", + content: Template({ + ...args, + text: "Learn more", + isQuiet: true, + }, context) + }, context) + ], +}, context);