Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .storybook/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export default {
'@storybook/addon-docs',
'@storybook/addon-vitest',
'storybook-addon-tag-badges',
'storybook-font-inspector',
],

docs: {
Expand Down
20 changes: 20 additions & 0 deletions .storybook/preview-head.html
Original file line number Diff line number Diff line change
@@ -1,3 +1,23 @@
<script>
window.global = window;
</script>

<!-- Storybook default: `padding: 1rem` on `.sb-show-main.sb-main-padded` (base-preview-head.html).
- Main canvas: CFPB 15px ($space-sm).
- Nested "All viewports" iframes (`?responsivePreview=off`): no body padding so story content
lines up with the viewport labels outside the iframe. -->
<script>
(function applyCfpbStorybookCanvasPadding() {
var style = document.createElement('style');
if (new URLSearchParams(window.location.search).get('responsivePreview') === 'off') {
style.textContent =
'.sb-show-main.sb-main-padded { padding: 0 !important; }' +
'.sb-show-main.sb-main-centered #storybook-root { padding: 0 !important; }';
} else {
style.textContent =
'.sb-show-main.sb-main-padded { padding: 15px; }' +
'.sb-show-main.sb-main-centered #storybook-root { padding: 15px; }';
}
document.head.appendChild(style);
})();
</script>
129 changes: 91 additions & 38 deletions .storybook/preview.js
Original file line number Diff line number Diff line change
@@ -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',
Expand Down Expand Up @@ -63,26 +67,31 @@ 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 rootStyle = win?.getComputedStyle(storyRoot);
const bodyStyle = win?.getComputedStyle(body);
const rootVerticalPadding =
parseFloat(rootStyle?.paddingTop ?? '0') +
parseFloat(rootStyle?.paddingBottom ?? '0');
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). Always add body vertical padding when
// `.sb-main-padded` is active — omitting it sizes the iframe too short and clips (e.g. links).
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 + rootVerticalPadding + bodyVerticalPadding,
);
}
}

return Math.max(
Expand All @@ -94,12 +103,12 @@ 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));
// No large minimum — small atoms (radio, label) should hug content. Floor avoids 0 during load.
setHeight(Math.max(Math.ceil(measuredHeight), 1));
}, []);

return React.createElement('iframe', {
Expand Down Expand Up @@ -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,
},
Expand All @@ -149,9 +162,9 @@ const renderResponsivePreviews = (Story, context) => {
style: {
boxSizing: 'border-box',
display: 'grid',
gap: '24px',
gap: '45px',
overflowX: 'auto',
padding: '24px',
padding: '30px',
},
},
responsivePreviewOptions.map(([key, viewport]) =>
Expand All @@ -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}`,
Expand All @@ -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 `<Canvas>`.
*
* 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',
Expand All @@ -203,19 +252,23 @@ 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,

initialGlobals,

parameters: {
// Default canvas padding matches Overview `<Canvas>` (`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,
},
Expand Down
2 changes: 1 addition & 1 deletion .storybook/themeCFPB.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Binary file not shown.
Binary file not shown.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
67 changes: 46 additions & 21 deletions src/assets/styles/_shared.scss
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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,
Expand All @@ -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.
Expand Down
20 changes: 11 additions & 9 deletions src/components/Footer/back-to-top.tsx
Original file line number Diff line number Diff line change
@@ -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 => (
<Link label="Back to top"
isButton
className='a-btn--secondary a-btn--full-on-xs o-footer__top-button u-show-on-mobile u-mb45'
data-gtm_ignore='true'
data-js-hook='behavior_return-to-top'
data-testid='back-to-top'
href='#'
iconRight='arrow-up'/>
<Link
label='Back to top'
isButton
className='a-btn--secondary a-btn--full-on-xs o-footer__top-button u-show-on-mobile u-mb45'
data-gtm_ignore='true'
data-js-hook='behavior_return-to-top'
data-testid='back-to-top'
href='#'
iconRight='arrow-up'
/>
);
Loading
Loading