diff --git a/MIGRATION.md b/MIGRATION.md
index 96ae5db1d8f5..49708f886919 100644
--- a/MIGRATION.md
+++ b/MIGRATION.md
@@ -3,6 +3,7 @@
- [From version 6.0.x to 6.1.0](#from-version-60x-to-610)
- [6.1 deprecations](#61-deprecations)
- [Deprecated onBeforeRender](#deprecated-onbeforerender)
+ - [Deprecated grid parameter](#deprecated-grid-parameter)
- [From version 5.3.x to 6.0.x](#from-version-53x-to-60x)
- [Hoisted CSF annotations](#hoisted-csf-annotations)
- [Zero config typescript](#zero-config-typescript)
@@ -142,6 +143,37 @@ The `@storybook/addon-docs` previously accepted a `jsx` option called `onBeforeR
We've renamed it `transformSource` and also allowed it to receive the `StoryContext` in case source rendering requires additional information.
+#### Deprecated grid parameter
+
+Previously when using `@storybook/addon-backgrounds` if you wanted to customize the grid, you would define a parameter like this:
+
+```js
+export const Basic = () =>
+Basic.parameters: {
+ grid: {
+ cellSize: 10
+ }
+},
+```
+
+As grid is not an addon, but rather backgrounds is, the grid configuration was moved to be inside `backgrounds` parameter instead. Also, there are new properties that can be used to further customize the grid. Here's an example with the default values:
+
+```js
+export const Basic = () =>
+Basic.parameters: {
+ backgrounds: {
+ grid: {
+ disable: false,
+ cellSize: 20,
+ opacity: 0.5,
+ cellAmount: 5,
+ offsetX: 16, // default is 0 if story has 'fullscreen' layout, 16 if layout is 'padded'
+ offsetY: 16, // default is 0 if story has 'fullscreen' layout, 16 if layout is 'padded'
+ }
+ }
+},
+```
+
## From version 5.3.x to 6.0.x
### Hoisted CSF annotations
diff --git a/addons/backgrounds/package.json b/addons/backgrounds/package.json
index 84736ef7e7ae..55943a30e511 100644
--- a/addons/backgrounds/package.json
+++ b/addons/backgrounds/package.json
@@ -39,10 +39,13 @@
"@storybook/core-events": "6.1.0-alpha.15",
"@storybook/theming": "6.1.0-alpha.15",
"core-js": "^3.0.1",
+ "global": "^4.3.2",
"memoizerific": "^1.11.3",
"react": "^16.8.3",
"react-dom": "^16.8.3",
- "regenerator-runtime": "^0.13.3"
+ "regenerator-runtime": "^0.13.3",
+ "ts-dedent": "^1.1.1",
+ "util-deprecate": "^1.0.2"
},
"devDependencies": {
"@types/webpack-env": "^1.15.2"
@@ -58,4 +61,4 @@
]
}
}
-}
+}
\ No newline at end of file
diff --git a/addons/backgrounds/src/containers/BackgroundSelector.tsx b/addons/backgrounds/src/containers/BackgroundSelector.tsx
index 7532a4c184b2..269b7fed32a7 100644
--- a/addons/backgrounds/src/containers/BackgroundSelector.tsx
+++ b/addons/backgrounds/src/containers/BackgroundSelector.tsx
@@ -1,51 +1,14 @@
-import React, { Component, Fragment, ReactElement } from 'react';
+import React, { FunctionComponent, Fragment, useCallback, useMemo, memo } from 'react';
import memoize from 'memoizerific';
-import { Combo, Consumer, API } from '@storybook/api';
-import { Global, Theme } from '@storybook/theming';
+import { useParameter, useGlobals } from '@storybook/api';
import { logger } from '@storybook/client-logger';
-
import { Icons, IconButton, WithTooltip, TooltipLinkList } from '@storybook/components';
-import { PARAM_KEY as BACKGROUNDS_PARAM_KEY, EVENTS } from '../constants';
+import { PARAM_KEY as BACKGROUNDS_PARAM_KEY } from '../constants';
import { ColorIcon } from '../components/ColorIcon';
-
-interface GlobalState {
- name: string | undefined;
- selected: string | undefined;
-}
-
-interface Props {
- api: API;
-}
-
-interface BackgroundSelectorItem {
- id: string;
- title: string;
- onClick: () => void;
- value: string;
- right?: ReactElement;
-}
-
-interface Background {
- name: string;
- value: string;
-}
-
-interface BackgroundsParameter {
- default?: string;
- disable?: boolean;
- values: Background[];
-}
-
-interface BackgroundsConfig {
- backgrounds: Background[] | null;
- selectedBackground: string | null;
- defaultBackgroundName: string | null;
- disable: boolean;
-}
-
-const iframeId = 'storybook-preview-iframe';
+import { BackgroundSelectorItem, Background, BackgroundsParameter, GlobalState } from '../types';
+import { getBackgroundColorByName } from '../helpers';
const createBackgroundSelectorItem = memoize(1000)(
(
@@ -53,7 +16,8 @@ const createBackgroundSelectorItem = memoize(1000)(
name: string,
value: string,
hasSwatch: boolean,
- change: (arg: { selected: string; name: string }) => void
+ change: (arg: { selected: string; name: string }) => void,
+ active: boolean
): BackgroundSelectorItem => ({
id: id || name,
title: name,
@@ -62,6 +26,7 @@ const createBackgroundSelectorItem = memoize(1000)(
},
value,
right: hasSwatch ? : undefined,
+ active,
})
);
@@ -72,12 +37,26 @@ const getDisplayedItems = memoize(10)(
change: (arg: { selected: string; name: string }) => void
) => {
const backgroundSelectorItems = backgrounds.map(({ name, value }) =>
- createBackgroundSelectorItem(null, name, value, true, change)
+ createBackgroundSelectorItem(
+ null,
+ name,
+ value,
+ true,
+ change,
+ value === selectedBackgroundColor
+ )
);
if (selectedBackgroundColor !== 'transparent') {
return [
- createBackgroundSelectorItem('reset', 'Clear background', 'transparent', null, change),
+ createBackgroundSelectorItem(
+ 'reset',
+ 'Clear background',
+ 'transparent',
+ null,
+ change,
+ false
+ ),
...backgroundSelectorItems,
];
}
@@ -86,131 +65,78 @@ const getDisplayedItems = memoize(10)(
}
);
-const getSelectedBackgroundColor = (
- backgrounds: Background[] = [],
- currentSelectedValue: string,
- defaultName: string
-): string => {
- if (currentSelectedValue === 'transparent') {
- return 'transparent';
- }
-
- if (backgrounds.find((background) => background.value === currentSelectedValue)) {
- return currentSelectedValue;
- }
+const DEFAULT_BACKGROUNDS_CONFIG: BackgroundsParameter = {
+ default: null,
+ disable: true,
+ values: [],
+};
- const defaultBackground = backgrounds.find((background) => background.name === defaultName);
- if (defaultBackground) {
- return defaultBackground.value;
- }
+export const BackgroundSelector: FunctionComponent = memo(() => {
+ const backgroundsConfig = useParameter(
+ BACKGROUNDS_PARAM_KEY,
+ DEFAULT_BACKGROUNDS_CONFIG
+ );
- if (defaultName) {
- const availableColors = backgrounds.map((background) => background.name).join(', ');
- logger.warn(
- `Backgrounds Addon: could not find the default color "${defaultName}".
- These are the available colors for your story based on your configuration: ${availableColors}`
- );
- }
+ const [globals, updateGlobals] = useGlobals();
- return 'transparent';
-};
+ const globalsBackgroundColor = globals[BACKGROUNDS_PARAM_KEY]?.value;
-const getBackgroundsConfig = ({ api, state }: Combo): BackgroundsConfig => {
- const backgroundsParameter = api.getCurrentParameter(BACKGROUNDS_PARAM_KEY);
- const selectedBackgroundValue = state.addons[BACKGROUNDS_PARAM_KEY] || null;
+ const selectedBackgroundColor = useMemo(() => {
+ return getBackgroundColorByName(
+ globalsBackgroundColor,
+ backgroundsConfig.values,
+ backgroundsConfig.default
+ );
+ }, [backgroundsConfig, globalsBackgroundColor]);
- if (Array.isArray(backgroundsParameter)) {
+ if (Array.isArray(backgroundsConfig)) {
logger.warn(
'Addon Backgrounds api has changed in Storybook 6.0. Please refer to the migration guide: https://github.com/storybookjs/storybook/blob/next/MIGRATION.md'
);
}
- const isBackgroundsEmpty = !backgroundsParameter?.values?.length;
- if (backgroundsParameter?.disable || isBackgroundsEmpty) {
- // other null properties are necessary to keep the same return shape for Consumer memoization
- return {
- disable: true,
- backgrounds: null,
- selectedBackground: null,
- defaultBackgroundName: null,
- };
- }
-
- return {
- disable: false,
- backgrounds: backgroundsParameter?.values,
- selectedBackground: selectedBackgroundValue,
- defaultBackgroundName: backgroundsParameter?.default,
- };
-};
-
-export class BackgroundSelector extends Component {
- change = ({ selected, name }: GlobalState) => {
- const { api } = this.props;
- if (typeof selected === 'string') {
- api.setAddonState(BACKGROUNDS_PARAM_KEY, selected);
- }
- api.emit(EVENTS.UPDATE, { selected, name });
- };
-
- render() {
- return (
-
- {({
- disable,
- backgrounds,
- selectedBackground,
- defaultBackgroundName,
- }: BackgroundsConfig) => {
- if (disable) {
- return null;
- }
+ const onBackgroundChange = useCallback(
+ (value: string) => {
+ updateGlobals({ [BACKGROUNDS_PARAM_KEY]: { ...globals[BACKGROUNDS_PARAM_KEY], value } });
+ },
+ [backgroundsConfig, globals, updateGlobals]
+ );
- const selectedBackgroundColor = getSelectedBackgroundColor(
- backgrounds,
- selectedBackground,
- defaultBackgroundName
- );
+ if (backgroundsConfig.disable) {
+ return null;
+ }
+ return (
+
+ {
return (
-
- {selectedBackgroundColor ? (
- ({
- [`#${iframeId}`]: {
- background:
- selectedBackgroundColor === 'transparent'
- ? theme.background.content
- : selectedBackgroundColor,
- },
- })}
- />
- ) : null}
- (
- {
- this.change(i);
- onHide();
- })}
- />
- )}
- >
-
-
-
-
-
+ {
+ if (selectedBackgroundColor !== selected) {
+ onBackgroundChange(selected);
+ }
+ onHide();
+ }
+ )}
+ />
);
}}
-
- );
- }
-}
+ >
+
+
+
+
+
+ );
+});
diff --git a/addons/backgrounds/src/containers/GridSelector.tsx b/addons/backgrounds/src/containers/GridSelector.tsx
index b078b540d1d5..ca97ad9ab878 100644
--- a/addons/backgrounds/src/containers/GridSelector.tsx
+++ b/addons/backgrounds/src/containers/GridSelector.tsx
@@ -1,51 +1,35 @@
import React, { FunctionComponent, memo } from 'react';
-import { useAddonState, useParameter } from '@storybook/api';
-import { Global } from '@storybook/theming';
+import { useGlobals, useParameter } from '@storybook/api';
import { Icons, IconButton } from '@storybook/components';
-import { ADDON_ID, GRID_PARAM_KEY } from '../constants';
+import { PARAM_KEY as BACKGROUNDS_PARAM_KEY } from '../constants';
-export interface BackgroundGridParameters {
- cellSize: number;
-}
+export const GridSelector: FunctionComponent = memo(() => {
+ const [globals, updateGlobals] = useGlobals();
-const iframeId = 'storybook-preview-iframe';
+ const { grid } = useParameter(BACKGROUNDS_PARAM_KEY, {
+ grid: { disable: false },
+ });
-export const GridSelector: FunctionComponent = memo(() => {
- const [state, setState] = useAddonState(`${ADDON_ID}/grid`);
- const { cellSize } = useParameter(GRID_PARAM_KEY, { cellSize: 20 });
+ if (grid.disable) {
+ return null;
+ }
+
+ const isActive = globals[BACKGROUNDS_PARAM_KEY]?.grid || false;
return (
setState(!state)}
+ active={isActive}
+ title="Apply a grid to the preview"
+ onClick={() =>
+ updateGlobals({
+ [BACKGROUNDS_PARAM_KEY]: { ...globals[BACKGROUNDS_PARAM_KEY], grid: !isActive },
+ })
+ }
>
- {state ? (
-
- ) : null}
);
});
diff --git a/addons/backgrounds/src/decorators/index.ts b/addons/backgrounds/src/decorators/index.ts
new file mode 100644
index 000000000000..cf4a28890479
--- /dev/null
+++ b/addons/backgrounds/src/decorators/index.ts
@@ -0,0 +1,2 @@
+export * from './withBackground';
+export * from './withGrid';
diff --git a/addons/backgrounds/src/decorators/withBackground.ts b/addons/backgrounds/src/decorators/withBackground.ts
new file mode 100644
index 000000000000..defad6f99c71
--- /dev/null
+++ b/addons/backgrounds/src/decorators/withBackground.ts
@@ -0,0 +1,59 @@
+import { StoryFn as StoryFunction, StoryContext, useMemo, useEffect } from '@storybook/addons';
+
+import { PARAM_KEY as BACKGROUNDS_PARAM_KEY } from '../constants';
+import { clearStyles, addBackgroundStyle, getBackgroundColorByName } from '../helpers';
+
+export const withBackground = (StoryFn: StoryFunction, context: StoryContext) => {
+ const { globals, parameters } = context;
+ const globalsBackgroundColor = globals[BACKGROUNDS_PARAM_KEY]?.value;
+ const backgroundsConfig = parameters[BACKGROUNDS_PARAM_KEY];
+
+ const selectedBackgroundColor = useMemo(() => {
+ if (backgroundsConfig.disable) {
+ return 'transparent';
+ }
+
+ return getBackgroundColorByName(
+ globalsBackgroundColor,
+ backgroundsConfig.values,
+ backgroundsConfig.default
+ );
+ }, [backgroundsConfig, globalsBackgroundColor]);
+
+ const isActive = useMemo(
+ () => selectedBackgroundColor && selectedBackgroundColor !== 'transparent',
+ [selectedBackgroundColor]
+ );
+
+ const selector =
+ context.viewMode === 'docs' ? `#anchor--${context.id} .docs-story` : '.sb-show-main';
+
+ const backgroundStyles = useMemo(() => {
+ return `
+ ${selector} {
+ background: ${selectedBackgroundColor} !important;
+ transition: background-color 0.3s;
+ }
+ `;
+ }, [selectedBackgroundColor, selector]);
+
+ useEffect(() => {
+ const selectorId =
+ context.viewMode === 'docs'
+ ? `addon-backgrounds-docs-${context.id}`
+ : `addon-backgrounds-color`;
+
+ if (!isActive) {
+ clearStyles(selectorId);
+ return;
+ }
+
+ addBackgroundStyle(
+ selectorId,
+ backgroundStyles,
+ context.viewMode === 'docs' ? context.id : null
+ );
+ }, [isActive, backgroundStyles, context]);
+
+ return StoryFn();
+};
diff --git a/addons/backgrounds/src/decorators/withGrid.ts b/addons/backgrounds/src/decorators/withGrid.ts
new file mode 100644
index 000000000000..0252c57f6a49
--- /dev/null
+++ b/addons/backgrounds/src/decorators/withGrid.ts
@@ -0,0 +1,79 @@
+import dedent from 'ts-dedent';
+import deprecate from 'util-deprecate';
+import { StoryFn as StoryFunction, StoryContext, useMemo, useEffect } from '@storybook/addons';
+
+import { clearStyles, addGridStyle } from '../helpers';
+import { PARAM_KEY as BACKGROUNDS_PARAM_KEY } from '../constants';
+
+const deprecatedCellSizeWarning = deprecate(
+ () => {},
+ dedent`
+ Backgrounds Addon: The cell size parameter has been changed.
+
+ - parameters.grid.cellSize should now be parameters.backgrounds.grid.cellSize
+ See https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#deprecated-grid-parameter
+ `
+);
+
+export const withGrid = (StoryFn: StoryFunction, context: StoryContext) => {
+ const { globals, parameters } = context;
+ const gridParameters = parameters[BACKGROUNDS_PARAM_KEY].grid;
+ const isActive = globals[BACKGROUNDS_PARAM_KEY]?.grid === true && gridParameters.disable !== true;
+ const { cellAmount, cellSize, opacity } = gridParameters;
+ const isInDocs = context.viewMode === 'docs';
+
+ let gridSize: number;
+ if (parameters.grid?.cellSize) {
+ gridSize = parameters.grid.cellSize;
+ deprecatedCellSizeWarning();
+ } else {
+ gridSize = cellSize;
+ }
+
+ const isLayoutPadded = parameters.layout === undefined || parameters.layout === 'padded';
+ // 16px offset in the grid to account for padded layout
+ const defaultOffset = isLayoutPadded ? 16 : 0;
+ const offsetX = gridParameters.offsetX || isInDocs ? 20 : defaultOffset;
+ const offsetY = gridParameters.offsetY || isInDocs ? 20 : defaultOffset;
+
+ const gridStyles = useMemo(() => {
+ const selector =
+ context.viewMode === 'docs' ? `#anchor--${context.id} .docs-story` : '.sb-show-main';
+
+ const backgroundSize = [
+ `${gridSize * cellAmount}px ${gridSize * cellAmount}px`,
+ `${gridSize * cellAmount}px ${gridSize * cellAmount}px`,
+ `${gridSize}px ${gridSize}px`,
+ `${gridSize}px ${gridSize}px`,
+ ].join(', ');
+
+ return `
+ ${selector} {
+ background-size: ${backgroundSize} !important;
+ background-position: ${offsetX}px ${offsetY}px, ${offsetX}px ${offsetY}px, ${offsetX}px ${offsetY}px, ${offsetX}px ${offsetY}px !important;
+ background-blend-mode: difference !important;
+ background-image: linear-gradient(rgba(130, 130, 130, ${opacity}) 1px, transparent 1px),
+ linear-gradient(90deg, rgba(130, 130, 130, ${opacity}) 1px, transparent 1px),
+ linear-gradient(rgba(130, 130, 130, ${opacity / 2}) 1px, transparent 1px),
+ linear-gradient(90deg, rgba(130, 130, 130, ${
+ opacity / 2
+ }) 1px, transparent 1px) !important;
+ }
+ `;
+ }, [gridSize]);
+
+ useEffect(() => {
+ const selectorId =
+ context.viewMode === 'docs'
+ ? `addon-backgrounds-grid-docs-${context.id}`
+ : `addon-backgrounds-grid`;
+ if (!isActive) {
+ clearStyles(selectorId);
+ return;
+ }
+
+ addGridStyle(selectorId, gridStyles);
+ }, [isActive, gridStyles, context]);
+
+ return StoryFn();
+};
diff --git a/addons/backgrounds/src/helpers/index.ts b/addons/backgrounds/src/helpers/index.ts
new file mode 100644
index 000000000000..6fead3837610
--- /dev/null
+++ b/addons/backgrounds/src/helpers/index.ts
@@ -0,0 +1,86 @@
+import { document } from 'global';
+import dedent from 'ts-dedent';
+
+import { logger } from '@storybook/client-logger';
+
+import { Background } from '../types';
+
+export const getBackgroundColorByName = (
+ currentSelectedValue: string,
+ backgrounds: Background[] = [],
+ defaultName: string
+): string => {
+ if (currentSelectedValue === 'transparent') {
+ return 'transparent';
+ }
+
+ if (backgrounds.find((background) => background.value === currentSelectedValue)) {
+ return currentSelectedValue;
+ }
+
+ const defaultBackground = backgrounds.find((background) => background.name === defaultName);
+ if (defaultBackground) {
+ return defaultBackground.value;
+ }
+
+ if (defaultName) {
+ const availableColors = backgrounds.map((background) => background.name).join(', ');
+ logger.warn(
+ dedent`
+ Backgrounds Addon: could not find the default color "${defaultName}".
+ These are the available colors for your story based on your configuration:
+ ${availableColors}.
+ `
+ );
+ }
+
+ return 'transparent';
+};
+
+export const clearStyles = (selector: string | string[]) => {
+ const selectors = Array.isArray(selector) ? selector : [selector];
+ selectors.forEach(clearStyle);
+};
+
+const clearStyle = (selector: string) => {
+ const element = document.getElementById(selector) as HTMLElement;
+ if (element) {
+ element.parentElement.removeChild(element);
+ }
+};
+
+export const addGridStyle = (selector: string, css: string) => {
+ const existingStyle = document.getElementById(selector) as HTMLElement;
+ if (existingStyle) {
+ if (existingStyle.innerHTML !== css) {
+ existingStyle.innerHTML = css;
+ }
+ } else {
+ const style = document.createElement('style') as HTMLElement;
+ style.setAttribute('id', selector);
+ style.innerHTML = css;
+ document.head.appendChild(style);
+ }
+};
+
+export const addBackgroundStyle = (selector: string, css: string, storyId: string) => {
+ const existingStyle = document.getElementById(selector) as HTMLElement;
+ if (existingStyle) {
+ if (existingStyle.innerHTML !== css) {
+ existingStyle.innerHTML = css;
+ }
+ } else {
+ const style = document.createElement('style') as HTMLElement;
+ style.setAttribute('id', selector);
+ style.innerHTML = css;
+
+ const gridStyleSelector = `addon-backgrounds-grid${storyId ? `-docs-${storyId}` : ''}`;
+ // If grids already exist, we want to add the style tag BEFORE it so the background doesn't override grid
+ const existingGridStyle = document.getElementById(gridStyleSelector) as HTMLElement;
+ if (existingGridStyle) {
+ existingGridStyle.parentElement.insertBefore(style, existingGridStyle);
+ } else {
+ document.head.appendChild(style);
+ }
+ }
+};
diff --git a/addons/backgrounds/src/preset/addDecorator.tsx b/addons/backgrounds/src/preset/addDecorator.tsx
new file mode 100644
index 000000000000..93ccff632339
--- /dev/null
+++ b/addons/backgrounds/src/preset/addDecorator.tsx
@@ -0,0 +1,3 @@
+import { withGrid, withBackground } from '../decorators';
+
+export const decorators = [withGrid, withBackground];
diff --git a/addons/backgrounds/src/preset/defaultParameters.tsx b/addons/backgrounds/src/preset/addParameter.tsx
similarity index 66%
rename from addons/backgrounds/src/preset/defaultParameters.tsx
rename to addons/backgrounds/src/preset/addParameter.tsx
index 0abaae233b23..ceb1d3b41203 100644
--- a/addons/backgrounds/src/preset/defaultParameters.tsx
+++ b/addons/backgrounds/src/preset/addParameter.tsx
@@ -1,5 +1,10 @@
export const parameters = {
backgrounds: {
+ grid: {
+ cellSize: 20,
+ opacity: 0.5,
+ cellAmount: 5,
+ },
values: [
{ name: 'light', value: '#F8F8F8' },
{ name: 'dark', value: '#333333' },
diff --git a/addons/backgrounds/src/preset/index.ts b/addons/backgrounds/src/preset/index.ts
index 5575756415e5..2d2621e66659 100644
--- a/addons/backgrounds/src/preset/index.ts
+++ b/addons/backgrounds/src/preset/index.ts
@@ -1,5 +1,5 @@
export function config(entry: any[] = []) {
- return [...entry, require.resolve('./defaultParameters')];
+ return [...entry, require.resolve('./addDecorator'), require.resolve('./addParameter')];
}
export function managerEntries(entry: any[] = [], options: any) {
diff --git a/addons/backgrounds/src/register.tsx b/addons/backgrounds/src/register.tsx
index dcdd31c6cf87..d3b8d095ec97 100644
--- a/addons/backgrounds/src/register.tsx
+++ b/addons/backgrounds/src/register.tsx
@@ -5,14 +5,14 @@ import { ADDON_ID } from './constants';
import { BackgroundSelector } from './containers/BackgroundSelector';
import { GridSelector } from './containers/GridSelector';
-addons.register(ADDON_ID, (api) => {
+addons.register(ADDON_ID, () => {
addons.add(ADDON_ID, {
title: 'Backgrounds',
type: types.TOOL,
- match: ({ viewMode }) => viewMode === 'story',
+ match: ({ viewMode }) => !!(viewMode && viewMode.match(/^(story|docs)$/)),
render: () => (
-
+
),
diff --git a/addons/backgrounds/src/types/index.ts b/addons/backgrounds/src/types/index.ts
new file mode 100644
index 000000000000..a77e4faf9322
--- /dev/null
+++ b/addons/backgrounds/src/types/index.ts
@@ -0,0 +1,33 @@
+import { ReactElement } from 'react';
+
+export interface GlobalState {
+ name: string | undefined;
+ selected: string | undefined;
+}
+
+export interface BackgroundSelectorItem {
+ id: string;
+ title: string;
+ onClick: () => void;
+ value: string;
+ active: boolean;
+ right?: ReactElement;
+}
+
+export interface Background {
+ name: string;
+ value: string;
+}
+
+export interface BackgroundsParameter {
+ default?: string;
+ disable?: boolean;
+ values: Background[];
+}
+
+export interface BackgroundsConfig {
+ backgrounds: Background[] | null;
+ selectedBackgroundName: string | null;
+ defaultBackgroundName: string | null;
+ disable: boolean;
+}
diff --git a/addons/backgrounds/src/typings.d.ts b/addons/backgrounds/src/typings.d.ts
new file mode 100644
index 000000000000..2f4eb9cf4fd9
--- /dev/null
+++ b/addons/backgrounds/src/typings.d.ts
@@ -0,0 +1 @@
+declare module 'global';
diff --git a/cypress/generated/addon-backgrounds.spec.ts b/cypress/generated/addon-backgrounds.spec.ts
new file mode 100644
index 000000000000..04201f225952
--- /dev/null
+++ b/cypress/generated/addon-backgrounds.spec.ts
@@ -0,0 +1,26 @@
+describe('addon-backgrounds', () => {
+ before(() => {
+ cy.visitStorybook();
+ });
+
+ it('should have a dark background', () => {
+ // click on the button
+ cy.navigateToStory('example-button', 'primary');
+
+ // Click on the addon and select dark background
+ cy.get('[title="Change the background of the preview"]').click();
+ cy.get('#dark').click();
+
+ cy.getCanvasBodyElement().should('have.css', 'background-color', 'rgb(51, 51, 51)');
+ });
+
+ it('should apply a grid', () => {
+ // click on the button
+ cy.navigateToStory('example-button', 'primary');
+
+ // Toggle grid view
+ cy.get('[title="Apply a grid to the preview"]').click();
+
+ cy.getCanvasBodyElement().should('have.css', 'background-image');
+ });
+});
diff --git a/cypress/support/commands.js b/cypress/support/commands.js
index 045b5f6aa1f7..ccd076d41f1f 100644
--- a/cypress/support/commands.js
+++ b/cypress/support/commands.js
@@ -74,6 +74,22 @@ Cypress.Commands.add('getDocsElement', {}, () => {
.then((storyRoot) => cy.wrap(storyRoot, { log: false }));
});
+Cypress.Commands.add('getCanvasElement', {}, () => {
+ cy.log('getCanvasElement');
+ return cy
+ .get(`#storybook-preview-iframe`, { log: false })
+ .then((iframe) => cy.wrap(iframe, { log: false }));
+});
+
+Cypress.Commands.add('getCanvasBodyElement', {}, () => {
+ cy.log('getCanvasBodyElement');
+ return cy
+ .getCanvasElement()
+ .its('0.contentDocument.body', { log: false })
+ .should('not.be.empty')
+ .then((body) => cy.wrap(body, { log: false }));
+});
+
Cypress.Commands.add('navigateToStory', (kind, name) => {
const kindId = kind.replace(/ /g, '-').toLowerCase();
const storyId = name.replace(/ /g, '-').toLowerCase();
diff --git a/cypress/support/index.d.ts b/cypress/support/index.d.ts
index 2c303f0eb2ac..0b1b2b22c6e5 100644
--- a/cypress/support/index.d.ts
+++ b/cypress/support/index.d.ts
@@ -13,11 +13,22 @@ declare namespace Cypress {
* Custom command to select the DOM element of a story in the canvas tab.
*/
getStoryElement(): Chainable;
+
/**
* Custom command to select the DOM element of a docs story in the canvas tab.
*/
getDocsElement(): Chainable;
+ /**
+ * Custom command to select the DOM element of the preview iframe in the canvas tab.
+ */
+ getCanvasElement(): Chainable;
+
+ /**
+ * Custom command to select the DOM element of the body from the preview iframe in the canvas tab.
+ */
+ getCanvasBodyElement(): Chainable;
+
/**
* Navigate to a story.
* 'Storybook Example/Button'
diff --git a/docs/essentials/backgrounds.md b/docs/essentials/backgrounds.md
index 271eb4de6621..6c308b40f7bc 100644
--- a/docs/essentials/backgrounds.md
+++ b/docs/essentials/backgrounds.md
@@ -13,7 +13,7 @@ The backgrounds toolbar item allows you to adjust the background that your story
## Configuration
-By default, the background toolbar presents you with a light and dark background.
+By default, the backgrounds toolbar presents you with a light and dark background.
But you're not restricted to these two backgrounds, you can configure your own set of colors with the `parameters.backgrounds` [parameter](../writing-stories/parameters.md) in your [`.storybook/preview.js`](../configure/overview.md#configure-story-rendering):
@@ -27,25 +27,27 @@ But you're not restricted to these two backgrounds, you can configure your own s
+If you define the `default` property, the backgrounds toolbar will set that color for every story where the parameter is applied to. If you don't set it, the colors will be available but not automatically set when a story is rendered.
+
You can also set backgrounds on per-story or per-component basis by using [parameter inheritance](../writing-stories/parameters.md#component-parameters):
-You can also only override a single key on the background parameter, for instance to set a different default value for a single story:
+You can also only override a single key on the backgrounds parameter, for instance to set a different default value for a single story:
@@ -57,8 +59,38 @@ If you want to disable backgrounds in a story, you can do so by setting the `bac
+
+## Grid
+
+Backgrounds toolbar also comes with a Grid selector. This way you can easily see if your components are aligned.
+
+By default you don't need to configure anything in order to use it, but the properties of the grid are fully configurable.
+
+Each of these properties have the following default values in case they are not passed:
+
+
+
+
+
+
+
+If you wish to disable the grid in a story, you can do so by setting the `backgrounds` parameter like so:
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/snippets/common/button-story-configure-backgrounds.js.mdx b/docs/snippets/common/storybook-addon-backgrounds-configure-backgrounds.js.mdx
similarity index 100%
rename from docs/snippets/common/button-story-configure-backgrounds.js.mdx
rename to docs/snippets/common/storybook-addon-backgrounds-configure-backgrounds.js.mdx
diff --git a/docs/snippets/common/storybook-addon-backgrounds-configure-grid.js.mdx b/docs/snippets/common/storybook-addon-backgrounds-configure-grid.js.mdx
new file mode 100644
index 000000000000..0ce5ab9f7776
--- /dev/null
+++ b/docs/snippets/common/storybook-addon-backgrounds-configure-grid.js.mdx
@@ -0,0 +1,17 @@
+```js
+// Button.stories.js
+
+// To apply a grid to all stories of Button:
+export default {
+ title: 'Button',
+ parameters: {
+ grid: {
+ cellSize: 20,
+ opacity: 0.5,
+ cellAmount: 5,
+ offsetX: 16, // default is 0 if story has 'fullscreen' layout, 16 if layout is 'padded'
+ offsetY: 16, // default is 0 if story has 'fullscreen' layout, 16 if layout is 'padded'
+ },
+ },
+};
+```
\ No newline at end of file
diff --git a/docs/snippets/common/button-story-disable-backgrounds.js.mdx b/docs/snippets/common/storybook-addon-backgrounds-disable-backgrounds.js.mdx
similarity index 100%
rename from docs/snippets/common/button-story-disable-backgrounds.js.mdx
rename to docs/snippets/common/storybook-addon-backgrounds-disable-backgrounds.js.mdx
diff --git a/docs/snippets/common/storybook-addon-backgrounds-disable-grid.js.mdx b/docs/snippets/common/storybook-addon-backgrounds-disable-grid.js.mdx
new file mode 100644
index 000000000000..c64222a064a0
--- /dev/null
+++ b/docs/snippets/common/storybook-addon-backgrounds-disable-grid.js.mdx
@@ -0,0 +1,12 @@
+```js
+// Button.stories.js
+
+export const Large = Template.bind({});
+Large.parameters = {
+ backgrounds: {
+ grid: {
+ disable: true
+ }
+ }
+};
+```
diff --git a/docs/snippets/common/button-story-override-background-color.js.mdx b/docs/snippets/common/storybook-addon-backgrounds-override-background-color.js.mdx
similarity index 100%
rename from docs/snippets/common/button-story-override-background-color.js.mdx
rename to docs/snippets/common/storybook-addon-backgrounds-override-background-color.js.mdx
diff --git a/docs/snippets/common/storybook-preview-configure-background-colors.js.mdx b/docs/snippets/common/storybook-preview-configure-background-colors.js.mdx
index 68fc18fc4522..e18e34881729 100644
--- a/docs/snippets/common/storybook-preview-configure-background-colors.js.mdx
+++ b/docs/snippets/common/storybook-preview-configure-background-colors.js.mdx
@@ -2,17 +2,18 @@
// .storybook/preview.js
export const parameters = {
-backgrounds: {
+ backgrounds: {
default: 'twitter',
values: [
- {
- name: 'twitter',
- value: '#00aced'
- },
- {
- name: 'facebook',
- value: '#3b5998'
- },
- ],
- }
-```
\ No newline at end of file
+ {
+ name: 'twitter',
+ value: '#00aced',
+ },
+ {
+ name: 'facebook',
+ value: '#3b5998',
+ },
+ ],
+ },
+};
+```
diff --git a/examples/official-storybook/stories/addon-backgrounds.stories.js b/examples/official-storybook/stories/addon-backgrounds.stories.js
index a20948759131..3ad15dc5a00b 100644
--- a/examples/official-storybook/stories/addon-backgrounds.stories.js
+++ b/examples/official-storybook/stories/addon-backgrounds.stories.js
@@ -4,7 +4,6 @@ import BaseButton from '../components/BaseButton';
export default {
title: 'Addons/Backgrounds',
-
parameters: {
backgrounds: {
default: 'dark',
@@ -19,18 +18,22 @@ export default {
},
};
-export const Story1 = () => (
-
-);
-
-Story1.storyName = 'story 1';
+const Template = (args) => ;
-export const Story2 = () => ;
-
-Story2.storyName = 'story 2';
+export const Story1 = Template.bind({});
+Story1.args = {
+ label: 'You should be able to switch backgrounds for this story',
+};
-export const Overridden = () => ;
+export const Story2 = Template.bind({});
+Story2.args = {
+ label: 'This one too!',
+};
+export const Overridden = Template.bind({});
+Overridden.args = {
+ label: 'This one should have different backgrounds',
+};
Overridden.parameters = {
backgrounds: {
default: 'blue',
@@ -41,20 +44,74 @@ Overridden.parameters = {
},
};
-export const SkippedViaDisableTrue = () => (
-
-);
+export const WithGradient = Template.bind({});
+WithGradient.args = {
+ label: 'This one should have a nice gradient',
+};
+WithGradient.parameters = {
+ backgrounds: {
+ default: 'gradient',
+ values: [
+ {
+ name: 'gradient',
+ value:
+ 'linear-gradient(90deg, rgba(2,0,36,1) 0%, rgba(9,9,121,1) 35%, rgba(0,212,255,1) 100%)',
+ },
+ ],
+ },
+};
-SkippedViaDisableTrue.storyName = 'skipped via disable: true';
+export const WithImage = Template.bind({});
+WithImage.args = {
+ label: 'This one should have an image background',
+};
+WithImage.parameters = {
+ backgrounds: {
+ default: 'space',
+ values: [
+ {
+ name: 'space',
+ value: 'url(https://cdn.pixabay.com/photo/2017/08/30/01/05/milky-way-2695569_960_720.jpg)',
+ },
+ ],
+ },
+};
-SkippedViaDisableTrue.parameters = {
+export const DisabledBackgrounds = Template.bind({});
+DisabledBackgrounds.args = {
+ label: 'This one should not use backgrounds',
+};
+DisabledBackgrounds.parameters = {
backgrounds: { disable: true },
};
-export const GridCellSize = () => (
-
-);
+export const DisabledGrid = Template.bind({});
+DisabledGrid.args = {
+ label: 'This one should not use grid',
+};
+DisabledGrid.parameters = {
+ backgrounds: {
+ grid: { disable: true },
+ },
+};
+export const GridCellProperties = Template.bind({});
+GridCellProperties.args = {
+ label: 'This one should have different grid properties',
+};
+GridCellProperties.parameters = {
+ backgrounds: {
+ grid: {
+ cellSize: 10,
+ cellAmount: 4,
+ opacity: 0.2,
+ },
+ },
+};
-GridCellSize.parameters = {
- grid: { cellSize: 10 },
+export const AlignedGridWhenFullScreen = Template.bind({});
+AlignedGridWhenFullScreen.args = {
+ label: 'Grid should have an offset of 0 when in fullscreen',
+};
+AlignedGridWhenFullScreen.parameters = {
+ layout: 'fullscreen',
};
diff --git a/lib/api/src/modules/globals.ts b/lib/api/src/modules/globals.ts
index 464f1c923b8e..172aaafa0934 100644
--- a/lib/api/src/modules/globals.ts
+++ b/lib/api/src/modules/globals.ts
@@ -1,5 +1,6 @@
import { SET_STORIES, UPDATE_GLOBALS, GLOBALS_UPDATED } from '@storybook/core-events';
import { logger } from '@storybook/client-logger';
+import deepEqual from 'fast-deep-equal';
import { Args, ModuleFn } from '../index';
@@ -32,12 +33,21 @@ export const init: ModuleFn = ({ store, fullAPI }) => {
globals: {},
};
+ const updateGlobals = (globals: Args) => {
+ const currentGlobals = store.getState()?.globals;
+ if (!deepEqual(globals, currentGlobals)) {
+ store.setState({ globals });
+ } else {
+ logger.info('Tried to update globals but the old and new values are equal.');
+ }
+ };
+
const initModule = () => {
fullAPI.on(GLOBALS_UPDATED, function handleGlobalsUpdated({ globals }: { globals: Args }) {
const { ref } = getEventMetadata(this, fullAPI);
if (!ref) {
- store.setState({ globals });
+ updateGlobals(globals);
} else {
logger.warn(
'received a GLOBALS_UPDATED from a non-local ref. This is not currently supported.'
@@ -48,7 +58,7 @@ export const init: ModuleFn = ({ store, fullAPI }) => {
const { ref } = getEventMetadata(this, fullAPI);
if (!ref) {
- store.setState({ globals });
+ updateGlobals(globals);
} else if (Object.keys(globals).length > 0) {
logger.warn('received globals from a non-local ref. This is not currently supported.');
}
diff --git a/lib/components/src/blocks/DocsPage.tsx b/lib/components/src/blocks/DocsPage.tsx
index 1bc70960a584..2ef0df238460 100644
--- a/lib/components/src/blocks/DocsPage.tsx
+++ b/lib/components/src/blocks/DocsPage.tsx
@@ -50,6 +50,8 @@ export const DocsWrapper = styled.div<{}>(({ theme }) => ({
display: 'flex',
justifyContent: 'center',
padding: '4rem 20px',
+ minHeight: '100vh',
+ boxSizing: 'border-box',
[`@media (min-width: ${breakpoint}px)`]: {},
}));
diff --git a/lib/components/src/blocks/Preview.tsx b/lib/components/src/blocks/Preview.tsx
index da1410f1be3e..1e58b51aabd1 100644
--- a/lib/components/src/blocks/Preview.tsx
+++ b/lib/components/src/blocks/Preview.tsx
@@ -213,7 +213,7 @@ const Preview: FunctionComponent = ({
/>
)}
-
+
{
cwd: path.join(siblingDir, `${name}-${version}`),
};
+ logger.log();
logger.info(`🏃♀️ Starting for ${name} ${version}`);
logger.log();
logger.debug(options);
@@ -298,14 +299,19 @@ const runTests = async ({ name, version, ...rest }: Parameters) => {
};
// Run tests!
-const runE2E = (parameters: Parameters) =>
- runTests(parameters)
+const runE2E = async (parameters: Parameters) => {
+ const { name, version } = parameters;
+ const cwd = path.join(siblingDir, `${name}-${version}`);
+ if (startWithCleanSlate) {
+ logger.log();
+ logger.info(`♻️ Starting with a clean slate, removing existing ${name} folder`);
+ await cleanDirectory({ ...parameters, cwd });
+ }
+
+ return runTests(parameters)
.then(async () => {
if (!process.env.CI) {
- const { name, version } = parameters;
- const cwd = path.join(siblingDir, `${name}-${version}`);
-
- const { cleanup } = await prompt({
+ const { cleanup } = await prompt<{ cleanup: boolean }>({
type: 'confirm',
name: 'cleanup',
message: 'Should perform cleanup?',
@@ -328,7 +334,9 @@ const runE2E = (parameters: Parameters) =>
logger.log();
process.exitCode = 1;
});
+};
+program.option('--clean', 'Clean up existing projects before running the tests', false);
program.option('--use-yarn-2', 'Run tests using Yarn 2 instead of Yarn 1 + npx', false);
program.option(
'--use-local-sb-cli',
@@ -337,7 +345,7 @@ program.option(
);
program.parse(process.argv);
-const { useYarn2, useLocalSbCli, args: frameworkArgs } = program;
+const { useYarn2, useLocalSbCli, clean: startWithCleanSlate, args: frameworkArgs } = program;
const typedConfigs: { [key: string]: Parameters } = configs;
let e2eConfigs: { [key: string]: Parameters } = {};