+
+ );
+
+ expect(getByText('Modified')).toHaveStyleRule('color', 'hotpink');
+ });
+ });
+
describe('nested EuiThemeProviders', () => {
it('renders with a span wrapper that sets the inherited text color', () => {
const { container } = render(
Top-level provider{' '}
- Nested
+
+ Nested
+
+ Double nested
+
+ Triple nested
+
+
+
);
From fe02eb1c6595986ab5c5e4be136d27ea70fddf25 Mon Sep 17 00:00:00 2001
From: Cee Chen
Date: Thu, 13 Jul 2023 15:01:47 -0700
Subject: [PATCH 04/12] [cleanup] Convert more EuiProvider tests to RTL from
Enzyme
---
.../__snapshots__/provider.test.tsx.snap | 380 ------------------
src/components/provider/provider.test.tsx | 36 +-
2 files changed, 28 insertions(+), 388 deletions(-)
diff --git a/src/components/provider/__snapshots__/provider.test.tsx.snap b/src/components/provider/__snapshots__/provider.test.tsx.snap
index 510a41558f6..cbf8151d0bf 100644
--- a/src/components/provider/__snapshots__/provider.test.tsx.snap
+++ b/src/components/provider/__snapshots__/provider.test.tsx.snap
@@ -1,353 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`EuiProvider is rendered 1`] = `
-,
- "ctr": 0,
- "insertionPoint": undefined,
- "isSpeedy": false,
- "key": "css",
- "nonce": undefined,
- "prepend": undefined,
- "speedy": [Function],
- "tags": Array [],
- },
- }
- }
->
-
-
-
-
-
-
-
-
-
-
-`;
-
exports[`EuiProvider providing an @emotion cache config applies the cache to all styles 1`] = `
`;
-
-exports[`EuiProvider using \`null\` theme option does not add global styles 1`] = `
-,
- "ctr": 0,
- "insertionPoint": undefined,
- "isSpeedy": false,
- "key": "css",
- "nonce": undefined,
- "prepend": undefined,
- "speedy": [Function],
- "tags": Array [],
- },
- }
- }
->
-
-
-
-
-`;
diff --git a/src/components/provider/provider.test.tsx b/src/components/provider/provider.test.tsx
index 38335517965..d010506ab6f 100644
--- a/src/components/provider/provider.test.tsx
+++ b/src/components/provider/provider.test.tsx
@@ -14,17 +14,37 @@ import createCache from '@emotion/cache';
import { EuiProvider } from './provider';
describe('EuiProvider', () => {
- it('is rendered', () => {
- const component = shallow();
-
- expect(component).toMatchSnapshot();
+ it('renders children', () => {
+ const { container } = render(
+
+ Hello world
+
+ );
+
+ expect(container.firstChild).toMatchInlineSnapshot(`
+
+ Hello world
+
+ `);
});
- describe('using `null` theme option', () => {
- it('does not add global styles', () => {
- const component = shallow();
+ describe('global styles and reset CSS', () => {
+ it('renders by default', () => {
+ render();
- expect(component).toMatchSnapshot();
+ const globalStyleElement = document.querySelector(
+ 'style[data-emotion="css-global"]'
+ );
+ expect(globalStyleElement).not.toEqual(null);
+ });
+
+ it('does not render when `theme` is null', () => {
+ render();
+
+ const globalStyleElement = document.querySelector(
+ 'style[data-emotion="css-global"]'
+ );
+ expect(globalStyleElement).toEqual(null);
});
});
From 13a4bf3ce513e8f7bddc8d353f591585da97fbae Mon Sep 17 00:00:00 2001
From: Cee Chen
Date: Thu, 13 Jul 2023 15:03:12 -0700
Subject: [PATCH 05/12] [cleanup] Convert `EuiProvider` cache tests to RTL from
Enzyme
---
.../__snapshots__/provider.test.tsx.snap | 1831 -----------------
src/components/provider/provider.test.tsx | 58 +-
2 files changed, 39 insertions(+), 1850 deletions(-)
delete mode 100644 src/components/provider/__snapshots__/provider.test.tsx.snap
diff --git a/src/components/provider/__snapshots__/provider.test.tsx.snap b/src/components/provider/__snapshots__/provider.test.tsx.snap
deleted file mode 100644
index cbf8151d0bf..00000000000
--- a/src/components/provider/__snapshots__/provider.test.tsx.snap
+++ /dev/null
@@ -1,1831 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`EuiProvider providing an @emotion cache config applies the cache to all styles 1`] = `
-,
- "ctr": 0,
- "insertionPoint": undefined,
- "isSpeedy": false,
- "key": "default",
- "nonce": undefined,
- "prepend": undefined,
- "tags": Array [],
- },
- }
- }
->
-
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`EuiProvider providing an @emotion cache config applies the cache to each location separately 1`] = `
-,
- "ctr": 0,
- "insertionPoint": undefined,
- "isSpeedy": false,
- "key": "default",
- "nonce": undefined,
- "prepend": undefined,
- "tags": Array [],
- },
- }
- }
->
-
- ,
- "ctr": 0,
- "insertionPoint": undefined,
- "isSpeedy": false,
- "key": "global",
- "nonce": undefined,
- "prepend": undefined,
- "tags": Array [],
- },
- }
- }
- >
-
-
- ,
- "ctr": 0,
- "insertionPoint": undefined,
- "isSpeedy": false,
- "key": "utility",
- "nonce": undefined,
- "prepend": undefined,
- "tags": Array [],
- },
- }
- }
- >
-
-
-
-
-
-`;
-
-exports[`EuiProvider providing an @emotion cache config applies the cache to global styles 1`] = `
-,
- "ctr": 0,
- "insertionPoint": undefined,
- "isSpeedy": false,
- "key": "css",
- "nonce": undefined,
- "prepend": undefined,
- "speedy": [Function],
- "tags": Array [],
- },
- }
- }
->
-
- ,
- "ctr": 0,
- "insertionPoint": undefined,
- "isSpeedy": false,
- "key": "global",
- "nonce": undefined,
- "prepend": undefined,
- "tags": Array [],
- },
- }
- }
- >
-
-
-
-
-
-
-
-
-`;
-
-exports[`EuiProvider providing an @emotion cache config applies the cache to utility styles 1`] = `
-,
- "ctr": 0,
- "insertionPoint": undefined,
- "isSpeedy": false,
- "key": "css",
- "nonce": undefined,
- "prepend": undefined,
- "speedy": [Function],
- "tags": Array [],
- },
- }
- }
->
-
-
-
-
- ,
- "ctr": 0,
- "insertionPoint": undefined,
- "isSpeedy": false,
- "key": "utility",
- "nonce": undefined,
- "prepend": undefined,
- "tags": Array [],
- },
- }
- }
- >
-
-
-
-
-
-`;
-
-exports[`EuiProvider providing an @emotion cache config provides a default cache from Emotion when configured without a cache 1`] = `
-,
- "ctr": 0,
- "insertionPoint": undefined,
- "isSpeedy": false,
- "key": "css",
- "nonce": undefined,
- "prepend": undefined,
- "speedy": [Function],
- "tags": Array [],
- },
- }
- }
->
-
-
-
-
-
-
-
-
-
-
-`;
diff --git a/src/components/provider/provider.test.tsx b/src/components/provider/provider.test.tsx
index d010506ab6f..3de71b6daab 100644
--- a/src/components/provider/provider.test.tsx
+++ b/src/components/provider/provider.test.tsx
@@ -7,8 +7,8 @@
*/
import React from 'react';
-import { shallow } from 'enzyme';
import { render } from '@testing-library/react'; // Note - don't use the EUI custom RTL `render`, as it auto-wraps an `EuiProvider`
+import { cache as emotionCache } from '@emotion/css';
import createCache from '@emotion/cache';
import { EuiProvider } from './provider';
@@ -59,47 +59,67 @@ describe('EuiProvider', () => {
key: 'utility',
});
- it('provides a default cache from Emotion when configured without a cache', () => {
- const component = shallow();
+ const getStyleByCss = (content: string) => {
+ return Array.from(document.querySelectorAll('style[data-emotion]')).find(
+ (el) => el?.textContent?.includes(content)
+ ) as HTMLStyleElement;
+ };
+
+ it('uses a default cache from Emotion when configured without a cache', () => {
+ render();
- expect(component).toMatchSnapshot();
- expect(component.prop('cache').key).toEqual('css');
- expect(component.prop('cache').compat).toEqual(true);
+ expect(emotionCache.key).toEqual('css');
+ expect(getStyleByCss('html').dataset.emotion).toEqual('css-global');
+ expect(getStyleByCss('.eui-displayBlock').dataset.emotion).toEqual(
+ 'css-global'
+ );
});
+
it('applies the cache to all styles', () => {
- const component = shallow();
+ render();
- expect(component).toMatchSnapshot();
+ expect(getStyleByCss('html').dataset.emotion).toEqual('default-global');
+ expect(getStyleByCss('.eui-displayBlock').dataset.emotion).toEqual(
+ 'default-global'
+ );
});
it('applies the cache to global styles', () => {
- const component = shallow(
-
- );
+ render();
- expect(component).toMatchSnapshot();
+ expect(getStyleByCss('html').dataset.emotion).toEqual('global-global');
+ expect(getStyleByCss('.eui-displayBlock').dataset.emotion).toEqual(
+ 'css-global'
+ );
});
it('applies the cache to utility styles', () => {
- const component = shallow(
-
- );
+ render();
- expect(component).toMatchSnapshot();
+ expect(getStyleByCss('html').dataset.emotion).toEqual('css-global');
+ expect(getStyleByCss('.eui-displayBlock').dataset.emotion).toEqual(
+ 'utility-global'
+ );
});
it('applies the cache to each location separately', () => {
- const component = shallow(
+ render(
+ >
+
+
);
- expect(component).toMatchSnapshot();
+ expect(getStyleByCss('.default-').dataset.emotion).toEqual('default');
+ expect(getStyleByCss('html').dataset.emotion).toEqual('global-global');
+ expect(getStyleByCss('.eui-displayBlock').dataset.emotion).toEqual(
+ 'utility-global'
+ );
});
});
From 73bf66cad8132794a4c596563d6b48686a55d400 Mon Sep 17 00:00:00 2001
From: Cee Chen
Date: Thu, 13 Jul 2023 15:16:20 -0700
Subject: [PATCH 06/12] Set up a context to check for nested `EuiProvider`s
---
src/components/provider/nested/index.ts | 9 ++++
.../provider/nested/nested_context.test.tsx | 30 ++++++++++++
.../provider/nested/nested_context.tsx | 28 +++++++++++
src/components/provider/provider.tsx | 48 +++++++++++--------
4 files changed, 94 insertions(+), 21 deletions(-)
create mode 100644 src/components/provider/nested/index.ts
create mode 100644 src/components/provider/nested/nested_context.test.tsx
create mode 100644 src/components/provider/nested/nested_context.tsx
diff --git a/src/components/provider/nested/index.ts b/src/components/provider/nested/index.ts
new file mode 100644
index 00000000000..4c1cbd4c9b0
--- /dev/null
+++ b/src/components/provider/nested/index.ts
@@ -0,0 +1,9 @@
+/*
+ * 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.
+ */
+
+export * from './nested_context';
diff --git a/src/components/provider/nested/nested_context.test.tsx b/src/components/provider/nested/nested_context.test.tsx
new file mode 100644
index 00000000000..1cfc2163642
--- /dev/null
+++ b/src/components/provider/nested/nested_context.test.tsx
@@ -0,0 +1,30 @@
+/*
+ * 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 { renderHook } from '@testing-library/react-hooks';
+
+import {
+ EuiProviderNestedCheck,
+ useIsNestedEuiProvider,
+} from './nested_context';
+
+describe('useIsNestedEuiProvider', () => {
+ it('is false if an EuiProviderNestedCheck wrapper has not yet been instantiated', () => {
+ const { result } = renderHook(useIsNestedEuiProvider);
+
+ expect(result.current).toEqual(false);
+ });
+
+ it('is true after an EuiProviderNestedCheck wrapper has been instantiated', () => {
+ const { result } = renderHook(useIsNestedEuiProvider, {
+ wrapper: EuiProviderNestedCheck,
+ });
+
+ expect(result.current).toEqual(true);
+ });
+});
diff --git a/src/components/provider/nested/nested_context.tsx b/src/components/provider/nested/nested_context.tsx
new file mode 100644
index 00000000000..57550d8eb57
--- /dev/null
+++ b/src/components/provider/nested/nested_context.tsx
@@ -0,0 +1,28 @@
+/*
+ * 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, { createContext, useContext, PropsWithChildren } from 'react';
+
+/**
+ * This util creates a context for EuiProviders to use and determine if they're
+ * the only (top-most) EuiProvider in the app. If they aren't (i.e., they're
+ * nested within another EuiProvider) we should throw a warning and not
+ * render instantiate the nested EuiProvider.
+ */
+
+export const EuiNestedProviderContext = createContext(false);
+
+export const EuiProviderNestedCheck = ({ children }: PropsWithChildren<{}>) => (
+
+ {children}
+
+);
+
+export const useIsNestedEuiProvider = () => {
+ return !!useContext(EuiNestedProviderContext);
+};
diff --git a/src/components/provider/provider.tsx b/src/components/provider/provider.tsx
index 50b4c29f17b..9c98cee5093 100644
--- a/src/components/provider/provider.tsx
+++ b/src/components/provider/provider.tsx
@@ -22,6 +22,7 @@ import {
} from '../../services';
import { EuiThemeAmsterdam } from '../../themes';
import { EuiCacheProvider } from './cache';
+import { EuiProviderNestedCheck } from './nested';
const isEmotionCacheObject = (
obj: EmotionCache | Object
@@ -94,27 +95,32 @@ export const EuiProvider = ({
utilityCache = cache.utility;
}
}
+
return (
-
-
- {theme && (
- <>
- }
- />
- }
- />
- >
- )}
- {children}
-
-
+
+
+
+ {theme && (
+ <>
+ }
+ />
+ }
+ />
+ >
+ )}
+
+ {children}
+
+
+
+
);
};
From 365e6e35b70f5f5fd36f661475dd6653e050fd97 Mon Sep 17 00:00:00 2001
From: Cee Chen
Date: Thu, 13 Jul 2023 15:18:07 -0700
Subject: [PATCH 07/12] Update `EuiProvider` to return early and emit a warning
if nested usage is detected
---
src/components/provider/provider.test.tsx | 57 +++++++++++++++++++++++
src/components/provider/provider.tsx | 12 ++++-
2 files changed, 68 insertions(+), 1 deletion(-)
diff --git a/src/components/provider/provider.test.tsx b/src/components/provider/provider.test.tsx
index 3de71b6daab..84fc7a9c96e 100644
--- a/src/components/provider/provider.test.tsx
+++ b/src/components/provider/provider.test.tsx
@@ -11,6 +11,7 @@ import { render } from '@testing-library/react'; // Note - don't use the EUI cus
import { cache as emotionCache } from '@emotion/css';
import createCache from '@emotion/cache';
+import { setEuiDevProviderWarning } from '../../services';
import { EuiProvider } from './provider';
describe('EuiProvider', () => {
@@ -155,4 +156,60 @@ describe('EuiProvider', () => {
expect(getByText('Dark mode')).toHaveStyleRule('color', '#333');
});
});
+
+ describe('nested EuiProviders', () => {
+ it('emits a log/error/warning per `euiDevProviderWarning` levels', () => {
+ const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); // Silence warning
+ setEuiDevProviderWarning('warn');
+
+ render(
+
+ Top-level provider
+ Nested
+
+ );
+
+ expect(warnSpy).toHaveBeenCalledWith(
+ expect.stringContaining(
+ '`EuiProvider` should not be nested or used more than once'
+ )
+ );
+
+ setEuiDevProviderWarning(undefined);
+ warnSpy.mockRestore();
+ });
+
+ it('returns children as-is without rendering any nested contexts', () => {
+ const { container } = render(
+
+ Top-level provider
+
+ Nested
+ Nested again
+
+
+ );
+
+ expect(container).toMatchInlineSnapshot(`
+
+ Top-level provider
+ Nested
+ Nested again
+
+ `);
+ });
+
+ it('does not instantiate any extra logic, including setting cache behavior', () => {
+ const ignoredCache = createCache({ key: 'ignore' });
+
+ render(
+
+ Top-level provider
+ Nested
+
+ );
+
+ expect(ignoredCache.compat).not.toEqual(true);
+ });
+ });
});
diff --git a/src/components/provider/provider.tsx b/src/components/provider/provider.tsx
index 9c98cee5093..ecb35228a87 100644
--- a/src/components/provider/provider.tsx
+++ b/src/components/provider/provider.tsx
@@ -20,9 +20,10 @@ import {
EuiThemeSystem,
CurrentEuiBreakpointProvider,
} from '../../services';
+import { emitEuiProviderWarning } from '../../services/theme/warning';
import { EuiThemeAmsterdam } from '../../themes';
import { EuiCacheProvider } from './cache';
-import { EuiProviderNestedCheck } from './nested';
+import { EuiProviderNestedCheck, useIsNestedEuiProvider } from './nested';
const isEmotionCacheObject = (
obj: EmotionCache | Object
@@ -73,6 +74,15 @@ export const EuiProvider = ({
modify,
children,
}: PropsWithChildren>) => {
+ const isNested = useIsNestedEuiProvider();
+ if (isNested) {
+ const providerMessage = `\`EuiProvider\` should not be nested or used more than once, other than at the top level of your app.
+ Use \`EuiThemeProvider\` instead for nested component-level theming: https://ela.st/euiprovider.`;
+
+ emitEuiProviderWarning(providerMessage);
+ return children as any;
+ }
+
let defaultCache;
let globalCache;
let utilityCache;
From 61e8630d8f3e3d46f79f9f91dd4f8c1058aad225 Mon Sep 17 00:00:00 2001
From: Cee Chen
Date: Thu, 13 Jul 2023 15:18:36 -0700
Subject: [PATCH 08/12] Fix types on EuiProvider's props table
---
src/components/provider/provider.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/components/provider/provider.tsx b/src/components/provider/provider.tsx
index ecb35228a87..ed744cd000b 100644
--- a/src/components/provider/provider.tsx
+++ b/src/components/provider/provider.tsx
@@ -30,7 +30,7 @@ const isEmotionCacheObject = (
): obj is EmotionCache => obj.hasOwnProperty('key');
export interface EuiProviderProps
- extends Omit, 'children' | 'theme'>,
+ extends Pick, 'colorMode' | 'modify'>,
EuiGlobalStylesProps {
/**
* Provide a specific EuiTheme; Defaults to EuiThemeAmsterdam;
From a9a4725b0d337973b0f2df4a2e0e1978f4738dde Mon Sep 17 00:00:00 2001
From: Cee Chen
Date: Thu, 13 Jul 2023 17:07:48 -0700
Subject: [PATCH 09/12] [docs] Improve `EuiProvider` and `EuiThemeProvider`
docs
- clarify in as many places as possible
- remove uncertain language (e.g. 'currently', 'future') that were still not yet figured out when docs were written
- try to remove overlap in docs between provider and theme provider, try to clarify nested usage of theme provider
- add examples to warning section
---
.../src/views/provider/provider_example.js | 114 ++++++++++--------
.../src/views/provider/provider_styles.tsx | 4 +-
.../src/views/provider/provider_warning.tsx | 37 ++++++
src-docs/src/views/theme/theme_example.js | 29 ++---
4 files changed, 109 insertions(+), 75 deletions(-)
create mode 100644 src-docs/src/views/provider/provider_warning.tsx
diff --git a/src-docs/src/views/provider/provider_example.js b/src-docs/src/views/provider/provider_example.js
index 3c1665b5576..a6ac1a53ec6 100644
--- a/src-docs/src/views/provider/provider_example.js
+++ b/src-docs/src/views/provider/provider_example.js
@@ -14,6 +14,7 @@ import { GuideSectionPropsTable } from '../../components/guide_section/guide_sec
import Setup from './provider_setup';
import GlobalStyles from './provider_styles';
+import Warnings from './provider_warning';
export const ProviderExample = {
title: 'Provider',
@@ -21,12 +22,9 @@ export const ProviderExample = {
EuiProvider contains all necessary context providers
- required for full functionality and styling of EUI. It currently
- includes the{' '}
-
- EuiThemeProvider
- {' '}
- for theming and writing custom styles.
+ required for full functionality and styling of EUI. A single instance of{' '}
+ EuiProvider should exist at the top level of your app,
+ where functionality will flow down the component tree.
For EUI to work correctly, set up EuiProvider at
- the root of your application.
+ the root of your application:
- See{' '}
-
+ EuiProvider includes global reset and utilites
+ styles and other app-wide contexts, functionality, and configuration
+ options. It should only be instantiated once. This
+ requirement is enforced internally - if another nested instance of{' '}
+ EuiProvider is detected, that instance will return
+ early without further processing, and will{' '}
+ warn if configured to do so. Nested
+ instances of EuiThemeProvider, however, are valid.
+
+
+ ),
+ },
+ {
+ title: 'Theming and global styles',
+ text: (
+
+
+ To customize the global theme of your app, use the{' '}
+ theme, colorMode, and{' '}
+ modify props (documented in{' '}
+
EuiThemeProvider
- {' '}
- for full documentation as all relevant props will pass through. For
- instance, it's likely that you will want to implement color
- mode switching at this level:
+
+ ). For instance, it's likely that you will want to implement
+ color mode switching at the top level:
{""}
-
-
+
- It is not recommended to recreate the functionality of{' '}
- EuiProvider by composing its constituent parts.
- More context, functionality, and configurations will be added to{' '}
- EuiProvider in future releases. Nested instances of{' '}
-
- EuiThemeProvider
+ If you do not wish your app to include EUI's default global reset
+ CSS or{' '}
+ utility CSS classes
+ , this is configurable via the globalStyles or{' '}
+ utilityClasses props. You can either pass in your
+ own as a React component returning an{' '}
+
+ Emotion Global
- , however, are valid.
-
- The provider includes general reset and global styles, applied via
- Emotion. These only need to be applied once so to
- prevent these styles from loading in nested instances of the
- provider, pass
- {'globalStyles={false}'}.
+ , or remove them completely by setting the props to{' '}
+ false:
In the case that your app has its own static stylesheet,{' '}
@@ -108,9 +116,7 @@ export const ProviderExample = {
utility properties on the{' '}
cache prop to further define where specific
styles should be inserted. See{' '}
-
- the props documentation
- {' '}
+ the props documentation{' '}
for details.
@@ -122,13 +128,9 @@ export const ProviderExample = {
>
the createCache API
{' '}
- will be respected by EUI.
-
-
-
- Note that EUI does not include the @emotion/cache{' '}
- library, so you will need to add it to your application
- dependencies.
+ will be respected by EUI. Note that EUI does not include the{' '}
+ @emotion/cache library, so you will need to add
+ it to your application dependencies.
For complex applications with multiple mount points or template
- wrappers, it may be beneficial to enable logging when components do
- not have access to a parent EuiProvider.
+ wrappers, it may be beneficial to enable logging. Doing so will
+ allow you to see warnings for duplicate{' '}
+ EuiProviders, as well as when components do not
+ have access to a parent EuiProvider. To enable
+ logging or erroring, use setEuiDevProviderWarning
+ :
+
+
+
- setEuiDevProviderWarning is a function that will
- enable adding logging or erroring if the Provider is missing. It
+ setEuiDevProviderWarning
accepts three levels:
+
+
+ {`const AppWithMissingProvider = () => (
+
+ {/* Will render, but will warn about missing EuiProvider */}
+
+);
+
+const App = () => (
+
+ {/* Content */}
+
+);
+const AppWithDuplicateProvider = () => (
+
+ {/* Will warn about multiple providers */}
+
+
+)`}
+
+ >
+ );
+};
diff --git a/src-docs/src/views/theme/theme_example.js b/src-docs/src/views/theme/theme_example.js
index 969a9f6c230..eb88f718b90 100644
--- a/src-docs/src/views/theme/theme_example.js
+++ b/src-docs/src/views/theme/theme_example.js
@@ -1,4 +1,5 @@
import React from 'react';
+import { Link } from 'react-router-dom';
import { GuideSectionTypes } from '../../components';
@@ -35,11 +36,17 @@ export const ThemeExample = {
EUI is in the progress of switching it's core styles processor
from Sass to Emotion. To
take full advantage of this context layer, wrap the root of your
- application with{' '}
+ application with a single{' '}
EuiProvider
- .
+ . While EuiProvider should not be included more than
+ once, you may use multiple nested EuiThemeProviders{' '}
+ to customize section-specific or component-specific{' '}
+
+ color modes
+ {' '}
+ or theme overrides.
>
@@ -51,23 +58,7 @@ export const ThemeExample = {
<>
The context layer that enables theming (including the default theme
- styles) comes from EuiThemeProvider. It is a thin
- wrapper around and caching layer built onto{' '}
- React.Context.Provider.
-
-
- Typically your app will only need a single instance at the top level
- and the functionality will flow down the component tree. We
- recommend using{' '}
-
- EuiProvider
- {' '}
- at this level as it includes reset styles and future configuration
- options. It is also possible to use several nested theme providers.
- In this case each nested provider will inherit from its closest
- ancestor provider.
-
-
+ styles) comes from EuiThemeProvider.{' '}
EuiThemeProvider accepts three props, all of
which have default values and are therefore optional. To use the
default EUI theme, no configuration is required.
From 16f08a4ec0101bed64e943732dd435dcb8311d52 Mon Sep 17 00:00:00 2001
From: Cee Chen
Date: Thu, 13 Jul 2023 17:21:31 -0700
Subject: [PATCH 10/12] changelog
---
upcoming_changelogs/6949.md | 3 +++
1 file changed, 3 insertions(+)
create mode 100644 upcoming_changelogs/6949.md
diff --git a/upcoming_changelogs/6949.md b/upcoming_changelogs/6949.md
new file mode 100644
index 00000000000..f7909a37f41
--- /dev/null
+++ b/upcoming_changelogs/6949.md
@@ -0,0 +1,3 @@
+**Breaking changes**
+
+- `EuiProvider` will no longer render multiple or duplicate nested instances of itself. If a nested `EuiProvider` is detected, that instance will return early without further processing, and will warn if configured to do so via `setEuiDevProviderWarning`. For nested theming, use `EuiThemeProvider` instead.
From 1db5f2229dfe2122276be9dc8504d067226f7605 Mon Sep 17 00:00:00 2001
From: Cee Chen
Date: Fri, 14 Jul 2023 18:37:52 -0700
Subject: [PATCH 11/12] Fix Cypress tests failing due to nested EuiProviders
---
.../breakpoint/current_breakpoint.spec.tsx | 5 +++--
.../breakpoint/is_within_hooks.spec.tsx | 21 ++++++++++---------
2 files changed, 14 insertions(+), 12 deletions(-)
diff --git a/src/services/breakpoint/current_breakpoint.spec.tsx b/src/services/breakpoint/current_breakpoint.spec.tsx
index ce0cdca7af6..d64842bdf5f 100644
--- a/src/services/breakpoint/current_breakpoint.spec.tsx
+++ b/src/services/breakpoint/current_breakpoint.spec.tsx
@@ -11,6 +11,7 @@
///
import React from 'react';
+import { mount } from 'cypress/react'; // cy.mount is configured to automatically wrap , which we're already using manually here
import { EuiProvider } from '../../components/provider';
import { useCurrentEuiBreakpoint } from './';
@@ -28,7 +29,7 @@ describe('useCurrentEuiBreakpoint', () => {
describe('with default EUI theme breakpoints', () => {
beforeEach(() => {
cy.viewport(1600, 600);
- cy.mount(
+ mount(
@@ -70,7 +71,7 @@ describe('useCurrentEuiBreakpoint', () => {
describe('with custom breakpoints', () => {
beforeEach(() => {
- cy.mount(
+ mount(
import React, { FunctionComponent } from 'react';
+import { mount } from 'cypress/react'; // mount is configured to automatically wrap , which we're already using manually here
import { EuiProvider } from '../../components/provider';
import { _EuiThemeBreakpoint } from '../../global_styling/variables/breakpoint';
@@ -31,7 +32,7 @@ describe('useIsWithinBreakpoints', () => {
it('returns true if the current breakpoint size is in the passed sizes array', () => {
cy.viewport(300, 600);
- cy.mount(
+ mount(
@@ -41,7 +42,7 @@ describe('useIsWithinBreakpoints', () => {
it('returns false if the current breakpoint size is outside the passed sizes array', () => {
cy.viewport(1400, 600);
- cy.mount(
+ mount(
@@ -51,7 +52,7 @@ describe('useIsWithinBreakpoints', () => {
it('returns false always if isResponsive is passed as false', () => {
cy.viewport(300, 600);
- cy.mount(
+ mount(
@@ -61,7 +62,7 @@ describe('useIsWithinBreakpoints', () => {
it('correctly handles custom breakpoint sizes', () => {
cy.viewport(1500, 600);
- cy.mount(
+ mount(
{
it('returns true if the current breakpoint size is smaller than the passed max size', () => {
cy.viewport(300, 600);
- cy.mount(
+ mount(
@@ -100,7 +101,7 @@ describe('useIsWithinMaxBreakpoint', () => {
it('returns false if the current breakpoint size is larger than the passed max size', () => {
cy.viewport(1400, 600);
- cy.mount(
+ mount(
@@ -110,7 +111,7 @@ describe('useIsWithinMaxBreakpoint', () => {
it('correctly handles custom breakpoint sizes', () => {
cy.viewport(1400, 600);
- cy.mount(
+ mount(
{
it('returns true if the current breakpoint size is larger than the passed min size', () => {
cy.viewport(800, 600);
- cy.mount(
+ mount(
@@ -147,7 +148,7 @@ describe('useIsWithinMinBreakpoint', () => {
it('returns false if the current breakpoint size is smaller than the passed min size', () => {
cy.viewport(600, 600);
- cy.mount(
+ mount(
@@ -157,7 +158,7 @@ describe('useIsWithinMinBreakpoint', () => {
it('correctly handles custom breakpoint sizes', () => {
cy.viewport(600, 600);
- cy.mount(
+ mount(
Date: Mon, 17 Jul 2023 09:42:53 -0700
Subject: [PATCH 12/12] [PR feedback] Allow `cy.mount` to have a configurable
provider props
---
cypress/support/index.d.ts | 7 +-
cypress/support/setup/mount.js | 5 +-
.../breakpoint/current_breakpoint.spec.tsx | 37 ++----
.../breakpoint/is_within_hooks.spec.tsx | 111 ++++++------------
4 files changed, 55 insertions(+), 105 deletions(-)
diff --git a/cypress/support/index.d.ts b/cypress/support/index.d.ts
index f20e645816e..b2d2eb20f2b 100644
--- a/cypress/support/index.d.ts
+++ b/cypress/support/index.d.ts
@@ -1,6 +1,8 @@
+import type { ReactNode } from 'react';
import { mount } from 'cypress/react';
import { ContextObject, Result, RunOptions } from 'axe-core';
import { realPress } from 'cypress-real-events/commands/realPress';
+import type { EuiProviderProps } from '../../src/components/provider';
type KeyOrShortcut = Parameters[0];
type RealPressOptions = Parameters[1];
@@ -30,7 +32,10 @@ declare global {
/**
* Mounts components with a basic `EuiProvider` wrapper
*/
- mount: typeof mount;
+ mount: (
+ children: ReactNode,
+ options?: { providerProps?: Partial> }
+ ) => ReturnType;
/**
* This ensures the correct testing window has focus when using Cypress Real Events.
diff --git a/cypress/support/setup/mount.js b/cypress/support/setup/mount.js
index 192ce0641b6..c0f7edf3e3f 100644
--- a/cypress/support/setup/mount.js
+++ b/cypress/support/setup/mount.js
@@ -10,6 +10,7 @@ import React from 'react';
import { mount as cypressMount } from 'cypress/react';
import { EuiProvider } from '../../../src';
-Cypress.Commands.add('mount', (children) => {
- return cypressMount({children});
+Cypress.Commands.add('mount', (children, options = {}) => {
+ const { providerProps } = options;
+ return cypressMount({children});
});
diff --git a/src/services/breakpoint/current_breakpoint.spec.tsx b/src/services/breakpoint/current_breakpoint.spec.tsx
index d64842bdf5f..74998e9a88c 100644
--- a/src/services/breakpoint/current_breakpoint.spec.tsx
+++ b/src/services/breakpoint/current_breakpoint.spec.tsx
@@ -11,9 +11,7 @@
///
import React from 'react';
-import { mount } from 'cypress/react'; // cy.mount is configured to automatically wrap , which we're already using manually here
-import { EuiProvider } from '../../components/provider';
import { useCurrentEuiBreakpoint } from './';
describe('useCurrentEuiBreakpoint', () => {
@@ -29,11 +27,7 @@ describe('useCurrentEuiBreakpoint', () => {
describe('with default EUI theme breakpoints', () => {
beforeEach(() => {
cy.viewport(1600, 600);
- mount(
-
-
-
- );
+ cy.mount();
cy.wait(50); // Throttle race conditions - won't typically happen in production, but Cypress does everything extremely fast
});
@@ -71,23 +65,18 @@ describe('useCurrentEuiBreakpoint', () => {
describe('with custom breakpoints', () => {
beforeEach(() => {
- mount(
-
-
-
- );
+ const customBreakpoints = {
+ xxs: 0,
+ xs: 250,
+ s: 500,
+ m: 1000,
+ l: 1500,
+ xl: 2000,
+ xxl: 2500,
+ };
+ cy.mount(, {
+ providerProps: { modify: { breakpoint: customBreakpoints } },
+ });
cy.wait(50); // Throttle race conditions - won't typically happen in production, but Cypress does everything extremely fast
});
diff --git a/src/services/breakpoint/is_within_hooks.spec.tsx b/src/services/breakpoint/is_within_hooks.spec.tsx
index ca685a27cb8..33e9d2cbd28 100644
--- a/src/services/breakpoint/is_within_hooks.spec.tsx
+++ b/src/services/breakpoint/is_within_hooks.spec.tsx
@@ -11,9 +11,7 @@
///
import React, { FunctionComponent } from 'react';
-import { mount } from 'cypress/react'; // mount is configured to automatically wrap , which we're already using manually here
-import { EuiProvider } from '../../components/provider';
import { _EuiThemeBreakpoint } from '../../global_styling/variables/breakpoint';
import {
useIsWithinBreakpoints,
@@ -32,51 +30,34 @@ describe('useIsWithinBreakpoints', () => {
it('returns true if the current breakpoint size is in the passed sizes array', () => {
cy.viewport(300, 600);
- mount(
-
-
-
- );
+ cy.mount();
cy.get('[data-test-subj]').should('exist');
});
it('returns false if the current breakpoint size is outside the passed sizes array', () => {
cy.viewport(1400, 600);
- mount(
-
-
-
- );
+ cy.mount();
cy.get('[data-test-subj]').should('not.exist');
});
it('returns false always if isResponsive is passed as false', () => {
cy.viewport(300, 600);
- mount(
-
-
-
- );
+ cy.mount();
cy.get('[data-test-subj]').should('not.exist');
});
it('correctly handles custom breakpoint sizes', () => {
+ const customBreakpoints = {
+ xs: 0,
+ s: 500,
+ m: 1000,
+ l: 1500,
+ xl: 2000,
+ };
cy.viewport(1500, 600);
- mount(
-
-
-
- );
+ cy.mount(, {
+ providerProps: { modify: { breakpoint: customBreakpoints } },
+ });
cy.get('[data-test-subj]').should('exist');
});
});
@@ -91,39 +72,26 @@ describe('useIsWithinMaxBreakpoint', () => {
it('returns true if the current breakpoint size is smaller than the passed max size', () => {
cy.viewport(300, 600);
- mount(
-
-
-
- );
+ cy.mount();
cy.get('[data-test-subj]').should('exist');
});
it('returns false if the current breakpoint size is larger than the passed max size', () => {
cy.viewport(1400, 600);
- mount(
-
-
-
- );
+ cy.mount();
cy.get('[data-test-subj]').should('not.exist');
});
it('correctly handles custom breakpoint sizes', () => {
+ const customBreakpoints = {
+ m: 1500,
+ l: 1800,
+ xl: 2000,
+ };
cy.viewport(1400, 600);
- mount(
-
-
-
- );
+ cy.mount(, {
+ providerProps: { modify: { breakpoint: customBreakpoints } },
+ });
cy.get('[data-test-subj]').should('exist');
});
});
@@ -138,39 +106,26 @@ describe('useIsWithinMinBreakpoint', () => {
it('returns true if the current breakpoint size is larger than the passed min size', () => {
cy.viewport(800, 600);
- mount(
-
-
-
- );
+ cy.mount();
cy.get('[data-test-subj]').should('exist');
});
it('returns false if the current breakpoint size is smaller than the passed min size', () => {
cy.viewport(600, 600);
- mount(
-
-
-
- );
+ cy.mount();
cy.get('[data-test-subj]').should('not.exist');
});
it('correctly handles custom breakpoint sizes', () => {
+ const customBreakpoints = {
+ m: 600,
+ l: 800,
+ xl: 1000,
+ };
cy.viewport(600, 600);
- mount(
-
-
-
- );
+ cy.mount(, {
+ providerProps: { modify: { breakpoint: customBreakpoints } },
+ });
cy.get('[data-test-subj]').should('exist');
});
});