From cadcba4f7992aee4957ae6c0c5c7e4dc1000adde Mon Sep 17 00:00:00 2001 From: Ken McGrady Date: Tue, 12 Mar 2024 12:32:21 -0700 Subject: [PATCH] Allow custom themes to override embed options query parameter (#8021) ## Describe your changes We set the theme based on the query param each time the script run is finished. This can break other theme settings, so we want to respect the query parameters only on initial load (in determining the default theme. ## GitHub Issue Link (if applicable) Closes #7118 ## Testing Plan - Updated JS Unit Tests for the default theme. There were no tests for the script finished action, so I did not include tests for the removal. --- **Contribution License Agreement** By submitting this pull request you agree that all contributions to this project are made under the Apache 2.0 license. --- ...ate-customization[dark_theme-chromium].png | Bin 17183 -> 17267 bytes frontend/app/src/App.tsx | 11 -- frontend/lib/src/theme/utils.test.ts | 116 +++++++++++----- frontend/lib/src/theme/utils.ts | 29 ++-- frontend/lib/src/util/utils.test.ts | 129 ++++++++++++++++++ frontend/lib/src/util/utils.ts | 11 +- 6 files changed, 238 insertions(+), 58 deletions(-) diff --git a/e2e_playwright/__snapshots__/linux/st_plotly_chart_test/st_plotly_chart-template-customization[dark_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_plotly_chart_test/st_plotly_chart-template-customization[dark_theme-chromium].png index fd291815b18b8e02a8837280f296a4a42f42d8b5..b611481d6f79ee17e55177f2f1ed41b1f7c251f7 100644 GIT binary patch literal 17267 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 W{G3R#fCV@o14K>d4o3C2Xa5%^=U2r5 diff --git a/frontend/app/src/App.tsx b/frontend/app/src/App.tsx index 382f12bda8d4..8d0e40ba7519 100644 --- a/frontend/app/src/App.tsx +++ b/frontend/app/src/App.tsx @@ -43,10 +43,8 @@ import { getIFrameEnclosingApp, hashString, isColoredLineDisplayed, - isDarkTheme, isEmbed, isInChildFrame, - isLightTheme, isPaddingDisplayed, isScrollingHidden, isToolbarDisplayed, @@ -107,7 +105,6 @@ import { LibConfig, AppConfig, } from "@streamlit/lib" -import noop from "lodash/noop" import without from "lodash/without" import { UserSettings } from "@streamlit/app/src/components/StreamlitDialog/UserSettings" @@ -1058,14 +1055,6 @@ export class App extends PureComponent { const successful = status === ForwardMsg.ScriptFinishedStatus.FINISHED_SUCCESSFULLY window.setTimeout(() => { - // Set the theme if url query param ?embed_options=[light,dark]_theme is set - const [light, dark] = this.props.theme.availableThemes.slice(1, 3) - if (isLightTheme()) { - this.setAndSendTheme(light) - } else if (isDarkTheme()) { - this.setAndSendTheme(dark) - } else noop() // Do nothing when ?embed_options=[light,dark]_theme is not set - // Notify any subscribers of this event (and do it on the next cycle of // the event loop) this.state.scriptFinishedHandlers.map(handler => handler()) diff --git a/frontend/lib/src/theme/utils.test.ts b/frontend/lib/src/theme/utils.test.ts index 89352ba34b72..59a173272e74 100644 --- a/frontend/lib/src/theme/utils.test.ts +++ b/frontend/lib/src/theme/utils.test.ts @@ -54,6 +54,34 @@ const matchMediaFillers = { dispatchEvent: jest.fn(), } +const windowLocationSearch = (search: string): any => ({ + location: { + search, + }, +}) + +const windowMatchMedia = (theme: "light" | "dark"): any => ({ + matchMedia: (query: any) => ({ + matches: query === `(prefers-color-scheme: ${theme})`, + media: query, + ...matchMediaFillers, + }), +}) + +const mockWindow = (...overrides: object[]): jest.SpyInstance => { + const localStorage = window.localStorage + const windowSpy = jest.spyOn(window, "window", "get") + + windowSpy.mockImplementation(() => ({ + localStorage, + ...windowLocationSearch(""), + ...windowMatchMedia("light"), + ...Object.assign({}, ...overrides), + })) + + return windowSpy +} + describe("Styling utils", () => { describe("computeSpacingStyle", () => { test("pulls correct theme values", async () => { @@ -359,48 +387,36 @@ describe("createTheme", () => { }) describe("getSystemTheme", () => { + let windowSpy: jest.SpyInstance + + afterEach(() => { + windowSpy.mockRestore() + window.localStorage.clear() + }) + it("returns lightTheme when matchMedia does *not* match dark", () => { - Object.defineProperty(window, "matchMedia", { - writable: true, - value: jest.fn().mockImplementation(query => ({ - matches: false, - media: query, - ...matchMediaFillers, - })), - }) + windowSpy = mockWindow() expect(getSystemTheme().name).toBe("Light") }) it("returns darkTheme when matchMedia does match dark", () => { - Object.defineProperty(window, "matchMedia", { - writable: true, - value: jest.fn().mockImplementation(query => ({ - matches: true, - media: query, - ...matchMediaFillers, - })), - }) + windowSpy = mockWindow(windowMatchMedia("dark")) expect(getSystemTheme().name).toBe("Dark") }) }) describe("getDefaultTheme", () => { - beforeEach(() => { - // sourced from: - // https://jestjs.io/docs/en/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom - Object.defineProperty(window, "matchMedia", { - writable: true, - value: jest.fn().mockImplementation(query => ({ - matches: false, - media: query, - ...matchMediaFillers, - })), - }) + let windowSpy: jest.SpyInstance + + afterEach(() => { + windowSpy.mockRestore() + window.localStorage.clear() }) it("sets default to the auto theme when there is no cached theme", () => { + windowSpy = mockWindow() const defaultTheme = getDefaultTheme() expect(defaultTheme.name).toBe(AUTO_THEME_NAME) @@ -409,16 +425,7 @@ describe("getDefaultTheme", () => { }) it("sets the auto theme correctly when the OS preference is dark", () => { - // sourced from: - // https://jestjs.io/docs/en/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom - Object.defineProperty(window, "matchMedia", { - writable: true, - value: jest.fn().mockImplementation(query => ({ - matches: true, - media: query, - ...matchMediaFillers, - })), - }) + mockWindow(windowSpy, windowMatchMedia("dark")) const defaultTheme = getDefaultTheme() @@ -427,6 +434,7 @@ describe("getDefaultTheme", () => { }) it("sets the default to the user preference when one is set", () => { + windowSpy = mockWindow() setCachedTheme(darkTheme) const defaultTheme = getDefaultTheme() @@ -434,6 +442,40 @@ describe("getDefaultTheme", () => { expect(defaultTheme.name).toBe("Dark") expect(defaultTheme.emotion.colors).toEqual(darkTheme.emotion.colors) }) + + it("sets default to the light theme when an embed query parameter is set", () => { + windowSpy = mockWindow( + windowLocationSearch("?embed=true&embed_options=light_theme") + ) + const defaultTheme = getDefaultTheme() + + expect(defaultTheme.name).toBe("Light") + // Also verify that the theme is our lightTheme. + expect(defaultTheme.emotion.colors).toEqual(lightTheme.emotion.colors) + }) + + it("sets default to the dark theme when an embed query parameter is set", () => { + windowSpy = mockWindow( + windowLocationSearch("?embed=true&embed_options=dark_theme") + ) + const defaultTheme = getDefaultTheme() + + expect(defaultTheme.name).toBe("Dark") + // Also verify that the theme is our darkTheme. + expect(defaultTheme.emotion.colors).toEqual(darkTheme.emotion.colors) + }) + + it("respects embed query parameter is set over system theme", () => { + windowSpy = mockWindow( + windowMatchMedia("dark"), + windowLocationSearch("?embed=true&embed_options=light_theme") + ) + const defaultTheme = getDefaultTheme() + + expect(defaultTheme.name).toBe("Light") + // Also verify that the theme is our lightTheme. + expect(defaultTheme.emotion.colors).toEqual(lightTheme.emotion.colors) + }) }) describe("isColor", () => { diff --git a/frontend/lib/src/theme/utils.ts b/frontend/lib/src/theme/utils.ts index 2b14c2782658..b883d0a7846f 100644 --- a/frontend/lib/src/theme/utils.ts +++ b/frontend/lib/src/theme/utils.ts @@ -39,6 +39,8 @@ import { ThemeSpacing, } from "@streamlit/lib/src/theme" +import { isLightTheme, isDarkTheme } from "@streamlit/lib/src/util/utils" + import { fonts } from "./primitives/typography" import { computeDerivedColors, @@ -379,15 +381,26 @@ export const removeCachedTheme = (): void => { export const getDefaultTheme = (): ThemeConfig => { // Priority for default theme - // 1. Previous user preference - // 2. OS preference - // If local storage has Auto, refetch system theme as it may have changed - // based on time of day. We shouldn't ever have this saved in our storage - // but checking in case! const cachedTheme = getCachedTheme() - return cachedTheme && cachedTheme.name !== AUTO_THEME_NAME - ? cachedTheme - : createAutoTheme() + + // 1. Previous user preference + // We shouldn't ever have auto saved in our storage in case + // OS theme changes but we explicitly check in case! + if (cachedTheme && cachedTheme.name !== AUTO_THEME_NAME) { + return cachedTheme + } + + // 2. Embed Parameter preference + if (isLightTheme()) { + return lightTheme + } + + if (isDarkTheme()) { + return darkTheme + } + + // 3. OS preference + return createAutoTheme() } const whiteSpace = /\s+/ diff --git a/frontend/lib/src/util/utils.test.ts b/frontend/lib/src/util/utils.test.ts index 81d072d4bb44..f49eea5f8f5d 100644 --- a/frontend/lib/src/util/utils.test.ts +++ b/frontend/lib/src/util/utils.test.ts @@ -24,6 +24,12 @@ import { isEmbed, setCookie, preserveEmbedQueryParams, + isColoredLineDisplayed, + isToolbarDisplayed, + isPaddingDisplayed, + isScrollingHidden, + isLightTheme, + isDarkTheme, } from "./utils" describe("getCookie", () => { @@ -234,6 +240,129 @@ describe("isEmbed", () => { expect(isEmbed()).toBe(true) }) + it("embed Options should return false even if ?embed=true", () => { + windowSpy.mockImplementation(() => ({ + location: { + search: "?embed=true", + }, + })) + + expect(isColoredLineDisplayed()).toBe(false) + expect(isToolbarDisplayed()).toBe(false) + expect(isPaddingDisplayed()).toBe(false) + expect(isScrollingHidden()).toBe(false) + expect(isLightTheme()).toBe(false) + expect(isDarkTheme()).toBe(false) + }) + + it("embed Options should return false even if ?embed=false", () => { + windowSpy.mockImplementation(() => ({ + location: { + search: + "?embed=false&embed_options=show_colored_line,show_toolbar,show_padding,disable_scrolling", + }, + })) + + expect(isColoredLineDisplayed()).toBe(false) + expect(isToolbarDisplayed()).toBe(false) + expect(isPaddingDisplayed()).toBe(false) + expect(isScrollingHidden()).toBe(false) + }) + + it("embed Options should return false even if ?embed is not set", () => { + windowSpy.mockImplementation(() => ({ + location: { + search: + "?embed_options=show_colored_line,show_toolbar,show_padding,disable_scrolling", + }, + })) + + expect(isColoredLineDisplayed()).toBe(false) + expect(isToolbarDisplayed()).toBe(false) + expect(isPaddingDisplayed()).toBe(false) + expect(isScrollingHidden()).toBe(false) + }) + + it("should specify light theme if in embed options", () => { + windowSpy.mockImplementation(() => ({ + location: { + search: "?embed_options=light_theme", + }, + })) + + expect(isLightTheme()).toBe(true) + }) + + it("should specify dark theme if in embed options", () => { + windowSpy.mockImplementation(() => ({ + location: { + search: "?embed_options=dark_theme", + }, + })) + + expect(isDarkTheme()).toBe(true) + }) + + it("should disable scrolling if in embed options", () => { + windowSpy.mockImplementation(() => ({ + location: { + search: "?embed=true&embed_options=disable_scrolling", + }, + })) + + expect(isColoredLineDisplayed()).toBe(false) + expect(isToolbarDisplayed()).toBe(false) + expect(isPaddingDisplayed()).toBe(false) + expect(isScrollingHidden()).toBe(true) + expect(isLightTheme()).toBe(false) + expect(isDarkTheme()).toBe(false) + }) + + it("should show padding if in embed options", () => { + windowSpy.mockImplementation(() => ({ + location: { + search: "?embed=true&embed_options=show_padding", + }, + })) + + expect(isColoredLineDisplayed()).toBe(false) + expect(isToolbarDisplayed()).toBe(false) + expect(isPaddingDisplayed()).toBe(true) + expect(isScrollingHidden()).toBe(false) + expect(isLightTheme()).toBe(false) + expect(isDarkTheme()).toBe(false) + }) + + it("should show the toolbar if in embed options", () => { + windowSpy.mockImplementation(() => ({ + location: { + search: "?embed=true&embed_options=show_toolbar", + }, + })) + + expect(isColoredLineDisplayed()).toBe(false) + expect(isToolbarDisplayed()).toBe(true) + expect(isPaddingDisplayed()).toBe(false) + expect(isScrollingHidden()).toBe(false) + expect(isLightTheme()).toBe(false) + expect(isDarkTheme()).toBe(false) + }) + + it("should show the colored line if in embed options", () => { + windowSpy.mockImplementation(() => ({ + location: { + search: "?embed=true&embed_options=show_colored_line", + }, + })) + + expect(isColoredLineDisplayed()).toBe(true) + expect(isToolbarDisplayed()).toBe(false) + expect(isPaddingDisplayed()).toBe(false) + expect(isScrollingHidden()).toBe(false) + expect(isLightTheme()).toBe(false) + expect(isDarkTheme()).toBe(false) + }) + it("isEmbed is case insensitive, so should return true when ?embed=TrUe", () => { windowSpy.mockImplementation(() => ({ location: { diff --git a/frontend/lib/src/util/utils.ts b/frontend/lib/src/util/utils.ts index f1728edd463a..86b1da07b677 100644 --- a/frontend/lib/src/util/utils.ts +++ b/frontend/lib/src/util/utils.ts @@ -156,8 +156,11 @@ export function isToolbarDisplayed(): boolean { * Returns true if the URL parameters contain ?embed=true&embed_options=disable_scrolling (case insensitive). */ export function isScrollingHidden(): boolean { - return getEmbedUrlParams(EMBED_OPTIONS_QUERY_PARAM_KEY).has( - EMBED_DISABLE_SCROLLING + return ( + isEmbed() && + getEmbedUrlParams(EMBED_OPTIONS_QUERY_PARAM_KEY).has( + EMBED_DISABLE_SCROLLING + ) ) } @@ -175,6 +178,8 @@ export function isPaddingDisplayed(): boolean { * Returns true if the URL parameters contain ?embed_options=light_theme (case insensitive). */ export function isLightTheme(): boolean { + // NOTE: We don't check for ?embed=true here, because we want to allow display without any + // other embed options (for example in our e2e tests). return getEmbedUrlParams(EMBED_OPTIONS_QUERY_PARAM_KEY).has( EMBED_LIGHT_THEME ) @@ -184,6 +189,8 @@ export function isLightTheme(): boolean { * Returns true if the URL parameters contain ?embed_options=dark_theme (case insensitive). */ export function isDarkTheme(): boolean { + // NOTE: We don't check for ?embed=true here, because we want to allow display without any + // other embed options (for example in our e2e tests). return getEmbedUrlParams(EMBED_OPTIONS_QUERY_PARAM_KEY).has(EMBED_DARK_THEME) }