[Shared UX] Adopt multi-package strategy - ExitFullScreenButton (#130355)

* [Shared UX] Adopt multi-package strategy - ExitFullScreenButton

* Fixing checks errors

* Fix i18n

* Remove SharedUX deps from Maps

Co-authored-by: Spencer <spencer@elastic.co>
This commit is contained in:
Clint Andrew Hall 2022-04-15 19:12:46 -05:00 committed by GitHub
parent a54d0d66db
commit e47bf4b205
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 886 additions and 303 deletions

1
.github/CODEOWNERS vendored
View file

@ -630,6 +630,7 @@ x-pack/plugins/security_solution/public/common/components/sessions_viewer @elast
## Shared UX
/src/plugins/shared_ux/ @elastic/shared-ux
/packages/shared-ux/ @elastic/shared-ux
/packages/shared-ux-*/ @elastic/shared-ux
/src/plugins/kibana_react/ @elastic/shared-ux
/src/plugins/kibana_react/public/code_editor @elastic/shared-ux @elastic/kibana-presentation

View file

@ -66,6 +66,7 @@
"share": "src/plugins/share",
"sharedUX": "src/plugins/shared_ux",
"sharedUXComponents": "packages/kbn-shared-ux-components/src",
"sharedUXPackages": "packages/shared-ux",
"coloring": "packages/kbn-coloring/src",
"statusPage": "src/legacy/core_plugins/status_page",
"telemetry": [

View file

@ -173,6 +173,7 @@
"@kbn/securitysolution-utils": "link:bazel-bin/packages/kbn-securitysolution-utils",
"@kbn/server-http-tools": "link:bazel-bin/packages/kbn-server-http-tools",
"@kbn/server-route-repository": "link:bazel-bin/packages/kbn-server-route-repository",
"@kbn/shared-ux-button-exit-full-screen": "link:bazel-bin/packages/shared-ux/button/exit_full_screen",
"@kbn/shared-ux-components": "link:bazel-bin/packages/kbn-shared-ux-components",
"@kbn/shared-ux-services": "link:bazel-bin/packages/kbn-shared-ux-services",
"@kbn/shared-ux-storybook": "link:bazel-bin/packages/kbn-shared-ux-storybook",
@ -637,6 +638,7 @@
"@types/kbn__securitysolution-utils": "link:bazel-bin/packages/kbn-securitysolution-utils/npm_module_types",
"@types/kbn__server-http-tools": "link:bazel-bin/packages/kbn-server-http-tools/npm_module_types",
"@types/kbn__server-route-repository": "link:bazel-bin/packages/kbn-server-route-repository/npm_module_types",
"@types/kbn__shared-ux-button-exit-full-screen": "link:bazel-bin/packages/shared-ux/button/exit_full_screen/npm_module_types",
"@types/kbn__shared-ux-components": "link:bazel-bin/packages/kbn-shared-ux-components/npm_module_types",
"@types/kbn__shared-ux-services": "link:bazel-bin/packages/kbn-shared-ux-services/npm_module_types",
"@types/kbn__shared-ux-storybook": "link:bazel-bin/packages/kbn-shared-ux-storybook/npm_module_types",

View file

@ -93,6 +93,7 @@ filegroup(
"//packages/kbn-ui-theme:build",
"//packages/kbn-utility-types:build",
"//packages/kbn-utils:build",
"//packages/shared-ux/button/exit_full_screen:build",
],
)
@ -170,6 +171,7 @@ filegroup(
"//packages/kbn-ui-theme:build_types",
"//packages/kbn-utility-types:build_types",
"//packages/kbn-utils:build_types",
"//packages/shared-ux/button/exit_full_screen:build_types",
],
)

View file

@ -16,10 +16,10 @@ import { REPO_ROOT } from '@kbn/utils';
* include `**` in these, one or two `*` segments is acceptable, we need this search
* to be super fast so please avoid deep recursive searching.
*
* eg. src/vis-editors => would find a package at src/vis-editors/foo/package.json
* src/vis-editors/* => would find a package at src/vis-editors/foo/bar/package.json
* eg. src/vis_editors => would find a package at src/vis_editors/foo/package.json
* src/vis_editors/* => would find a package at src/vis_editors/foo/bar/package.json
*/
export const BAZEL_PACKAGE_DIRS = ['packages'];
export const BAZEL_PACKAGE_DIRS = ['packages', 'packages/shared-ux', 'packages/shared-ux/*'];
/**
* Resolve all the BAZEL_PACKAGE_DIRS to absolute paths

View file

@ -1,148 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<ExitFullScreenButton /> is rendered 1`] = `
<SharedUxServicesProvider
application={
Object {
"currentAppId$": Observable {},
"navigateToUrl": [Function],
}
}
docLinks={
Object {
"dataViewsDocLink": "dummy link",
}
}
editors={
Object {
"openDataViewEditor": [MockFunction],
}
}
http={
Object {
"addBasePath": [MockFunction],
}
}
permissions={
Object {
"canAccessFleet": true,
"canCreateNewDataView": true,
}
}
platform={
Object {
"setIsFullscreen": [MockFunction] {
"calls": Array [
Array [
false,
],
],
"results": Array [
Object {
"type": "return",
"value": undefined,
},
],
},
}
}
>
<ExitFullScreenButton
onExit={[MockFunction]}
>
<ExitFullScreenButton
css={
Object {
"map": undefined,
"name": "knjqgf",
"next": undefined,
"styles": "
bottom: 8px;
left: 8px;
position: fixed;
z-index: 5;
",
"toString": [Function],
}
}
onClick={[Function]}
>
<div>
<EuiScreenReaderOnly>
<p
aria-live="polite"
className="euiScreenReaderOnly"
>
In full screen mode, press ESC to exit.
</p>
</EuiScreenReaderOnly>
<button
className="exitFullScreenButton"
data-test-subj="exitFullScreenModeButton"
onClick={[Function]}
>
<EuiFlexGroup
alignItems="center"
component="span"
gutterSize="s"
responsive={false}
>
<span
className="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow"
>
<EuiFlexItem
component="span"
grow={false}
>
<span
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<EuiIcon
size="m"
type="logoElastic"
>
<span
data-euiicon-type="logoElastic"
size="m"
/>
</EuiIcon>
</span>
</EuiFlexItem>
<EuiFlexItem
component="span"
data-test-subj="exitFullScreenModeText"
grow={false}
>
<span
className="euiFlexItem euiFlexItem--flexGrowZero"
data-test-subj="exitFullScreenModeText"
>
Exit full screen
</span>
</EuiFlexItem>
<EuiFlexItem
component="span"
grow={false}
>
<span
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<EuiIcon
size="s"
type="fullScreenExit"
>
<span
data-euiicon-type="fullScreenExit"
size="s"
/>
</EuiIcon>
</span>
</EuiFlexItem>
</span>
</EuiFlexGroup>
</button>
</div>
</ExitFullScreenButton>
</ExitFullScreenButton>
</SharedUxServicesProvider>
`;

View file

@ -1,20 +0,0 @@
---
id: sharedUX/Components/ExitFullScreenButton
slug: /shared-ux/components/exit-full-screen-button
title: Exit Full Screen Button
summary: A button that floats over the plugin workspace and allows one to exit "full screen" mode.
tags: ['shared-ux', 'component']
date: 2021-12-28
---
> This documentation is in-progress.
When a plugin moves to "full screen" mode, the Kibana Chrome can be hidden entirely. This button floats over the plugin workspace and allows someone to exit full screen mode and restore the Kibana Chrome.
The pure component, `exit_full_screen_button.tsx`, contains the base styles and behaviors.
The connected component, `exit_full_screen_button.tsx`, uses services from the `shared_ux` plugin to show and hide the Kibana chrome. You must wrap your plugin app in the `ServicesContext` provided by the start contract of the `shared_ux` plugin to use it.
This component is provided with `React.lazy` to avoid bundle bloat.
This component is not currently eligible for promotion to EUI.

View file

@ -1,80 +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 from 'react';
import { mount as enzymeMount, ReactWrapper } from 'enzyme';
import { keys } from '@elastic/eui';
import {
SharedUxServicesProvider,
SharedUxServices,
mockServicesFactory,
} from '@kbn/shared-ux-services';
import { ExitFullScreenButton } from './exit_full_screen_button';
describe('<ExitFullScreenButton />', () => {
let services: SharedUxServices;
let mount: (element: JSX.Element) => ReactWrapper;
beforeEach(() => {
services = mockServicesFactory();
mount = (element: JSX.Element) =>
enzymeMount(<SharedUxServicesProvider {...services}>{element}</SharedUxServicesProvider>);
});
afterEach(() => {
jest.resetAllMocks();
});
test('is rendered', () => {
const component = mount(<ExitFullScreenButton onExit={jest.fn()} />);
expect(component).toMatchSnapshot();
});
test('passing `false` to toggleChrome does not toggle chrome', () => {
const component = mount(<ExitFullScreenButton onExit={jest.fn()} toggleChrome={false} />);
expect(services.platform.setIsFullscreen).toHaveBeenCalledTimes(0);
component.unmount();
expect(services.platform.setIsFullscreen).toHaveBeenCalledTimes(0);
});
describe('onExit', () => {
const onExitHandler = jest.fn();
let component: ReactWrapper;
beforeEach(() => {
component = mount(<ExitFullScreenButton onExit={onExitHandler} toggleChrome={true} />);
});
test('is called when the button is pressed', () => {
expect(services.platform.setIsFullscreen).toHaveBeenLastCalledWith(false);
component.find('button').simulate('click');
expect(onExitHandler).toHaveBeenCalledTimes(1);
component.unmount();
expect(services.platform.setIsFullscreen).toHaveBeenLastCalledWith(true);
});
test('is called when the ESC key is pressed', () => {
expect(services.platform.setIsFullscreen).toHaveBeenLastCalledWith(false);
const escapeKeyEvent = new KeyboardEvent('keydown', { key: keys.ESCAPE } as any);
document.dispatchEvent(escapeKeyEvent);
expect(onExitHandler).toHaveBeenCalledTimes(1);
component.unmount();
expect(services.platform.setIsFullscreen).toHaveBeenLastCalledWith(true);
});
});
});

View file

@ -9,16 +9,6 @@
import React from 'react';
import { withSuspense } from '@kbn/shared-ux-utility';
/**
* The Lazily-loaded `ExitFullScreenButton` component. Consumers should use `React.Suspense` or the
* `withSuspense` HOC to load this component.
*/
export const LazyExitFullScreenButton = React.lazy(() =>
import('./exit_full_screen_button').then(({ ExitFullScreenButton }) => ({
default: ExitFullScreenButton,
}))
);
export const LazyToolbarButton = React.lazy(() =>
import('./toolbar/index').then(({ ToolbarButton }) => ({
default: ToolbarButton,
@ -27,13 +17,6 @@ export const LazyToolbarButton = React.lazy(() =>
export const RedirectAppLinks = React.lazy(() => import('./redirect_app_links'));
/**
* A `ExitFullScreenButton` component that is wrapped by the `withSuspense` HOC. This component can
* be used directly by consumers and will load the `LazyExitFullScreenButton` component lazily with
* a predefined fallback and error boundary.
*/
export const ExitFullScreenButton = withSuspense(LazyExitFullScreenButton);
/**
* A `ToolbarButton` component that is wrapped by the `withSuspense` HOC. This component can
* be used directly by consumers and will load the `LazyToolbarButton` component lazily with

View file

@ -11,6 +11,7 @@ import { defaultConfig } from '@kbn/storybook';
module.exports = {
...defaultConfig,
stories: [
'../../../shared-ux/**/*.stories.+(tsx|mdx)',
'../../../kbn-shared-ux*/**/*.stories.+(tsx|mdx)',
'../../../../src/plugins/shared_ux/**/*.stories.+(tsx|mdx)',
],

View file

@ -0,0 +1,118 @@
# Shared UX Packages
This directory contains directories of packages of shared components and other code for use in Kibana solutions.
## How to use these components
Each package exports one or more components that can be consumed.
### Lazy by default
All components are exported to be lazily-loaded with a default `React.Suspense` default most appropriate to its nature.
If a solution needs to alter the `React.Suspense` behavior, (e.g. a different "loading" component), one can import the `Lazy[ComponentName]` version and surround it with a custom `React.Suspense` component.
### "Pure" and "Connected" components
If a package contains a component with functionality that relies on a Kibana core or plugin dependency, there are two components exported: a `pure` component and a `connected` component.
__Pure__ components:
- are focused on how a component looks and behaves;
- have their props and handlers exposed as simple types;
- have no logic specific to Kibana.
__Connected__ components, by contrast:
- *compose* their pure counterparts;
- rely on Kibana core and plugin dependencies to provide Kibana-specific logic;
- require a `ContextProvider` packaged with the component to provide stateful services from a start contract.
For example, the `ExitFullScreenButton` "pure" component is a button that is styled with the appropriate translated text. It is simple and without dependency.
The "connected" component *composes* that pure component and:
- applies EUI theme state;
- uses the `coreStart.chrome.setIsVisible` API to change the full screen state `onClick`;
- applies `emotion` styles to position the button in the window.
### Connected component providers
Connected components are accompanied by a `Provider` which is intended to provide their external services. We typically provide two: one that abstracts away the dependency into a simplified set of functions, and one that maps to the intended dependency directly.
For example, the `ExitFullScreenButton` relies on the `coreStart.chrome.setIsVisible` API to interact with the full screen state. The package contains two providers.
The `ExitFullScreenButtonProvider` simply expects a single function, `setIsFullScreen`. This pattern is useful for more complicated components that may rely on a number of dependencies:
```
<ExitFullScreenButtonProvider setIsFullScreen={...}>
<ExitFullScreenButton />
</ExitFullScreenButtonProvider>
```
The `ExitFullScreenButtonKibanaProvider` creates a facsimile of the `coreStart` contract type, containing only the portions it expects to use. This is a kind of "syntactic-sugar-workaround" to the fact plugin start contracts are not typically available to packages:
```
<ExitFullScreenButtonKibanaProvider coreStart={...}>
<ExitFullScreenButton />
</ExitFullScreenButtonProvider>
```
Plugins can use either of these providers in their plugin at either the root of their plugin application or at any level of their React tree, wherever it makes sense. Component compositions can do the same. Either Provider can be used, depending on the situation.
## How can I contribute a component?
*__Yes, please!__ :elasticheart:*
The easiest way to contribute a shared component to Kibana is to follow our pattern and create a single package containing that contribution. You can use the `generate` script to create a new boilerplate package.
> More detail on this is coming soon. Contact the Shared UX team for more information.
## How this collection is organized
Typically, the `/packages` directory contains a flat list of packages, where each directory matches the name of the package. Given that we expect to create a large number of packages, we're going to organize them into a loose tree structure:
```
- packages
- shared-ux
- button
- exit_full_screen
- [component type]
- baz
- qux
- [subject matter]
- foo
- bar
```
This structure should then map to the name of the package:
```
- @kbn/shared-ux-button-exit-full-screen
- @kbn/shared-ux-[subject matter]-[foo]
- @kbn/shared-ux-[subject matter]-[bar]
- @kbn/shared-ux-[component-type]-[baz]
- @kbn/shared-ux-[component-type]-[qux]
```
## Why?
When we started exploring how to effectively share code between Kibana solutions, we realized-- admittedly through some trial and error-- that the usual ways in which we share code between plugins wasn't going to work.
### Why not a plugin?
First, with each component that we create, those components inevitably begin to depend on other plugins. Once our plugin depends on another, that plugin then becomes unable to use Shared UX components without creating a circular dependency.
Second, the components, while useful to a plugin, are not actually dependent on the *plugin lifecycle*. They are stateless. Containing-- restricting-- them to a plugin lifecycle adds unnecessary complexity.
So we opted to organize our code in packages.
### Why not a single package of components?
We started that way, and quickly ran into the "`lodash` bundle problem": containing all of our components in one package (and all of our services in another) meant that any plugin using even one component would inherit *all* of them... even if they weren't used. Bundle sizes would increase dramatically, as well as build times: any minor change would cascade through the entire monorepo.
Therefore we've opted to create a package for each component.
### How do your components share code?
At present, *they don't*. Some utility code is shared, but this is code that should change very rarely, at most.
But that doesn't mean they cannot be *composed* together. `ComponentA` can certainly compose `ComponentB` and `ComponentC`. What's great is the dependencies become very clear, top-down, and reflect the granular nature of each component.

View file

@ -0,0 +1,153 @@
load("@npm//@bazel/typescript:index.bzl", "ts_config")
load("@build_bazel_rules_nodejs//:index.bzl", "js_library")
load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project")
PKG_DIRNAME = "kbn-shared-ux-button-exit-full-screen"
PKG_REQUIRE_NAME = "@kbn/shared-ux-button-exit-full-screen"
SOURCE_FILES = glob(
[
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.scss",
"src/**/*.mdx",
"src/**/*.svg",
],
exclude = [
"**/*.test.*",
],
)
SRCS = SOURCE_FILES
filegroup(
name = "srcs",
srcs = SRCS,
)
NPM_MODULE_EXTRA_FILES = [
"package.json",
]
# In this array place runtime dependencies, including other packages and NPM packages
# which must be available for this code to run.
#
# To reference other packages use:
# "//repo/relative/path/to/package"
# eg. "//packages/kbn-utils"
#
# To reference a NPM package use:
# "@npm//name-of-package"
# eg. "@npm//lodash"
RUNTIME_DEPS = [
"@npm//@elastic/eui",
"@npm//@emotion/css",
"@npm//@emotion/react",
"@npm//@storybook/addon-actions",
"@npm//classnames",
"@npm//enzyme",
"@npm//react-use",
"@npm//react",
"@npm//url-loader",
"//packages/kbn-i18n-react",
"//packages/kbn-i18n",
"//packages/kbn-shared-ux-utility",
]
# In this array place dependencies necessary to build the types, which will include the
# :npm_module_types target of other packages and packages from NPM, including @types/*
# packages.
#
# To reference the types for another package use:
# "//repo/relative/path/to/package:npm_module_types"
# eg. "//packages/kbn-utils:npm_module_types"
#
# References to NPM packages work the same as RUNTIME_DEPS
TYPES_DEPS = [
"@npm//@elastic/eui",
"@npm//@emotion/css",
"@npm//@emotion/react",
"@npm//@storybook/addon-actions",
"@npm//@types/classnames",
"@npm//@types/enzyme",
"@npm//@types/jest",
"@npm//@types/node",
"@npm//@types/react",
"@npm//react-use",
"//packages/kbn-ambient-ui-types",
"//packages/kbn-i18n-react:npm_module_types",
"//packages/kbn-i18n:npm_module_types",
"//packages/kbn-shared-ux-utility:npm_module_types",
]
jsts_transpiler(
name = "target_node",
srcs = SRCS,
build_pkg_name = package_name(),
)
jsts_transpiler(
name = "target_web",
srcs = SRCS,
build_pkg_name = package_name(),
web = True,
additional_args = [
"--copy-files",
"--quiet"
],
)
ts_config(
name = "tsconfig",
src = "tsconfig.json",
deps = [
"//:tsconfig.base.json",
"//:tsconfig.bazel.json",
],
)
ts_project(
name = "tsc_types",
args = ['--pretty'],
srcs = SRCS,
deps = TYPES_DEPS,
declaration = True,
emit_declaration_only = True,
out_dir = "target_types",
root_dir = "src",
tsconfig = ":tsconfig",
)
js_library(
name = PKG_DIRNAME,
srcs = NPM_MODULE_EXTRA_FILES,
deps = RUNTIME_DEPS + [":target_node", ":target_web"],
package_name = PKG_REQUIRE_NAME,
visibility = ["//visibility:public"],
)
pkg_npm(
name = "npm_module",
deps = [":" + PKG_DIRNAME],
)
filegroup(
name = "build",
srcs = [":npm_module"],
visibility = ["//visibility:public"],
)
pkg_npm_types(
name = "npm_module_types",
srcs = SRCS,
deps = [":tsc_types"],
package_name = PKG_REQUIRE_NAME,
tsconfig = ":tsconfig",
visibility = ["//visibility:public"],
)
filegroup(
name = "build_types",
srcs = [":npm_module_types"],
visibility = ["//visibility:public"],
)

View file

@ -0,0 +1,28 @@
---
id: sharedUX/Components/ExitFullScreenButton
slug: /shared-ux/components/exit-full-screen-button
title: Exit Full Screen Button
summary: A button that floats over the plugin workspace and allows someone to exit "full screen" mode.
tags: ['shared-ux', 'component']
date: 2021-12-28
---
## Description
Many plugins have a "full screen mode", where the chrome is hidden and the workspace of the plugin is maximized. This button is meant to appear when the plugin is in full screen mode to allow the user to exit that state and return the relevant Kibana Chrome.
## Pure Component: `ExitFullScreenButtonComponent`
The pure component is a simple button representing the style and text of a button to exit full screen mode. It does not interact with Kibana or browser chrome in any way, nor does it respond to global key presses. You can use this component with your own logic and positioning, if necessary.
## Connected Component: `ExitFullScreenButton`
The connected component:
- uses the `setIsVisible` API from `coreStart.chrome` to enter and exit full screen;
- monitors relevant external key presses to exit full screen mode;
- allows provides the `toggleChrome` prop, which provides the ability to call `setIsVisible` on render, which allows a consumer to enter full screen simply by rendering the component and exit full screen by interacting with it.
## EUI Promotion Status
This component is not currently considered for promotion to EUI.

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/shared-ux/button/exit_full_screen'],
};

View file

@ -0,0 +1,8 @@
{
"name": "@kbn/shared-ux-button-exit-full-screen",
"private": true,
"version": "1.0.0",
"main": "./target_node/index.js",
"browser": "./target_web/index.js",
"license": "SSPL-1.0 OR Elastic License 2.0"
}

View file

@ -0,0 +1,241 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<ExitFullScreenButton /> with kibana services is rendered 1`] = `
<ExitFullScreenButtonKibanaProvider
coreStart={
Object {
"chrome": Object {
"setIsVisible": [MockFunction] {
"calls": Array [
Array [
false,
],
],
"results": Array [
Object {
"type": "return",
"value": undefined,
},
],
},
},
}
}
>
<ExitFullScreenButton
onExit={[MockFunction]}
>
<ExitFullScreenButton
css={
Object {
"map": undefined,
"name": "knjqgf",
"next": undefined,
"styles": "
bottom: 8px;
left: 8px;
position: fixed;
z-index: 5;
",
"toString": [Function],
}
}
onClick={[Function]}
>
<div>
<EuiScreenReaderOnly>
<p
aria-live="polite"
className="euiScreenReaderOnly"
>
In full screen mode, press ESC to exit.
</p>
</EuiScreenReaderOnly>
<button
className="exitFullScreenButton"
data-test-subj="exitFullScreenModeButton"
onClick={[Function]}
>
<EuiFlexGroup
alignItems="center"
component="span"
gutterSize="s"
responsive={false}
>
<span
className="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow"
>
<EuiFlexItem
component="span"
grow={false}
>
<span
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<EuiIcon
size="m"
type="logoElastic"
>
<span
data-euiicon-type="logoElastic"
size="m"
/>
</EuiIcon>
</span>
</EuiFlexItem>
<EuiFlexItem
component="span"
data-test-subj="exitFullScreenModeText"
grow={false}
>
<span
className="euiFlexItem euiFlexItem--flexGrowZero"
data-test-subj="exitFullScreenModeText"
>
Exit full screen
</span>
</EuiFlexItem>
<EuiFlexItem
component="span"
grow={false}
>
<span
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<EuiIcon
size="s"
type="fullScreenExit"
>
<span
data-euiicon-type="fullScreenExit"
size="s"
/>
</EuiIcon>
</span>
</EuiFlexItem>
</span>
</EuiFlexGroup>
</button>
</div>
</ExitFullScreenButton>
</ExitFullScreenButton>
</ExitFullScreenButtonKibanaProvider>
`;
exports[`<ExitFullScreenButton /> with manual services is rendered 1`] = `
<ExitFullScreenButtonProvider
setIsFullscreen={
[MockFunction] {
"calls": Array [
Array [
false,
],
],
"results": Array [
Object {
"type": "return",
"value": undefined,
},
],
}
}
>
<ExitFullScreenButton
onExit={[MockFunction]}
>
<ExitFullScreenButton
css={
Object {
"map": undefined,
"name": "knjqgf",
"next": undefined,
"styles": "
bottom: 8px;
left: 8px;
position: fixed;
z-index: 5;
",
"toString": [Function],
}
}
onClick={[Function]}
>
<div>
<EuiScreenReaderOnly>
<p
aria-live="polite"
className="euiScreenReaderOnly"
>
In full screen mode, press ESC to exit.
</p>
</EuiScreenReaderOnly>
<button
className="exitFullScreenButton"
data-test-subj="exitFullScreenModeButton"
onClick={[Function]}
>
<EuiFlexGroup
alignItems="center"
component="span"
gutterSize="s"
responsive={false}
>
<span
className="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow"
>
<EuiFlexItem
component="span"
grow={false}
>
<span
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<EuiIcon
size="m"
type="logoElastic"
>
<span
data-euiicon-type="logoElastic"
size="m"
/>
</EuiIcon>
</span>
</EuiFlexItem>
<EuiFlexItem
component="span"
data-test-subj="exitFullScreenModeText"
grow={false}
>
<span
className="euiFlexItem euiFlexItem--flexGrowZero"
data-test-subj="exitFullScreenModeText"
>
Exit full screen
</span>
</EuiFlexItem>
<EuiFlexItem
component="span"
grow={false}
>
<span
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<EuiIcon
size="s"
type="fullScreenExit"
>
<span
data-euiicon-type="fullScreenExit"
size="s"
/>
</EuiIcon>
</span>
</EuiFlexItem>
</span>
</EuiFlexGroup>
</button>
</div>
</ExitFullScreenButton>
</ExitFullScreenButton>
</ExitFullScreenButtonProvider>
`;

View file

@ -21,22 +21,19 @@ import cx from 'classnames';
import './exit_full_screen_button.scss';
const text = i18n.translate(
'sharedUXComponents.exitFullScreenButton.exitFullScreenModeButtonText',
{
defaultMessage: 'Exit full screen',
}
);
const text = i18n.translate('sharedUXPackages.exitFullScreenButton.exitFullScreenModeButtonText', {
defaultMessage: 'Exit full screen',
});
const description = i18n.translate(
'sharedUXComponents.exitFullScreenButton.fullScreenModeDescription',
'sharedUXPackages.exitFullScreenButton.fullScreenModeDescription',
{
defaultMessage: 'In full screen mode, press ESC to exit.',
}
);
/**
* Props for the Exit Full Screen button component.
* Props for the `ExitFullScreenButton` component.
*/
export interface Props extends Pick<HTMLAttributes<HTMLDivElement>, 'className'> {
onClick: MouseEventHandler<HTMLButtonElement>;

View file

@ -9,9 +9,10 @@
import React from 'react';
import { action } from '@storybook/addon-actions';
import { ExitFullScreenButtonProvider } from './services';
import { ExitFullScreenButton as ExitFullScreenButtonComponent } from './exit_full_screen_button.component';
import { ExitFullScreenButton } from './exit_full_screen_button';
import mdx from './exit_full_screen_button.mdx';
import mdx from '../README.mdx';
export default {
title: 'Exit Full Screen Button',
@ -25,7 +26,11 @@ export default {
};
export const ConnectedComponent = ({ toggleChrome = true }: { toggleChrome: boolean }) => {
return <ExitFullScreenButton onExit={action('onExit')} toggleChrome={toggleChrome} />;
return (
<ExitFullScreenButtonProvider setIsFullscreen={action('setIsFullscreen')}>
<ExitFullScreenButton onExit={action('onExit')} toggleChrome={toggleChrome} />
</ExitFullScreenButtonProvider>
);
};
ConnectedComponent.argTypes = {

View file

@ -0,0 +1,118 @@
/*
* 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 { ReactWrapper } from 'enzyme';
import { keys } from '@elastic/eui';
import { ExitFullScreenButton } from './exit_full_screen_button';
import { componentMount, componentServices, kibanaMount, kibanaServices } from './mocks';
describe('<ExitFullScreenButton />', () => {
afterEach(() => {
jest.resetAllMocks();
});
describe('with manual services', () => {
test('is rendered', () => {
const component = componentMount(<ExitFullScreenButton onExit={jest.fn()} />);
expect(component).toMatchSnapshot();
});
test('passing `false` to toggleChrome does not toggle chrome', () => {
const component = componentMount(
<ExitFullScreenButton onExit={jest.fn()} toggleChrome={false} />
);
expect(componentServices.setIsFullscreen).toHaveBeenCalledTimes(0);
component.unmount();
expect(componentServices.setIsFullscreen).toHaveBeenCalledTimes(0);
});
describe('onExit', () => {
const onExitHandler = jest.fn();
let component: ReactWrapper;
beforeEach(() => {
component = componentMount(
<ExitFullScreenButton onExit={onExitHandler} toggleChrome={true} />
);
});
test('is called when the button is pressed', () => {
expect(componentServices.setIsFullscreen).toHaveBeenLastCalledWith(false);
component.find('button').simulate('click');
expect(onExitHandler).toHaveBeenCalledTimes(1);
component.unmount();
expect(componentServices.setIsFullscreen).toHaveBeenLastCalledWith(true);
});
test('is called when the ESC key is pressed', () => {
expect(componentServices.setIsFullscreen).toHaveBeenLastCalledWith(false);
const escapeKeyEvent = new KeyboardEvent('keydown', { key: keys.ESCAPE } as any);
document.dispatchEvent(escapeKeyEvent);
expect(onExitHandler).toHaveBeenCalledTimes(1);
component.unmount();
expect(componentServices.setIsFullscreen).toHaveBeenLastCalledWith(true);
});
});
});
describe('with kibana services', () => {
test('is rendered', () => {
const component = kibanaMount(<ExitFullScreenButton onExit={jest.fn()} />);
expect(component).toMatchSnapshot();
});
test('passing `false` to toggleChrome does not toggle chrome', () => {
const component = kibanaMount(
<ExitFullScreenButton onExit={jest.fn()} toggleChrome={false} />
);
expect(kibanaServices.coreStart.chrome.setIsVisible).toHaveBeenCalledTimes(0);
component.unmount();
expect(kibanaServices.coreStart.chrome.setIsVisible).toHaveBeenCalledTimes(0);
});
describe('onExit', () => {
const onExitHandler = jest.fn();
let component: ReactWrapper;
beforeEach(() => {
component = kibanaMount(
<ExitFullScreenButton onExit={onExitHandler} toggleChrome={true} />
);
});
test('is called when the button is pressed', () => {
expect(kibanaServices.coreStart.chrome.setIsVisible).toHaveBeenLastCalledWith(false);
component.find('button').simulate('click');
expect(onExitHandler).toHaveBeenCalledTimes(1);
component.unmount();
expect(kibanaServices.coreStart.chrome.setIsVisible).toHaveBeenLastCalledWith(true);
});
test('is called when the ESC key is pressed', () => {
expect(kibanaServices.coreStart.chrome.setIsVisible).toHaveBeenLastCalledWith(false);
const escapeKeyEvent = new KeyboardEvent('keydown', { key: keys.ESCAPE } as any);
document.dispatchEvent(escapeKeyEvent);
expect(onExitHandler).toHaveBeenCalledTimes(1);
component.unmount();
expect(kibanaServices.coreStart.chrome.setIsVisible).toHaveBeenLastCalledWith(true);
});
});
});
});

View file

@ -11,12 +11,11 @@ import { useEuiTheme, keys } from '@elastic/eui';
import { css } from '@emotion/react';
import useMountedState from 'react-use/lib/useMountedState';
import { usePlatformService } from '@kbn/shared-ux-services';
import { ExitFullScreenButton as Component } from './exit_full_screen_button.component';
import { useServices } from './services';
/**
* Props for the service-enabled Exit Full Screen button component.
* Props for the service-enabled `ExitFullScreenButton` component.
*/
export interface Props {
/** Optional handler to call when one exits full-screen mode. */
@ -27,16 +26,11 @@ export interface Props {
/**
* A service-enabled component that provides Kibana-specific functionality to the `ExitFullScreenButton`
* component.
*
* Use of this component requires both the `EuiTheme` context as well as either a configured Shared UX
* `ServicesProvider` or the `ServicesContext` provided by the Shared UX public plugin contract.
*
* See shared-ux/public/services for information.
* pure component.
*/
export const ExitFullScreenButton = ({ onExit = () => {}, toggleChrome = true }: Props) => {
const { euiTheme } = useEuiTheme();
const { setIsFullscreen } = usePlatformService();
const { setIsFullscreen } = useServices();
const isMounted = useMountedState();
const onClick = useCallback(() => {

View file

@ -0,0 +1,41 @@
/*
* 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 { withSuspense } from '@kbn/shared-ux-utility';
export { ExitFullScreenButtonKibanaProvider, ExitFullScreenButtonProvider } from './services';
/**
* Lazy-loaded pure component. Must be wrapped in `React.Suspense`.
*/
export const LazyExitFullScreenButtonComponent = React.lazy(() =>
import('./exit_full_screen_button.component').then(({ ExitFullScreenButton }) => ({
default: ExitFullScreenButton,
}))
);
/**
* A pure component that resembles a button to exit full screen mode.
*/
export const ExitFullScreenButtonComponent = withSuspense(LazyExitFullScreenButtonComponent);
/**
* Lazy-loaded connected component. Must be wrapped in `React.Suspense` and a Provider.
*/
export const LazyExitFullScreenButton = React.lazy(() =>
import('./exit_full_screen_button').then(({ ExitFullScreenButton }) => ({
default: ExitFullScreenButton,
}))
);
/**
* A component that can be used to exit full screen mode in Kibana. Requires a Provider for
* relevant services.
*/
export const ExitFullScreenButton = withSuspense(LazyExitFullScreenButton);

View file

@ -0,0 +1,25 @@
/*
* 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 { mount as enzymeMount } from 'enzyme';
import { ExitFullScreenButtonKibanaProvider, ExitFullScreenButtonProvider } from './services';
export const componentServices = { setIsFullscreen: jest.fn() };
export const componentMount = (element: JSX.Element) =>
enzymeMount(
<ExitFullScreenButtonProvider {...componentServices}>{element}</ExitFullScreenButtonProvider>
);
export const kibanaServices = { coreStart: { chrome: { setIsVisible: jest.fn() } } };
export const kibanaMount = (element: JSX.Element) =>
enzymeMount(
<ExitFullScreenButtonKibanaProvider {...kibanaServices}>
{element}
</ExitFullScreenButtonKibanaProvider>
);

View file

@ -0,0 +1,73 @@
/*
* 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, useContext } from 'react';
/**
* Abstract external services for this component.
*/
export interface Services {
setIsFullscreen: (isFullscreen: boolean) => void;
}
const ExitFullScreenButtonContext = React.createContext<Services | null>(null);
/**
* Abstract external service Provider.
*/
export const ExitFullScreenButtonProvider: FC<Services> = ({ children, ...services }) => {
return (
<ExitFullScreenButtonContext.Provider value={services}>
{children}
</ExitFullScreenButtonContext.Provider>
);
};
/**
* Kibana-specific service types.
*/
export interface KibanaServices {
coreStart: {
chrome: {
setIsVisible: (isVisible: boolean) => void;
};
};
}
/**
* Kibana-specific Provider that maps to known dependency types.
*/
export const ExitFullScreenButtonKibanaProvider: FC<KibanaServices> = ({
children,
...services
}) => {
return (
<ExitFullScreenButtonContext.Provider
value={{
setIsFullscreen: services.coreStart.chrome.setIsVisible,
}}
>
{children}
</ExitFullScreenButtonContext.Provider>
);
};
/**
* React hook for accessing pre-wired services.
*/
export function useServices() {
const context = useContext(ExitFullScreenButtonContext);
if (!context) {
throw new Error(
'ExitFullScreenButtonContext is missing. Ensure your component or React root is wrapped with ExitFullScreenButtonProvider.'
);
}
return context;
}

View file

@ -0,0 +1,19 @@
{
"extends": "../../../../tsconfig.bazel.json",
"compilerOptions": {
"declaration": true,
"emitDeclarationOnly": true,
"outDir": "target_types",
"rootDir": "src",
"stripInternal": false,
"types": [
"jest",
"node",
"@emotion/react/types/css-prop",
"@kbn/ambient-ui-types"
]
},
"include": [
"src/**/*"
]
}

View file

@ -6,4 +6,8 @@
* Side Public License, v 1.
*/
export { ExitFullScreenButton } from './exit_full_screen_button';
module.exports = {
preset: '@kbn/test',
rootDir: '../..',
roots: ['<rootDir>/packages/shared-ux'],
};

View file

@ -25,8 +25,7 @@
"mapsEms",
"savedObjects",
"share",
"presentationUtil",
"sharedUX"
"presentationUtil"
],
"optionalPlugins": [
"cloud",

View file

@ -15,7 +15,7 @@ import { Filter } from '@kbn/es-query';
import { ActionExecutionContext, Action } from 'src/plugins/ui_actions/public';
import { Observable } from 'rxjs';
import moment from 'moment';
import { ExitFullScreenButton } from '@kbn/shared-ux-components';
import { ExitFullScreenButton } from '@kbn/shared-ux-button-exit-full-screen';
import { MBMap } from '../mb_map';
import { RightSideControls } from '../right_side_controls';
import { Timeslider } from '../timeslider';

View file

@ -64,7 +64,6 @@ export const getSecurityService = () => pluginsStart.security;
export const getSpacesApi = () => pluginsStart.spaces;
export const getTheme = () => coreStart.theme;
export const getUsageCollection = () => pluginsStart.usageCollection;
export const getSharedUXPluginContext = () => pluginsStart.sharedUX;
export const getApplication = () => coreStart.application;
// xpack.maps.* kibana.yml settings from this plugin

View file

@ -79,7 +79,6 @@ import type { CloudSetup } from '../../cloud/public';
import type { LensPublicSetup } from '../../lens/public';
import { setupLensChoroplethChart } from './lens';
import { SharedUXPluginStart } from '../../../../src/plugins/shared_ux/public';
export interface MapsPluginSetupDependencies {
cloud?: CloudSetup;
@ -115,7 +114,6 @@ export interface MapsPluginStartDependencies {
spaces?: SpacesPluginStart;
mapsEms: MapsEmsPluginPublicStart;
usageCollection?: UsageCollectionSetup;
sharedUX: SharedUXPluginStart;
}
/**

View file

@ -10,7 +10,7 @@ import { render, unmountComponentAtNode } from 'react-dom';
import { Router, Switch, Route, Redirect, RouteComponentProps } from 'react-router-dom';
import { i18n } from '@kbn/i18n';
import type { AppMountParameters } from 'kibana/public';
import { SharedUxServicesProvider } from '@kbn/shared-ux-services';
import { ExitFullScreenButtonKibanaProvider } from '@kbn/shared-ux-button-exit-full-screen';
import { KibanaThemeProvider } from '../../../../src/plugins/kibana_react/public';
import {
getCoreChrome,
@ -19,7 +19,7 @@ import {
getToasts,
getEmbeddableService,
getDocLinks,
getSharedUXPluginContext,
getCore,
} from './kibana_services';
import {
createKbnUrlStateStorage,
@ -96,7 +96,7 @@ export async function renderApp(
}
return (
<SharedUxServicesProvider {...getSharedUXPluginContext().getContextServices()}>
<ExitFullScreenButtonKibanaProvider coreStart={getCore()}>
<MapPage
mapEmbeddableInput={mapEmbeddableInput}
embeddableId={embeddableId}
@ -108,7 +108,7 @@ export async function renderApp(
history={history}
key={routeProps.match.params.savedMapId ? routeProps.match.params.savedMapId : 'new'}
/>
</SharedUxServicesProvider>
</ExitFullScreenButtonKibanaProvider>
);
}

View file

@ -4954,8 +4954,8 @@
"share.urlService.redirect.RedirectManager.missingParamLocator": "ロケーターIDが指定されていません。URLで「l」検索パラメーターを指定します。これは既存のロケーターIDにしてください。",
"share.urlService.redirect.RedirectManager.missingParamParams": "ロケーターパラメーターが指定されていません。URLで「p」検索パラメーターを指定します。これはロケーターパラメーターのJSONシリアル化オブジェクトにしてください。",
"share.urlService.redirect.RedirectManager.missingParamVersion": "ロケーターパラメーターバージョンが指定されていません。URLで「v」検索パラメーターを指定します。これはロケーターパラメーターが生成されたときのKibanaのリリースバージョンです。",
"sharedUXComponents.exitFullScreenButton.exitFullScreenModeButtonText": "全画面を終了",
"sharedUXComponents.exitFullScreenButton.fullScreenModeDescription": "ESC キーで全画面モードを終了します。",
"sharedUXPackages.exitFullScreenButton.exitFullScreenModeButtonText": "全画面を終了",
"sharedUXPackages.exitFullScreenButton.fullScreenModeDescription": "ESC キーで全画面モードを終了します。",
"telemetry.callout.appliesSettingTitle": "この設定に加えた変更は {allOfKibanaText} に適用され、自動的に保存されます。",
"telemetry.callout.appliesSettingTitle.allOfKibanaText": "Kibana のすべて",
"telemetry.callout.clusterStatisticsDescription": "これは収集される基本的なクラスター統計の例です。インデックス、シャード、ノードの数が含まれます。監視がオンになっているかどうかなどのハイレベルの使用統計も含まれます。",

View file

@ -4961,8 +4961,8 @@
"share.urlService.redirect.RedirectManager.missingParamLocator": "未指定定位器 ID。在 URL 中指定“l”搜索参数其应为现有定位器 ID。",
"share.urlService.redirect.RedirectManager.missingParamParams": "定位器参数未指定。在 URL 中指定“p”搜索参数其应为定位器参数的 JSON 序列化对象。",
"share.urlService.redirect.RedirectManager.missingParamVersion": "定位器参数版本未指定。在 URL 中指定“v”搜索参数其应为生成定位器参数时 Kibana 的版本。",
"sharedUXComponents.exitFullScreenButton.exitFullScreenModeButtonText": "退出全屏",
"sharedUXComponents.exitFullScreenButton.fullScreenModeDescription": "在全屏模式下,按 ESC 键可退出。",
"sharedUXPackages.exitFullScreenButton.exitFullScreenModeButtonText": "退出全屏",
"sharedUXPackages.exitFullScreenButton.fullScreenModeDescription": "在全屏模式下,按 ESC 键可退出。",
"telemetry.callout.appliesSettingTitle": "对此设置的更改将应用到{allOfKibanaText} 且会自动保存。",
"telemetry.callout.appliesSettingTitle.allOfKibanaText": "整个 Kibana",
"telemetry.callout.clusterStatisticsDescription": "这是我们将收集的基本集群统计信息的示例。其包括索引、分片和节点的数目。还包括概括性的使用情况统计信息,例如监测是否打开。",

View file

@ -3156,6 +3156,10 @@
version "0.0.0"
uid ""
"@kbn/shared-ux-button-exit-full-screen@link:bazel-bin/packages/shared-ux/button/exit_full_screen":
version "0.0.0"
uid ""
"@kbn/shared-ux-components@link:bazel-bin/packages/kbn-shared-ux-components":
version "0.0.0"
uid ""
@ -6168,6 +6172,10 @@
version "0.0.0"
uid ""
"@types/kbn__shared-ux-button-exit-full-screen@link:bazel-bin/packages/shared-ux/button/exit_full_screen/npm_module_types":
version "0.0.0"
uid ""
"@types/kbn__shared-ux-components@link:bazel-bin/packages/kbn-shared-ux-components/npm_module_types":
version "0.0.0"
uid ""