From c0425ff7d0b9572f7551f114133fd0302980815f Mon Sep 17 00:00:00 2001 From: Josh Black Date: Wed, 7 Aug 2024 10:57:48 -0500 Subject: [PATCH] refactor(Blankslate): add support for css modules to Blankslate (#4810) * test(e2e): add snapshots for Blankslate * test(e2e): add support for feature flags in globals * refactor(Blankslate): add support for css modules to Blankslate * chore: remove local snapshots * test(e2e): add fallback snapshots for styled-components * chore: add changeset * fix(stylelint): address stylelint errors * chore: fix export size script * chore: fix feature flag on action * test(vrt): update snapshots * test(vrt): update snapshots * test(e2e): remove styled-components snapshots * refactor(e2e): update snapshots to point to same image --------- Co-authored-by: Josh Black --- .changeset/tough-pans-punch.md | 5 + .stylelintignore | 8 +- e2e/components/Blankslate.test.ts | 55 ++++++++- e2e/test-helpers/storybook.ts | 37 +++++- packages/react/script/get-export-sizes.js | 26 +++-- .../src/Blankslate/Blankslate.module.css | 109 ++++++++++++++++++ packages/react/src/Blankslate/Blankslate.tsx | 59 +++++++++- 7 files changed, 275 insertions(+), 24 deletions(-) create mode 100644 .changeset/tough-pans-punch.md create mode 100644 packages/react/src/Blankslate/Blankslate.module.css diff --git a/.changeset/tough-pans-punch.md b/.changeset/tough-pans-punch.md new file mode 100644 index 00000000000..5fd7e66a3b6 --- /dev/null +++ b/.changeset/tough-pans-punch.md @@ -0,0 +1,5 @@ +--- +'@primer/react': minor +--- + +Update Blankslate component to use CSS Modules behind a feature flag diff --git a/.stylelintignore b/.stylelintignore index c4649bdcba0..2556ce3912b 100644 --- a/.stylelintignore +++ b/.stylelintignore @@ -1,5 +1,5 @@ docs/public/**/*.css -lib-esm/**/*.css -lib/**/*.css -dist/**/*.css -.next/**/*.css +**/lib-esm/**/*.css +**/lib/**/*.css +**/dist/**/*.css +**/.next/**/*.css diff --git a/e2e/components/Blankslate.test.ts b/e2e/components/Blankslate.test.ts index 49ff3f2a4cd..796af59762d 100644 --- a/e2e/components/Blankslate.test.ts +++ b/e2e/components/Blankslate.test.ts @@ -51,6 +51,24 @@ test.describe('Blankslate', () => { id: story.id, globals: { colorScheme: theme, + featureFlags: { + primer_react_css_modules: true, + }, + }, + }) + + // Default state + expect(await page.screenshot()).toMatchSnapshot(`Blankslate.${story.title}.${theme}.png`) + }) + + test('default (styled-components) @vrt', async ({page}) => { + await visit(page, { + id: story.id, + globals: { + colorScheme: theme, + featureFlags: { + primer_react_css_modules: false, + }, }, }) @@ -59,6 +77,19 @@ test.describe('Blankslate', () => { }) test('axe @aat', async ({page}) => { + await visit(page, { + id: story.id, + globals: { + colorScheme: theme, + featureFlags: { + primer_react_css_modules: true, + }, + }, + }) + await expect(page).toHaveNoViolations() + }) + + test('axe (styled-components) @aat', async ({page}) => { await visit(page, { id: story.id, globals: { @@ -75,7 +106,29 @@ test.describe('Blankslate', () => { test(`${name} @vrt`, async ({page}) => { await visit(page, { id: story.id, - globals: {}, + globals: { + featureFlags: { + primer_react_css_modules: true, + }, + }, + }) + const width = viewports[name] + + await page.setViewportSize({ + width, + height: 667, + }) + expect(await page.screenshot()).toMatchSnapshot(`Blankslate.${story.title}.${name}.png`) + }) + + test(`${name} (styled-components) @vrt`, async ({page}) => { + await visit(page, { + id: story.id, + globals: { + featureFlags: { + primer_react_css_modules: false, + }, + }, }) const width = viewports[name] diff --git a/e2e/test-helpers/storybook.ts b/e2e/test-helpers/storybook.ts index 1fb1110d307..cd07cbb4629 100644 --- a/e2e/test-helpers/storybook.ts +++ b/e2e/test-helpers/storybook.ts @@ -1,10 +1,18 @@ import type {Page} from '@playwright/test' import {waitForImages} from './waitForImages' +type Value = + | string + | boolean + | number + | { + [Key: string]: Value + } + interface Options { id: string args?: Record - globals?: Record + globals?: Record } const {STORYBOOK_URL = 'http://localhost:6006'} = process.env @@ -28,9 +36,13 @@ export async function visit(page: Page, options: Options) { let params = '' for (const [key, value] of Object.entries(globals)) { if (params !== '') { - params += '&' + params += ';' + } + if (typeof value === 'object') { + params += serializeObject(value, key) + } else { + params += `${key}:${value}` } - params += `${key}:${value}` } url.searchParams.set('globals', params) } @@ -46,3 +58,22 @@ export async function visit(page: Page, options: Options) { await waitForImages(page) } + +function serializeObject(object: T, parentPath: string): string { + return Object.entries(object) + .map(([key, value]) => { + if (typeof value === 'object') { + return serializeObject(value, `${parentPath}.${key}`) + } + return `${parentPath}.${key}:${serialize(value)}` + }) + .join(';') +} + +function serialize(value: Value): string { + if (typeof value === 'boolean') { + return `!${value}` + } + + return `${value}` +} diff --git a/packages/react/script/get-export-sizes.js b/packages/react/script/get-export-sizes.js index d51a2735615..5079553cda4 100644 --- a/packages/react/script/get-export-sizes.js +++ b/packages/react/script/get-export-sizes.js @@ -11,6 +11,19 @@ const {rollup} = require('rollup') const {minify} = require('terser') const gzipSize = require('gzip-size') +const noopCSSModules = { + name: 'empty-css-modules', + + transform(_code, id) { + if (!id.endsWith('.css')) { + return + } + return { + code: `export default {}`, + } + }, +} + async function main() { const rootDirectory = path.resolve(__dirname, '..') const packageJsonPath = path.join(rootDirectory, 'package.json') @@ -41,6 +54,7 @@ async function main() { commonjs({ include: [/node_modules/], }), + noopCSSModules, ], onwarn: () => {}, }) @@ -63,17 +77,7 @@ async function main() { commonjs({ include: /node_modules/, }), - { - name: 'empty-css-modules', - transform(_code, id) { - if (!id.endsWith('.css')) { - return - } - return { - code: `export default {}`, - } - }, - }, + noopCSSModules, virtual({ __entrypoint__: `export { ${identifier} } from '${filepath}';`, }), diff --git a/packages/react/src/Blankslate/Blankslate.module.css b/packages/react/src/Blankslate/Blankslate.module.css new file mode 100644 index 00000000000..0f20d8255cc --- /dev/null +++ b/packages/react/src/Blankslate/Blankslate.module.css @@ -0,0 +1,109 @@ +.Container { + container: blankslate / inline-size; +} + +.Blankslate { + --blankslate-outer-padding-block: var(--base-size-32, 2rem); + --blankslate-outer-padding-inline: var(--base-size-32, 2rem); + + display: grid; + justify-items: center; + /* stylelint-disable-next-line primer/spacing */ + padding: var(--blankslate-outer-padding-block) var(--blankslate-outer-padding-inline); +} + +.Blankslate[data-spacious='true'] { + --blankslate-outer-padding-block: var(--base-size-80, 5rem); + --blankslate-outer-padding-inline: var(--base-size-40, 2.5rem); +} + +.Blankslate[data-border='true'] { + border: var(--borderWidth-thin) solid var(--borderColor-default); + border-radius: var(--borderRadius-medium); +} + +.Blankslate[data-narrow='true'] { + max-width: 485px; + margin: 0 auto; +} + +.Heading, +.Description { + margin: 0; + /* stylelint-disable-next-line primer/spacing */ + margin-bottom: var(--stack-gap-condensed, var(--base-size-8)); +} + +.Heading { + font-size: var(--text-title-size-medium, 1.25rem); + font-weight: var(--text-title-weight-medium, 600); +} + +.Description { + font-size: var(--text-body-size-large, 1rem); + line-height: var(--text-body-lineHeight-large, 1.5); + color: var(--fgColor-muted); +} + +.Action { + /* stylelint-disable-next-line primer/spacing */ + margin-top: var(--stack-gap-normal, var(--base-size-16)); +} + +.Action:first-of-type { + /* stylelint-disable-next-line primer/spacing */ + margin-top: var(--stack-gap-spacious, var(--base-size-24)); +} + +.Action:last-of-type { + /* stylelint-disable-next-line primer/spacing */ + margin-bottom: var(--stack-gap-condensed, var(--base-size-8)); +} + +/* At the time these styles were written, 34rem was our "small" breakpoint width */ +/* stylelint-disable-next-line plugin/no-unsupported-browser-features */ +@container blankslate (max-width: 34rem) { + .Blankslate { + --blankslate-outer-padding-block: var(--base-size-20); + --blankslate-outer-padding-inline: var(--base-size-20); + } + + .Blankslate[data-spacious='true'] { + --blankslate-outer-padding-block: var(--base-size-44); + --blankslate-outer-padding-inline: var(--base-size-28); + } + + .Visual { + max-width: var(--base-size-24); + /* stylelint-disable-next-line primer/spacing */ + margin-bottom: var(--stack-gap-condensed, var(--base-size-8)); + } + + /* stylelint-disable-next-line selector-max-type */ + .Visual svg { + width: 100%; + } + + .Heading { + font-size: var(--text-title-size-small); + } + + .Description { + font-size: var(--text-body-size-medium); + } + + .Action { + /* stylelint-disable-next-line primer/spacing */ + margin-top: var(--stack-gap-condensed, var(--base-size-8)); + } + + .Action:first-of-type { + /* stylelint-disable-next-line primer/spacing */ + margin-top: var(--stack-gap-normal, var(--base-size-16)); + } + + .Action:last-of-type { + /* stylelint-disable-next-line primer/spacing */ + margin-bottom: calc(var(--stack-gap-condensed, var(--base-size-8)) / 2); + } +} diff --git a/packages/react/src/Blankslate/Blankslate.tsx b/packages/react/src/Blankslate/Blankslate.tsx index 29126a5e36e..4a2738cce9e 100644 --- a/packages/react/src/Blankslate/Blankslate.tsx +++ b/packages/react/src/Blankslate/Blankslate.tsx @@ -1,9 +1,12 @@ +import cx from 'clsx' import React from 'react' import Box from '../Box' import {Button} from '../Button' import Link from '../Link' import {get} from '../constants' import styled from 'styled-components' +import classes from './Blankslate.module.css' +import {useFeatureFlag} from '../FeatureFlags' export type BlankslateProps = React.PropsWithChildren<{ /** @@ -124,6 +127,18 @@ const BlankslateContainerQuery = ` ` function Blankslate({border, children, narrow, spacious}: BlankslateProps) { + const enabled = useFeatureFlag('primer_react_css_modules') + + if (enabled) { + return ( +
+
+ {children} +
+
+ ) + } + return ( <> {/* @@ -143,7 +158,16 @@ function Blankslate({border, children, narrow, spacious}: BlankslateProps) { export type VisualProps = React.PropsWithChildren function Visual({children}: VisualProps) { - return {children} + const enabled = useFeatureFlag('primer_react_css_modules') + return ( + + {children} + + ) } export type HeadingProps = React.PropsWithChildren<{ @@ -151,8 +175,14 @@ export type HeadingProps = React.PropsWithChildren<{ }> function Heading({as = 'h2', children}: HeadingProps) { + const enabled = useFeatureFlag('primer_react_css_modules') return ( - + {children} ) @@ -161,7 +191,16 @@ function Heading({as = 'h2', children}: HeadingProps) { export type DescriptionProps = React.PropsWithChildren function Description({children}: DescriptionProps) { - return

{children}

+ const enabled = useFeatureFlag('primer_react_css_modules') + return ( +

+ {children} +

+ ) } export type PrimaryActionProps = React.PropsWithChildren<{ @@ -169,8 +208,13 @@ export type PrimaryActionProps = React.PropsWithChildren<{ }> function PrimaryAction({children, href}: PrimaryActionProps) { + const enabled = useFeatureFlag('primer_react_css_modules') return ( -
+
@@ -183,8 +227,13 @@ export type SecondaryActionProps = React.PropsWithChildren<{ }> function SecondaryAction({children, href}: SecondaryActionProps) { + const enabled = useFeatureFlag('primer_react_css_modules') return ( -
+
{children}
)