diff --git a/.changeset/kind-avocados-poke.md b/.changeset/kind-avocados-poke.md new file mode 100644 index 000000000..32930b83b --- /dev/null +++ b/.changeset/kind-avocados-poke.md @@ -0,0 +1,9 @@ +--- +'@primer/react-brand': minor +--- + +Anti-aliasing is now applied automatically to all `Text` instances _except_ under these conditions: + +- When explicitly disabled via `antialiasing={false}` +- When font weight is `light` or `extralight` AND size is `'100'` or `'200'` +- When size is `100` (regardless of weight) diff --git a/packages/react/src/Accordion/Accordion.module.css b/packages/react/src/Accordion/Accordion.module.css index 5b8b10447..8b9b2e39f 100644 --- a/packages/react/src/Accordion/Accordion.module.css +++ b/packages/react/src/Accordion/Accordion.module.css @@ -144,6 +144,11 @@ details[open] > .Accordion__summary.Accordion__summary--default .Accordion__summ line-height: var(--brand-text-lineHeight-200); } +[data-color-mode='dark'] .Accordion__content { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: auto; +} + .Accordion__content p:not(:first-child) { margin-block-start: var(--base-size-16); } diff --git a/packages/react/src/InlineLink/InlineLink.module.css b/packages/react/src/InlineLink/InlineLink.module.css index 5d1fbc0ee..4cad5ef08 100644 --- a/packages/react/src/InlineLink/InlineLink.module.css +++ b/packages/react/src/InlineLink/InlineLink.module.css @@ -23,3 +23,8 @@ .InlineLink:active > span { color: var(--brand-InlineLink-color-pressed); } + +[data-color-mode='dark'] .InlineLink { + -webkit-font-smoothing: subpixel-antialiased; + -moz-osx-font-smoothing: auto; +} diff --git a/packages/react/src/Label/Label.module.css b/packages/react/src/Label/Label.module.css index 52ff419b0..aef2fef00 100644 --- a/packages/react/src/Label/Label.module.css +++ b/packages/react/src/Label/Label.module.css @@ -20,6 +20,11 @@ text-align: center; } +[data-color-mode='dark'] .Label { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: auto; +} + .Label::before { content: ''; position: absolute; diff --git a/packages/react/src/Prose/Prose.module.css b/packages/react/src/Prose/Prose.module.css index 2a087171b..8316dd844 100644 --- a/packages/react/src/Prose/Prose.module.css +++ b/packages/react/src/Prose/Prose.module.css @@ -34,6 +34,8 @@ /* ---------------------------------------------------------- */ .Prose * + * { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: auto; margin-block-start: var(--spacing, 1em); } diff --git a/packages/react/src/SubNav/SubNav.module.css b/packages/react/src/SubNav/SubNav.module.css index bc48ca5c6..8e8f31385 100644 --- a/packages/react/src/SubNav/SubNav.module.css +++ b/packages/react/src/SubNav/SubNav.module.css @@ -16,6 +16,11 @@ z-index: 1; } +[data-color-mode='dark'] .SubNav { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + .SubNav__heading { font-weight: var(--base-text-weight-semibold); color: var(--brand-color-text-default); diff --git a/packages/react/src/Text/Text.module.css b/packages/react/src/Text/Text.module.css index 48263842c..eb8a54157 100644 --- a/packages/react/src/Text/Text.module.css +++ b/packages/react/src/Text/Text.module.css @@ -2,6 +2,11 @@ font-family: var(--brand-body-fontFamily); } +[data-color-mode='dark'] .Text--antialiased { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: auto; +} + .Text--default { color: var(--brand-color-text-default); } diff --git a/packages/react/src/Text/Text.module.css.d.ts b/packages/react/src/Text/Text.module.css.d.ts index bf8e64ef6..dce7fd144 100644 --- a/packages/react/src/Text/Text.module.css.d.ts +++ b/packages/react/src/Text/Text.module.css.d.ts @@ -1,5 +1,6 @@ declare const styles: { readonly "Text": string; + readonly "Text--antialiased": string; readonly "Text--default": string; readonly "Text--muted": string; readonly "Text-font--mona-sans": string; diff --git a/packages/react/src/Text/Text.stories.tsx b/packages/react/src/Text/Text.stories.tsx index 34baf6571..536a8a2a0 100644 --- a/packages/react/src/Text/Text.stories.tsx +++ b/packages/react/src/Text/Text.stories.tsx @@ -1,7 +1,7 @@ import {Meta, StoryFn} from '@storybook/react' import React from 'react' import {Text, TextFontVariants, TextSizes, TextWeights} from '.' -import {Stack, Box, Grid} from '../' +import {Stack, Box, Grid, ThemeProvider} from '../' import styles from './Text.stories.module.css' @@ -43,6 +43,23 @@ Playground.args = { size: '200', } +export const AntiAliasingOff = () => ( + + + + Anti aliasing is disabled for light text at 16px or smaller on a dark backgrounds. + + + Anti aliasing is enabled for medium text at 16px or larger on a dark backgrounds. + + + +) +AntiAliasingOff.args = { + size: '200', + weight: 'light', +} + export const Scale: StoryFn = args => ( <> {TextSizes.map(size => ( diff --git a/packages/react/src/Text/Text.test.tsx b/packages/react/src/Text/Text.test.tsx index 9acac0eeb..44ebecaba 100644 --- a/packages/react/src/Text/Text.test.tsx +++ b/packages/react/src/Text/Text.test.tsx @@ -160,4 +160,52 @@ describe('Text', () => { } } }) + it('should not apply antialiasing when disabled', () => { + const mockId = 'mock-id' + const {getByText} = render( + + AA disabled + , + ) + const el = getByText('AA disabled') + expect(el).not.toHaveClass('Text--antialiased') + }) + + it('should not apply antialiasing for light weight and small sizes', () => { + const {getByText} = render( + <> + + light 100 + + + light 200 + + + extralight 100 + + + extralight 200 + + , + ) + + expect(getByText('light 100')).not.toHaveClass('Text--antialiased') + expect(getByText('light 200')).not.toHaveClass('Text--antialiased') + expect(getByText('extralight 100')).not.toHaveClass('Text--antialiased') + expect(getByText('extralight 200')).not.toHaveClass('Text--antialiased') + }) + + it('should not apply antialiasing for size 100', () => { + const {getByText} = render(size 100) + expect(getByText('size 100')).not.toHaveClass('Text--antialiased') + }) + + it('should apply antialiasing by default for other combinations', () => { + const {getByText} = render( + + medium 300 + , + ) + expect(getByText('medium 300')).toHaveClass('Text--antialiased') + }) }) diff --git a/packages/react/src/Text/Text.tsx b/packages/react/src/Text/Text.tsx index 90ea880c5..ee4c0aca1 100644 --- a/packages/react/src/Text/Text.tsx +++ b/packages/react/src/Text/Text.tsx @@ -67,6 +67,10 @@ export type TextProps = { * Note: Only applies to block elements. */ align?: 'start' | 'center' | 'end' + /** + * Toggle antialiasing on or off + */ + antialiasing?: boolean } & TextTags export function Text({ @@ -80,6 +84,7 @@ export function Text({ variant = defaultTextVariant, weight, style, + antialiasing = true, ...rest }: PropsWithChildren) { const {classes: animationClasses, styles: animationInlineStyles} = useAnimation(animate) @@ -96,12 +101,22 @@ export function Text({ .join(' ') }, [weight]) + const dontApplyAA = Boolean( + // When explicitly disabled + !antialiasing || + // When selected font weight is not suitable for anti-aliasing + (weight && ['light', 'extralight'].includes(weight as TextWeightVariants) && ['100', '200'].includes(size)) || + // When size is too small + size === '100', + ) + const textClassName = clsx( animationClasses, styles.Text, styles[`Text-font--${font}`], styles[`Text--${variant}`], styles[`Text--${size}`], + !dontApplyAA && styles['Text--antialiased'], weight && weightClass, align && styles[`Text-align--${align}`], className, diff --git a/packages/react/src/Text/Text.visual.spec.ts b/packages/react/src/Text/Text.visual.spec.ts index df7cfd6a5..b07155160 100644 --- a/packages/react/src/Text/Text.visual.spec.ts +++ b/packages/react/src/Text/Text.visual.spec.ts @@ -21,6 +21,13 @@ test.describe('Visual Comparison: Text', () => { expect(await page.screenshot({fullPage: true})).toMatchSnapshot() }) + test('Text / Anti Aliasing Off', async ({page}) => { + await page.goto('http://localhost:6006/iframe.html?args=&id=components-text--anti-aliasing-off&viewMode=story') + + await page.waitForTimeout(500) + expect(await page.screenshot({fullPage: true})).toMatchSnapshot() + }) + test('Text / Scale', async ({page}) => { await page.goto('http://localhost:6006/iframe.html?args=&id=components-text--scale&viewMode=story') diff --git a/packages/react/src/Text/Text.visual.spec.ts-snapshots/Visual-Comparison-Text-Text-Anti-Aliasing-Off-1-linux.png b/packages/react/src/Text/Text.visual.spec.ts-snapshots/Visual-Comparison-Text-Text-Anti-Aliasing-Off-1-linux.png new file mode 100644 index 000000000..b7d613337 Binary files /dev/null and b/packages/react/src/Text/Text.visual.spec.ts-snapshots/Visual-Comparison-Text-Text-Anti-Aliasing-Off-1-linux.png differ