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

[EuiProvider] Add a check and dev warning+early return for nested usage #6949

Merged
merged 12 commits into from
Jul 17, 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
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
Loading