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

[EuiThemeProvider] Add context & state required for theme CSS variables architecture #7132

Merged
merged 3 commits into from
Aug 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/services/theme/__snapshots__/provider.test.tsx.snap
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`EuiThemeProvider CSS variables allows child components to set non-global theme CSS variables 1`] = `
<span
class="euiThemeProvider emotion-euiCSSVariables-euiColorMode--colorClassName"
/>
`;

exports[`EuiThemeProvider nested EuiThemeProviders allows avoiding the extra span wrapper with \`wrapperProps.cloneElement\` 1`] = `
<div>
Top-level provider
Expand Down
2 changes: 2 additions & 0 deletions src/services/theme/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,6 @@ export const EuiNestedThemeContext = createContext<EuiThemeNested>({
hasDifferentColorFromGlobalTheme: false,
bodyColor: '',
colorClassName: '',
setGlobalCSSVariables: () => {},
setNearestThemeCSSVariables: () => {},
});
23 changes: 22 additions & 1 deletion src/services/theme/hooks.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,18 @@
*/

import React from 'react';
import { render } from '@testing-library/react';
import { render, act } from '@testing-library/react';
import { renderHook } from '../../test/rtl';

import { EuiProvider } from '../../components';

import { setEuiDevProviderWarning } from './warning';
import {
useEuiTheme,
UseEuiTheme,
withEuiTheme,
RenderWithEuiTheme,
useEuiThemeCSSVariables,
} from './hooks';

describe('useEuiTheme', () => {
Expand Down Expand Up @@ -82,3 +85,21 @@ describe('RenderWithEuiTheme', () => {
);
});
});

describe('useEuiThemeCSSVariables', () => {
it('returns CSS variable related state setters/getters', () => {
const { result } = renderHook(useEuiThemeCSSVariables, {
wrapper: EuiProvider,
});
expect(result.current.globalCSSVariables).toBeUndefined();
expect(result.current.themeCSSVariables).toBeUndefined();

act(() => {
result.current.setNearestThemeCSSVariables({ '--hello': 'world' });
});

// In this case, the nearest theme is the global one, so it should set both
expect(result.current.globalCSSVariables).toEqual({ '--hello': 'world' });
expect(result.current.themeCSSVariables).toEqual({ '--hello': 'world' });
});
});
21 changes: 21 additions & 0 deletions src/services/theme/hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
EuiModificationsContext,
EuiColorModeContext,
defaultComputedTheme,
EuiNestedThemeContext,
} from './context';
import { emitEuiProviderWarning } from './warning';
import {
Expand Down Expand Up @@ -95,3 +96,23 @@ export const RenderWithEuiTheme = <T extends {} = {}>({
const theme = useEuiTheme<T>();
return children(theme);
};

/**
* Minor syntactical sugar hook for theme CSS variables.
* Primarily meant for internal EUI usage.
*/
export const useEuiThemeCSSVariables = () => {
const {
setGlobalCSSVariables,
globalCSSVariables,
setNearestThemeCSSVariables,
themeCSSVariables,
} = useContext(EuiNestedThemeContext);

return {
setGlobalCSSVariables,
globalCSSVariables,
setNearestThemeCSSVariables,
themeCSSVariables,
};
};
7 changes: 6 additions & 1 deletion src/services/theme/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@ export {
EuiColorModeContext,
} from './context';
export type { UseEuiTheme, WithEuiThemeProps } from './hooks';
export { useEuiTheme, withEuiTheme, RenderWithEuiTheme } from './hooks';
export {
useEuiTheme,
withEuiTheme,
RenderWithEuiTheme,
useEuiThemeCSSVariables,
} from './hooks';
export type { EuiThemeProviderProps } from './provider';
export { EuiThemeProvider } from './provider';
export { getEuiDevProviderWarning, setEuiDevProviderWarning } from './warning';
Expand Down
98 changes: 98 additions & 0 deletions src/services/theme/provider.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import React, { FunctionComponent, useEffect } from 'react';
import type { Meta, StoryObj } from '@storybook/react';

import { useEuiThemeCSSVariables } from './hooks';
import { EuiThemeProvider, EuiThemeProviderProps } from './provider';

const meta: Meta<EuiThemeProviderProps<{}>> = {
title: 'EuiThemeProvider',
component: EuiThemeProvider,
};

export default meta;
type Story = StoryObj<EuiThemeProviderProps<{}>>;

export const WrapperCloneElement: Story = {
render: () => (
<>
<EuiThemeProvider wrapperProps={{ cloneElement: true }}>
<main className="clonedExample">
This example should only have 1 main wrapper rendered.
</main>
</EuiThemeProvider>
</>
),
};
Comment on lines +23 to +33
Copy link
Member Author

@cee-chen cee-chen Aug 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This story isn't related to this PR, I just thought I'd add it as a tech debt item while here (and also as a comparison to the existing stories)


export const CSSVariablesNearest: Story = {
render: () => (
<>
<MockComponent color="red">
This component sets the nearest theme provider (the global theme) with a
red CSS variable color. Inspect the `:root` styles to see the variable
set.
</MockComponent>
<EuiThemeProvider>
<MockComponent color="blue">
This component sets the nearest local theme provider with a blue CSS
variable color. Inspect the parent theme wrapper to see the variable
set.
</MockComponent>
</EuiThemeProvider>
</>
),
};

export const CSSVariablesGlobal: Story = {
render: () => (
<>
<MockComponent color="red">
This component sets the nearest theme provider (the global theme) with a
red CSS variable color. However, it should be overridden by the next
component.
</MockComponent>
<EuiThemeProvider>
<MockComponent color="blue" global={true}>
This component sets the global theme with a blue CSS variable color.
It should override the previous component. Inspect the `:root` styles
to see this behavior
</MockComponent>
</EuiThemeProvider>
</>
),
};

/**
* Component for QA/testing purposes that mocks an EUI component
* that sets global or theme-level CSS variables
*/
const MockComponent: FunctionComponent<{
global?: boolean;
color: string;
children: any;
}> = ({ global, color, children }) => {
const { setGlobalCSSVariables, setNearestThemeCSSVariables } =
useEuiThemeCSSVariables();

useEffect(() => {
if (global) {
setGlobalCSSVariables({ '--testColor': color });
} else {
setNearestThemeCSSVariables({ '--testColor': color });
}
}, [global, color, setGlobalCSSVariables, setNearestThemeCSSVariables]);

return (
<p style={{ color: 'var(--testColor)', marginBlockEnd: '1em' }}>
{children}
</p>
);
};
81 changes: 80 additions & 1 deletion src/services/theme/provider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@
* Side Public License, v 1.
*/

import React from 'react';
import React, { FunctionComponent, useContext, useEffect } from 'react';
import { render } from '@testing-library/react'; // Note - don't use the EUI custom RTL `render`, as it auto-wraps an `EuiProvider`
import { css } from '@emotion/react';

import { EuiProvider } from '../../components/provider';
import { EuiNestedThemeContext } from './context';
import { EuiThemeProvider } from './provider';

describe('EuiThemeProvider', () => {
Expand Down Expand Up @@ -136,4 +137,82 @@ describe('EuiThemeProvider', () => {
expect(container.querySelector('.hello.world')).toBeTruthy();
});
});

describe('CSS variables', () => {
const MockEuiComponent: FunctionComponent<{ global?: boolean }> = ({
global,
}) => {
const {
globalCSSVariables,
setGlobalCSSVariables,
setNearestThemeCSSVariables,
} = useContext(EuiNestedThemeContext);

useEffect(() => {
if (global) {
setGlobalCSSVariables({ '--hello': 'global-world' });
} else {
setNearestThemeCSSVariables({ '--hello': 'world' });
}
}, [global, setGlobalCSSVariables, setNearestThemeCSSVariables]);

// Our current version of jsdom doesn't yet support :root (currently on v11,
// need to be on at least v20), so we'll mock something to assert on in the interim
Comment on lines +159 to +160
Copy link
Member Author

@cee-chen cee-chen Aug 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reference: https://github.com/jsdom/jsdom/releases/tag/20.0.0

We'll need to update Jest to latest to get a more recent version of jsdom (#6813)

return <>{JSON.stringify(globalCSSVariables)}</>;
};

const getThemeProvider = (container: HTMLElement) =>
container.querySelector('.euiThemeProvider')!;
const getThemeClassName = (container: HTMLElement) =>
getThemeProvider(container).className;

it('allows child components to set non-global theme CSS variables', () => {
const { container } = render(
<EuiProvider>
<EuiThemeProvider>
<MockEuiComponent />
</EuiThemeProvider>
</EuiProvider>
);
expect(getThemeClassName(container)).toContain('euiCSSVariables');
expect(container.firstChild).toHaveStyleRule('--hello', 'world');
expect(container.firstChild).toMatchSnapshot();
});

it('sets global CSS variables when the nearest theme provider is the top-level one', () => {
const { container } = render(
<EuiProvider>
<MockEuiComponent />
</EuiProvider>
);
expect(container.textContent).toContain('{"--hello":"world"}');
});

it('allows child components to set global CSS variables from any nested theme provider', () => {
const { container } = render(
<EuiProvider>
<EuiThemeProvider>
<MockEuiComponent global={true} />
</EuiThemeProvider>
</EuiProvider>
);
expect(getThemeClassName(container)).not.toContain('euiCSSVariables');
expect(container.textContent).toContain('{"--hello":"global-world"}');
});

it('can set both global and nearest theme variables without conflicting', () => {
const { container } = render(
<EuiProvider>
<MockEuiComponent />
<EuiThemeProvider>
<MockEuiComponent />
<MockEuiComponent global={true} />
</EuiThemeProvider>
</EuiProvider>
);
expect(getThemeClassName(container)).toContain('euiCSSVariables');
expect(getThemeProvider(container)).toHaveStyleRule('--hello', 'world');
expect(container.textContent).toContain('{"--hello":"global-world"}');
});
});
});
Loading