diff --git a/.storybook/main.js b/.storybook/main.js index a8c71dd3e6..4a60775617 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -11,6 +11,7 @@ export default { '@storybook/addon-docs', '@storybook/addon-vitest', 'storybook-addon-tag-badges', + 'storybook-font-inspector', ], docs: { diff --git a/.storybook/preview-head.html b/.storybook/preview-head.html index 05da1e9dfb..dfe615d866 100644 --- a/.storybook/preview-head.html +++ b/.storybook/preview-head.html @@ -1,3 +1,23 @@ + + + \ No newline at end of file diff --git a/.storybook/preview.js b/.storybook/preview.js index 25339fa079..ba05fa65a8 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -1,15 +1,19 @@ import React from 'react'; -import '@fontsource-variable/source-sans-3'; import '../src/assets/styles/_shared.scss'; import themeCFPB from './themeCFPB'; const responsivePreviewQueryParameter = 'responsivePreview'; +// Match CFPB design-system breakpoints (16px root): large layout from 63.8125em (~1021px). +// Hero `.m-hero__wrapper` uses min-height + em padding there; 1230px aligns with typical +// max-width + gutters. Storybook also accepts ad-hoc sizes via globals.viewport.value like +// `1230-900` (width-height, px by default) without adding an entry here. const viewportOptions = { desktop: { name: 'Desktop (901px and above)', styles: { - width: '1280px', + // Match design width; iframe uses content-box so border does not shrink the inner viewport. + width: '1230px', height: '900px', }, type: 'desktop', @@ -34,6 +38,10 @@ const viewportOptions = { const responsivePreviewOptions = Object.entries(viewportOptions); +/** Extra height on nested All-viewports iframes so :focus-visible rings are not clipped (outline + * does not affect layout / scrollHeight). Covers checkbox, large target, fieldset, select, etc. */ +const RESPONSIVE_PREVIEW_FOCUS_VERTICAL_BUFFER_PX = 40; + const shouldRenderSinglePreview = (context) => { const searchParameters = new URLSearchParams(globalThis.location.search); @@ -63,26 +71,26 @@ const getFrameHeight = (frame) => { const { body, documentElement } = frameDocument; const storyRoot = frameDocument.getElementById('storybook-root'); - if (storyRoot) { - const bodyStyles = frame.contentWindow?.getComputedStyle(body); - const verticalPadding = - parseFloat(bodyStyles?.paddingTop ?? '0') + - parseFloat(bodyStyles?.paddingBottom ?? '0'); - const rootTop = storyRoot.getBoundingClientRect().top; - const contentBottom = Array.from(storyRoot.querySelectorAll('*')).reduce( - (bottom, element) => - Math.max(bottom, element.getBoundingClientRect().bottom), - storyRoot.getBoundingClientRect().bottom, + if (storyRoot && body) { + const win = frame.contentWindow; + const bodyStyle = win?.getComputedStyle(body); + const bodyVerticalPadding = + parseFloat(bodyStyle?.paddingTop ?? '0') + + parseFloat(bodyStyle?.paddingBottom ?? '0'); + + // Prefer #storybook-root box for content height. `body.scrollHeight` alone often tracks the + // iframe’s current height (min-height / 100% chains). Add body vertical padding when the body + // has inset (single canvas). Do not add #storybook-root padding again — offsetHeight / + // scrollHeight already include it when `box-sizing: border-box` (nested All viewports). + const fromRoot = Math.max( + storyRoot.scrollHeight, + storyRoot.offsetHeight, + storyRoot.getBoundingClientRect().height, ); - return Math.ceil( - Math.max( - storyRoot.getBoundingClientRect().height, - storyRoot.scrollHeight, - storyRoot.offsetHeight, - contentBottom - rootTop, - ) + verticalPadding, - ); + if (fromRoot > 0) { + return Math.ceil(fromRoot + bodyVerticalPadding); + } } return Math.max( @@ -94,12 +102,13 @@ const getFrameHeight = (frame) => { }; const ResponsivePreviewFrame = ({ storyId, viewport }) => { - const [height, setHeight] = React.useState(240); + const [height, setHeight] = React.useState(64); const updateHeight = React.useCallback((frame) => { const measuredHeight = getFrameHeight(frame); - - setHeight(Math.max(measuredHeight + 16, 160)); + const padded = measuredHeight + RESPONSIVE_PREVIEW_FOCUS_VERTICAL_BUFFER_PX; + // Floor avoids 0 during load; buffer leaves room for focus outlines outside the layout box. + setHeight(Math.max(Math.ceil(padded), 1)); }, []); return React.createElement('iframe', { @@ -130,8 +139,12 @@ const ResponsivePreviewFrame = ({ storyId, viewport }) => { title: `${viewport.name} preview`, style: { background: 'white', - border: '1px solid #d0d0ce', - boxSizing: 'border-box', + // set border around the iframe + border: 'none', + // border-box would make width include the border, so a 900px frame only has ~898px for + // the document — content-box keeps viewport.styles.width as the iframe layout width. + boxSizing: 'content-box', + display: 'block', height, width: viewport.styles.width, }, @@ -149,9 +162,9 @@ const renderResponsivePreviews = (Story, context) => { style: { boxSizing: 'border-box', display: 'grid', - gap: '24px', + gap: '45px', overflowX: 'auto', - padding: '24px', + padding: '0px', }, }, responsivePreviewOptions.map(([key, viewport]) => @@ -161,19 +174,16 @@ const renderResponsivePreviews = (Story, context) => { key, style: { display: 'grid', - gap: '8px', + // gap: '15px', justifyItems: 'start', }, }, React.createElement( - 'div', + 'p', { style: { - color: '#5a5d61', - fontFamily: 'Source Sans 3 Variable, sans-serif', - fontSize: '14px', - fontWeight: 600, - lineHeight: 1.25, + color: '#43484e', + fontWeight: 500, }, }, `${viewport.name}`, @@ -187,6 +197,45 @@ const renderResponsivePreviews = (Story, context) => { ); }; +/** Storybook body classes applied by `parameters.layout` (see prepareForStory / WebView). */ +const STORYBOOK_LAYOUT_BODY_CLASSES = [ + 'sb-main-padded', + 'sb-main-centered', + 'sb-main-fullscreen', +]; + +/** + * Only force `sb-main-fullscreen` when a story opts in with `parameters.layout: 'fullscreen'`. + * Global `layout: 'fullscreen'` in preview was removed because it merges into docs ``. + * + * For the default (undefined / `padded`), Storybook already applies `sb-main-padded` — the same + * ~1rem inset as Overview / autodocs previews. Do not override that here. + * + * @type {(Story: any, context: any) => import('react').ReactElement} + */ +const withExplicitFullscreenStoryCanvas = (Story, context) => { + React.useLayoutEffect(() => { + if (context.viewMode !== 'story') { + return undefined; + } + + if (context.parameters?.layout !== 'fullscreen') { + return undefined; + } + + const { body } = document; + + for (const className of STORYBOOK_LAYOUT_BODY_CLASSES) { + body.classList.remove(className); + } + + body.classList.add('sb-main-fullscreen'); + return undefined; + }, [context.viewMode, context.id, context.parameters?.layout]); + + return React.createElement(Story); +}; + export const globalTypes = { responsivePreview: { name: 'Responsive preview', @@ -203,12 +252,14 @@ export const globalTypes = { export const initialGlobals = { responsivePreview: 'single', - viewport: { - value: 'responsive', - }, + // Omit `globals.viewport` so the toolbar defaults to "Reset viewport" (full canvas). + // Setting `value: 'desktop'` (or any named key) forces that preset for every story. }; -export const decorators = [renderResponsivePreviews]; +export const decorators = [ + renderResponsivePreviews, + withExplicitFullscreenStoryCanvas, +]; export const preview = { globalTypes, @@ -216,6 +267,11 @@ export const preview = { initialGlobals, parameters: { + // Default canvas padding matches Overview `` (`sb-main-padded`, 1rem). Stories that + // need edge-to-edge can set `parameters.layout: 'fullscreen'` (see + // `withExplicitFullscreenStoryCanvas`). + // https://storybook.js.org/docs/configure/story-layout + viewport: { options: viewportOptions, }, diff --git a/.storybook/themeCFPB.js b/.storybook/themeCFPB.js index de06a276b3..9b52357bf1 100644 --- a/.storybook/themeCFPB.js +++ b/.storybook/themeCFPB.js @@ -24,7 +24,7 @@ export default create({ brandUrl: 'https://cfpb.github.io/design-system-react/', brandTarget: '_blank', - fontBase: '"Source Sans 3 Variable", Arial ,sans-serif', + fontBase: '"Source Sans 3 Variable", Arial, sans-serif', // App appBorderColor: colors.gray, diff --git a/.yarn/cache/@storybook-icons-npm-1.6.0-b280501ee8-f9036ca3b0.zip b/.yarn/cache/@storybook-icons-npm-1.6.0-b280501ee8-f9036ca3b0.zip new file mode 100644 index 0000000000..0ebe4f4b43 Binary files /dev/null and b/.yarn/cache/@storybook-icons-npm-1.6.0-b280501ee8-f9036ca3b0.zip differ diff --git a/.yarn/cache/storybook-font-inspector-npm-1.1.7-5822083df7-8864b2725a.zip b/.yarn/cache/storybook-font-inspector-npm-1.1.7-5822083df7-8864b2725a.zip new file mode 100644 index 0000000000..9d9bb72907 Binary files /dev/null and b/.yarn/cache/storybook-font-inspector-npm-1.1.7-5822083df7-8864b2725a.zip differ diff --git a/package.json b/package.json index 4e24642206..047bb85dfb 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "@cfpb/cfpb-design-system": "5.3.3", "@chromatic-com/storybook": "^5.1.2", "@eslint/js": "^10.0.1", + "@fontsource-variable/source-sans-3": "5.2.9", "@nabla/vite-plugin-eslint": "^3.0.1", "@storybook/addon-a11y": "^10.3.6", "@storybook/addon-docs": "^10.3.6", @@ -113,6 +114,7 @@ "start-server-and-test": "3.0.2", "storybook": "^10.3.6", "storybook-addon-tag-badges": "^3.1.0", + "storybook-font-inspector": "^1.1.7", "stylelint": "^17.11.0", "stylelint-config-standard-scss": "^17.0.0", "typescript": "^6.0.3", diff --git a/src/assets/styles/_shared.scss b/src/assets/styles/_shared.scss index 04aee7bb28..49f1dd6950 100644 --- a/src/assets/styles/_shared.scss +++ b/src/assets/styles/_shared.scss @@ -1,5 +1,6 @@ @use './variables'; @use 'sass:math'; +@use 'sass:string'; @use '@cfpb/cfpb-design-system/src/index' as *; @use '@cfpb/cfpb-design-system/src/elements/abstracts' as *; @use '@cfpb/cfpb-design-system/src/elements/base' as base; @@ -11,20 +12,45 @@ */ /** -/* Storybook components are not rendered within a CFPB layout so -/* the DS' max-width restriction for text content is not applied. -/* -/* https://github.com/cfpb/design-system/blob/main/packages/cfpb-layout/src/cfpb-layout.scss#L210-L223 -/* -/* A similar adjustment was made for the DS docs pages -/* https://github.com/cfpb/design-system/pull/1220 -**/ -:root, + * Source Sans 3 is loaded via @fontsource in app / Storybook preview entrypoints. + * + * DS `custom-props` sets `--font-stack-branded: initial`, so `var(--font-stack)` falls back to + * `system-ui`. Other DS chunks can repeat that `:root` block when code-split; in dev the last + * chunk can win and wipe a non-`!important` override. Pin both tokens with `!important` so inputs, + * buttons, and body always match. + * + * `html` / `body` / Storybook roots also get an explicit stack so we are not only relying on + * custom properties for the main canvas. + */ +// Literal stack (quoted multi-word family) — matches app / design-system expectations. +$_ds-font-stack: string.unquote('"Source Sans 3 Variable", Arial, sans-serif'); + +:root { + --font-stack-branded: #{$_ds-font-stack} !important; + --font-stack: #{$_ds-font-stack} !important; +} + +// `!important` beats DS normalize `html { font-family: sans-serif }` if chunk +// order ever places that rule after this file (e.g. code-splitting in dev). +html, +body, #storybook-root, #storybook-docs { - --font-stack: 'Source Sans 3 Variable', 'Source Sans Pro', 'Arial', sans-serif; - font-family: var(--font-stack); + font-family: #{$_ds-font-stack} !important; +} +/** + * Long-form / layout simulation for the Docs tab only. + * + * Do not target `#storybook-root` here: the story canvas should match production + * (DS + component CSS only). Rules like paragraph max-width were affecting heroes + * and other components inside the iframe. + * + * DS max-width context: + * https://github.com/cfpb/design-system/blob/main/packages/cfpb-layout/src/cfpb-layout.scss#L210-L223 + * https://github.com/cfpb/design-system/pull/1220 + */ +#storybook-docs { dd, dt, label, @@ -41,23 +67,22 @@ font-size: 16px; } - /* - Make code block plain-text readable (workaround since we can't change code highlighting theme) + /* + Make code block plain-text readable (workaround since we can't change code highlighting theme) https://github.com/storybookjs/storybook/issues/9641 */ pre { background-color: #2b2b2b; color: white; + line-height: 22px !important; } -} -/* -Apply a global line-height that doesn't interfere with component Stories -that have their own line-height settings. -*/ -pre, -#storybook-docs :where(p:not(.sb-story *)) { - line-height: 22px !important; + /* + Readable docs prose line-height (avoid touching `.sb-story` embeds). + */ + :where(p:not(.sb-story *)) { + line-height: 22px !important; + } } // Override DS wrapper match-content width to 1170px content with 30px gutters. diff --git a/src/components/Footer/back-to-top.tsx b/src/components/Footer/back-to-top.tsx index bc9baeff9e..2861c4b2b0 100644 --- a/src/components/Footer/back-to-top.tsx +++ b/src/components/Footer/back-to-top.tsx @@ -1,13 +1,15 @@ import { JSX } from 'react'; -/* eslint-disable jsx-a11y/anchor-is-valid */ + import Link from '../Link/link'; export const BackToTop = (): JSX.Element => ( - + ); diff --git a/src/components/Headings/headings.stories.tsx b/src/components/Headings/headings.stories.tsx index 23be2bc455..1a54e83e33 100644 --- a/src/components/Headings/headings.stories.tsx +++ b/src/components/Headings/headings.stories.tsx @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; import { Heading } from '~/src/index'; - +import { expect, within } from 'storybook/test'; /** * A successful type hierarchy establishes the order of importance of elements on a page. Consistent scaling, weights, and capitalization are used to create distinction between headings and provide users with familiar focus points when scanning text. * @@ -76,6 +76,20 @@ export const Eyebrow: Story = {
Heading 1
), + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const textElement = canvas.getByText(/Heading 1/i); + + // Wait for all fonts in the document to finish loading + await document.fonts.ready; + // Check if your specific font is loaded and active + const isFontLoaded = document.fonts.check('16px "Source Sans 3 Variable"'); + // Assert that the font is correctly loaded + await expect(isFontLoaded).toBe(true); + // Alternative: Assert the computed style of the element + const computedStyle = globalThis.getComputedStyle(textElement); + await expect(computedStyle.fontFamily).toContain('Source Sans 3 Variable'); + }, }; export const Slug: Story = { diff --git a/yarn.lock b/yarn.lock index eab237f7d3..4e9fa1fa9c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1247,6 +1247,7 @@ __metadata: "@cfpb/cfpb-design-system": "npm:5.3.3" "@chromatic-com/storybook": "npm:^5.1.2" "@eslint/js": "npm:^10.0.1" + "@fontsource-variable/source-sans-3": "npm:5.2.9" "@nabla/vite-plugin-eslint": "npm:^3.0.1" "@storybook/addon-a11y": "npm:^10.3.6" "@storybook/addon-docs": "npm:^10.3.6" @@ -1303,6 +1304,7 @@ __metadata: start-server-and-test: "npm:3.0.2" storybook: "npm:^10.3.6" storybook-addon-tag-badges: "npm:^3.1.0" + storybook-font-inspector: "npm:^1.1.7" stylelint: "npm:^17.11.0" stylelint-config-standard-scss: "npm:^17.0.0" typescript: "npm:^6.0.3" @@ -3031,6 +3033,16 @@ __metadata: languageName: node linkType: hard +"@storybook/icons@npm:^1.4.0": + version: 1.6.0 + resolution: "@storybook/icons@npm:1.6.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + checksum: 10/f9036ca3b0d2904778eb4e202305f2780b549434380f9760f0bc704fe3ee19d7332f9560a66435ebb2156346cee9a863e40fa5e4b27790bf993b0c1180a3146d + languageName: node + linkType: hard + "@storybook/icons@npm:^2.0.1": version: 2.0.2 resolution: "@storybook/icons@npm:2.0.2" @@ -10376,6 +10388,17 @@ __metadata: languageName: node linkType: hard +"storybook-font-inspector@npm:^1.1.7": + version: 1.1.7 + resolution: "storybook-font-inspector@npm:1.1.7" + dependencies: + "@storybook/icons": "npm:^1.4.0" + peerDependencies: + storybook: ^9.0.0 + checksum: 10/8864b2725a17432926d36384357cc7050db6e9efb2085528167b0f7c1534e96931215896a302852b0de814cd924e556436f0580c4116fa0e56d64c7403d12364 + languageName: node + linkType: hard + "storybook@npm:^10.3.6": version: 10.3.6 resolution: "storybook@npm:10.3.6"