Skip to content

Commit

Permalink
[EuiProvider] Add a check and dev warning+early return for nested usa…
Browse files Browse the repository at this point in the history
…ge (#6949)

* [setup] DRY out warning logic and write more robust unit tests for it

* [cleanup] Move `EuiThemeProvider` nested tests to theme provider file

- they don't really belong in `EuiProvider`

* [cleanup] Improve EuiThemeProvider's unit testing

- make `EuiProvider`s tests more basic in comparison

- use `toHaveStyleRule` instead of snapshots

* [cleanup] Convert more EuiProvider tests to RTL from Enzyme

* [cleanup] Convert `EuiProvider` cache tests to RTL from Enzyme

* Set up a context to check for nested `EuiProvider`s

* Update `EuiProvider` to return early and emit a warning if nested usage is detected

* Fix types on EuiProvider's props table

* [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

* changelog

* Fix Cypress tests failing due to nested EuiProviders

* [PR feedback] Allow `cy.mount` to have a configurable provider props
  • Loading branch information
cee-chen authored Jul 17, 2023
1 parent abde51b commit 42ecae5
Show file tree
Hide file tree
Showing 20 changed files with 691 additions and 3,252 deletions.
7 changes: 6 additions & 1 deletion cypress/support/index.d.ts
Original file line number Diff line number Diff line change
@@ -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<typeof realPress>[0];
type RealPressOptions = Parameters<typeof realPress>[1];
Expand Down Expand Up @@ -30,7 +32,10 @@ declare global {
/**
* Mounts components with a basic `EuiProvider` wrapper
*/
mount: typeof mount;
mount: <T = {}>(
children: ReactNode,
options?: { providerProps?: Partial<EuiProviderProps<T>> }
) => ReturnType<typeof mount>;

/**
* This ensures the correct testing window has focus when using Cypress Real Events.
Expand Down
5 changes: 3 additions & 2 deletions cypress/support/setup/mount.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(<EuiProvider>{children}</EuiProvider>);
Cypress.Commands.add('mount', (children, options = {}) => {
const { providerProps } = options;
return cypressMount(<EuiProvider {...providerProps}>{children}</EuiProvider>);
});
114 changes: 61 additions & 53 deletions src-docs/src/views/provider/provider_example.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,17 @@ 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',
intro: (
<EuiText>
<p>
<strong>EuiProvider</strong> contains all necessary context providers
required for full functionality and styling of EUI. It currently
includes the{' '}
<Link to="/theming/theme-provider">
<strong>EuiThemeProvider</strong>
</Link>{' '}
for theming and writing custom styles.
required for full functionality and styling of EUI. A single instance of{' '}
<strong>EuiProvider</strong> should exist at the top level of your app,
where functionality will flow down the component tree.
</p>
</EuiText>
),
Expand All @@ -37,54 +35,64 @@ export const ProviderExample = {
<EuiText>
<p>
For EUI to work correctly, set up <strong>EuiProvider</strong> at
the root of your application.
the root of your application:
</p>

<Setup />

<p>
See{' '}
<EuiLink href="#/theming/theme-provider">
<strong>EuiProvider</strong> includes global reset and utilites
styles and other app-wide contexts, functionality, and configuration
options. It should only be instantiated <strong>once</strong>. This
requirement is enforced internally - if another nested instance of{' '}
<strong>EuiProvider</strong> is detected, that instance will return
early without further processing, and will{' '}
<Link to="#enforce-usage">warn if configured to do so</Link>. Nested
instances of <strong>EuiThemeProvider</strong>, however, are valid.
</p>
</EuiText>
),
},
{
title: 'Theming and global styles',
text: (
<EuiText>
<p>
To customize the global theme of your app, use the{' '}
<EuiCode>theme</EuiCode>, <EuiCode>colorMode</EuiCode>, and{' '}
<EuiCode>modify</EuiCode> props (documented in{' '}
<Link to="/theming/theme-provider">
<strong>EuiThemeProvider</strong>
</EuiLink>{' '}
for full documentation as all relevant props will pass through. For
instance, it&apos;s likely that you will want to implement color
mode switching at this level:
</Link>
). For instance, it&apos;s likely that you will want to implement
color mode switching at the top level:
</p>

<EuiCodeBlock language="tsx" fontSize="m" isCopyable>
{"<EuiProvider colorMode={isDark ? 'dark' : 'light'} />"}
</EuiCodeBlock>

<EuiSpacer size="s" />
<EuiSpacer />

<p>
It is not recommended to recreate the functionality of{' '}
<strong>EuiProvider</strong> by composing its constituent parts.
More context, functionality, and configurations will be added to{' '}
<strong>EuiProvider</strong> in future releases. Nested instances of{' '}
<EuiLink href="#/theming/theme-provider">
<strong>EuiThemeProvider</strong>
If you do not wish your app to include EUI's default global reset
CSS or{' '}
<Link to="/utilities/css-utility-classes">utility CSS classes</Link>
, this is configurable via the <EuiCode>globalStyles</EuiCode> or{' '}
<EuiCode>utilityClasses</EuiCode> props. You can either pass in your
own as a React component returning an{' '}
<EuiLink href="https://emotion.sh/docs/globals" target="_blank">
Emotion Global
</EuiLink>
, however, are valid.
</p>
</EuiText>
),
},
{
title: 'Global styles',
text: (
<EuiText>
<p>
The provider includes general reset and global styles, applied via
Emotion. These only need to be applied <strong>once</strong> so to
prevent these styles from loading in nested instances of the
provider, pass
<EuiCode>{'globalStyles={false}'}</EuiCode>.
, or remove them completely by setting the props to{' '}
<EuiCode>false</EuiCode>:
</p>

<h3>
<EuiCode>@emotion/cache</EuiCode> and style injection location
<EuiCodeBlock language="tsx" fontSize="m" isCopyable>
{'<EuiProvider globalStyles={false} utilityClasses={false} />'}
</EuiCodeBlock>

<h3 id="cache-location">
@emotion/cache and style injection location
</h3>
<p>
In the case that your app has its own static stylesheet,{' '}
Expand All @@ -108,9 +116,7 @@ export const ProviderExample = {
<EuiCode>utility</EuiCode> properties on the{' '}
<EuiCode>cache</EuiCode> prop to further define where specific
styles should be inserted. See{' '}
<EuiLink href="#/utilities/provider#euiprovider-props">
the props documentation
</EuiLink>{' '}
<EuiLink href="#euiprovider-props">the props documentation</EuiLink>{' '}
for details.
</p>

Expand All @@ -122,13 +128,9 @@ export const ProviderExample = {
>
the <strong>createCache</strong> API
</EuiLink>{' '}
will be respected by EUI.
</p>

<p>
Note that EUI does not include the <EuiCode>@emotion/cache</EuiCode>{' '}
library, so you will need to add it to your application
dependencies.
will be respected by EUI. Note that EUI does not include the{' '}
<EuiCode>@emotion/cache</EuiCode> library, so you will need to add
it to your application dependencies.
</p>
</EuiText>
),
Expand All @@ -139,12 +141,18 @@ export const ProviderExample = {
<EuiText>
<p>
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 <EuiCode>EuiProvider</EuiCode>.
wrappers, it may be beneficial to enable logging. Doing so will
allow you to see warnings for duplicate{' '}
<strong>EuiProviders</strong>, as well as when components do not
have access to a parent <strong>EuiProvider</strong>. To enable
logging or erroring, use <EuiCode>setEuiDevProviderWarning</EuiCode>
:
</p>

<Warnings />

<p>
<EuiCode>setEuiDevProviderWarning</EuiCode> is a function that will
enable adding logging or erroring if the Provider is missing. It
<EuiCode>setEuiDevProviderWarning</EuiCode>
accepts three levels:
</p>
<ul>
Expand All @@ -157,7 +165,7 @@ export const ProviderExample = {
<EuiCode>console.warn</EuiCode>
</li>
<li>
<EuiCode>&apos;error&apos;</EuiCode>: <EuiCode>Throw</EuiCode> an
<EuiCode>&apos;error&apos;</EuiCode>: <EuiCode>Throw</EuiCode>s an
exception
</li>
</ul>
Expand Down
4 changes: 1 addition & 3 deletions src-docs/src/views/provider/provider_styles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,7 @@ const euiCache = createCache({
});
cache.compat = true;
<EuiProvider${
colorMode === 'DARK' ? ' colorMode="dark"' : ''
} cache={euiCache}'>
<EuiProvider${colorMode === 'DARK' ? ' colorMode="dark"' : ''} cache={euiCache}>
{/* Content */}
</EuiProvider>
`}
Expand Down
37 changes: 37 additions & 0 deletions src-docs/src/views/provider/provider_warning.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React from 'react';

import { EuiCodeBlock } from '../../../../src';

export default () => {
return (
<>
<EuiCodeBlock language="jsx" fontSize="m" isCopyable>
{`import { setEuiDevProviderWarning } from '@elastic/eui';
setEuiDevProviderWarning('warn');`}
</EuiCodeBlock>

<p>Examples of apps that would cause warnings:</p>

<EuiCodeBlock language="jsx" fontSize="m" isCopyable>
{`const AppWithMissingProvider = () => (
<EuiPageTemplate>
{/* Will render, but will warn about missing EuiProvider */}
</EuiPageTemplate>
);
const App = () => (
<EuiProvider>
{/* Content */}
</EuiProvider>
);
const AppWithDuplicateProvider = () => (
<EuiProvider>
{/* Will warn about multiple providers */}
<App />
</EuiProvider>
)`}
</EuiCodeBlock>
</>
);
};
29 changes: 10 additions & 19 deletions src-docs/src/views/theme/theme_example.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import { Link } from 'react-router-dom';

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

Expand Down Expand Up @@ -35,11 +36,17 @@ export const ThemeExample = {
EUI is in the progress of switching it&apos;s core styles processor
from Sass to <EuiLink href="https://emotion.sh">Emotion</EuiLink>. To
take full advantage of this context layer, wrap the root of your
application with{' '}
application with a single{' '}
<EuiLink href="#utilities/provider">
<strong>EuiProvider</strong>
</EuiLink>
.
. While <strong>EuiProvider</strong> should not be included more than
once, you may use multiple nested <strong>EuiThemeProviders</strong>{' '}
to customize section-specific or component-specific{' '}
<Link to="/theming/color-mode#rendering-a-specific-color-mode">
color modes
</Link>{' '}
or theme overrides.
</p>
</EuiText>
</>
Expand All @@ -51,23 +58,7 @@ export const ThemeExample = {
<>
<p>
The context layer that enables theming (including the default theme
styles) comes from <EuiCode>EuiThemeProvider</EuiCode>. It is a thin
wrapper around and caching layer built onto{' '}
<EuiCode>React.Context.Provider</EuiCode>.
</p>
<p>
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{' '}
<EuiLink href="#utilities/provider">
<strong>EuiProvider</strong>
</EuiLink>{' '}
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.
</p>
<p>
styles) comes from <EuiCode>EuiThemeProvider</EuiCode>.{' '}
<EuiCode>EuiThemeProvider</EuiCode> accepts three props, all of
which have default values and are therefore optional. To use the
default EUI theme, no configuration is required.
Expand Down
Loading

0 comments on commit 42ecae5

Please sign in to comment.