I want to write different stories, one for each @media
query in my CSS.
For example, I hide certain elements for @media print
. What I would like to achieve is something like this:
export const PrintFoo: Story = {
render: (args) => (
<MediaWrapper media="print">
<Foo .../>
</List>
),
};
This should be a common task in Storybook (for testing different screen sizes) but I couldn't find any documentation how to do this.
I found JavaScript which modifies the style sheet (replaces print
with screen
: https://github.com/RRMoelker/print-css-toggle/blob/master/src/index.js) but for this, I would need a "post rendering hook but before compare" where I can call this from a story.
Or do I have to write an addon for this?
Here is a wrapper element (React with TypeScript) which can simulate the print mode. The wrapper collects all media rules of the CSS stylesheets attached to the document. If you switch to @media print
, it will change all @media screen
rules to @media disabled
(effectively rendering them uselss) and replace print
with screen
in all @media print
rules, making them the default.
import { ReactElement, ReactNode } from "react";
type MediaWrapperProps = {
children: ReactNode;
media: string;
};
export const MediaWrapper = ({ children, media }: MediaWrapperProps): ReactElement => {
const result = <>{children}</>;
console.log("MediaWrapper", media, result);
const mediaRules = collectRules();
if (media === "print") {
console.log("MediaWrapper: Simulating print mode");
replaceInAllRules(mediaRules.screen, "disabled");
replaceInAllRules(mediaRules.print, "screen");
console.log(mediaRules);
} else if (media === "screen") {
console.log("MediaWrapper: Simulating screen mode");
replaceInAllRules(mediaRules.screen, "screen");
replaceInAllRules(mediaRules.print, "print");
console.log(mediaRules);
}
return result;
};
type MediaRules = {
print: CSSMediaRuleWithConditionText[];
screen: CSSMediaRuleWithConditionText[];
};
const replaceInAllRules = (rules: CSSMediaRuleWithConditionText[], replacement: string): void => {
rules.forEach((rule) => {
rule.media.mediaText = replacement;
});
};
// node_modules/typescript/lib/lib.dom.d.ts doesn't have all the properties of the browser's CSSMediaRule
type CSSMediaRuleWithConditionText = CSSMediaRule & {
conditionText: string;
};
const collectRules = (): MediaRules => {
const styleSheets = document.styleSheets;
const printRules = [];
const screenRules = [];
const disabledRules = [];
for (const sheet of styleSheets) {
const rules = sheet.cssRules || sheet.rules; // IE <= 8 use "rules" property
for (const rule of rules) {
if (rule.constructor.name === "CSSMediaRule") {
const cast = rule as CSSMediaRuleWithConditionText;
const condition = cast.conditionText;
if (condition.includes("screen")) {
screenRules.push(cast);
} else if (condition.includes("print")) {
printRules.push(cast);
} else if (cast.media.mediaText === "disabled") {
disabledRules.push(cast);
}
}
}
}
if (disabledRules.length) {
// Storybook doesn't always reset the CSS stylesheets in the iframe.
// if this happened, then there will be "disabled" rules from the last "print mode" story.
// Return the disabled rules as "screen" and "screen" rules as "print".
return {
print: screenRules,
screen: disabledRules,
};
}
return {
print: printRules,
screen: screenRules,
};
};
If you have a component that uses @media print
like this one:
@media print {
.printMe {
display: block;
background-color: white;
}
.hideMe {
display: none;
}
}
@media screen {
.printMe {
background-color: blue;
}
.hideMe {
background-color: green;
}
}
export const PrintTestComponent = (): ReactElement => {
return (
<div>
<div className="printMe">Print Mode</div>
<div className="hideMe">Screen Mode</div>
</div>
);
};
then you can test this in two stories:
import { MediaWrapper } from "./MediaWrapper";
import { PrintTestComponent } from "./PrintTestComponent";
import { ReactElement } from "react";
type StoryProps = {
media: "screen" | "print";
};
export default {
title: "Print Test",
component: (args: StoryProps): ReactElement => (
<MediaWrapper media={args.media}>
<PrintTestComponent />
</MediaWrapper>
),
parameters: {
media: "print",
},
};
export const Screen = {
storyName: "Screen Mode",
args: {
media: "screen",
},
};
export const Print = {
storyName: "Print Mode",
args: {
media: "print",
},
};