[context] Unify Contexts, deprecate others (#161914)

> Pre-req for https://github.com/elastic/kibana/issues/56406

## Summary

We've had a long-standing problem in Kibana around our use of React
context, particularly with EUI and i18n. There hasn't existed an
idempotent context structure, and that has lead to a lot of unexpected
results, (e.g. missing translations, inconsistent dark mode, excess
context providers, etc).

The biggest change coming from this PR is knowing exactly which provider
to use in a particular use case. This means, for example,
`ReactDOM.render` calls won't be missing `i18n` or `theme` due to a
missing context. It also allows consumers to use `darkMode` without
having to read the `uiSetting` themselves, instead allowing the context
to do it for them.

We also haven't been honoring the intended [`EuiProvider`
API](https://eui.elastic.co/#/utilities/provider#theming-and-global-styles)...
in some cases we've been creating and re-creating the Emotion caches,
often by copy/paste of the cache code. We've also been nesting
`EuiThemeProvider` contexts unnecessarily-- thinking we need to render a
theme provider in an isolated component-- which renders an additional
`span` element into the DOM.

This PR attempts to address this inconsistency by creating a set of
context providers divided by use case:


![diagram](e01c6296-1b7a-4639-ae96-946866950efe)

### `KibanaRootContextProvider`
A root context provider for Kibana. This is the top level context
provider that wraps the entire application. It is responsible for
initializing all of the other contexts and providing them to the
application. It's provided as a package for specific use cases, (e.g.
the `RenderingService`, cases where we replace the entire page content,
Storybook, testing, etc), but not intended for plugins.

### `KibanaRenderContextProvider`
A render context provider for Kibana. This context is designed to be
used with ad-hoc renders of React components, (usually with
`ReactDOM.render`).

### `KibanaThemeContextProvider`
A theme context provider for Kibana. A corollary to EUI's
`EuiThemeProvider`, it uses Kibana services to ensure the EUI Theme is
customized correctly.

### (deprecated) `KibanaStyledComponentsThemeProvider`
A styled components theme provider for Kibana. This package is supplied
for compatibility with legacy code, but should not be used in new code.

## Deprecation strategy
This PR does *not* change any use of context by consumers. It maps the
existing contexts in `kibanaReact` to the new contexts, (along with the
loose API). This means that we won't have completely fixed all of our
dark mode issues yet. But this is necessary to keep this PR focused on
the change, rather than drawing in a lot of teams to review individual
uses.

We should, however, see an immediate performance improvement in the UI
from the reduction in `EuiProvider` calls.

## Open questions
- [ ] Does it make sense to expose a `useTheme` hook from
`@kbn/react-kibana-context-theme` to replace `useEuiTheme`?

## Next steps
- [ ] Update deprecated uses to new contexts.
- [ ] Audit and update calls to `ReactDOM.render`.
- [ ] Add ESLint rule to warn for use of EUI contexts.
- [ ] Delete code from `kibanaReact`.
This commit is contained in:
Clint Andrew Hall 2023-07-28 18:30:08 +02:00 committed by GitHub
parent 0ecc28b3f2
commit 477505a2dd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
112 changed files with 2140 additions and 1367 deletions

9
.github/CODEOWNERS vendored
View file

@ -541,6 +541,12 @@ src/plugins/presentation_util @elastic/kibana-presentation
x-pack/plugins/profiling @elastic/profiling-ui
x-pack/packages/kbn-random-sampling @elastic/kibana-visualizations
packages/kbn-react-field @elastic/kibana-data-discovery
packages/react/kibana_context/common @elastic/appex-sharedux
packages/react/kibana_context/render @elastic/appex-sharedux
packages/react/kibana_context/root @elastic/appex-sharedux
packages/react/kibana_context/styled @elastic/appex-sharedux
packages/react/kibana_context/theme @elastic/appex-sharedux
packages/react/kibana_mount @elastic/appex-sharedux
x-pack/plugins/remote_clusters @elastic/platform-deployment-management
test/plugin_functional/plugins/rendering_plugin @elastic/kibana-core
packages/kbn-repo-file-maps @elastic/kibana-operations
@ -1299,6 +1305,9 @@ x-pack/plugins/translations/translations
# Profiling api integration testing
x-pack/test/profiling_api_integration @elastic/profiling-ui
# Shared UX
packages/react @elastic/appex-sharedux
####
## These rules are always last so they take ultimate priority over everything else
####

View file

@ -83,6 +83,7 @@
"newsfeed": "src/plugins/newsfeed",
"presentationUtil": "src/plugins/presentation_util",
"randomSampling": "x-pack/packages/kbn-random-sampling",
"reactPackages": "packages/react",
"textBasedEditor": "packages/kbn-text-based-editor",
"reporting": "packages/kbn-reporting/common",
"savedObjects": "src/plugins/saved_objects",

View file

@ -549,6 +549,12 @@
"@kbn/profiling-plugin": "link:x-pack/plugins/profiling",
"@kbn/random-sampling": "link:x-pack/packages/kbn-random-sampling",
"@kbn/react-field": "link:packages/kbn-react-field",
"@kbn/react-kibana-context-common": "link:packages/react/kibana_context/common",
"@kbn/react-kibana-context-render": "link:packages/react/kibana_context/render",
"@kbn/react-kibana-context-root": "link:packages/react/kibana_context/root",
"@kbn/react-kibana-context-styled": "link:packages/react/kibana_context/styled",
"@kbn/react-kibana-context-theme": "link:packages/react/kibana_context/theme",
"@kbn/react-kibana-mount": "link:packages/react/kibana_mount",
"@kbn/remote-clusters-plugin": "link:x-pack/plugins/remote_clusters",
"@kbn/rendering-plugin": "link:test/plugin_functional/plugins/rendering_plugin",
"@kbn/repo-info": "link:packages/kbn-repo-info",

View file

@ -3,7 +3,7 @@
exports[`#add() deletes all children of rootDomElement and renders <FatalErrorScreen /> into it: fatal error screen component 1`] = `
Array [
Array [
<CoreContextProvider
<KibanaRootContextProvider
globalStyles={true}
i18n={
Object {
@ -20,7 +20,7 @@ Array [
errorInfo$={Rx.Observable}
kibanaVersion="kibanaVersion"
/>
</CoreContextProvider>,
</KibanaRootContextProvider>,
<div />,
],
]

View file

@ -14,7 +14,7 @@ import type { InternalInjectedMetadataSetup } from '@kbn/core-injected-metadata-
import type { ThemeServiceSetup } from '@kbn/core-theme-browser';
import type { I18nStart } from '@kbn/core-i18n-browser';
import type { FatalErrorInfo, FatalErrorsSetup } from '@kbn/core-fatal-errors-browser';
import { CoreContextProvider } from '@kbn/core-theme-browser-internal';
import { KibanaRootContextProvider } from '@kbn/react-kibana-context-root';
import { FatalErrorsScreen } from './fatal_errors_screen';
import { getErrorInfo } from './get_error_info';
@ -95,13 +95,13 @@ export class FatalErrorsService {
this.rootDomElement.appendChild(container);
render(
<CoreContextProvider i18n={i18n} theme={theme} globalStyles={true}>
<KibanaRootContextProvider i18n={i18n} theme={theme} globalStyles={true}>
<FatalErrorsScreen
buildNumber={injectedMetadata.getKibanaBuildNumber()}
kibanaVersion={injectedMetadata.getKibanaVersion()}
errorInfo$={this.errorInfo$}
/>
</CoreContextProvider>,
</KibanaRootContextProvider>,
container
);
}

View file

@ -15,7 +15,6 @@
"kbn_references": [
"@kbn/core-injected-metadata-browser-internal",
"@kbn/core-theme-browser",
"@kbn/core-theme-browser-internal",
"@kbn/core-i18n-browser",
"@kbn/core-fatal-errors-browser",
"@kbn/i18n-react",
@ -23,6 +22,7 @@
"@kbn/core-theme-browser-mocks",
"@kbn/test-subj-selector",
"@kbn/test-jest-helpers",
"@kbn/react-kibana-context-root",
],
"exclude": [
"target/**/*",

View file

@ -3,7 +3,7 @@
exports[`#start() renders the GlobalToastList into the targetDomElement param 1`] = `
Array [
Array [
<CoreContextProvider
<KibanaRenderContextProvider
i18n={
Object {
"Context": [Function],
@ -33,7 +33,7 @@ Array [
}
}
/>
</CoreContextProvider>,
</KibanaRenderContextProvider>,
<div
test="target-dom-element"
/>,

View file

@ -23,7 +23,7 @@ import { FormattedMessage } from '@kbn/i18n-react';
import type { I18nStart } from '@kbn/core-i18n-browser';
import type { OverlayStart } from '@kbn/core-overlays-browser';
import { ThemeServiceStart } from '@kbn/core-theme-browser';
import { CoreContextProvider } from '@kbn/core-theme-browser-internal';
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
interface ErrorToastProps {
title: string;
@ -71,7 +71,7 @@ export function showErrorDialog({
const modal = openModal(
mount(
<CoreContextProvider i18n={i18n} theme={theme}>
<KibanaRenderContextProvider i18n={i18n} theme={theme}>
<EuiModalHeader>
<EuiModalHeaderTitle>{title}</EuiModalHeaderTitle>
</EuiModalHeader>
@ -94,7 +94,7 @@ export function showErrorDialog({
/>
</EuiButton>
</EuiModalFooter>
</CoreContextProvider>
</KibanaRenderContextProvider>
)
);
}

View file

@ -11,9 +11,9 @@ import { render, unmountComponentAtNode } from 'react-dom';
import type { ThemeServiceStart } from '@kbn/core-theme-browser';
import type { I18nStart } from '@kbn/core-i18n-browser';
import { CoreContextProvider } from '@kbn/core-theme-browser-internal';
import type { IUiSettingsClient } from '@kbn/core-ui-settings-browser';
import type { OverlayStart } from '@kbn/core-overlays-browser';
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
import { GlobalToastList } from './global_toast_list';
import { ToastsApi } from './toasts_api';
@ -42,12 +42,12 @@ export class ToastsService {
this.targetDomElement = targetDomElement;
render(
<CoreContextProvider i18n={i18n} theme={theme}>
<KibanaRenderContextProvider i18n={i18n} theme={theme}>
<GlobalToastList
dismissToast={(toastId: string) => this.api!.remove(toastId)}
toasts$={this.api!.get$()}
/>
</CoreContextProvider>,
</KibanaRenderContextProvider>,
targetDomElement
);

View file

@ -17,7 +17,6 @@
"@kbn/i18n",
"@kbn/utility-types",
"@kbn/core-theme-browser",
"@kbn/core-theme-browser-internal",
"@kbn/core-i18n-browser",
"@kbn/core-ui-settings-browser",
"@kbn/core-overlays-browser",
@ -29,6 +28,7 @@
"@kbn/core-overlays-browser-mocks",
"@kbn/core-theme-browser-mocks",
"@kbn/core-mount-utils-browser",
"@kbn/react-kibana-context-render",
],
"exclude": [
"target/**/*",

View file

@ -11,7 +11,7 @@ Array [
exports[`FlyoutService openFlyout() renders a flyout to the DOM 1`] = `
Array [
Array [
<CoreContextProvider
<KibanaRenderContextProvider
i18n={
Object {
"Context": [MockFunction],
@ -33,7 +33,7 @@ Array [
mount={[Function]}
/>
</EuiFlyout>
</CoreContextProvider>,
</KibanaRenderContextProvider>,
<div />,
],
]
@ -44,21 +44,50 @@ exports[`FlyoutService openFlyout() renders a flyout to the DOM 2`] = `"<div dat
exports[`FlyoutService openFlyout() with a currently active flyout replaces the current flyout with a new one 1`] = `
Array [
Array [
<CoreContextProvider
<KibanaRenderContextProvider
i18n={
Object {
"Context": [MockFunction] {
"calls": Array [
Array [
Object {
"children": <CoreThemeProvider
globalStyles={false}
theme$={
Observable {
"_subscribe": [Function],
"children": <KibanaThemeProvider
theme={
Object {
"theme$": Observable {
"_subscribe": [Function],
},
}
}
>
<EuiErrorBoundary>
<EuiFlyout
onClose={[Function]}
>
<MountWrapper
className="kbnOverlayMountWrapper"
mount={[Function]}
/>
</EuiFlyout>
</EuiErrorBoundary>
</KibanaThemeProvider>,
},
Object {},
],
],
"results": Array [
Object {
"type": "return",
"value": <KibanaThemeProvider
theme={
Object {
"theme$": Observable {
"_subscribe": [Function],
},
}
}
>
<EuiErrorBoundary>
<EuiFlyout
onClose={[Function]}
>
@ -67,31 +96,8 @@ Array [
mount={[Function]}
/>
</EuiFlyout>
</CoreThemeProvider>,
},
Object {},
],
],
"results": Array [
Object {
"type": "return",
"value": <CoreThemeProvider
globalStyles={false}
theme$={
Observable {
"_subscribe": [Function],
}
}
>
<EuiFlyout
onClose={[Function]}
>
<MountWrapper
className="kbnOverlayMountWrapper"
mount={[Function]}
/>
</EuiFlyout>
</CoreThemeProvider>,
</EuiErrorBoundary>
</KibanaThemeProvider>,
},
],
},
@ -113,25 +119,54 @@ Array [
mount={[Function]}
/>
</EuiFlyout>
</CoreContextProvider>,
</KibanaRenderContextProvider>,
<div />,
],
Array [
<CoreContextProvider
<KibanaRenderContextProvider
i18n={
Object {
"Context": [MockFunction] {
"calls": Array [
Array [
Object {
"children": <CoreThemeProvider
globalStyles={false}
theme$={
Observable {
"_subscribe": [Function],
"children": <KibanaThemeProvider
theme={
Object {
"theme$": Observable {
"_subscribe": [Function],
},
}
}
>
<EuiErrorBoundary>
<EuiFlyout
onClose={[Function]}
>
<MountWrapper
className="kbnOverlayMountWrapper"
mount={[Function]}
/>
</EuiFlyout>
</EuiErrorBoundary>
</KibanaThemeProvider>,
},
Object {},
],
],
"results": Array [
Object {
"type": "return",
"value": <KibanaThemeProvider
theme={
Object {
"theme$": Observable {
"_subscribe": [Function],
},
}
}
>
<EuiErrorBoundary>
<EuiFlyout
onClose={[Function]}
>
@ -140,31 +175,8 @@ Array [
mount={[Function]}
/>
</EuiFlyout>
</CoreThemeProvider>,
},
Object {},
],
],
"results": Array [
Object {
"type": "return",
"value": <CoreThemeProvider
globalStyles={false}
theme$={
Observable {
"_subscribe": [Function],
}
}
>
<EuiFlyout
onClose={[Function]}
>
<MountWrapper
className="kbnOverlayMountWrapper"
mount={[Function]}
/>
</EuiFlyout>
</CoreThemeProvider>,
</EuiErrorBoundary>
</KibanaThemeProvider>,
},
],
},
@ -186,7 +198,7 @@ Array [
mount={[Function]}
/>
</EuiFlyout>
</CoreContextProvider>,
</KibanaRenderContextProvider>,
<div />,
],
]

View file

@ -14,10 +14,10 @@ import { render, unmountComponentAtNode } from 'react-dom';
import { Subject } from 'rxjs';
import type { ThemeServiceStart } from '@kbn/core-theme-browser';
import type { I18nStart } from '@kbn/core-i18n-browser';
import { CoreContextProvider } from '@kbn/core-theme-browser-internal';
import type { MountPoint, OverlayRef } from '@kbn/core-mount-utils-browser';
import { MountWrapper } from '@kbn/core-mount-utils-browser-internal';
import type { OverlayFlyoutOpenOptions, OverlayFlyoutStart } from '@kbn/core-overlays-browser';
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
/**
* A FlyoutRef is a reference to an opened flyout panel. It offers methods to
@ -101,11 +101,11 @@ export class FlyoutService {
};
render(
<CoreContextProvider i18n={i18n} theme={theme}>
<KibanaRenderContextProvider i18n={i18n} theme={theme}>
<EuiFlyout {...options} onClose={onCloseFlyout}>
<MountWrapper mount={mount} className="kbnOverlayMountWrapper" />
</EuiFlyout>
</CoreContextProvider>,
</KibanaRenderContextProvider>,
this.targetDomElement
);

View file

@ -15,7 +15,6 @@ import { render, unmountComponentAtNode } from 'react-dom';
import { Subject } from 'rxjs';
import type { ThemeServiceStart } from '@kbn/core-theme-browser';
import type { I18nStart } from '@kbn/core-i18n-browser';
import { CoreContextProvider } from '@kbn/core-theme-browser-internal';
import type { MountPoint, OverlayRef } from '@kbn/core-mount-utils-browser';
import { MountWrapper } from '@kbn/core-mount-utils-browser-internal';
import type {
@ -23,6 +22,7 @@ import type {
OverlayModalOpenOptions,
OverlayModalStart,
} from '@kbn/core-overlays-browser';
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
/**
* A ModalRef is a reference to an opened modal. It offers methods to
@ -87,11 +87,11 @@ export class ModalService {
this.activeModal = modal;
render(
<CoreContextProvider i18n={i18n} theme={theme}>
<KibanaRenderContextProvider i18n={i18n} theme={theme}>
<EuiModal {...options} onClose={() => modal.close()}>
<MountWrapper mount={mount} className="kbnOverlayMountWrapper" />
</EuiModal>
</CoreContextProvider>,
</KibanaRenderContextProvider>,
targetDomElement
);
@ -147,9 +147,9 @@ export class ModalService {
};
render(
<CoreContextProvider i18n={i18n} theme={theme}>
<KibanaRenderContextProvider i18n={i18n} theme={theme}>
<EuiConfirmModal {...props} />
</CoreContextProvider>,
</KibanaRenderContextProvider>,
targetDomElement
);
});

View file

@ -15,7 +15,6 @@
"kbn_references": [
"@kbn/i18n-react",
"@kbn/core-theme-browser",
"@kbn/core-theme-browser-internal",
"@kbn/core-mount-utils-browser-internal",
"@kbn/core-i18n-browser",
"@kbn/core-ui-settings-browser",
@ -25,6 +24,7 @@
"@kbn/core-mount-utils-browser",
"@kbn/core-theme-browser-mocks",
"@kbn/i18n",
"@kbn/react-kibana-context-render",
],
"exclude": [
"target/**/*",

View file

@ -106,7 +106,7 @@ describe('RenderingService#start', () => {
`);
});
it('adds global styles via `CoreContextProvider` `globalStyles` configuration', () => {
it('adds global styles via `KibanaRootRenderingContext` `globalStyles` configuration', () => {
startService();
expect(document.querySelector(`style[data-emotion="eui-styles-global"]`)).toBeDefined();
});

View file

@ -12,7 +12,7 @@ import { pairwise, startWith } from 'rxjs/operators';
import type { ThemeServiceStart } from '@kbn/core-theme-browser';
import type { I18nStart } from '@kbn/core-i18n-browser';
import { CoreContextProvider } from '@kbn/core-theme-browser-internal';
import { KibanaRootContextProvider } from '@kbn/react-kibana-context-root';
import type { OverlayStart } from '@kbn/core-overlays-browser';
import type { InternalApplicationStart } from '@kbn/core-application-browser-internal';
import type { InternalChromeStart } from '@kbn/core-chrome-browser-internal';
@ -51,7 +51,7 @@ export class RenderingService {
});
ReactDOM.render(
<CoreContextProvider i18n={i18n} theme={theme} globalStyles={true}>
<KibanaRootContextProvider i18n={i18n} theme={theme} globalStyles={true}>
<>
{/* Fixed headers */}
{chromeHeader}
@ -68,7 +68,7 @@ export class RenderingService {
{appComponent}
</AppWrapper>
</>
</CoreContextProvider>,
</KibanaRootContextProvider>,
targetDomElement
);
}

View file

@ -16,7 +16,6 @@
"@kbn/core-application-common",
"@kbn/core-application-browser-internal",
"@kbn/core-theme-browser",
"@kbn/core-theme-browser-internal",
"@kbn/core-i18n-browser",
"@kbn/core-overlays-browser",
"@kbn/core-chrome-browser-internal",
@ -25,6 +24,7 @@
"@kbn/core-overlays-browser-mocks",
"@kbn/core-theme-browser-mocks",
"@kbn/core-i18n-browser-mocks",
"@kbn/react-kibana-context-root",
],
"exclude": [
"target/**/*",

View file

@ -9,4 +9,3 @@
export { ThemeService } from './src/theme_service';
export { CoreThemeProvider } from './src/core_theme_provider';
export type { ThemeServiceSetupDeps } from './src/theme_service';
export { CoreContextProvider } from './src/core_context_provider';

View file

@ -1,19 +0,0 @@
/*
* 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 { convertCoreTheme } from './convert_core_theme';
describe('convertCoreTheme', () => {
it('returns the correct `colorMode` when `darkMode` is enabled', () => {
expect(convertCoreTheme({ darkMode: true }).colorMode).toEqual('DARK');
});
it('returns the correct `colorMode` when `darkMode` is disabled', () => {
expect(convertCoreTheme({ darkMode: false }).colorMode).toEqual('LIGHT');
});
});

View file

@ -1,25 +0,0 @@
/*
* 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 { COLOR_MODES_STANDARD } from '@elastic/eui';
import type { EuiThemeSystem, EuiThemeColorModeStandard } from '@elastic/eui';
import type { CoreTheme } from '@kbn/core-theme-browser';
/** @internal */
export interface EuiTheme {
colorMode: EuiThemeColorModeStandard;
euiThemeSystem?: EuiThemeSystem;
}
/** @internal */
export const convertCoreTheme = (coreTheme: CoreTheme): EuiTheme => {
const { darkMode } = coreTheme;
return {
colorMode: darkMode ? COLOR_MODES_STANDARD.dark : COLOR_MODES_STANDARD.light,
};
};

View file

@ -1,40 +0,0 @@
/*
* 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, { FC } from 'react';
import type { ThemeServiceStart } from '@kbn/core-theme-browser';
import type { I18nStart } from '@kbn/core-i18n-browser';
import { CoreThemeProvider } from './core_theme_provider';
interface CoreContextProviderProps {
theme: ThemeServiceStart;
i18n: I18nStart;
globalStyles?: boolean;
}
/**
* utility component exposing all the context providers required by core when integrating with react
**/
export const CoreContextProvider: FC<CoreContextProviderProps> = ({
i18n,
theme,
children,
globalStyles = false,
}) => {
// `globalStyles` and `utilityClasses` default values are inverted from that of `EuiProvider`.
// Default to `false` (does not add EUI global styles) because more instances use that value.
// A value of `true` (does add EUI global styles) will have `EuiProvider` use its default value.
const includeGlobalStyles = globalStyles === false ? false : undefined;
return (
<i18n.Context>
<CoreThemeProvider theme$={theme.theme$} globalStyles={includeGlobalStyles}>
{children}
</CoreThemeProvider>
</i18n.Context>
);
};

View file

@ -6,61 +6,25 @@
* Side Public License, v 1.
*/
import React, { FC, useMemo } from 'react';
import React, { type FC } from 'react';
import { CoreTheme } from '@kbn/core-theme-browser/src/types';
import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme';
import { Observable } from 'rxjs';
import useObservable from 'react-use/lib/useObservable';
import createCache from '@emotion/cache';
import { EuiProvider } from '@elastic/eui';
import { EUI_STYLES_GLOBAL, EUI_STYLES_UTILS } from '@kbn/core-base-common';
import type { CoreTheme } from '@kbn/core-theme-browser';
import { convertCoreTheme } from './convert_core_theme';
const defaultTheme: CoreTheme = {
darkMode: false,
};
interface CoreThemeProviderProps {
theme$: Observable<CoreTheme>;
globalStyles?: boolean;
}
const globalCache = createCache({
key: EUI_STYLES_GLOBAL,
container: document.querySelector(`meta[name="${EUI_STYLES_GLOBAL}"]`) as HTMLElement,
});
globalCache.compat = true;
const utilitiesCache = createCache({
key: EUI_STYLES_UTILS,
container: document.querySelector(`meta[name="${EUI_STYLES_UTILS}"]`) as HTMLElement,
});
utilitiesCache.compat = true;
const emotionCache = createCache({
key: 'css',
container: document.querySelector('meta[name="emotion"]') as HTMLElement,
});
emotionCache.compat = true;
/**
* Wrapper around `EuiProvider` converting (and exposing) core's theme to EUI theme.
* @internal Only meant to be used within core for internal usages of EUI/React
* @deprecated use `KibanaThemeProvider` from `@kbn/react-kibana-context-theme
*/
export const CoreThemeProvider: FC<CoreThemeProviderProps> = ({
theme$,
children,
globalStyles,
}) => {
const coreTheme = useObservable(theme$, defaultTheme);
const euiTheme = useMemo(() => convertCoreTheme(coreTheme), [coreTheme]);
const includeGlobalStyles = globalStyles === false ? false : undefined;
return (
<EuiProvider
globalStyles={includeGlobalStyles}
utilityClasses={includeGlobalStyles}
colorMode={euiTheme.colorMode}
theme={euiTheme.euiThemeSystem}
cache={{ default: emotionCache, global: globalCache, utility: utilitiesCache }}
>
{children}
</EuiProvider>
);
};
children,
}) => (
<KibanaThemeProvider {...{ theme: { theme$ }, globalStyles }}>{children}</KibanaThemeProvider>
);

View file

@ -13,12 +13,11 @@
"**/*.tsx",
],
"kbn_references": [
"@kbn/core-base-common",
"@kbn/core-injected-metadata-browser-internal",
"@kbn/core-theme-browser",
"@kbn/core-i18n-browser",
"@kbn/core-injected-metadata-browser-mocks",
"@kbn/test-jest-helpers",
"@kbn/react-kibana-context-theme",
],
"exclude": [
"target/**/*",

View file

@ -6,44 +6,38 @@
* Side Public License, v 1.
*/
import React from 'react';
import { EUI_STYLES_GLOBAL, EUI_STYLES_UTILS } from '@kbn/core-base-common';
import { EuiProvider } from '@elastic/eui';
import createCache from '@emotion/cache';
import React, { useEffect } from 'react';
import type { DecoratorFn } from '@storybook/react';
import { I18nProvider } from '@kbn/i18n-react';
import 'core_styles';
import { BehaviorSubject } from 'rxjs';
import { CoreTheme } from '@kbn/core-theme-browser';
import { I18nStart } from '@kbn/core-i18n-browser';
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
const theme$ = new BehaviorSubject<CoreTheme>({ darkMode: false });
const i18n: I18nStart = {
Context: ({ children }) => <I18nProvider>{children}</I18nProvider>,
};
/**
* Storybook decorator using the EUI provider. Uses the value from
* `globals` provided by the Storybook theme switcher.
* Storybook decorator using the `KibanaContextProvider`. Uses the value from
* `globals` provided by the Storybook theme switcher to set the `colorMode`.
*/
const EuiProviderDecorator: DecoratorFn = (storyFn, { globals }) => {
const KibanaContextDecorator: DecoratorFn = (storyFn, { globals }) => {
const colorMode = globals.euiTheme === 'v8.dark' ? 'dark' : 'light';
const globalCache = createCache({
key: EUI_STYLES_GLOBAL,
container: document.querySelector(`meta[name="${EUI_STYLES_GLOBAL}"]`) as HTMLElement,
});
globalCache.compat = true;
const utilitiesCache = createCache({
key: EUI_STYLES_UTILS,
container: document.querySelector(`meta[name="${EUI_STYLES_UTILS}"]`) as HTMLElement,
});
utilitiesCache.compat = true;
const emotionCache = createCache({
key: 'css',
container: document.querySelector('meta[name="emotion"]') as HTMLElement,
});
emotionCache.compat = true;
useEffect(() => {
theme$.next({ darkMode: colorMode === 'dark' });
}, [colorMode]);
return (
<EuiProvider
colorMode={colorMode}
cache={{ default: emotionCache, global: globalCache, utility: utilitiesCache }}
>
<KibanaRenderContextProvider {...{ theme: { theme$ }, i18n }}>
{storyFn()}
</EuiProvider>
</KibanaRenderContextProvider>
);
};
export const decorators = [EuiProviderDecorator];
export const decorators = [KibanaContextDecorator];

View file

@ -16,7 +16,10 @@
"@kbn/ui-shared-deps-src",
"@kbn/repo-info",
"@kbn/dev-cli-runner",
"@kbn/core-base-common",
"@kbn/core-theme-browser",
"@kbn/i18n-react",
"@kbn/core-i18n-browser",
"@kbn/react-kibana-context-render",
],
"exclude": [
"target/**/*",

View file

@ -76,7 +76,7 @@ module.exports = {
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
snapshotSerializers: [
'<rootDir>/src/plugins/kibana_react/public/util/test_helpers/react_mount_serializer.ts',
'<rootDir>/packages/react/kibana_mount/test_helpers/react_mount_serializer.ts',
'enzyme-to-json/serializer',
'<rootDir>/packages/kbn-test/src/jest/setup/emotion.js',
],

View file

@ -0,0 +1,22 @@
---
id: react/context
slug: /react/context
title: React Contexts in Kibana
description: Kibana uses React Context to manage several global states. This is a collection of packages supporting those states.
tags: ['shared-ux', 'react', 'context']
date: 2023-07-25
---
## Description
Kibana uses React Context to manage several global states. Those states have been divided into several reusable components in relevant packages.
![Architecture Diagram](./assets/diagram.png)
- `KibanaRootContextProvider` - A root context provider for Kibana. This is the top level context provider that wraps the entire application. It is responsible for initializing all of the other contexts and providing them to the application.
- `KibanaRenderContextProvider` - A render context provider for Kibana. This context is designed to be used with ad-hoc renders of React components, (usually with `ReactDOM.render`).
- `KibanaThemeContextProvider` - A theme context provider for Kibana. A corollary to EUI's `EuiThemeProvider`, it uses Kibana services to ensure the EUI Theme is customized correctly.
## Deprecated Context Providers
- `KibanaStyledComponentsThemeProvider` - A styled components theme provider for Kibana. This package is supplied for compatibility with legacy code, but should not be used in new code.

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

View file

@ -0,0 +1,13 @@
---
id: react/context/common
slug: /react/context/common
title: React Context - Common Types and Utilities
description: The React contexts Kibana uses have a lot of common types and utilities. This package is a collection of those types and utilities.
tags: ['shared-ux', 'react', 'context']
date: 2023-07-25
---
## Description
This package contains common types and utilities used by the different React context providers in Kibana.

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import { getColorMode } from './utils';
import { getColorMode } from './color_mode';
describe('getColorMode', () => {
it('returns the correct `colorMode` when `darkMode` is enabled', () => {

View file

@ -8,12 +8,13 @@
import { COLOR_MODES_STANDARD } from '@elastic/eui';
import type { EuiThemeColorModeStandard } from '@elastic/eui';
import type { CoreTheme } from '@kbn/core/public';
import type { KibanaTheme } from './types';
/**
* Copied from the `kibana_react` plugin, to avoid cyclical dependency
* Given a `KibanaTheme`, provide a color mode for use with EUI.
* @param theme KibanaTheme
* @returns EuiThemeColorModeStandard
*/
export const getColorMode = (theme: CoreTheme): EuiThemeColorModeStandard => {
export const getColorMode = (theme: KibanaTheme): EuiThemeColorModeStandard => {
return theme.darkMode ? COLOR_MODES_STANDARD.dark : COLOR_MODES_STANDARD.light;
};

View file

@ -0,0 +1,20 @@
/*
* 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 { getColorMode } from './color_mode';
export type { KibanaTheme, ThemeServiceStart } from './types';
import type { KibanaTheme } from './types';
/**
* The default `KibanaTheme` for use in Storybook, Jest, or initialization. At
* runtime, the theme should always be provided by the `ThemeService`.
*/
export const defaultTheme: KibanaTheme = {
darkMode: false,
};

View file

@ -6,7 +6,8 @@
* Side Public License, v 1.
*/
export { wrapWithTheme } from './wrap_with_theme';
export { KibanaThemeProvider } from './kibana_theme_provider';
export { useKibanaTheme } from './use_theme';
export type { EuiTheme } from './types';
module.exports = {
preset: '@kbn/test/jest_node',
rootDir: '../../../..',
roots: ['<rootDir>/packages/react/kibana_context/common'],
};

View file

@ -0,0 +1,5 @@
{
"type": "shared-common",
"id": "@kbn/react-kibana-context-common",
"owner": "@elastic/appex-sharedux"
}

View file

@ -0,0 +1,6 @@
{
"name": "@kbn/react-kibana-context-common",
"private": true,
"version": "1.0.0",
"license": "SSPL-1.0 OR Elastic License 2.0"
}

View file

@ -0,0 +1,17 @@
{
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
"types": [
"jest",
"node"
]
},
"include": [
"**/*.ts",
],
"exclude": [
"target/**/*"
],
"kbn_references": []
}

View file

@ -0,0 +1,33 @@
/*
* 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 { Observable } from 'rxjs';
// To avoid a circular dependency with the deprecation of `CoreThemeProvider`,
// we need to define the theme type here.
//
// TODO: clintandrewhall - remove this file once `CoreThemeProvider` is removed
/**
* The representation of the Kibana theme, (not to be confused with the EUI theme).
*/
export interface KibanaTheme {
/** is dark mode enabled or not */
readonly darkMode: boolean;
}
// To avoid a circular dependency with the deprecation of `CoreThemeProvider`,
// we need to define the theme type here.
//
// TODO: clintandrewhall - remove this file once `CoreThemeProvider` is removed
/**
* The `ThemeService` start contract, provided to plugins during the `start` lifecycle.
*/
export interface ThemeServiceStart {
theme$: Observable<KibanaTheme>;
}

View file

@ -0,0 +1,31 @@
---
id: react/context/render
slug: /react/context/render
title: React Context - Rendering Provider
description: This context provider is used to render a new component tree _without_ the initialization of EUI or Emotion. This provider is typically used with `ReactDOM.render()` calls.
tags: ['shared-ux', 'react', 'context']
date: 2023-07-25
---
## Description
The `KibanaRenderContextProvider` is designed to be used with ad-hoc renders of React components, (usually with `ReactDOM.render`).
When Kibana starts, the `KibanaRootContextProvider` is used by the `RenderService` to initialize EUI and Emotion... it should only be rendered _once_. Still, there are times when you need to render a new component tree and need things like `i18n` and the current theme to be made available in a consistent way. The `KibanaRenderContextProvider` is designed to be used in these cases. In addition, it allows us to abstract away any changes or other complexities with React context that we may introduce in the future.
```ts
import ReactDOM from 'react-dom';
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
import { MyComponent } from './my_component';
const App = ({i18n, theme}: CoreStart) => {
return (
<KibanaRenderContextProvider {...{i18n, theme}}>
<MyComponent />
</KibanaRenderContextProvider>
);
};
ReactDOM.render(<App />, document.getElementById('some_node'));
```

View file

@ -6,6 +6,7 @@
* Side Public License, v 1.
*/
import type { UseEuiTheme } from '@elastic/eui';
export type EuiTheme = UseEuiTheme;
export {
KibanaRenderContextProvider,
type KibanaRenderContextProviderProps,
} from './render_provider';

View file

@ -0,0 +1,13 @@
/*
* 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.
*/
module.exports = {
preset: '@kbn/test',
rootDir: '../../../..',
roots: ['<rootDir>/packages/react/kibana_context/render'],
};

View file

@ -0,0 +1,5 @@
{
"type": "shared-common",
"id": "@kbn/react-kibana-context-render",
"owner": "@elastic/appex-sharedux"
}

View file

@ -0,0 +1,6 @@
{
"name": "@kbn/react-kibana-context-render",
"private": true,
"version": "1.0.0",
"license": "SSPL-1.0 OR Elastic License 2.0"
}

View file

@ -0,0 +1,40 @@
/*
* 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, { FC } from 'react';
import { EuiErrorBoundary } from '@elastic/eui';
import type { I18nStart } from '@kbn/core-i18n-browser';
import {
KibanaThemeProvider,
type KibanaThemeProviderProps,
} from '@kbn/react-kibana-context-theme';
/** Props for the KibanaContextProvider */
export interface KibanaRenderContextProviderProps extends KibanaThemeProviderProps {
/** The `I18nStart` API from `CoreStart`. */
i18n: I18nStart;
}
/**
* The `KibanaRenderContextProvider` provides the necessary context for an out-of-current
* React render, such as using `ReactDOM.render()`.
*/
export const KibanaRenderContextProvider: FC<KibanaRenderContextProviderProps> = ({
children,
i18n,
...props
}) => {
return (
<i18n.Context>
<KibanaThemeProvider {...props}>
<EuiErrorBoundary>{children}</EuiErrorBoundary>
</KibanaThemeProvider>
</i18n.Context>
);
};

View file

@ -0,0 +1,22 @@
{
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
"types": [
"jest",
"node",
"react"
]
},
"include": [
"**/*.ts",
"**/*.tsx",
],
"exclude": [
"target/**/*"
],
"kbn_references": [
"@kbn/core-i18n-browser",
"@kbn/react-kibana-context-theme",
]
}

View file

@ -0,0 +1,47 @@
---
id: react/context/root
slug: /react/context/root
title: React Context - Root
description: This context provider is used only used by the very base root of Kibana. Unless you're writing tests, a Storybook, or working in core code, you likely don't need this.
tags: ['shared-ux', 'react', 'context']
date: 2023-07-25
---
## Description
This package contains a root context provider for Kibana rendering. It handles operations that should only happen _once_ when the browser loads a page.
While it would be safer to isolate this in a `core` package, we need to use it in other contexts-- like Storybook and Jest.
```ts
import React, { useEffect } from 'react';
import { BehaviorSubject } from 'rxjs';
import { I18nProvider } from '@kbn/i18n-react';
import { KibanaRootContextProvider } from '@kbn/react-kibana-context-root';
import type { DecoratorFn } from '@storybook/react';
import type { CoreTheme } from '@kbn/core-theme-browser';
import type { I18nStart } from '@kbn/core-i18n-browser';
const theme$ = new BehaviorSubject<CoreTheme>({ darkMode: false });
const i18n: I18nStart = {
Context: ({ children }) => <I18nProvider>{children}</I18nProvider>,
};
export const KibanaContextDecorator: DecoratorFn = (storyFn, { globals }) => {
const colorMode = globals.euiTheme === 'v8.dark' ? 'dark' : 'light';
useEffect(() => {
theme$.next({ darkMode: colorMode === 'dark' });
}, [colorMode]);
return (
<KibanaRootContextProvider {...{ theme: { theme$ }, i18n }}>
{storyFn()}
</KibanaRootContextProvider>
);
};
```

View file

@ -0,0 +1,92 @@
/*
* 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 { useEuiTheme } from '@elastic/eui';
import type { ReactWrapper } from 'enzyme';
import type { FC } from 'react';
import React, { useEffect } from 'react';
import { act } from 'react-dom/test-utils';
import { BehaviorSubject, of } from 'rxjs';
import { mountWithIntl } from '@kbn/test-jest-helpers';
import type { KibanaTheme } from '@kbn/react-kibana-context-common';
import { KibanaEuiProvider } from './eui_provider';
describe('KibanaEuiProvider', () => {
let euiTheme: ReturnType<typeof useEuiTheme> | undefined;
let consoleWarnMock: jest.SpyInstance;
beforeEach(() => {
euiTheme = undefined;
consoleWarnMock = jest.spyOn(global.console, 'warn').mockImplementation(() => {});
});
const flushPromises = async () => {
await new Promise<void>(async (resolve, reject) => {
try {
setImmediate(() => resolve());
} catch (error) {
reject(error);
}
});
};
const InnerComponent: FC = () => {
const theme = useEuiTheme();
useEffect(() => {
euiTheme = theme;
}, [theme]);
return <div>foo</div>;
};
const refresh = async (wrapper: ReactWrapper<unknown>) => {
await act(async () => {
await flushPromises();
wrapper.update();
});
};
it('exposes the EUI theme provider', async () => {
const coreTheme: KibanaTheme = { darkMode: true };
const wrapper = mountWithIntl(
<KibanaEuiProvider theme={{ theme$: of(coreTheme) }}>
<InnerComponent />
</KibanaEuiProvider>
);
await refresh(wrapper);
expect(euiTheme!.colorMode).toEqual('DARK');
expect(consoleWarnMock).not.toBeCalled();
});
it('propagates changes of the coreTheme observable', async () => {
const coreTheme$ = new BehaviorSubject<KibanaTheme>({ darkMode: true });
const wrapper = mountWithIntl(
<KibanaEuiProvider theme={{ theme$: coreTheme$ }}>
<InnerComponent />
</KibanaEuiProvider>
);
await refresh(wrapper);
expect(euiTheme!.colorMode).toEqual('DARK');
await act(async () => {
coreTheme$.next({ darkMode: false });
});
await refresh(wrapper);
expect(euiTheme!.colorMode).toEqual('LIGHT');
expect(consoleWarnMock).not.toBeCalled();
});
});

View file

@ -0,0 +1,75 @@
/*
* 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, { FC, useMemo } from 'react';
import useObservable from 'react-use/lib/useObservable';
import createCache from '@emotion/cache';
import { EuiProvider, EuiProviderProps } from '@elastic/eui';
import { EUI_STYLES_GLOBAL, EUI_STYLES_UTILS } from '@kbn/core-base-common';
import { getColorMode, defaultTheme } from '@kbn/react-kibana-context-common';
import { ThemeServiceStart } from '@kbn/react-kibana-context-common';
/**
* Props for the KibanaEuiProvider.
*/
export interface KibanaEuiProviderProps extends Pick<EuiProviderProps<{}>, 'modify' | 'colorMode'> {
theme: ThemeServiceStart;
globalStyles?: boolean;
}
// Set up the caches.
// https://eui.elastic.co/#/utilities/provider#cache-location
const emotionCache = createCache({
key: 'css',
container: document.querySelector('meta[name="emotion"]') as HTMLElement,
});
const globalCache = createCache({
key: EUI_STYLES_GLOBAL,
container: document.querySelector(`meta[name="${EUI_STYLES_GLOBAL}"]`) as HTMLElement,
});
const utilitiesCache = createCache({
key: EUI_STYLES_UTILS,
container: document.querySelector(`meta[name="${EUI_STYLES_UTILS}"]`) as HTMLElement,
});
// Enable "compat mode" in Emotion caches.
emotionCache.compat = true;
globalCache.compat = true;
utilitiesCache.compat = true;
const cache = { default: emotionCache, global: globalCache, utility: utilitiesCache };
/**
* Prepares and returns a configured `EuiProvider` for use in Kibana roots.
*/
export const KibanaEuiProvider: FC<KibanaEuiProviderProps> = ({
theme: { theme$ },
globalStyles: globalStylesProp,
colorMode: colorModeProp,
children,
}) => {
const theme = useObservable(theme$, defaultTheme);
const themeColorMode = useMemo(() => getColorMode(theme), [theme]);
// In some cases-- like in Storybook or testing-- we want to explicitly override the
// colorMode provided by the `theme`.
const colorMode = colorModeProp || themeColorMode;
// This logic was drawn from the Core theme provider, and wasn't present (or even used)
// elsewhere. Should be a passive addition to anyone using the older theme provider(s).
const globalStyles = globalStylesProp === false ? false : undefined;
return (
<EuiProvider {...{ cache, colorMode, globalStyles, utilityClasses: globalStyles }}>
{children}
</EuiProvider>
);
};

View file

@ -6,4 +6,4 @@
* Side Public License, v 1.
*/
export { KibanaThemeProvider } from './kibana_theme_provider';
export { KibanaRootContextProvider, type KibanaRootContextProviderProps } from './root_provider';

View file

@ -0,0 +1,13 @@
/*
* 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.
*/
module.exports = {
preset: '@kbn/test',
rootDir: '../../../..',
roots: ['<rootDir>/packages/react/kibana_context/root'],
};

View file

@ -0,0 +1,5 @@
{
"type": "shared-common",
"id": "@kbn/react-kibana-context-root",
"owner": "@elastic/appex-sharedux"
}

View file

@ -0,0 +1,6 @@
{
"name": "@kbn/react-kibana-context-root",
"private": true,
"version": "1.0.0",
"license": "SSPL-1.0 OR Elastic License 2.0"
}

View file

@ -13,14 +13,18 @@ import { of, BehaviorSubject } from 'rxjs';
import { useEuiTheme } from '@elastic/eui';
import type { UseEuiTheme } from '@elastic/eui';
import { mountWithIntl } from '@kbn/test-jest-helpers';
import type { CoreTheme } from '@kbn/core/public';
import { KibanaThemeProvider } from './kibana_theme_provider';
import type { KibanaTheme } from '@kbn/react-kibana-context-common';
import { i18nServiceMock } from '@kbn/core-i18n-browser-mocks';
import { KibanaRootContextProvider } from './root_provider';
import { I18nStart } from '@kbn/core-i18n-browser';
describe('KibanaThemeProvider', () => {
describe('KibanaRootContextProvider', () => {
let euiTheme: UseEuiTheme | undefined;
let i18nMock: I18nStart;
beforeEach(() => {
euiTheme = undefined;
i18nMock = i18nServiceMock.createStartContract();
});
const flushPromises = async () => {
@ -49,12 +53,12 @@ describe('KibanaThemeProvider', () => {
};
it('exposes the EUI theme provider', async () => {
const coreTheme: CoreTheme = { darkMode: true };
const coreTheme: KibanaTheme = { darkMode: true };
const wrapper = mountWithIntl(
<KibanaThemeProvider theme$={of(coreTheme)}>
<KibanaRootContextProvider i18n={i18nMock} theme={{ theme$: of(coreTheme) }}>
<InnerComponent />
</KibanaThemeProvider>
</KibanaRootContextProvider>
);
await refresh(wrapper);
@ -63,12 +67,12 @@ describe('KibanaThemeProvider', () => {
});
it('propagates changes of the coreTheme observable', async () => {
const coreTheme$ = new BehaviorSubject<CoreTheme>({ darkMode: true });
const coreTheme$ = new BehaviorSubject<KibanaTheme>({ darkMode: true });
const wrapper = mountWithIntl(
<KibanaThemeProvider theme$={coreTheme$}>
<KibanaRootContextProvider i18n={i18nMock} theme={{ theme$: coreTheme$ }}>
<InnerComponent />
</KibanaThemeProvider>
</KibanaRootContextProvider>
);
await refresh(wrapper);

View file

@ -0,0 +1,42 @@
/*
* 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 { I18nStart } from '@kbn/core-i18n-browser';
import React, { FC } from 'react';
import { KibanaEuiProvider, type KibanaEuiProviderProps } from './eui_provider';
/** Props for the KibanaRootContextProvider */
export interface KibanaRootContextProviderProps extends KibanaEuiProviderProps {
/** The `I18nStart` API from `CoreStart`. */
i18n: I18nStart;
}
/**
* The `KibanaRootContextProvider` provides the necessary context at the root of Kibana, including
* initialization and the theme and i18n contexts. This context should only be used _once_, and
* at the _very top_ of the application root, rendered by the `RenderingService`.
*
* While this context is exposed for edge cases and tooling, (e.g. Storybook, Jest, etc.), it should
* _not_ be used in applications. Instead, applications should choose the context that makes the
* most sense for the problem they are trying to solve:
*
* - Consider `KibanaRenderContextProvider` for rendering components outside the current tree, (e.g.
* with `ReactDOM.render`).
* - Consider `KibanaThemeContextProvider` for altering the theme of a component or tree of components.
*
*/
export const KibanaRootContextProvider: FC<KibanaRootContextProviderProps> = ({
children,
i18n,
...props
}) => (
<KibanaEuiProvider {...props}>
<i18n.Context>{children}</i18n.Context>
</KibanaEuiProvider>
);

View file

@ -0,0 +1,25 @@
{
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
"types": [
"jest",
"node",
"react"
]
},
"include": [
"**/*.ts",
"**/*.tsx",
],
"exclude": [
"target/**/*"
],
"kbn_references": [
"@kbn/test-jest-helpers",
"@kbn/react-kibana-context-common",
"@kbn/core-i18n-browser-mocks",
"@kbn/core-i18n-browser",
"@kbn/core-base-common",
]
}

View file

@ -0,0 +1,15 @@
---
id: react/context/styled
slug: /react/context/styled
title: React Context - `styled-components` provider
description: Before `emotion` was introduced, some components used `styled-components` to easily apply EUI variables and apply style. This package is for compatibility with those components and should _not_ be used in new code.
tags: ['shared-ux', 'react', 'context']
date: 2023-07-25
---
## Description
Before `emotion` was introduced, some components used `styled-components` to easily apply EUI variables and apply style. This package is an isolated location for this code and included for compatibility with those components.
It should _not_ be used in new code.

View file

@ -0,0 +1,26 @@
/*
* 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 {
/** @deprecated All Kibana components need to migrate to Emotion. */
createGlobalStyle,
/** @deprecated All Kibana components need to migrate to Emotion. */
type EuiTheme,
/** @deprecated All Kibana components need to migrate to Emotion. */
css,
/** @deprecated All Kibana components need to migrate to Emotion. */
euiStyled,
/** @deprecated All Kibana components need to migrate to Emotion. */
keyframes,
/** @deprecated All Kibana components need to migrate to Emotion. */
withTheme,
/** @deprecated All Kibana components need to migrate to Emotion. */
KibanaStyledComponentsThemeProvider,
/** @deprecated All Kibana components need to migrate to Emotion. */
KibanaStyledComponentsThemeProviderDecorator,
} from './styled_provider';

View file

@ -0,0 +1,13 @@
/*
* 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.
*/
module.exports = {
preset: '@kbn/test',
rootDir: '../../../..',
roots: ['<rootDir>/packages/react/kibana_context/styled'],
};

View file

@ -0,0 +1,5 @@
{
"type": "shared-common",
"id": "@kbn/react-kibana-context-styled",
"owner": "@elastic/appex-sharedux"
}

View file

@ -0,0 +1,6 @@
{
"name": "@kbn/react-kibana-context-styled",
"private": true,
"version": "1.0.0",
"license": "SSPL-1.0 OR Elastic License 2.0"
}

View file

@ -0,0 +1,84 @@
/*
* 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 type { DecoratorFn } from '@storybook/react';
import React from 'react';
// eslint-disable-next-line @kbn/eslint/module_migration
import * as styledComponents from 'styled-components';
// eslint-disable-next-line @kbn/eslint/module_migration
import { ThemedStyledComponentsModule, ThemeProvider, ThemeProviderProps } from 'styled-components';
import { euiThemeVars, euiLightVars, euiDarkVars } from '@kbn/ui-theme';
/**
* A `deprecated` structure representing a Kibana theme containing variables from the current EUI theme.
*/
export interface EuiTheme {
/** EUI theme vars that automaticall adjust to light and dark mode. */
eui: typeof euiThemeVars;
/** True if the theme is in "dark" mode, false otherwise. */
darkMode: boolean;
}
/**
* A `styled-components` `ThemeProvider` that incorporates EUI dark mode.
*/
const KibanaStyledComponentsThemeProvider = <
OuterTheme extends styledComponents.DefaultTheme = styledComponents.DefaultTheme
>({
darkMode = false,
...otherProps
}: Omit<ThemeProviderProps<OuterTheme, OuterTheme & EuiTheme>, 'theme'> & {
darkMode?: boolean;
}) => (
<ThemeProvider
{...otherProps}
theme={(outerTheme?: OuterTheme) => ({
...outerTheme,
eui: darkMode ? euiDarkVars : euiLightVars,
darkMode,
})}
/>
);
/**
* Storybook decorator using the EUI theme provider. Uses the value from
* `globals` provided by the Storybook theme switcher.
*
* @deprecated All Kibana components need to migrate to Emotion.
*/
export const KibanaStyledComponentsThemeProviderDecorator: DecoratorFn = (storyFn, { globals }) => {
const darkMode = globals.euiTheme === 'v8.dark' || globals.euiTheme === 'v7.dark';
return (
<KibanaStyledComponentsThemeProvider darkMode={darkMode}>
{storyFn()}
</KibanaStyledComponentsThemeProvider>
);
};
const {
/** see https://styled-components.com/docs/api#styled */
default: euiStyled,
/** see https://styled-components.com/docs/api#css-prop */
css,
/** see https://styled-components.com/docs/api#createglobalstyle */
createGlobalStyle,
/** see https://styled-components.com/docs/api#keyframes */
keyframes,
/** see https://styled-components.com/docs/api#withtheme */
withTheme,
} = styledComponents as unknown as ThemedStyledComponentsModule<EuiTheme>;
export {
css,
euiStyled,
KibanaStyledComponentsThemeProvider,
createGlobalStyle,
keyframes,
withTheme,
};

View file

@ -0,0 +1,21 @@
{
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
"types": [
"jest",
"node",
"react"
]
},
"include": [
"**/*.ts",
"**/*.tsx",
],
"exclude": [
"target/**/*"
],
"kbn_references": [
"@kbn/ui-theme",
]
}

View file

@ -0,0 +1,66 @@
---
id: react/context/theme
slug: /react/context/theme
title: React Context - Theme
description: This context allows a one to alter the theme for a given component. This is likely to be the context that is used most often.
tags: ['shared-ux', 'react', 'context']
date: 2023-07-25
---
## Description
This package contains a "theming" context for Kibana. A corollary to EUI's `EuiThemeProvider`, it uses Kibana services to ensure the EUI Theme is customized correctly.
Up until now, there has been some confusion as to the difference between `EuiThemeProvider` and `EuiProvider`. They've been used interchangeably, which created some unnoticed
side effects. In addition, _nesting_ of `EuiThemeProvider` has led to additional nodes being rendered to the DOM.
This context allows us to have a single source of truth for the theme in Kibana, and ensures that the theme is applied correctly. It also abstracts away any updates or changes
made to the `EuiThemeProvider` in the future.
```ts
// Make a component always display in dark mode.
import { BehaviorSubject } from 'rxjs';
import { KibanaThemeContextProvider, type KibanaTheme } from '@kbn/react-kibana-context-theme';
import { MyComponent } from './my_component';
export const AlwaysDarkMode = () => {
// We've purposefully excluded `colorMode` from the props of the provider
// to enforce the use of the `theme$` observable. This prevents consumers
// from confusing which takes precedence, (or what needs to be set in most
// cases).
const theme$ = new BehaviorSubject<KibanaTheme>({ darkMode: true }));
return (
<KibanaThemeContextProvider theme={{ theme$ }}>
<MyComponent />
</KibanaThemeContextProvider>
);
};
import { EuiThemeShape, RecursivePartial } from '@elastic/eui';
// Change the EUI theme colors in dark and light mode.
export const ChangeEuiTheme = ({ theme }: CoreStart) => {
const modify: RecursivePartial<EuiThemeShape> = {
colors: {
DARK: {
text: '#abc',
accent: '#123',
},
LIGHT: {
text: '#123',
accent: '#abc',
},
},
};
return (
<KibanaThemeProvider {...{ theme, modify }}>
<MyComponent />
</KibanaThemeProvider>
);
};
```

View file

@ -0,0 +1,13 @@
/*
* 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 { KibanaThemeProvider, type KibanaThemeProviderProps } from './theme_provider';
export { wrapWithTheme } from './with_theme';
// Re-exporting from @kbn/react-kibana-context-common for convenience to consumers.
export { defaultTheme, type KibanaTheme } from '@kbn/react-kibana-context-common';

View file

@ -0,0 +1,13 @@
/*
* 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.
*/
module.exports = {
preset: '@kbn/test',
rootDir: '../../../..',
roots: ['<rootDir>/packages/react/kibana_context/theme'],
};

View file

@ -0,0 +1,5 @@
{
"type": "shared-common",
"id": "@kbn/react-kibana-context-theme",
"owner": "@elastic/appex-sharedux"
}

View file

@ -0,0 +1,6 @@
{
"name": "@kbn/react-kibana-context-theme",
"private": true,
"version": "1.0.0",
"license": "SSPL-1.0 OR Elastic License 2.0"
}

View file

@ -11,12 +11,12 @@ import type { ReactWrapper } from 'enzyme';
import type { FC } from 'react';
import React, { useEffect } from 'react';
import { act } from 'react-dom/test-utils';
import { BehaviorSubject, of } from 'rxjs';
import { BehaviorSubject } from 'rxjs';
import type { CoreTheme } from '@kbn/core/public';
import { mountWithIntl } from '@kbn/test-jest-helpers';
import { KibanaThemeProvider } from './kibana_theme_provider';
import type { KibanaTheme } from '@kbn/react-kibana-context-common';
import { KibanaThemeProvider } from './theme_provider';
describe('KibanaThemeProvider', () => {
let euiTheme: ReturnType<typeof useEuiTheme> | undefined;
@ -51,10 +51,10 @@ describe('KibanaThemeProvider', () => {
};
it('exposes the EUI theme provider', async () => {
const coreTheme: CoreTheme = { darkMode: true };
const coreTheme$ = new BehaviorSubject<KibanaTheme>({ darkMode: true });
const wrapper = mountWithIntl(
<KibanaThemeProvider theme$={of(coreTheme)}>
<KibanaThemeProvider theme={{ theme$: coreTheme$ }}>
<InnerComponent />
</KibanaThemeProvider>
);
@ -65,10 +65,10 @@ describe('KibanaThemeProvider', () => {
});
it('propagates changes of the coreTheme observable', async () => {
const coreTheme$ = new BehaviorSubject<CoreTheme>({ darkMode: true });
const coreTheme$ = new BehaviorSubject<KibanaTheme>({ darkMode: true });
const wrapper = mountWithIntl(
<KibanaThemeProvider theme$={coreTheme$}>
<KibanaThemeProvider theme={{ theme$: coreTheme$ }}>
<InnerComponent />
</KibanaThemeProvider>
);

View file

@ -0,0 +1,63 @@
/*
* 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, { useMemo } from 'react';
import useObservable from 'react-use/lib/useObservable';
import {
CurrentEuiBreakpointProvider,
EuiThemeProvider,
EuiThemeProviderProps,
} from '@elastic/eui';
import {
getColorMode,
defaultTheme,
type ThemeServiceStart,
} from '@kbn/react-kibana-context-common';
// Extract the `theme` from `EuiThemeProviderProps` as a type.
type EuiTheme<T = {}> = EuiThemeProviderProps<T>['theme'];
// Omit the `theme` and `colorMode` props from `EuiThemeProviderProps` so we can
// add our own `euiTheme` prop and derive `colorMode` from the Kibana theme.
interface EuiProps<T = {}> extends Omit<EuiThemeProviderProps<T>, 'theme' | 'colorMode'> {
euiTheme?: EuiTheme<T>;
}
/**
* Props for the `KibanaThemeProvider`.
*/
export interface KibanaThemeProviderProps extends EuiProps {
/** The `ThemeServiceStart` API. */
theme: ThemeServiceStart;
}
/**
* A Kibana-specific theme provider that uses the Kibana theme service to customize the EUI theme.
*/
export const KibanaThemeProvider = ({
theme: { theme$ },
euiTheme: theme,
children,
...props
}: KibanaThemeProviderProps) => {
const kibanaTheme = useObservable(theme$, defaultTheme);
const colorMode = useMemo(() => getColorMode(kibanaTheme), [kibanaTheme]);
// We have to add a breakpoint provider, because the `EuiProvider` we were using-- instead
// of `EuiThemeProvider`-- adds a breakpoint. Without it here now, several Kibana layouts
// break, particularly sidebars.
//
// We can investigate removing it later, but I'm adding it here for now.
return (
<EuiThemeProvider {...{ colorMode, theme, ...props }}>
<CurrentEuiBreakpointProvider>{children}</CurrentEuiBreakpointProvider>
</EuiThemeProvider>
);
};

View file

@ -0,0 +1,22 @@
{
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
"types": [
"jest",
"node",
"react"
]
},
"include": [
"**/*.ts",
"**/*.tsx",
],
"exclude": [
"target/**/*"
],
"kbn_references": [
"@kbn/test-jest-helpers",
"@kbn/react-kibana-context-common",
]
}

View file

@ -0,0 +1,21 @@
/*
* 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 { ThemeServiceStart } from '@kbn/react-kibana-context-common';
import React from 'react';
import { KibanaThemeProvider } from './theme_provider';
/**
* A React HOC that wraps a component with the `KibanaThemeProvider`.
* @param node The node to wrap.
* @param theme The `ThemeServiceStart` API.
*/
export const wrapWithTheme = (node: React.ReactNode, theme: ThemeServiceStart) => (
<KibanaThemeProvider {...{ theme }}>{node}</KibanaThemeProvider>
);

View file

@ -0,0 +1,3 @@
# @kbn/react-kibana-mount
Empty package generated by @kbn/generate

View file

@ -7,6 +7,7 @@
*/
export { toMountPoint } from './to_mount_point';
export type { ToMountPointOptions } from './to_mount_point';
export type { ToMountPointParams } from './to_mount_point';
export { MountPointPortal } from './mount_point_portal';
export type { MountPointPortalProps } from './mount_point_portal';
export { useIfMounted } from './utils';

View file

@ -0,0 +1,13 @@
/*
* 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.
*/
module.exports = {
preset: '@kbn/test',
rootDir: '../../..',
roots: ['<rootDir>/packages/react/kibana_mount'],
};

View file

@ -0,0 +1,5 @@
{
"type": "shared-common",
"id": "@kbn/react-kibana-mount",
"owner": "@elastic/appex-sharedux"
}

View file

@ -12,7 +12,7 @@ import ReactDOM from 'react-dom';
import { MountPoint } from '@kbn/core/public';
import { useIfMounted } from './utils';
interface MountPointPortalProps {
export interface MountPointPortalProps {
setMountPoint: (mountPoint: MountPoint<HTMLElement>) => void;
}
@ -48,7 +48,7 @@ export const MountPointPortal: React.FC<MountPointPortalProps> = ({ children, se
el.current = undefined;
});
};
}, [setMountPoint]);
}, [setMountPoint, ifMounted]);
if (shouldRender && el.current) {
return ReactDOM.createPortal(
@ -77,7 +77,7 @@ class MountPointPortalErrorBoundary extends Component<{}, { error?: unknown }> {
if (this.state.error) {
return (
<p>
{i18n.translate('kibana-react.mountPointPortal.errorMessage', {
{i18n.translate('reactPackages.mountPointPortal.errorMessage', {
defaultMessage: 'Error rendering portal content',
})}
</p>

View file

@ -0,0 +1,6 @@
{
"name": "@kbn/react-kibana-mount",
"private": true,
"version": "1.0.0",
"license": "SSPL-1.0 OR Elastic License 2.0"
}

View file

@ -13,13 +13,11 @@ import { useEuiTheme } from '@elastic/eui';
import type { UseEuiTheme } from '@elastic/eui';
import type { CoreTheme } from '@kbn/core/public';
import { toMountPoint } from './to_mount_point';
import { i18nServiceMock } from '@kbn/core-i18n-browser-mocks';
describe('toMountPoint', () => {
let euiTheme: UseEuiTheme | undefined;
beforeEach(() => {
euiTheme = undefined;
});
let euiTheme: UseEuiTheme;
const i18n = i18nServiceMock.createStartContract();
const InnerComponent: FC = () => {
const theme = useEuiTheme();
@ -40,8 +38,8 @@ describe('toMountPoint', () => {
};
it('exposes the euiTheme when `theme$` is provided', async () => {
const theme$ = of<CoreTheme>({ darkMode: true });
const mount = toMountPoint(<InnerComponent />, { theme$ });
const theme = { theme$: of<CoreTheme>({ darkMode: true }) };
const mount = toMountPoint(<InnerComponent />, { theme, i18n });
const targetEl = document.createElement('div');
mount(targetEl);
@ -54,7 +52,7 @@ describe('toMountPoint', () => {
it('propagates changes of the theme$ observable', async () => {
const theme$ = new BehaviorSubject<CoreTheme>({ darkMode: true });
const mount = toMountPoint(<InnerComponent />, { theme$ });
const mount = toMountPoint(<InnerComponent />, { theme: { theme$ }, i18n });
const targetEl = document.createElement('div');
mount(targetEl);

View file

@ -8,32 +8,32 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { Observable } from 'rxjs';
import { I18nProvider } from '@kbn/i18n-react';
import type { MountPoint, CoreTheme } from '@kbn/core/public';
import { KibanaThemeProvider } from '../theme/kibana_theme_provider';
import type { MountPoint } from '@kbn/core/public';
import {
KibanaRenderContextProvider,
KibanaRenderContextProviderProps,
} from '@kbn/react-kibana-context-render';
export interface ToMountPointOptions {
theme$?: Observable<CoreTheme>;
}
export type ToMountPointParams = Pick<KibanaRenderContextProviderProps, 'i18n' | 'theme'>;
/**
* MountPoint converter for react nodes.
*
* @param node to get a mount point for
*/
export const toMountPoint = (
node: React.ReactNode,
{ theme$ }: ToMountPointOptions = {}
): MountPoint => {
const content = theme$ ? <KibanaThemeProvider theme$={theme$}>{node}</KibanaThemeProvider> : node;
export const toMountPoint = (node: React.ReactNode, params: ToMountPointParams): MountPoint => {
const mount = (element: HTMLElement) => {
ReactDOM.render(<I18nProvider>{content}</I18nProvider>, element);
ReactDOM.render(
<KibanaRenderContextProvider {...params}>{node}</KibanaRenderContextProvider>,
element
);
return () => ReactDOM.unmountComponentAtNode(element);
};
// only used for tests and snapshots serialization
if (process.env.NODE_ENV !== 'production') {
mount.__reactMount__ = node;
}
return mount;
};

View file

@ -0,0 +1,24 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
"types": [
"jest",
"node",
"react"
]
},
"include": [
"**/*.ts",
"**/*.tsx",
],
"exclude": [
"target/**/*"
],
"kbn_references": [
"@kbn/core",
"@kbn/i18n",
"@kbn/core-i18n-browser-mocks",
"@kbn/react-kibana-context-render",
]
}

View file

@ -141,7 +141,7 @@ export const CodeEditor: React.FC<Props> = ({
overrideEditorWillMount,
editorDidMount,
editorWillMount,
useDarkTheme,
useDarkTheme: useDarkThemeProp,
transparentBackground,
suggestionProvider,
signatureProvider,
@ -154,7 +154,8 @@ export const CodeEditor: React.FC<Props> = ({
isCopyable = false,
allowFullScreen = false,
}) => {
const { euiTheme } = useEuiTheme();
const { colorMode, euiTheme } = useEuiTheme();
const useDarkTheme = useDarkThemeProp ?? colorMode === 'DARK';
// We need to be able to mock the MonacoEditor in our test in order to not test implementation
// detail and not have to call methods on the <CodeEditor /> component instance.

View file

@ -15,7 +15,6 @@ type PropArguments = Pick<
| 'value'
| 'aria-label'
| 'allowFullScreen'
| 'useDarkTheme'
| 'transparentBackground'
| 'placeholder'
>;
@ -58,12 +57,6 @@ export class CodeEditorStorybookMock extends AbstractStorybookMock<
},
defaultValue: false,
},
useDarkTheme: {
control: {
type: 'boolean',
},
defaultValue: false,
},
transparentBackground: {
control: {
type: 'boolean',
@ -87,7 +80,6 @@ export class CodeEditorStorybookMock extends AbstractStorybookMock<
value: this.getArgumentValue('value', params),
'aria-label': this.getArgumentValue('aria-label', params),
allowFullScreen: this.getArgumentValue('allowFullScreen', params),
useDarkTheme: this.getArgumentValue('useDarkTheme', params),
transparentBackground: this.getArgumentValue('transparentBackground', params),
placeholder: this.getArgumentValue('placeholder', params),
};

View file

@ -21,12 +21,6 @@ jest.mock('@kbn/kibana-react-plugin/public/ui_settings/use_ui_setting', () => ({
useUiSetting: jest.fn(),
}));
jest.mock('@kbn/kibana-react-plugin/public/theme/use_theme', () => ({
useKibanaTheme: jest.fn(() => {
return { darkMode: false };
}),
}));
const defaults = {
requiresPageReload: false,
readOnly: false,

View file

@ -0,0 +1,24 @@
/*
* 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 type { EuiProviderProps } from '@elastic/eui';
import React, { type FC } from 'react';
import type { Observable } from 'rxjs';
import type { CoreTheme } from '@kbn/core-theme-browser';
import { KibanaThemeProvider as KbnThemeProvider } from '@kbn/react-kibana-context-theme';
export interface KibanaThemeProviderProps {
theme$: Observable<CoreTheme>;
modify?: EuiProviderProps<{}>['modify'];
}
/** @deprecated use `KibanaThemeProvider` from `@kbn/react-kibana-context-theme */
export const KibanaThemeProvider: FC<KibanaThemeProviderProps> = ({ theme$, modify, children }) => (
<KbnThemeProvider {...{ theme: { theme$ }, modify }}>{children}</KbnThemeProvider>
);

View file

@ -1,64 +0,0 @@
/*
* 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 type { EuiProviderProps } from '@elastic/eui';
import { EuiProvider } from '@elastic/eui';
import createCache from '@emotion/cache';
import type { FC } from 'react';
import React, { useMemo } from 'react';
import useObservable from 'react-use/lib/useObservable';
import type { Observable } from 'rxjs';
import { EUI_STYLES_GLOBAL, EUI_STYLES_UTILS } from '@kbn/core-base-common';
import type { CoreTheme } from '@kbn/core/public';
import { getColorMode } from './utils';
interface KibanaThemeProviderProps {
theme$: Observable<CoreTheme>;
modify?: EuiProviderProps<{}>['modify'];
}
const defaultTheme: CoreTheme = {
darkMode: false,
};
const globalCache = createCache({
key: EUI_STYLES_GLOBAL,
container: document.querySelector(`meta[name="${EUI_STYLES_GLOBAL}"]`) as HTMLElement,
});
globalCache.compat = true;
const utilitiesCache = createCache({
key: EUI_STYLES_UTILS,
container: document.querySelector(`meta[name="${EUI_STYLES_UTILS}"]`) as HTMLElement,
});
utilitiesCache.compat = true;
const emotionCache = createCache({
key: 'css',
container: document.querySelector('meta[name="emotion"]') as HTMLElement,
});
emotionCache.compat = true;
/**
* Copied from the `kibana_react` plugin, remove once https://github.com/elastic/kibana/issues/119204 is implemented.
*/
export const KibanaThemeProvider: FC<KibanaThemeProviderProps> = ({ theme$, modify, children }) => {
const theme = useObservable(theme$, defaultTheme);
const colorMode = useMemo(() => getColorMode(theme), [theme]);
return (
<EuiProvider
colorMode={colorMode}
cache={{ default: emotionCache, global: globalCache, utility: utilitiesCache }}
globalStyles={false}
utilityClasses={false}
modify={modify}
>
{children}
</EuiProvider>
);
};

View file

@ -1,19 +0,0 @@
/*
* 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 { getColorMode } from './utils';
describe('getColorMode', () => {
it('returns the correct `colorMode` when `darkMode` is enabled', () => {
expect(getColorMode({ darkMode: true })).toEqual('DARK');
});
it('returns the correct `colorMode` when `darkMode` is disabled', () => {
expect(getColorMode({ darkMode: false })).toEqual('LIGHT');
});
});

View file

@ -1,19 +0,0 @@
/*
* 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 { COLOR_MODES_STANDARD } from '@elastic/eui';
import type { EuiThemeColorModeStandard } from '@elastic/eui';
import type { CoreTheme } from '@kbn/core/public';
/**
* Copied from the `kibana_react` plugin, remove once https://github.com/elastic/kibana/issues/119204 is implemented.
*/
export const getColorMode = (theme: CoreTheme): EuiThemeColorModeStandard => {
return theme.darkMode ? COLOR_MODES_STANDARD.dark : COLOR_MODES_STANDARD.light;
};

View file

@ -3,7 +3,11 @@
"compilerOptions": {
"outDir": "target/types",
},
"include": ["common/**/*", "public/**/*", "server/**/*"],
"include": [
"common/**/*",
"public/**/*",
"server/**/*"
],
"kbn_references": [
"@kbn/core",
"@kbn/i18n-react",
@ -19,7 +23,8 @@
"@kbn/utils",
"@kbn/core-logging-server-mocks",
"@kbn/core-preboot-server",
"@kbn/core-base-common",
"@kbn/react-kibana-context-theme",
"@kbn/core-theme-browser",
],
"exclude": [
"target/**/*",

View file

@ -6,51 +6,14 @@
* Side Public License, v 1.
*/
import type { DecoratorFn } from '@storybook/react';
import React from 'react';
import * as styledComponents from 'styled-components';
import { ThemedStyledComponentsModule, ThemeProvider, ThemeProviderProps } from 'styled-components';
import { euiThemeVars, euiLightVars, euiDarkVars } from '@kbn/ui-theme';
export interface EuiTheme {
eui: typeof euiThemeVars;
darkMode: boolean;
}
const EuiThemeProvider = <
OuterTheme extends styledComponents.DefaultTheme = styledComponents.DefaultTheme
>({
darkMode = false,
...otherProps
}: Omit<ThemeProviderProps<OuterTheme, OuterTheme & EuiTheme>, 'theme'> & {
darkMode?: boolean;
}) => (
<ThemeProvider
{...otherProps}
theme={(outerTheme?: OuterTheme) => ({
...outerTheme,
eui: darkMode ? euiDarkVars : euiLightVars,
darkMode,
})}
/>
);
/**
* Storybook decorator using the EUI theme provider. Uses the value from
* `globals` provided by the Storybook theme switcher.
*/
export const EuiThemeProviderDecorator: DecoratorFn = (storyFn, { globals }) => {
const darkMode = globals.euiTheme === 'v8.dark' || globals.euiTheme === 'v7.dark';
return <EuiThemeProvider darkMode={darkMode}>{storyFn()}</EuiThemeProvider>;
};
const {
default: euiStyled,
export {
css,
euiStyled,
KibanaStyledComponentsThemeProvider as EuiThemeProvider,
createGlobalStyle,
keyframes,
withTheme,
} = styledComponents as unknown as ThemedStyledComponentsModule<EuiTheme>;
KibanaStyledComponentsThemeProviderDecorator as EuiThemeProviderDecorator,
} from '@kbn/react-kibana-context-styled';
export { css, euiStyled, EuiThemeProvider, createGlobalStyle, keyframes, withTheme };
export type { EuiTheme } from '@kbn/react-kibana-context-styled';

View file

@ -20,6 +20,7 @@ import {
EuiCopy,
EuiFlexGroup,
EuiFlexItem,
useEuiTheme,
} from '@elastic/eui';
import { monaco } from '@kbn/monaco';
import { i18n } from '@kbn/i18n';
@ -140,7 +141,7 @@ export const CodeEditor: React.FC<Props> = ({
overrideEditorWillMount,
editorDidMount,
editorWillMount,
useDarkTheme,
useDarkTheme: useDarkThemeProp,
transparentBackground,
suggestionProvider,
signatureProvider,
@ -153,6 +154,9 @@ export const CodeEditor: React.FC<Props> = ({
isCopyable = false,
allowFullScreen = false,
}) => {
const { colorMode } = useEuiTheme();
const useDarkTheme = useDarkThemeProp ?? colorMode === 'DARK';
// We need to be able to mock the MonacoEditor in our test in order to not test implementation
// detail and not have to call methods on the <CodeEditor /> component instance.
const MonacoEditor: typeof ReactMonacoEditor = useMemo(() => {

View file

@ -7,9 +7,8 @@
*/
import React from 'react';
import { EuiDelayRender, EuiErrorBoundary, EuiSkeletonText } from '@elastic/eui';
import { EuiDelayRender, EuiErrorBoundary, EuiSkeletonText, useEuiTheme } from '@elastic/eui';
import { useKibanaTheme } from '../theme';
import type { Props } from './code_editor';
export * from './languages/constants';
@ -40,11 +39,12 @@ export type CodeEditorProps = Props;
* @see CodeEditorField to render a code editor in the same style as other EUI form fields.
*/
export const CodeEditor: React.FunctionComponent<Props> = (props) => {
const coreTheme = useKibanaTheme();
const { colorMode } = useEuiTheme();
return (
<EuiErrorBoundary>
<React.Suspense fallback={<Fallback height={props.height} />}>
<LazyBaseEditor {...props} useDarkTheme={coreTheme.darkMode} />
<LazyBaseEditor {...props} useDarkTheme={colorMode === 'DARK'} />
</React.Suspense>
</EuiErrorBoundary>
);
@ -54,11 +54,12 @@ export const CodeEditor: React.FunctionComponent<Props> = (props) => {
* Renders a Monaco code editor in the same style as other EUI form fields.
*/
export const CodeEditorField: React.FunctionComponent<Props> = (props) => {
const coreTheme = useKibanaTheme();
const { colorMode } = useEuiTheme();
return (
<EuiErrorBoundary>
<React.Suspense fallback={<Fallback height={props.height} />}>
<LazyCodeEditorField {...props} useDarkTheme={coreTheme.darkMode} />
<LazyCodeEditorField {...props} useDarkTheme={colorMode === 'DARK'} />
</React.Suspense>
</EuiErrorBoundary>
);

View file

@ -54,10 +54,13 @@ export const createKibanaReactContext = <Services extends KibanaServices>(
() => createKibanaReactContext({ ...services, ...oldValue.services, ...newServices }),
[services, oldValue, newServices]
);
return createElement(context.Provider, {
const newProvider = createElement(context.Provider, {
value: newValue,
children,
});
return newProvider;
};
return {

View file

@ -86,7 +86,8 @@ export type { ToMountPointOptions } from './util';
/** @deprecated Use `RedirectAppLinks` from `@kbn/shared-ux-link-redirect-app` */
export { RedirectAppLinks } from './app_links';
export { wrapWithTheme, KibanaThemeProvider, useKibanaTheme } from './theme';
/** @deprecated Use `KibanaThemeProvider`, `wrapWithTheme` from `@kbn/react-kibana-context-theme` */
export { KibanaThemeProvider, wrapWithTheme, type KibanaThemeProviderProps } from './theme';
/** dummy plugin, we just want kibanaReact to have its own bundle */
export function plugin() {

View file

@ -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 React from 'react';
import {
KibanaThemeProvider as KbnThemeProvider,
KibanaThemeProviderProps as KbnThemeProviderProps,
wrapWithTheme as kbnWrapWithTheme,
} from '@kbn/react-kibana-context-theme';
/** @deprecated Use `KibanaThemeProviderProps` from `@kbn/react-kibana-context-theme` */
export type KibanaThemeProviderProps = Pick<KbnThemeProviderProps, 'children' | 'modify'> &
KbnThemeProviderProps['theme'];
/** @deprecated Use `KibanaThemeProvider` from `@kbn/react-kibana-context-theme` */
export const KibanaThemeProvider = ({ children, theme$, modify }: KibanaThemeProviderProps) => (
<KbnThemeProvider theme={{ theme$ }} {...modify}>
{children}
</KbnThemeProvider>
);
type Theme = KbnThemeProviderProps['theme']['theme$'];
export const wrapWithTheme = (node: React.ReactNode, theme$: Theme) =>
kbnWrapWithTheme(node, { theme$ });

View file

@ -1,61 +0,0 @@
/*
* 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, { FC, useMemo } from 'react';
import useObservable from 'react-use/lib/useObservable';
import { Observable } from 'rxjs';
import { EuiProvider, EuiProviderProps } from '@elastic/eui';
import createCache from '@emotion/cache';
import { EUI_STYLES_GLOBAL, EUI_STYLES_UTILS } from '@kbn/core-base-common';
import type { CoreTheme } from '@kbn/core/public';
import { getColorMode } from './utils';
interface KibanaThemeProviderProps {
theme$: Observable<CoreTheme>;
modify?: EuiProviderProps<{}>['modify'];
}
const defaultTheme: CoreTheme = {
darkMode: false,
};
const globalCache = createCache({
key: EUI_STYLES_GLOBAL,
container: document.querySelector(`meta[name="${EUI_STYLES_GLOBAL}"]`) as HTMLElement,
});
globalCache.compat = true;
const utilitiesCache = createCache({
key: EUI_STYLES_UTILS,
container: document.querySelector(`meta[name="${EUI_STYLES_UTILS}"]`) as HTMLElement,
});
utilitiesCache.compat = true;
const emotionCache = createCache({
key: 'css',
container: document.querySelector('meta[name="emotion"]') as HTMLElement,
});
emotionCache.compat = true;
/* IMPORTANT: This code has been copied to the `interactive_setup` plugin, any changes here should be applied there too.
That copy and this comment can be removed once https://github.com/elastic/kibana/issues/119204 is implemented.*/
// IMPORTANT: This code has been copied to the `kibana_utils` plugin, to avoid cyclical dependency, any changes here should be applied there too.
export const KibanaThemeProvider: FC<KibanaThemeProviderProps> = ({ theme$, modify, children }) => {
const theme = useObservable(theme$, defaultTheme);
const colorMode = useMemo(() => getColorMode(theme), [theme]);
return (
<EuiProvider
colorMode={colorMode}
cache={{ default: emotionCache, global: globalCache, utility: utilitiesCache }}
globalStyles={false}
utilityClasses={false}
modify={modify}
>
{children}
</EuiProvider>
);
};

View file

@ -1,58 +0,0 @@
/*
* 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, { FC, useEffect } from 'react';
import { mountWithIntl } from '@kbn/test-jest-helpers';
import type { CoreTheme } from '@kbn/core/public';
import { KibanaContextProvider } from '../context';
import { themeServiceMock } from '@kbn/core/public/mocks';
import { useKibanaTheme } from './use_theme';
import { of } from 'rxjs';
describe('useKibanaTheme', () => {
let resultTheme: CoreTheme | undefined;
beforeEach(() => {
resultTheme = undefined;
});
const InnerComponent: FC = () => {
const theme = useKibanaTheme();
useEffect(() => {
resultTheme = theme;
}, [theme]);
return <div>foo</div>;
};
it('retrieve CoreTheme when theme service is provided in context', async () => {
const expectedCoreTheme: CoreTheme = { darkMode: true };
const themeServiceStart = themeServiceMock.createStartContract();
themeServiceStart.theme$ = of({ darkMode: true });
mountWithIntl(
<KibanaContextProvider services={{ theme: themeServiceStart }}>
<InnerComponent />
</KibanaContextProvider>
);
expect(resultTheme).toEqual(expectedCoreTheme);
});
it('does not throw error when theme service is not provided, default theme applied', async () => {
const expectedCoreTheme: CoreTheme = { darkMode: false };
mountWithIntl(
<KibanaContextProvider>
<InnerComponent />
</KibanaContextProvider>
);
expect(resultTheme).toEqual(expectedCoreTheme);
});
});

View file

@ -1,30 +0,0 @@
/*
* 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 { CoreTheme } from '@kbn/core-theme-browser';
import useObservable from 'react-use/lib/useObservable';
import { of } from 'rxjs';
import { useKibana } from '../context/context';
const defaultTheme: CoreTheme = { darkMode: false };
export const useKibanaTheme = (): CoreTheme => {
const {
services: { theme },
} = useKibana();
let themeObservable;
if (!theme) {
themeObservable = of(defaultTheme);
} else {
themeObservable = theme.theme$;
}
return useObservable(themeObservable, defaultTheme);
};

Some files were not shown because too many files have changed in this diff Show more