Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow applying @media, @keyframes and @import in custom CSS #7395

Merged
merged 16 commits into from Feb 12, 2024
6 changes: 6 additions & 0 deletions .changeset/true-beans-flow.md
@@ -0,0 +1,6 @@
---
"@gradio/app": patch
"gradio": patch
---

fix:Allow applying `@media`, `@keyframes` and `@import` in custom CSS
1 change: 1 addition & 0 deletions demo/custom_css/run.ipynb
@@ -0,0 +1 @@
{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: custom_css"]}, {"cell_type": "code", "execution_count": null, "id": "272996653310673477252411125948039410165", "metadata": {}, "outputs": [], "source": ["!pip install -q gradio "]}, {"cell_type": "code", "execution_count": null, "id": "288918539441861185822528903084949547379", "metadata": {}, "outputs": [], "source": ["import gradio as gr\n", "\n", "css = \"\"\"\n", "/* CSSKeyframesRule for animation */\n", "@keyframes animation {\n", " from {background-color: red;}\n", " to {background-color: blue;}\n", "}\n", "\n", ".cool-col {\n", " animation-name: animation;\n", " animation-duration: 4s;\n", " animation-iteration-count: infinite;\n", " border-radius: 10px;\n", " padding: 20px;\n", "}\n", "\n", "/* CSSStyleRule */\n", ".markdown {\n", " background-color: lightblue;\n", " padding: 20px;\n", "}\n", "\n", ".markdown p {\n", " color: royalblue;\n", "}\n", "\n", "/* CSSMediaRule */\n", "@media screen and (max-width: 600px) {\n", " .markdown {\n", " background: blue;\n", " }\n", " .markdown p {\n", " color: lightblue;\n", " }\n", "}\n", "\n", ".dark .markdown {\n", " background: pink;\n", "}\n", "\n", ".darktest h3 {\n", " color: black;\n", "}\n", "\n", ".dark .darktest h3 {\n", " color: yellow;\n", "}\n", "\n", "/* CSSFontFaceRule */\n", "@font-face {\n", " font-family: \"test-font\";\n", " src: url(\"https://mdn.github.io/css-examples/web-fonts/VeraSeBd.ttf\") format(\"truetype\");\n", "}\n", "\n", ".cool-col {\n", " font-family: \"test-font\";\n", "}\n", "\n", "/* CSSImportRule */\n", "@import url(\"https://fonts.googleapis.com/css2?family=Protest+Riot&display=swap\");\n", "\n", ".markdown {\n", " font-family: \"Protest Riot\", sans-serif;\n", "}\n", "\"\"\"\n", "\n", "with gr.Blocks(css=css) as demo:\n", " with gr.Column(elem_classes=\"cool-col\"):\n", " gr.Markdown(\"### Gradio Demo with Custom CSS\", elem_classes=\"darktest\")\n", " gr.Markdown(elem_classes=\"markdown\", value=\"Resize the browser window to see the CSS media query in action.\")\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch()\n"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}
74 changes: 74 additions & 0 deletions demo/custom_css/run.py
@@ -0,0 +1,74 @@
import gradio as gr

css = """
/* CSSKeyframesRule for animation */
@keyframes animation {
from {background-color: red;}
to {background-color: blue;}
}

.cool-col {
animation-name: animation;
animation-duration: 4s;
animation-iteration-count: infinite;
border-radius: 10px;
padding: 20px;
}

/* CSSStyleRule */
.markdown {
background-color: lightblue;
padding: 20px;
}

.markdown p {
color: royalblue;
}

/* CSSMediaRule */
@media screen and (max-width: 600px) {
.markdown {
background: blue;
}
.markdown p {
color: lightblue;
}
}

.dark .markdown {
background: pink;
}

.darktest h3 {
color: black;
}

.dark .darktest h3 {
color: yellow;
}

/* CSSFontFaceRule */
@font-face {
font-family: "test-font";
src: url("https://mdn.github.io/css-examples/web-fonts/VeraSeBd.ttf") format("truetype");
}

.cool-col {
font-family: "test-font";
}

/* CSSImportRule */
@import url("https://fonts.googleapis.com/css2?family=Protest+Riot&display=swap");

.markdown {
font-family: "Protest Riot", sans-serif;
}
"""

with gr.Blocks(css=css) as demo:
with gr.Column(elem_classes="cool-col"):
gr.Markdown("### Gradio Demo with Custom CSS", elem_classes="darktest")
gr.Markdown(elem_classes="markdown", value="Resize the browser window to see the CSS media query in action.")

if __name__ == "__main__":
demo.launch()
45 changes: 42 additions & 3 deletions js/app/src/css.ts
Expand Up @@ -29,9 +29,16 @@ export function prefix_css(
const stylesheet = new CSSStyleSheet();
stylesheet.replaceSync(string);

let importString = "";
string = string.replace(/@import\s+url\((.*?)\);\s*/g, (match, url) => {
importString += `@import url(${url});\n`;
return ""; // remove and store any @import statements from the CSS
});

const rules = stylesheet.cssRules;

let css_string = "";
let gradio_css_infix = `gradio-app .gradio-container.gradio-container-${version} .contain `;

for (let i = 0; i < rules.length; i++) {
const rule = rules[i];
Expand All @@ -45,17 +52,49 @@ export function prefix_css(
.split(",")
.map(
(s) =>
`${
is_dark_rule ? ".dark" : ""
} gradio-app .gradio-container.gradio-container-${version} .contain ${s.trim()} `
`${is_dark_rule ? ".dark" : ""} ${gradio_css_infix} ${s.trim()} `
)
.join(",");

css_string += rule.cssText;
css_string += rule.cssText.replace(selector, new_selector);
}
} else if (rule instanceof CSSMediaRule) {
let mediaCssString = `@media ${rule.media.mediaText} {`;
for (let j = 0; j < rule.cssRules.length; j++) {
const innerRule = rule.cssRules[j];
if (innerRule instanceof CSSStyleRule) {
let is_dark_rule = innerRule.cssText.includes(".dark ");
const selector = innerRule.selectorText;
const new_selector = selector
.replace(".dark", "")
.split(",")
.map(
(s) =>
`${
is_dark_rule ? ".dark" : ""
} ${gradio_css_infix} ${s.trim()} `
)
.join(",");
mediaCssString += innerRule.cssText.replace(selector, new_selector);
}
}
mediaCssString += "}";
css_string += mediaCssString;
} else if (rule instanceof CSSKeyframesRule) {
css_string += `@keyframes ${rule.name} {`;
for (let j = 0; j < rule.cssRules.length; j++) {
const innerRule = rule.cssRules[j];
if (innerRule instanceof CSSKeyframeRule) {
css_string += `${innerRule.keyText} { ${innerRule.style.cssText} }`;
}
}
css_string += "}";
} else if (rule instanceof CSSFontFaceRule) {
hannahblair marked this conversation as resolved.
Show resolved Hide resolved
css_string += `@font-face { ${rule.style.cssText} }`;
}
}
css_string = importString + css_string;
style_element.textContent = css_string;

document.head.appendChild(style_element);
Expand Down
71 changes: 71 additions & 0 deletions js/app/test/custom_css.spec.ts
@@ -0,0 +1,71 @@
import { test, expect } from "@gradio/tootils";

test("renders the correct elements", async ({ page }) => {
await expect(page.getByTestId("markdown")).toHaveCount(2);
});

test("applies the custom CSS styles", async ({ page }) => {
// Test for CSSKeyframesRule
const animationName = await page
.locator(".cool-col")
.evaluate((node) => getComputedStyle(node).animationName);
expect(animationName).toBe("animation");

// Test for CSSMediaRule and CSSStyleRule
await page.setViewportSize({ width: 500, height: 720 });
await expect(page.locator(".markdown").nth(1)).toHaveCSS(
"background-color",
"rgb(0, 0, 255)"
);
await expect(page.locator(".markdown p")).toHaveCSS(
"color",
"rgb(173, 216, 230)"
);

await page.setViewportSize({ width: 1280, height: 720 });
await expect(page.locator(".markdown").nth(1)).toHaveCSS(
"background-color",
"rgb(173, 216, 230)"
);
await expect(page.locator(".markdown p")).toHaveCSS(
"color",
"rgb(65, 105, 225)"
);
});

test("applies the custom font family", async ({ page }) => {
await expect(
page.getByRole("heading", { name: "Gradio Demo with Custom CSS" })
).toHaveCSS("font-family", "test-font");
});

test("applies resources from the @import rule", async ({ page }) => {
await expect(page.getByText("Resize the browser window to")).toHaveCSS(
"font-family",
'"Protest Riot", sans-serif'
);
});

test(".dark styles are applied corrently", async ({ page }) => {
await page.emulateMedia({ colorScheme: "dark" });

await expect(page.locator(".markdown").nth(1)).toHaveCSS(
"background-color",
"rgb(255, 192, 203)"
);
await expect(page.locator(".darktest h3")).toHaveCSS(
"color",
"rgb(255, 255, 0)"
);

await page.emulateMedia({ colorScheme: "light" });

await expect(page.locator(".markdown").nth(1)).toHaveCSS(
"background-color",
"rgb(173, 216, 230)"
);
await expect(page.locator(".darktest h3")).toHaveCSS(
"color",
"rgb(31, 41, 55)"
);
});