Как протестировать @media в Storybook?

Я хочу писать разные истории, по одной для каждого запроса @media в моем CSS.

Например, я скрываю определенные элементы для @media print. Я хотел бы добиться чего-то вроде этого:

export const PrintFoo: Story = {
  render: (args) => (
    <MediaWrapper media = "print">
      <Foo .../>
    </List>
  ),
};

Это должна быть обычная задача в Storybook (для тестирования экранов разных размеров), но я не смог найти документации, как это сделать.

Я нашел JavaScript, который изменяет таблицу стилей (заменяет print на screen: https://github.com/RRMoelker/print-css-toggle/blob/master/src/index.js), но для этого мне понадобится «крючок пост-рендеринга, но перед сравнением», где я могу назвать это из истории.

Или мне нужно написать для этого аддон?

🤔 А знаете ли вы, что...
CSS поддерживает медиа-запросы, что позволяет разрабатывать стили для разных устройств и разрешений экранов.


209
1

Ответ:

Решено

Вот элемент-оболочка (React с TypeScript), который может имитировать режим печати. Оболочка собирает все медиа-правила таблиц стилей CSS, прикрепленных к документу. Если вы переключитесь на @media print, все правила @media screen изменятся на @media disabled (фактически сделав их бесполезными) и заменят print на screen во всех @media print правилах, сделав их стандартными.

import { ReactElement, ReactNode } from "react";

type MediaWrapperProps = {
    children: ReactNode;
    media: string;
};

export const MediaWrapper = ({ children, media }: MediaWrapperProps): ReactElement => {
    const result = <>{children}</>;

    console.info("MediaWrapper", media, result);
    const mediaRules = collectRules();

    if (media === "print") {
        console.info("MediaWrapper: Simulating print mode");
        replaceInAllRules(mediaRules.screen, "disabled");
        replaceInAllRules(mediaRules.print, "screen");
        console.info(mediaRules);
    } else if (media === "screen") {
        console.info("MediaWrapper: Simulating screen mode");
        replaceInAllRules(mediaRules.screen, "screen");
        replaceInAllRules(mediaRules.print, "print");
        console.info(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,
    };
};

Если у вас есть компонент, который использует @media print, как этот:

@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>
    );
};

то вы можете проверить это в двух историях:

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",
    },
};

Интересные вопросы для изучения

Как применить стиль класса ко всему, кроме конкретного класса и его дочерних элементов? (Вложенная тема)Стиль по умолчанию мигает при изменении стилей с помощью модуля JavaScript и класса HTMLКак добавить определенный класс CSS только к последнему компоненту Blazor, созданному динамически, без использования JS Interop?Плавные соединительные переходы if-elseКак сделать так, чтобы ссылки внутри элемента QML «Текст» отображали указатель мыши при наведении курсора?Нужны предложения по решению проблемы с адаптивным дизайном: работает на настольном компьютере, но не на мобильном устройствеКак вертикально центрировать адаптивный макет и исправить отступы на мобильных устройствахКак создать страницу, адаптивную по высоте, а не по ширине?Запросы контейнеров на основе высоты не работаютКак прослушать изменение запроса стиля контейнера