Skip to content
Snippets Groups Projects
Unverified Commit 70eea116 authored by Phoomparin Mano's avatar Phoomparin Mano Committed by GitHub
Browse files

fix(sdk): missing css variables when rendering under a portal (#44473)

* apply css variables injection right under the public component

* refactor css variables

* fix name changes

* apply css variables to global styles

* update path

* render component wrapper as full-width

* move style resets to public component styles wrapper

* update unit tests for global styles
parent a57718bd
No related branches found
No related tags found
No related merge requests found
Showing
with 218 additions and 119 deletions
......@@ -7,7 +7,7 @@ import { useSdkSelector } from "embedding-sdk/store";
import { getIsInitialized } from "embedding-sdk/store/selectors";
import type { SDKConfig } from "embedding-sdk/types";
import { SdkContentWrapper } from "./SdkContentWrapper";
import { SdkGlobalStylesWrapper } from "./SdkGlobalStylesWrapper";
interface AppInitializeControllerProps {
children: ReactNode;
......@@ -25,11 +25,11 @@ export const AppInitializeController = ({
const isInitialized = useSdkSelector(getIsInitialized);
return (
<SdkContentWrapper
<SdkGlobalStylesWrapper
baseUrl={config.metabaseInstanceUrl}
id={EMBEDDING_SDK_ROOT_ELEMENT_ID}
>
{!isInitialized ? <div>{t`Loading…`}</div> : children}
</SdkContentWrapper>
</SdkGlobalStylesWrapper>
);
};
import styled from "@emotion/styled";
import { aceEditorStyles } from "metabase/query_builder/components/NativeQueryEditor/NativeQueryEditor.styled";
import { getMetabaseCssVariables } from "metabase/styled-components/theme/css-variables";
import { saveDomImageStyles } from "metabase/visualizations/lib/save-chart-image";
/**
* Injects CSS variables and styles to the SDK components underneath them.
* This is to ensure that the SDK components are styled correctly,
* even when rendered under a React portal.
*/
export const PublicComponentStylesWrapper = styled.div`
width: 100%;
font-weight: 400;
color: var(--mb-color-text-dark);
font-family: var(--mb-default-font-family), sans-serif;
${({ theme }) => getMetabaseCssVariables(theme)}
${aceEditorStyles}
${saveDomImageStyles}
:where(svg) {
display: inline;
}
`;
import type { JSX } from "react";
import { t } from "ttag";
import { PublicComponentStylesWrapper } from "embedding-sdk/components/private/PublicComponentStylesWrapper";
import { SdkError } from "embedding-sdk/components/private/PublicComponentWrapper/SdkError";
import { SdkLoader } from "embedding-sdk/components/private/PublicComponentWrapper/SdkLoader";
import { useSdkSelector } from "embedding-sdk/store";
......@@ -29,5 +30,7 @@ export const PublicComponentWrapper = ({
return <SdkError message={loginStatus.error.message} />;
}
return children;
return (
<PublicComponentStylesWrapper>{children}</PublicComponentStylesWrapper>
);
};
import { css } from "@emotion/react";
import styled from "@emotion/styled";
import type { HTMLAttributes } from "react";
import { rootStyle } from "metabase/css/core/base.styled";
import { defaultFontFiles } from "metabase/css/core/fonts.styled";
import { alpha, color, lighten } from "metabase/lib/colors";
import { useSelector } from "metabase/lib/redux";
import { aceEditorStyles } from "metabase/query_builder/components/NativeQueryEditor/NativeQueryEditor.styled";
import { getFontFiles } from "metabase/styled-components/selectors";
import { useThemeSpecificSelectors } from "metabase/styled-components/theme/theme";
import { saveDomImageStyles } from "metabase/visualizations/lib/save-chart-image";
import type { FontFile } from "metabase-types/api";
interface SdkContentWrapperProps {
baseUrl?: string;
}
export function SdkContentWrapper({
baseUrl,
...divProps
}: SdkContentWrapperProps & HTMLAttributes<HTMLDivElement>) {
const fontFiles = useSelector(getFontFiles);
const themeSpecificSelectors = useThemeSpecificSelectors();
return (
<SdkContentWrapperInner
baseUrl={baseUrl}
fontFiles={fontFiles}
themeSpecificSelectors={themeSpecificSelectors}
{...divProps}
/>
);
}
const SdkContentWrapperInner = styled.div<
SdkContentWrapperProps & {
fontFiles: FontFile[] | null;
themeSpecificSelectors: string;
}
>`
--mb-default-font-family: "${({ theme }) => theme.fontFamily}";
--mb-color-bg-light: ${({ theme }) => theme.fn.themeColor("bg-light")};
--mb-color-bg-dark: ${({ theme }) => theme.fn.themeColor("bg-dark")};
--mb-color-brand: ${({ theme }) => theme.fn.themeColor("brand")};
--mb-color-brand-light: ${({ theme }) =>
lighten(theme.fn.themeColor("brand"), 0.532)};
--mb-color-brand-lighter: ${({ theme }) =>
lighten(theme.fn.themeColor("brand"), 0.598)};
--mb-color-brand-alpha-04: ${({ theme }) =>
alpha(theme.fn.themeColor("brand"), 0.04)};
--mb-color-brand-alpha-88: ${({ theme }) =>
alpha(theme.fn.themeColor("brand"), 0.88)};
--mb-color-focus: ${({ theme }) => theme.fn.themeColor("focus")};
--mb-color-bg-white: ${({ theme }) => theme.fn.themeColor("bg-white")};
--mb-color-bg-black: ${({ theme }) => theme.fn.themeColor("bg-black")};
--mb-color-shadow: ${({ theme }) => theme.fn.themeColor("shadow")};
--mb-color-border: ${({ theme }) => theme.fn.themeColor("border")};
--mb-color-text-dark: ${({ theme }) => theme.fn.themeColor("text-dark")};
--mb-color-text-medium: ${({ theme }) => theme.fn.themeColor("text-medium")};
--mb-color-text-light: ${({ theme }) => theme.fn.themeColor("text-light")};
--mb-color-danger: ${({ theme }) => theme.fn.themeColor("danger")};
--mb-color-error: ${({ theme }) => theme.fn.themeColor("error")};
--mb-color-filter: ${({ theme }) => theme.fn.themeColor("filter")};
--mb-color-bg-error: ${() => color("bg-error")};
--mb-color-bg-medium: ${({ theme }) => theme.fn.themeColor("bg-medium")};
--mb-color-bg-night: ${() => color("bg-night")};
--mb-color-text-white: ${({ theme }) => theme.fn.themeColor("text-white")};
--mb-color-success: ${({ theme }) => theme.fn.themeColor("success")};
--mb-color-summarize: ${({ theme }) => theme.fn.themeColor("summarize")};
--mb-color-warning: ${({ theme }) => theme.fn.themeColor("warning")};
/**
Theming-specific CSS variables.
Keep in sync with [GlobalStyles.tsx] and [.storybook/preview-head.html].
Refer to DEFAULT_METABASE_COMPONENT_THEME for their defaults.
These CSS variables are not part of the core design system colors.
Do NOT add them to [palette.ts] and [colors.ts].
*/
${({ themeSpecificSelectors }) => themeSpecificSelectors}
font-size: ${({ theme }) => theme.other.fontSize};
${aceEditorStyles}
${saveDomImageStyles}
${rootStyle}
${({ baseUrl }) => defaultFontFiles({ baseUrl })}
${({ fontFiles }) =>
fontFiles?.map(
file => css`
@font-face {
font-family: "Custom";
src: url(${encodeURI(file.src)}) format("${file.fontFormat}");
font-weight: ${file.fontWeight};
font-style: normal;
font-display: swap;
}
`,
)}
:where(svg) {
display: inline;
}
`;
import { css } from "@emotion/react";
import styled from "@emotion/styled";
import type { HTMLAttributes } from "react";
import { rootStyle } from "metabase/css/core/base.styled";
import { defaultFontFiles } from "metabase/css/core/fonts.styled";
import { useSelector } from "metabase/lib/redux";
import { getFontFiles } from "metabase/styled-components/selectors";
import { getMetabaseCssVariables } from "metabase/styled-components/theme/css-variables";
import type { FontFile } from "metabase-types/api";
interface SdkContentWrapperProps {
baseUrl?: string;
}
export function SdkGlobalStylesWrapper({
baseUrl,
...divProps
}: SdkContentWrapperProps & HTMLAttributes<HTMLDivElement>) {
const fontFiles = useSelector(getFontFiles);
return (
<SdkGlobalStylesInner
baseUrl={baseUrl}
fontFiles={fontFiles}
{...divProps}
/>
);
}
const SdkGlobalStylesInner = styled.div<
SdkContentWrapperProps & {
fontFiles: FontFile[] | null;
}
>`
font-size: ${({ theme }) => theme.other.fontSize};
${({ theme }) => getMetabaseCssVariables(theme)}
${rootStyle}
${({ baseUrl }) => defaultFontFiles({ baseUrl })}
${({ fontFiles }) =>
fontFiles?.map(
file => css`
@font-face {
font-family: "Custom";
src: url(${encodeURI(file.src)}) format("${file.fontFormat}");
font-weight: ${file.fontWeight};
font-style: normal;
font-display: swap;
}
`,
)}
`;
import { renderWithProviders } from "__support__/ui";
import { SdkContentWrapper } from "embedding-sdk/components/private/SdkContentWrapper";
import { SdkGlobalStylesWrapper } from "embedding-sdk/components/private/SdkGlobalStylesWrapper";
import {
createMockSettingsState,
createMockState,
} from "metabase-types/store/mocks";
describe("SdkContentWrapper", () => {
describe("SdkGlobalStylesWrapper", () => {
it("injects the font-face declaration when available", () => {
const state = createMockState({
settings: createMockSettingsState({
......@@ -19,7 +19,9 @@ describe("SdkContentWrapper", () => {
}),
});
renderWithProviders(<SdkContentWrapper />, { storeInitialState: state });
renderWithProviders(<SdkGlobalStylesWrapper />, {
storeInitialState: state,
});
const rules = Array.from(document.styleSheets).flatMap(sheet =>
Array.from(sheet.cssRules || []),
......
......@@ -2,7 +2,7 @@
* NOTE: KEEP SYNCRONIZED WITH:
* frontend/src/metabase/ui/utils/colors.ts
* frontend/src/metabase/styled-components/containers/GlobalStyles/GlobalStyles.tsx
* enterprise/frontend/src/embedding-sdk/components/private/SdkContentWrapper.tsx
* frontend/src/metabase/styled-components/theme/css-variables.ts
* .storybook/preview-head.html
*/
:root {
......
......@@ -10,7 +10,7 @@ export const ACCENT_COUNT = 8;
// NOTE: KEEP SYNCRONIZED WITH:
// frontend/src/metabase/css/core/colors.module.css
// frontend/src/metabase/styled-components/containers/GlobalStyles/GlobalStyles.tsx
// enterprise/frontend/src/embedding-sdk/components/private/SdkContentWrapper.tsx
// frontend/src/metabase/styled-components/theme/css-variables.ts
// .storybook/preview-head.html
export const colors = {
brand: "#509EE3",
......
......@@ -6,7 +6,8 @@ import { alpha, color, lighten } from "metabase/lib/colors";
import { getSitePath } from "metabase/lib/dom";
import { useSelector } from "metabase/lib/redux";
import { aceEditorStyles } from "metabase/query_builder/components/NativeQueryEditor/NativeQueryEditor.styled";
import { useThemeSpecificSelectors } from "metabase/styled-components/theme/theme";
import { getThemeSpecificCssVariables } from "metabase/styled-components/theme/css-variables";
import { useMantineTheme } from "metabase/ui";
import { saveDomImageStyles } from "metabase/visualizations/lib/save-chart-image";
import { getFont, getFontFiles } from "../../selectors";
......@@ -15,9 +16,8 @@ export const GlobalStyles = (): JSX.Element => {
const font = useSelector(getFont);
const fontFiles = useSelector(getFontFiles);
const themeSpecificSelectors = useThemeSpecificSelectors();
const sitePath = getSitePath();
const theme = useMantineTheme();
const styles = css`
:root {
......@@ -66,7 +66,7 @@ export const GlobalStyles = (): JSX.Element => {
transparent
);
${themeSpecificSelectors}
${getThemeSpecificCssVariables(theme)}
}
${defaultFontFiles({ baseUrl: sitePath })}
......
import { css } from "@emotion/react";
import { get } from "lodash";
import type { MetabaseComponentTheme } from "embedding-sdk";
import { useMantineTheme } from "metabase/ui";
import { color } from "metabase/lib/colors";
import type { MantineTheme } from "metabase/ui";
// https://www.raygesualdo.com/posts/flattening-object-keys-with-typescript-types/
type FlattenObjectKeys<
......@@ -15,17 +17,87 @@ type FlattenObjectKeys<
type MetabaseComponentThemeKey = FlattenObjectKeys<MetabaseComponentTheme>;
/*
Theming-specific CSS variables.
These CSS variables are not part of the core design system colors.
/**
* Defines the CSS variables used across Metabase.
*/
export function getMetabaseCssVariables(theme: MantineTheme) {
return css`
${getDesignSystemCssVariables(theme)}
${getThemeSpecificCssVariables(theme)}
`;
}
/**
* Design System CSS variables.
* These CSS variables are part of the core design system colors.
* DO NOT ADD COLORS WITHOUT EXTREMELY GOOD REASON AND DESIGN REVIEW.
* KEEP SYNCHRONIZED WITH:
* frontend/src/metabase/ui/utils/colors.ts
* frontend/src/metabase/css/core/colors.module.css
* frontend/src/metabase/styled-components/containers/GlobalStyles/GlobalStyles.tsx
* .storybook/preview-head.html
**/
export const useThemeSpecificSelectors = () => {
const theme = useMantineTheme();
function getDesignSystemCssVariables(theme: MantineTheme) {
return css`
--mb-default-font-family: "${theme.fontFamily}";
--mb-color-bg-light: ${theme.fn.themeColor("bg-light")};
--mb-color-bg-dark: ${theme.fn.themeColor("bg-dark")};
--mb-color-brand: ${theme.fn.themeColor("brand")};
--mb-color-brand-light: color-mix(in srgb, var(--mb-color-brand) 53%, #fff);
--mb-color-brand-lighter: color-mix(
in srgb,
var(--mb-color-brand) 60%,
#fff
);
--mb-color-brand-alpha-04: color-mix(
in srgb,
var(--mb-color-brand) 4%,
transparent
);
--mb-color-brand-alpha-88: color-mix(
in srgb,
var(--mb-color-brand) 88%,
transparent
);
--mb-color-focus: ${theme.fn.themeColor("focus")};
--mb-color-bg-white: ${theme.fn.themeColor("bg-white")};
--mb-color-bg-black: ${theme.fn.themeColor("bg-black")};
--mb-color-shadow: ${theme.fn.themeColor("shadow")};
--mb-color-border: ${theme.fn.themeColor("border")};
--mb-color-text-dark: ${theme.fn.themeColor("text-dark")};
--mb-color-text-medium: ${theme.fn.themeColor("text-medium")};
--mb-color-text-light: ${theme.fn.themeColor("text-light")};
--mb-color-danger: ${theme.fn.themeColor("danger")};
--mb-color-error: ${theme.fn.themeColor("error")};
--mb-color-filter: ${theme.fn.themeColor("filter")};
--mb-color-bg-error: ${color("bg-error")};
--mb-color-bg-medium: ${theme.fn.themeColor("bg-medium")};
--mb-color-bg-night: ${color("bg-night")};
--mb-color-text-white: ${theme.fn.themeColor("text-white")};
--mb-color-success: ${theme.fn.themeColor("success")};
--mb-color-summarize: ${theme.fn.themeColor("summarize")};
--mb-color-warning: ${theme.fn.themeColor("warning")};
`;
}
// get value from theme.other, which is typed as MetabaseComponentTheme
/**
* Theming-specific CSS variables.
*
* These CSS variables are NOT part of the core design system colors.
* Do NOT add them to [palette.ts] and [colors.ts].
*
* Keep in sync with [GlobalStyles.tsx] and [.storybook/preview-head.html].
* Refer to DEFAULT_METABASE_COMPONENT_THEME for their defaults.
**/
export function getThemeSpecificCssVariables(theme: MantineTheme) {
// Get value from theme.other, which is typed as MetabaseComponentTheme
const getValue = (key: MetabaseComponentThemeKey) => get(theme.other, key);
return `
return css`
--mb-color-bg-dashboard: ${getValue("dashboard.backgroundColor")};
--mb-color-bg-dashboard-card: ${getValue("dashboard.card.backgroundColor")};
--mb-color-bg-question: ${getValue("question.backgroundColor")};
......@@ -42,6 +114,5 @@ export const useThemeSpecificSelectors = () => {
--mb-color-bg-collection-browser-expand-button-hover: ${getValue(
"collectionBrowser.breadcrumbs.expandButton.hoverBackgroundColor",
)};
`;
};
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment