From 9554450446f41878cb198b66ff0ca18986da1908 Mon Sep 17 00:00:00 2001 From: Cee Chen <549407+cee-chen@users.noreply.github.com> Date: Tue, 3 Oct 2023 11:10:19 -0700 Subject: [PATCH 01/38] [EuiCollapsibleNavBeta] Add global CSS variable for width offset (#7248) --- .../collapsible_nav_beta.stories.tsx | 19 +++++++++++++++++++ .../collapsible_nav_beta.tsx | 18 ++++++++++++++++-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/components/collapsible_nav_beta/collapsible_nav_beta.stories.tsx b/src/components/collapsible_nav_beta/collapsible_nav_beta.stories.tsx index b8a21163188..15c1b1148f7 100644 --- a/src/components/collapsible_nav_beta/collapsible_nav_beta.stories.tsx +++ b/src/components/collapsible_nav_beta/collapsible_nav_beta.stories.tsx @@ -11,6 +11,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { EuiHeader, EuiHeaderSection, EuiHeaderSectionItem } from '../header'; import { EuiPageTemplate } from '../page_template'; +import { EuiBottomBar } from '../bottom_bar'; import { EuiFlyout } from '../flyout'; import { EuiButton } from '../button'; import { EuiTitle } from '../title'; @@ -469,6 +470,24 @@ export const FlyoutInFixedHeaders: Story = { }, }; +export const GlobalCSSVariable: Story = { + render: ({ ...args }) => ( + <> + + + + This story tests the global `--euiCollapsibleNavOffset` CSS variable + + + + + This text should be visible at all times and the bar position should + update dynamically based on the nav width (including on mobile) + + + ), +}; + export const CollapsedStateInLocalStorage: Story = { render: () => { const key = 'EuiCollapsibleNav__isCollapsed'; diff --git a/src/components/collapsible_nav_beta/collapsible_nav_beta.tsx b/src/components/collapsible_nav_beta/collapsible_nav_beta.tsx index 5d3a62e91a7..121e594b47d 100644 --- a/src/components/collapsible_nav_beta/collapsible_nav_beta.tsx +++ b/src/components/collapsible_nav_beta/collapsible_nav_beta.tsx @@ -18,7 +18,12 @@ import React, { } from 'react'; import classNames from 'classnames'; -import { useEuiTheme, useGeneratedHtmlId, throttle } from '../../services'; +import { + useEuiTheme, + useEuiThemeCSSVariables, + useGeneratedHtmlId, + throttle, +} from '../../services'; import { CommonProps } from '../common'; import { EuiFlyout, EuiFlyoutProps } from '../flyout'; @@ -88,6 +93,7 @@ const _EuiCollapsibleNavBeta: FunctionComponent = ({ focusTrapProps: _focusTrapProps, ...rest }) => { + const { setGlobalCSSVariables } = useEuiThemeCSSVariables(); const euiTheme = useEuiTheme(); const headerHeight = euiHeaderVariables(euiTheme).height; @@ -138,9 +144,17 @@ const _EuiCollapsibleNavBeta: FunctionComponent = ({ const width = useMemo(() => { if (isOverlayFullWidth) return '100%'; if (isPush && isCollapsed) return headerHeight; - return _width; + return `${_width}px`; }, [_width, isOverlayFullWidth, isPush, isCollapsed, headerHeight]); + // Other UI elements may need to account for the nav width - + // set a global CSS variable that they can use + useEffect(() => { + setGlobalCSSVariables({ + '--euiCollapsibleNavOffset': isOverlay ? '0' : width, + }); + }, [width, isOverlay, setGlobalCSSVariables]); + /** * Prop setup */ From febcec76839b24eaf80e74cf87b4bfb21f652186 Mon Sep 17 00:00:00 2001 From: Cee Chen Date: Tue, 3 Oct 2023 11:34:55 -0700 Subject: [PATCH 02/38] i18n tokens and changelog --- CHANGELOG.md | 4 ++++ i18ntokens.json | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9380fce9570..13c4d05a593 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## [`88.5.4`](https://github.com/elastic/eui/tree/v88.5.4) + +- This release contains internal changes to a beta component needed by Kibana. + ## [`88.5.3`](https://github.com/elastic/eui/tree/v88.5.3) **Bug fixes** diff --git a/i18ntokens.json b/i18ntokens.json index 94affb3d45f..57ffd236887 100644 --- a/i18ntokens.json +++ b/i18ntokens.json @@ -455,14 +455,14 @@ "highlighting": "string", "loc": { "start": { - "line": 151, + "line": 165, "column": 27, - "index": 5087 + "index": 5469 }, "end": { - "line": 154, + "line": 168, "column": 3, - "index": 5157 + "index": 5539 } }, "filepath": "src/components/collapsible_nav_beta/collapsible_nav_beta.tsx" From cdf1e43ac506b1bdfbe57d362088bdb569d97fd5 Mon Sep 17 00:00:00 2001 From: Cee Chen Date: Tue, 3 Oct 2023 11:34:58 -0700 Subject: [PATCH 03/38] 88.5.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ca6ad3ae8bc..ea4704ddff0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@elastic/eui", "description": "Elastic UI Component Library", - "version": "88.5.3", + "version": "88.5.4", "license": "SEE LICENSE IN LICENSE.txt", "main": "lib", "module": "es", From afb99a4a51d4fae77656e0a5151c564f38b6c5d6 Mon Sep 17 00:00:00 2001 From: Cee Chen <549407+cee-chen@users.noreply.github.com> Date: Tue, 3 Oct 2023 14:25:28 -0700 Subject: [PATCH 04/38] [EuiIcon] Fix silenced test errors (#7249) --- .../icon/__snapshots__/icon.test.tsx.snap | 102 +++++++++--------- src/components/icon/icon.test.tsx | 86 ++++++--------- 2 files changed, 84 insertions(+), 104 deletions(-) diff --git a/src/components/icon/__snapshots__/icon.test.tsx.snap b/src/components/icon/__snapshots__/icon.test.tsx.snap index 28179a5e9b0..430ad7993c8 100644 --- a/src/components/icon/__snapshots__/icon.test.tsx.snap +++ b/src/components/icon/__snapshots__/icon.test.tsx.snap @@ -4617,7 +4617,7 @@ exports[`EuiIcon props type logoApache is rendered 1`] = ` xmlns="http://www.w3.org/2000/svg" > - - - + - - + - - + - - + - - + - - + - + - - - + - + `; @@ -5380,8 +5380,8 @@ exports[`EuiIcon props type logoGCP is rendered 1`] = ` role="img" viewBox="0 0 32 32" width="32" - xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" > - - + `; @@ -5886,7 +5886,7 @@ exports[`EuiIcon props type logoIBM is rendered 1`] = ` xmlns="http://www.w3.org/2000/svg" > - - - + - - + - - + - - + - + - - - + - - + - - + - + - - + { @@ -28,50 +30,18 @@ jest.mock('./icon', () => { beforeEach(() => clearIconComponentCache()); -const prettyHtml = cheerio.load(''); - -function testIcon(props: PropsOf) { - return () => { - expect.assertions(1); - return new Promise((resolve) => { - const onIconLoad = () => { - component.update(); - expect(prettyHtml(component.html())).toMatchSnapshot(); - resolve(); - }; - const component = mount(); - }); - }; -} - -describe('EuiIcon', () => { - let consoleErrorOverride: jest.SpyInstance; - beforeAll(() => { - // Ignore EuiIcon update not wrapped in act() warnings as they are triggered - // directly from the component componentDidUpdate() and loadIconComponent() - // TODO: Refactor EuiIcon to not cause this issue and think of a simpler - // implementation based on modern JS bundlers features instead of - // the EuiIcon caching layer. - const originalConsoleError: typeof console.error = console.error; - consoleErrorOverride = jest - .spyOn(console, 'error') - .mockImplementation((message, ...args) => { - if ( - message?.startsWith( - 'Warning: An update to %s inside a test was not wrapped in act(...).' - ) - ) { - return; - } - - originalConsoleError(message, ...args); - }); +const testIcon = (props: PropsOf) => async () => { + act(() => { + render(); }); - - afterAll(() => { - consoleErrorOverride.mockRestore(); + await waitFor(() => { + const icon = document.querySelector(`[data-icon-type=${props.type}]`); + expect(icon).toHaveAttribute('data-is-loaded', 'true'); + expect(icon).toMatchSnapshot(); }); +}; +describe('EuiIcon', () => { test('is rendered', testIcon({ type: 'search', ...requiredProps })); shouldRenderCustomStyles(); @@ -84,6 +54,17 @@ describe('EuiIcon', () => { }); describe('props', () => { + test('onIconLoad', async () => { + const onIconLoad = jest.fn(); + + render(); + expect(onIconLoad).toHaveBeenCalledTimes(0); + + await waitFor(() => { + expect(onIconLoad).toHaveBeenCalledTimes(1); + }); + }); + describe('other props', () => { test( 'are passed through to the icon', @@ -156,31 +137,30 @@ describe('EuiIcon', () => { ); }; - const component = mount(); - expect(prettyHtml(component.html())).toMatchSnapshot(); + const { container } = render(); + expect(container.firstChild).toMatchSnapshot(); }); describe('appendIconComponentCache', () => { it('does nothing if not called', () => { - const component = mount(); - expect(component.find('svg').prop('data-is-loading')).toEqual(true); + const { container } = render(); + expect(container.firstChild).toHaveAttribute('data-is-loading', 'true'); }); it('preloads the specified icon into the cache', () => { appendIconComponentCache({ videoPlayer: EuiIconVideoPlayer, }); - const component = mount(); - // Should not have either data-is-loading attr set to true, because it was pre-loaded - expect(component.find('svg').prop('data-is-loading')).not.toEqual(true); + const { container } = render(); + expect(container.firstChild).not.toHaveAttribute('data-is-loading'); }); it('does not impact non-loaded icons', () => { appendIconComponentCache({ videoPlayer: EuiIconVideoPlayer, }); - const component = mount(); - expect(component.find('svg').prop('data-is-loading')).toEqual(true); + const { container } = render(); + expect(container.firstChild).toHaveAttribute('data-is-loading', 'true'); }); }); }); From ce5133227573a08076b757fae5a16f38291958a3 Mon Sep 17 00:00:00 2001 From: Cee Chen <549407+cee-chen@users.noreply.github.com> Date: Tue, 3 Oct 2023 14:51:52 -0700 Subject: [PATCH 05/38] [Storybook] General consistency pass (#7245) --- .storybook/preview.tsx | 21 +++- .storybook/utils.test.ts | 49 ++++++++ .storybook/utils.ts | 64 ++++++++++ scripts/jest/config.js | 1 + .../button_empty/button_empty.stories.tsx | 17 +-- .../button_group/button_group.stories.tsx | 49 ++++---- .../button_icon/button_icon.stories.tsx | 17 +-- .../collapsible_nav.stories.tsx | 11 +- .../collapsible_nav_group.stories.tsx | 20 ++- .../collapsible_nav_beta.stories.tsx | 118 +++++++++--------- .../empty_prompt/empty_prompt.stories.tsx | 20 ++- .../error_boundary/error_boundary.stories.tsx | 31 ++--- .../filter_group/filter_button.stories.tsx | 35 ++++-- src/components/header/header.stories.tsx | 12 +- .../header_alert/header_alert.stories.tsx | 30 ++--- .../header_links/header_links.stories.tsx | 9 +- .../header_logo/header_logo.stories.tsx | 13 +- .../key_pad_menu_item.stories.tsx | 10 +- src/components/page/page.stories.tsx | 35 +++--- .../page/page_body/page_body.stories.tsx | 25 ++-- .../page/page_header/page_header.stories.tsx | 15 ++- .../page_section/page_section.stories.tsx | 19 ++- .../page_sidebar/page_sidebar.stories.tsx | 34 ++--- .../split_panel/split_panel_inner.stories.tsx | 15 +-- .../split_panel/split_panel_outer.stories.tsx | 10 +- .../resizable_button.stories.tsx | 11 +- .../resizable_collapse_button.stories.tsx | 32 +++-- src/components/side_nav/side_nav.stories.tsx | 59 +++++---- .../text_truncate/text_truncate.stories.tsx | 37 +++--- 29 files changed, 478 insertions(+), 341 deletions(-) create mode 100644 .storybook/utils.test.ts create mode 100644 .storybook/utils.ts diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index e2ba33fff7d..af501c4be44 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -38,6 +38,13 @@ import { writingModeStyles } from './writing_mode.styles'; // once all EUI components are converted to Emotion import '../dist/eui_theme_light.css'; +/** + * Prop controls + */ + +import type { CommonProps } from '../src/components/common'; +import { hideStorybookControls } from './utils'; + const preview: Preview = { decorators: [ (Story, context) => ( @@ -86,6 +93,7 @@ const preview: Preview = { parameters: { actions: { argTypesRegex: '^on[A-Z].*' }, backgrounds: { disable: true }, // Use colorMode instead + options: { showPanel: true }, // default to showing the controls panel controls: { expanded: true, sort: 'requiredFirst', @@ -100,12 +108,13 @@ const preview: Preview = { }, // Due to CommonProps, these props appear on almost every Story, but generally // aren't super useful to test - let's disable them by default and (if needed) - // individual stories can re-enable them - argTypes: { - css: { table: { disable: true } }, - className: { table: { disable: true } }, - 'data-test-subj': { table: { disable: true } }, - }, + // individual stories can re-enable them, e.g. by passing + // `argTypes: { 'data-test-subj': { table: { disable: false } } }` + argTypes: hideStorybookControls([ + 'css', + 'className', + 'data-test-subj', + ]), }; export default preview; diff --git a/.storybook/utils.test.ts b/.storybook/utils.test.ts new file mode 100644 index 00000000000..d570363a393 --- /dev/null +++ b/.storybook/utils.test.ts @@ -0,0 +1,49 @@ +/* + * 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 { hideStorybookControls, disableStorybookControls } from './utils'; + +describe('hideStorybookControls', () => { + it('outputs the expected `argTypes` object when passed prop name strings', () => { + expect( + hideStorybookControls(['isDisabled', 'isLoading', 'isInvalid']) + ).toEqual({ + isDisabled: { table: { disable: true } }, + isLoading: { table: { disable: true } }, + isInvalid: { table: { disable: true } }, + }); + }); + + it('throws a typescript error if a generic is passed and the prop names do not match', () => { + type TestComponentProps = { hello: boolean; world: boolean }; + // No typescript error + hideStorybookControls(['hello', 'world']); + // @ts-expect-error - will fail `yarn lint` if a TS error is *not* produced + hideStorybookControls(['hello', 'world', 'error']); + }); +}); + +describe('disableStorybookControls', () => { + it('outputs the expected `argTypes` object when passed prop name strings', () => { + expect( + disableStorybookControls(['isDisabled', 'isLoading', 'isInvalid']) + ).toEqual({ + isDisabled: { control: false }, + isLoading: { control: false }, + isInvalid: { control: false }, + }); + }); + + it('throws a typescript error if a generic is passed and the prop names do not match', () => { + type TestComponentProps = { hello: boolean; world: boolean }; + // No typescript error + disableStorybookControls(['hello', 'world']); + // @ts-expect-error - will fail `yarn lint` if a TS error is *not* produced + disableStorybookControls(['hello', 'world', 'error']); + }); +}); diff --git a/.storybook/utils.ts b/.storybook/utils.ts new file mode 100644 index 00000000000..517b93421c2 --- /dev/null +++ b/.storybook/utils.ts @@ -0,0 +1,64 @@ +/* + * 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. + */ + +/** + * argTypes configurations + */ + +/** + * Completely hide props from Storybook's controls panel. + * Should be passed or spread to `argTypes` + */ +export const hideStorybookControls = ( + propNames: Array +): Record | {} => { + return propNames.reduce( + (obj, name) => ({ ...obj, [name]: HIDE_CONTROL }), + {} + ); +}; +const HIDE_CONTROL = { table: { disable: true } }; + +/** + * Leave props visible in Storybook's controls panel, but disable them + * from being controllable (renders a `-`). + * + * Should be passed or spread to `argTypes` + */ +export const disableStorybookControls = ( + propNames: Array +): Record | {} => { + return propNames.reduce( + (obj, name) => ({ ...obj, [name]: DISABLE_CONTROL }), + {} + ); +}; +const DISABLE_CONTROL = { control: false }; + +/** + * parameters configurations + */ + +/** + * Will hide all props/controls. Pass to `parameters` + * + * TODO: Figure out some way to not show Storybook's "setup" text? + */ +export const hideAllStorybookControls = { + controls: { exclude: /.*/g }, +}; + +/** + * Will hide the control/addon panel entirely for a specific story. + * Should be passed or spread to to `parameters`. + * + * Note that users can choose to re-show the panel in the UI + */ +export const hidePanel = { + options: { showPanel: false }, +}; diff --git a/scripts/jest/config.js b/scripts/jest/config.js index 3174d93b6cb..84006d237aa 100644 --- a/scripts/jest/config.js +++ b/scripts/jest/config.js @@ -19,6 +19,7 @@ const config = { '/scripts/babel', '/scripts/tests', '/scripts/eslint-plugin', + '/.storybook', ], collectCoverageFrom: [ 'src/{components,services,global_styling}/**/*.{ts,tsx,js,jsx}', diff --git a/src/components/button/button_empty/button_empty.stories.tsx b/src/components/button/button_empty/button_empty.stories.tsx index fcbfdc29e62..ccf9cd2c12a 100644 --- a/src/components/button/button_empty/button_empty.stories.tsx +++ b/src/components/button/button_empty/button_empty.stories.tsx @@ -20,14 +20,8 @@ const meta: Meta = { }, iconType: { control: 'text' }, }, -}; - -export default meta; -type Story = StoryObj; - -export const Playground: Story = { args: { - children: 'Tertiary action', + // Component defaults color: 'primary', size: 'm', iconSize: 'm', @@ -37,3 +31,12 @@ export const Playground: Story = { isSelected: false, }, }; + +export default meta; +type Story = StoryObj; + +export const Playground: Story = { + args: { + children: 'Tertiary action', + }, +}; diff --git a/src/components/button/button_group/button_group.stories.tsx b/src/components/button/button_group/button_group.stories.tsx index 9e3c8cdd3cc..d4f0eef2928 100644 --- a/src/components/button/button_group/button_group.stories.tsx +++ b/src/components/button/button_group/button_group.stories.tsx @@ -8,6 +8,7 @@ import React, { useState } from 'react'; import type { Meta, StoryObj } from '@storybook/react'; +import { disableStorybookControls } from '../../../../.storybook/utils'; import { EuiButtonGroup, @@ -19,11 +20,6 @@ const meta: Meta = { title: 'EuiButtonGroup', // @ts-ignore This still works for Storybook controls, even though Typescript complains component: EuiButtonGroup, - parameters: { - controls: { - exclude: ['data-test-subj'], - }, - }, argTypes: { type: { options: ['single', 'multi'], @@ -44,6 +40,15 @@ const meta: Meta = { control: 'select', }, }, + args: { + // Component defaults + type: 'single', + buttonSize: 's', + color: 'text', + isDisabled: false, + isFullWidth: false, + isIconOnly: false, + }, }; export default meta; @@ -76,6 +81,17 @@ const EuiButtonGroupSingle = (props: any) => { ); }; +export const SingleSelection: Story = { + render: ({ ...args }) => , + args: { + legend: 'EuiButtonGroup - single selection', + options, + type: 'single', + idSelected: 'button1', + }, + argTypes: disableStorybookControls(['type']), +}; + const EuiButtonGroupMulti = (props: any) => { const [idToSelectedMap, setIdToSelectedMap] = useState< Record @@ -100,24 +116,13 @@ const EuiButtonGroupMulti = (props: any) => { ); }; -export const Playground: Story = { - render: ({ ...args }) => { - if (args.type === 'multi') { - return ; - } else { - return ; - } - }, +export const MultiSelection: Story = { + render: ({ ...args }) => , args: { - legend: 'EuiButtonGroup demo', - type: 'single', + legend: 'EuiButtonGroup - multiple selections', options, - idSelected: 'button1', + type: 'multi', idToSelectedMap: { button1: true }, - buttonSize: 's', - color: 'text', - isDisabled: false, - isFullWidth: false, - isIconOnly: false, - } as any, + }, + argTypes: disableStorybookControls(['type']), }; diff --git a/src/components/button/button_icon/button_icon.stories.tsx b/src/components/button/button_icon/button_icon.stories.tsx index 29d2e49cf62..8ef364ab30a 100644 --- a/src/components/button/button_icon/button_icon.stories.tsx +++ b/src/components/button/button_icon/button_icon.stories.tsx @@ -13,14 +13,8 @@ import { EuiButtonIcon, EuiButtonIconProps } from './button_icon'; const meta: Meta = { title: 'EuiButtonIcon', component: EuiButtonIcon, -}; - -export default meta; -type Story = StoryObj; - -export const Playground: Story = { args: { - iconType: 'faceHappy', + // Component defaults color: 'primary', display: 'empty', size: 'xs', @@ -30,3 +24,12 @@ export const Playground: Story = { isSelected: false, }, }; + +export default meta; +type Story = StoryObj; + +export const Playground: Story = { + args: { + iconType: 'faceHappy', + }, +}; diff --git a/src/components/collapsible_nav/collapsible_nav.stories.tsx b/src/components/collapsible_nav/collapsible_nav.stories.tsx index 77a89909ea2..efefaf47e94 100644 --- a/src/components/collapsible_nav/collapsible_nav.stories.tsx +++ b/src/components/collapsible_nav/collapsible_nav.stories.tsx @@ -15,6 +15,13 @@ import { EuiCollapsibleNav, EuiCollapsibleNavProps } from './collapsible_nav'; const meta: Meta = { title: 'EuiCollapsibleNav', component: EuiCollapsibleNav, + args: { + // Component defaults + isDocked: false, + dockedBreakpoint: 'l', + showButtonIfDocked: false, + size: 320, + }, // TODO: Improve props inherited from EuiFlyout, ideally through // a DRY import from `flyout.stories.tsx` once that's created }; @@ -43,9 +50,5 @@ export const Playground: Story = { args: { children: 'Collapsible nav content', isOpen: true, - isDocked: false, - dockedBreakpoint: 'l', - showButtonIfDocked: false, - size: 240, }, }; diff --git a/src/components/collapsible_nav/collapsible_nav_group/collapsible_nav_group.stories.tsx b/src/components/collapsible_nav/collapsible_nav_group/collapsible_nav_group.stories.tsx index 4f761dc65a0..ae4d317761d 100644 --- a/src/components/collapsible_nav/collapsible_nav_group/collapsible_nav_group.stories.tsx +++ b/src/components/collapsible_nav/collapsible_nav_group/collapsible_nav_group.stories.tsx @@ -43,6 +43,14 @@ const meta: Meta = { isDisabled: { if: { arg: 'isCollapsible' } }, element: { if: { arg: 'isCollapsible' } }, }, + args: { + iconType: 'logoElastic', + // Component defaults + iconSize: 'l', + titleSize: 'xxs', + titleElement: 'h3', + background: 'none', + }, }; export default meta; @@ -51,12 +59,7 @@ type Story = StoryObj; export const Accordion: Story = { args: { children: 'This is an accordion group with a title', - background: 'none', title: 'Nav group - accordion', - iconType: 'logoElastic', - iconSize: 'l', - titleElement: 'h3', - titleSize: 'xxs', initialIsOpen: true, isCollapsible: true, }, @@ -65,12 +68,6 @@ export const Accordion: Story = { export const NonAccordion: StoryObj = { args: { children: 'This is a group with a title', - background: 'none', - title: 'Nav group - non-accordion', - iconType: 'logoElastic', - iconSize: 'l', - titleElement: 'h3', - titleSize: 'xxs', isCollapsible: false, }, }; @@ -78,6 +75,5 @@ export const NonAccordion: StoryObj = { export const NoTitle: Story = { args: { children: 'This is a group without a title', - background: 'none', }, }; diff --git a/src/components/collapsible_nav_beta/collapsible_nav_beta.stories.tsx b/src/components/collapsible_nav_beta/collapsible_nav_beta.stories.tsx index 15c1b1148f7..dc3bc82410b 100644 --- a/src/components/collapsible_nav_beta/collapsible_nav_beta.stories.tsx +++ b/src/components/collapsible_nav_beta/collapsible_nav_beta.stories.tsx @@ -8,6 +8,10 @@ import React, { FunctionComponent, PropsWithChildren, useState } from 'react'; import type { Meta, StoryObj } from '@storybook/react'; +import { + hideStorybookControls, + hideAllStorybookControls, +} from '../../../.storybook/utils'; import { EuiHeader, EuiHeaderSection, EuiHeaderSectionItem } from '../header'; import { EuiPageTemplate } from '../page_template'; @@ -35,6 +39,7 @@ const meta: Meta = { }, }, args: { + // Component defaults side: 'left', initialIsCollapsed: false, width: 248, @@ -417,21 +422,57 @@ export const SecurityExample: Story = { ), }; -export const MultipleFixedHeaders: Story = { - render: ({ ...args }) => ( +export const CollapsedStateInLocalStorage: Story = { + render: () => { + const key = 'EuiCollapsibleNav__isCollapsed'; + const initialIsCollapsed = window.localStorage.getItem(key) === 'true'; + const onCollapseToggle = (isCollapsed: boolean) => + window.localStorage.setItem(key, String(isCollapsed)); + + return ( + <> + + + + + + + + Toggle the collapsed state and refresh the page. The collapsed state + should have been saved/remembered + + + + ); + }, + argTypes: hideStorybookControls(['aria-label', 'side', 'width']), +}; + +export const GlobalCSSVariable: Story = { + render: ({ side, ...args }) => ( <> - First header - - - This story tests that EuiCollapsibleNav automatically adjusts its - position & height for multiple fixed headers + + + This story tests the global `--euiCollapsibleNavOffset` CSS variable - Second header + {/* In production, would just be `left="var(--euiCollapsibleNavOffset, 0)"` if the nav isn't changing sides */} + + This text should be visible at all times and the bar position should + update dynamically based on the nav width (including on mobile) + ), + argTypes: hideStorybookControls([ + 'aria-label', + 'initialIsCollapsed', + 'onCollapseToggle', + ]), }; const MockConsumerFlyout: FunctionComponent = () => { @@ -439,13 +480,13 @@ const MockConsumerFlyout: FunctionComponent = () => { return ( <> setFlyoutOpen(!flyoutIsOpen)}> - Toggle a flyout + Toggle flyout {flyoutIsOpen && ( setFlyoutOpen(false)}> - Some other mock consumer flyout that should overlap - EuiCollapsibleNav + This flyout's mask should overlay / sit on top of the collapsible + nav, on both desktop and mobile )} @@ -453,12 +494,14 @@ const MockConsumerFlyout: FunctionComponent = () => { ); }; -export const FlyoutInFixedHeaders: Story = { - render: ({ ...args }) => { +export const FlyoutOverlay: Story = { + render: (_) => { return ( - Nav content + + Click the "Toggle flyout" button in the top right hand corner + @@ -468,50 +511,5 @@ export const FlyoutInFixedHeaders: Story = { ); }, -}; - -export const GlobalCSSVariable: Story = { - render: ({ ...args }) => ( - <> - - - - This story tests the global `--euiCollapsibleNavOffset` CSS variable - - - - - This text should be visible at all times and the bar position should - update dynamically based on the nav width (including on mobile) - - - ), -}; - -export const CollapsedStateInLocalStorage: Story = { - render: () => { - const key = 'EuiCollapsibleNav__isCollapsed'; - const initialIsCollapsed = window.localStorage.getItem(key) === 'true'; - const onCollapseToggle = (isCollapsed: boolean) => - window.localStorage.setItem(key, String(isCollapsed)); - - return ( - <> - - - - - - - - Toggle the collapsed state and refresh the page. The collapsed state - should have been saved/remembered - - - - ); - }, + parameters: hideAllStorybookControls, }; diff --git a/src/components/empty_prompt/empty_prompt.stories.tsx b/src/components/empty_prompt/empty_prompt.stories.tsx index b71e1f06a46..dcb64852d34 100644 --- a/src/components/empty_prompt/empty_prompt.stories.tsx +++ b/src/components/empty_prompt/empty_prompt.stories.tsx @@ -21,25 +21,24 @@ import { EuiEmptyPrompt, EuiEmptyPromptProps } from './empty_prompt'; const meta: Meta = { title: 'EuiEmptyPrompt', component: EuiEmptyPrompt, + args: { + // Component defaults + titleSize: 'm', + paddingSize: 'l', + color: 'plain', // Default is actually 'transparent', but for the purposes of easier testing in Storybook we'll set it to plain + layout: 'vertical', + hasBorder: false, + hasShadow: false, + }, }; export default meta; type Story = StoryObj; -const componentDefaults: EuiEmptyPromptProps = { - titleSize: 'm', - paddingSize: 'l', - color: 'plain', // The component default is actually 'transparent', but for the purposes of easier testing in Storybook we'll set it to plain - layout: 'vertical', -}; - export const Playground: Story = { args: { - ...componentDefaults, title:

Start adding cases

, iconType: 'logoSecurity', - hasBorder: false, - hasShadow: false, body: 'Add a new case or change your filter settings.', // Should be wrapped in a `

` in production usage, but using a string makes this easier to edit in Storybook controls actions: [ @@ -67,7 +66,6 @@ export const PageTemplate: Story = { ), args: { - ...componentDefaults, title:

Create your first visualization

, layout: 'horizontal', icon: , diff --git a/src/components/error_boundary/error_boundary.stories.tsx b/src/components/error_boundary/error_boundary.stories.tsx index 54b011ba4a7..f62dffba499 100644 --- a/src/components/error_boundary/error_boundary.stories.tsx +++ b/src/components/error_boundary/error_boundary.stories.tsx @@ -11,31 +11,26 @@ import type { Meta, StoryObj } from '@storybook/react'; import { EuiErrorBoundary, EuiErrorBoundaryProps } from './error_boundary'; -const ErrorContent = () => { - throw new Error( - "I'm here to kick butt and chew bubblegum.\n\nAnd I'm all out of gum." - ); -}; - const meta: Meta = { title: 'EuiErrorBoundary', - component: () => ( - - - - ), + component: EuiErrorBoundary, parameters: { layout: 'fullscreen', }, - argTypes: { - onError: { - description: - 'TODO: extract prop descriptions, defaults, and types from Typescript ', - }, - }, }; export default meta; type Story = StoryObj; -export const Default: Story = {}; +const ErrorContent = () => { + throw new Error( + "I'm here to kick butt and chew bubblegum.\n\nAnd I'm all out of gum." + ); +}; + +export const Playground: Story = { + args: { + children: , + onError: console.log, + }, +}; diff --git a/src/components/filter_group/filter_button.stories.tsx b/src/components/filter_group/filter_button.stories.tsx index 49a2f3802ca..03eb27542b5 100644 --- a/src/components/filter_group/filter_button.stories.tsx +++ b/src/components/filter_group/filter_button.stories.tsx @@ -9,20 +9,26 @@ import React from 'react'; import type { Meta, StoryObj } from '@storybook/react'; +import { BUTTON_COLORS } from '../../themes/amsterdam/global_styling/mixins'; + import { EuiFilterGroup } from './filter_group'; import { EuiFilterButton, EuiFilterButtonProps } from './filter_button'; const meta: Meta = { title: 'EuiFilterButton', component: EuiFilterButton as any, -}; - -const defaultProps = { - hasActiveFilters: true, - numFilters: 5, - iconType: 'arrowDown', - iconSide: 'right' as const, - isDisabled: false, + argTypes: { + color: { control: 'select', options: BUTTON_COLORS }, + }, + args: { + // Component defaults + iconSide: 'right', + color: 'text', + badgeColor: 'accent', + grow: true, + isSelected: false, + isDisabled: false, + }, }; export default meta; @@ -34,7 +40,11 @@ export const Playground: Story = { Filter ), - args: defaultProps, + args: { + hasActiveFilters: true, + numFilters: 5, + iconType: 'arrowDown', + }, }; export const MultipleButtons: Story = { @@ -45,7 +55,11 @@ export const MultipleButtons: Story = { Filter three ), - args: defaultProps, + args: { + hasActiveFilters: true, + numFilters: 5, + iconType: 'arrowDown', + }, }; export const FullWidthAndGrow: Story = { @@ -78,5 +92,4 @@ export const FullWidthAndGrow: Story = { ), - args: { isDisabled: false }, }; diff --git a/src/components/header/header.stories.tsx b/src/components/header/header.stories.tsx index 0e458880861..bb0b4a5bae8 100644 --- a/src/components/header/header.stories.tsx +++ b/src/components/header/header.stories.tsx @@ -26,18 +26,18 @@ import { EuiHeader, EuiHeaderProps } from './header'; const meta: Meta = { title: 'EuiHeader', component: EuiHeader, -}; - -export default meta; -type Story = StoryObj; - -export const Playground: Story = { args: { + // Component defaults position: 'static', theme: 'default', }, }; +export default meta; +type Story = StoryObj; + +export const Playground: Story = {}; + export const Sections: Story = { args: { position: 'fixed', diff --git a/src/components/header/header_alert/header_alert.stories.tsx b/src/components/header/header_alert/header_alert.stories.tsx index 1ffe3335566..91749eee728 100644 --- a/src/components/header/header_alert/header_alert.stories.tsx +++ b/src/components/header/header_alert/header_alert.stories.tsx @@ -40,26 +40,24 @@ import { EuiHeaderAlert, EuiHeaderAlertProps } from './header_alert'; const meta: Meta = { title: 'EuiHeaderAlert', component: EuiHeaderAlert, + args: { + // Not default props, set for demo purposes + title: '7.0 release notes', + date: 'January 1st 1970', + text: 'Stay up-to-date on the latest and greatest features.', + action: ( + + Check out the docs + + ), + badge: 7.0, + }, }; export default meta; type Story = StoryObj; -const defaultProps = { - title: '7.0 release notes', - date: 'January 1st 1970', - text: 'Stay up-to-date on the latest and greatest features.', - action: ( - - Check out the docs - - ), - badge: 7.0, -}; - -export const Playground: Story = { - args: defaultProps, -}; +export const Playground: Story = {}; /** * Flyout example @@ -121,7 +119,6 @@ const Flyout = (props: EuiHeaderAlertProps) => { }; export const FlyoutExample: Story = { render: ({ ...args }) => , - args: defaultProps, }; /** @@ -182,5 +179,4 @@ const Popover = (props: any) => { }; export const PopoverExample: Story = { render: ({ ...args }) => , - args: defaultProps, }; diff --git a/src/components/header/header_links/header_links.stories.tsx b/src/components/header/header_links/header_links.stories.tsx index 4a07d685a53..35ae96b895b 100644 --- a/src/components/header/header_links/header_links.stories.tsx +++ b/src/components/header/header_links/header_links.stories.tsx @@ -17,6 +17,11 @@ import { EuiHeaderLinks, EuiHeaderLinksProps } from './header_links'; const meta: Meta = { title: 'EuiHeaderLinks', component: EuiHeaderLinks, + args: { + // Component defaults + gutterSize: 's', + popoverBreakpoints: ['xs', 's'], + }, }; export default meta; @@ -36,8 +41,4 @@ export const Playground: Story = { ), - args: { - gutterSize: 's', - popoverBreakpoints: ['xs', 's'], - }, }; diff --git a/src/components/header/header_logo/header_logo.stories.tsx b/src/components/header/header_logo/header_logo.stories.tsx index 32eb29bf3ca..2ba258a2b84 100644 --- a/src/components/header/header_logo/header_logo.stories.tsx +++ b/src/components/header/header_logo/header_logo.stories.tsx @@ -15,6 +15,11 @@ import { EuiHeaderLogo, EuiHeaderLogoProps } from './header_logo'; const meta: Meta = { title: 'EuiHeaderLogo', component: EuiHeaderLogo, + args: { + // Not default props, set for demo purposes + iconType: 'logoElastic', + children: 'Elastic', + }, }; export default meta; @@ -28,10 +33,6 @@ export const Playground: Story = { ), - args: { - iconType: 'logoElastic', - iconTitle: 'Elastic', - }, }; export const WithText: Story = { @@ -42,8 +43,4 @@ export const WithText: Story = { ), - args: { - iconType: 'logoElastic', - children: 'Elastic', - }, }; diff --git a/src/components/key_pad_menu/key_pad_menu_item.stories.tsx b/src/components/key_pad_menu/key_pad_menu_item.stories.tsx index 051c15ad95f..c6e829f7057 100644 --- a/src/components/key_pad_menu/key_pad_menu_item.stories.tsx +++ b/src/components/key_pad_menu/key_pad_menu_item.stories.tsx @@ -19,6 +19,12 @@ const meta: Meta = { argTypes: { checkable: { options: [undefined, 'multi', 'single'] }, }, + args: { + label: 'Hello world', // String makes prop easier to change/toggle + // Component defaults + isDisabled: false, + isSelected: false, + }, }; export default meta; @@ -27,9 +33,5 @@ type Story = StoryObj; export const Playground: Story = { args: { children: , - // Make these props easier to change/toggle - label: 'Hello world', - isDisabled: false, - isSelected: false, }, }; diff --git a/src/components/page/page.stories.tsx b/src/components/page/page.stories.tsx index 7da4e143873..7b1415eb09a 100644 --- a/src/components/page/page.stories.tsx +++ b/src/components/page/page.stories.tsx @@ -8,6 +8,7 @@ import React from 'react'; import type { Meta, StoryObj } from '@storybook/react'; +import { hideStorybookControls } from '../../../.storybook/utils'; import { EuiFlexGroup } from '../flex'; import { EuiSkeletonText } from '../skeleton'; @@ -19,23 +20,22 @@ import { EuiPage, EuiPageProps } from './page'; const meta: Meta = { title: 'EuiPage', component: EuiPage, + argTypes: { + restrictWidth: { control: 'boolean' }, + }, + args: { + // Component defaults + paddingSize: 'none', + grow: true, + direction: 'row', + restrictWidth: false, + }, }; export default meta; type Story = StoryObj; -const componentDefaults: EuiPageProps = { - paddingSize: 'none', - grow: true, - direction: 'row', - restrictWidth: false, -}; - export const Playground: Story = { - args: componentDefaults, - argTypes: { - restrictWidth: { control: 'boolean' }, - }, render: ({ ...args }) => ( ([ + 'grow', + 'direction', + 'paddingSize', + ]), render: ({ ...args }) => {_pageContent}, }; diff --git a/src/components/page/page_body/page_body.stories.tsx b/src/components/page/page_body/page_body.stories.tsx index 9c0229a919c..4e9621b5107 100644 --- a/src/components/page/page_body/page_body.stories.tsx +++ b/src/components/page/page_body/page_body.stories.tsx @@ -9,28 +9,31 @@ import React from 'react'; import type { Meta, StoryObj } from '@storybook/react'; +import { BORDER_RADII, EuiPanelProps } from '../../panel/panel'; import { EuiSkeletonText } from '../../skeleton'; import { EuiPage } from '../page'; import { EuiPageBody, EuiPageBodyProps } from './page_body'; -const meta: Meta = { +const meta: Meta> = { title: 'EuiPageBody', component: EuiPageBody, + argTypes: { + borderRadius: { control: 'radio', options: BORDER_RADII }, + }, + args: { + // Component defaults + restrictWidth: false, + paddingSize: 'none', + borderRadius: 'none', + component: 'main', // This is not a component default, but for the purposes of easier testing in the DOM in Storybook we'll set it to main + }, }; export default meta; type Story = StoryObj; -const componentDefaults: EuiPageBodyProps = { - panelled: true, - restrictWidth: false, - paddingSize: 'm', // The component default is actually 'none', but for nicer visuals in Storybook we'll set it to 'm' - component: 'main', // This is not a component default, but for the purposes of easier testing in the DOM in Storybook we'll set it to main -}; - export const Playground: Story = { - args: componentDefaults, render: ({ ...args }) => ( @@ -43,4 +46,8 @@ export const Playground: Story = { ), + args: { + panelled: true, + paddingSize: 'm', + }, }; diff --git a/src/components/page/page_header/page_header.stories.tsx b/src/components/page/page_header/page_header.stories.tsx index aa0ec8a28d4..d692656150f 100644 --- a/src/components/page/page_header/page_header.stories.tsx +++ b/src/components/page/page_header/page_header.stories.tsx @@ -24,21 +24,20 @@ const meta: Meta = { breadcrumbProps: { control: 'object' }, tabsProps: { control: 'object' }, }, + args: { + // Component defaults + paddingSize: 'none', + responsive: true, + restrictWidth: false, + alignItems: undefined, + }, }; export default meta; type Story = StoryObj; -const componentDefaults: EuiPageHeaderProps = { - paddingSize: 'none', - responsive: true, - restrictWidth: false, - alignItems: undefined, -}; - export const Playground: Story = { args: { - ...componentDefaults, pageTitle: 'Page title', iconType: 'logoKibana', description: 'Example of a description.', diff --git a/src/components/page/page_section/page_section.stories.tsx b/src/components/page/page_section/page_section.stories.tsx index 13a939dc511..89559bc065c 100644 --- a/src/components/page/page_section/page_section.stories.tsx +++ b/src/components/page/page_section/page_section.stories.tsx @@ -19,22 +19,21 @@ const meta: Meta = { argTypes: { bottomBorder: { control: 'select', options: [true, false, 'extended'] }, }, + args: { + // Component defaults + restrictWidth: false, + color: 'plain', // The component default is actually 'transparent', but for the purposes of easier testing in Storybook we'll set it to plain + paddingSize: 'l', + alignment: 'top', + grow: false, + component: 'section', // This is not a component default, but for the purposes of easier testing in the DOM in Storybook we'll set it to section + }, }; export default meta; type Story = StoryObj; -const componentDefaults: EuiPageSectionProps = { - restrictWidth: false, - color: 'plain', // The component default is actually 'transparent', but for the purposes of easier testing in Storybook we'll set it to plain - paddingSize: 'l', - alignment: 'top', - grow: false, - component: 'section', // This is not a component default, but for the purposes of easier testing in the DOM in Storybook we'll set it to section -}; - export const Playground: Story = { - args: componentDefaults, render: ({ ...args }) => ( // Block size demos the grow prop diff --git a/src/components/page/page_sidebar/page_sidebar.stories.tsx b/src/components/page/page_sidebar/page_sidebar.stories.tsx index ec1096e72d7..5b0d1d7959c 100644 --- a/src/components/page/page_sidebar/page_sidebar.stories.tsx +++ b/src/components/page/page_sidebar/page_sidebar.stories.tsx @@ -8,6 +8,7 @@ import React from 'react'; import type { Meta, StoryObj } from '@storybook/react'; +import { hideStorybookControls } from '../../../../.storybook/utils'; import { EuiSkeletonText } from '../../skeleton'; import { EuiPageSection } from '../page_section'; @@ -21,23 +22,22 @@ const meta: Meta = { parameters: { layout: 'fullscreen', }, + argTypes: { + sticky: { control: 'boolean' }, + }, + args: { + // Component defaults + paddingSize: 'm', // The component default is actually 'none', but for nicer visuals in Storybook we'll set it to 'm' + sticky: false, + minWidth: 248, + responsive: ['xs', 's'], + }, }; export default meta; type Story = StoryObj; -const componentDefaults: EuiPageSidebarProps = { - paddingSize: 'm', // The component default is actually 'none', but for nicer visuals in Storybook we'll set it to 'm' - sticky: false, - minWidth: 248, - responsive: ['xs', 's'], -}; - export const Playground: Story = { - args: componentDefaults, - argTypes: { - sticky: { control: 'boolean' }, - }, render: ({ ...args }) => ( ({ @@ -60,14 +60,18 @@ export const Playground: Story = { export const StickyOffset: Story = { args: { - ...componentDefaults, sticky: { offset: 50 }, }, argTypes: { + sticky: { + control: 'object', + }, // This story demos the sticky functionality; removing other props to prevent confusion - minWidth: { table: { disable: true } }, - paddingSize: { table: { disable: true } }, - responsive: { table: { disable: true } }, + ...hideStorybookControls([ + 'minWidth', + 'paddingSize', + 'responsive', + ]), }, render: ({ ...args }) => ( = { component: EuiSplitPanel.Inner, argTypes: { color: { control: 'select', options: COLORS }, - panelRef: { control: false }, + ...disableStorybookControls<_EuiSplitPanelInnerProps>(['panelRef']), + }, + args: { + // Component defaults + color: 'transparent', + paddingSize: 'm', + grow: true, }, }; @@ -25,12 +32,6 @@ export default meta; type Story = StoryObj<_EuiSplitPanelInnerProps>; export const SplitPanelInner: Story = { - args: { - // Default props - color: 'transparent', - paddingSize: 'm', - grow: true, - }, render: ({ ...args }) => ( Top panel diff --git a/src/components/panel/split_panel/split_panel_outer.stories.tsx b/src/components/panel/split_panel/split_panel_outer.stories.tsx index b612dfc8c3c..e6b89dc0254 100644 --- a/src/components/panel/split_panel/split_panel_outer.stories.tsx +++ b/src/components/panel/split_panel/split_panel_outer.stories.tsx @@ -14,17 +14,17 @@ import { EuiSplitPanel, _EuiSplitPanelOuterProps } from './split_panel'; const meta: Meta<_EuiSplitPanelOuterProps> = { title: 'EuiSplitPanel', component: EuiSplitPanel.Outer, + args: { + // Component defaults + direction: 'column', + responsive: ['xs', 's'], + }, }; export default meta; type Story = StoryObj<_EuiSplitPanelOuterProps>; export const SplitPanelOuter: Story = { - args: { - // Default props - direction: 'column', - responsive: ['xs', 's'], - }, render: ({ ...args }) => ( Top or left panel diff --git a/src/components/resizable_container/resizable_button.stories.tsx b/src/components/resizable_container/resizable_button.stories.tsx index c3a2c33fddf..4cd4b2261b0 100644 --- a/src/components/resizable_container/resizable_button.stories.tsx +++ b/src/components/resizable_container/resizable_button.stories.tsx @@ -19,6 +19,12 @@ import { const meta: Meta = { title: 'EuiResizableButton', component: EuiResizableButton, + args: { + // Component defaults + alignIndicator: 'center', + disabled: false, + isHorizontal: false, + }, }; export default meta; @@ -30,9 +36,4 @@ export const Playground: Story = { ), - args: { - isHorizontal: true, - alignIndicator: 'center', - disabled: false, - }, }; diff --git a/src/components/resizable_container/resizable_collapse_button.stories.tsx b/src/components/resizable_container/resizable_collapse_button.stories.tsx index 9b318e4291c..efc09176eff 100644 --- a/src/components/resizable_container/resizable_collapse_button.stories.tsx +++ b/src/components/resizable_container/resizable_collapse_button.stories.tsx @@ -8,6 +8,7 @@ import React from 'react'; import type { Meta, StoryObj } from '@storybook/react'; +import { hideStorybookControls } from '../../../.storybook/utils'; import { EuiPanel } from '../panel'; import { EuiResizableContainer } from './resizable_container'; @@ -19,19 +20,20 @@ import { const meta: Meta = { title: 'EuiResizableCollapseButton', component: EuiResizableCollapseButton, -}; - -export default meta; -type Story = StoryObj; - -export const Playground: Story = { args: { + isVisible: true, + // Component defaults direction: 'horizontal', externalPosition: 'before', internalPosition: 'middle', - isVisible: true, isCollapsed: false, }, +}; + +export default meta; +type Story = StoryObj; + +export const Playground: Story = { render: ({ isCollapsed, direction, ...args }) => ( ([ + 'externalPosition', + 'isVisible', + 'isCollapsed', + ]), render: ({ direction, internalPosition }) => ( {(EuiResizablePanel, EuiResizableButton) => ( diff --git a/src/components/side_nav/side_nav.stories.tsx b/src/components/side_nav/side_nav.stories.tsx index fdbfae7c9e1..9baf22f8336 100644 --- a/src/components/side_nav/side_nav.stories.tsx +++ b/src/components/side_nav/side_nav.stories.tsx @@ -8,6 +8,7 @@ import React from 'react'; import type { Meta, StoryObj } from '@storybook/react'; +import { hideStorybookControls } from '../../../.storybook/utils'; import { EuiText } from '../text'; @@ -16,6 +17,12 @@ import { EuiSideNav, EuiSideNavProps } from './side_nav'; const meta: Meta = { title: 'EuiSideNav', component: EuiSideNav, + args: { + // Component defaults + mobileBreakpoints: ['xs', 's'], + items: [], + isOpenOnMobile: false, + }, decorators: [ (Story) => (
@@ -29,14 +36,6 @@ const meta: Meta = { export default meta; type Story = StoryObj; -const componentDefaults: EuiSideNavProps = { - mobileBreakpoints: ['xs', 's'], - items: [], - // mobileTitle does not have defaults; they are being set here as they are shared between examples - mobileTitle: 'Mobile navigation header', - isOpenOnMobile: false, -}; - const sharedSideNavItems = [ { name: 'Has nested children', @@ -83,29 +82,28 @@ const sharedSideNavItems = [ export const Playground: Story = { args: { - ...componentDefaults, heading: 'Elastic', headingProps: { element: 'h1', screenReaderOnly: false }, items: sharedSideNavItems, + mobileTitle: 'Mobile navigation header', }, }; export const MobileSideNav: Story = { args: { - ...componentDefaults, isOpenOnMobile: true, items: sharedSideNavItems, mobileTitle: 'Toggle isOpenOnMobile in the controls panel', }, - argTypes: { - // This story demos the side nav on smaller screens; removing other props to streamline controls - 'aria-label': { table: { disable: true } }, - heading: { table: { disable: true } }, - headingProps: { table: { disable: true } }, - items: { table: { disable: true } }, - renderItem: { table: { disable: true } }, - truncate: { table: { disable: true } }, - }, + // This story demos the side nav on smaller screens; removing other props to streamline controls + argTypes: hideStorybookControls([ + 'aria-label', + 'heading', + 'headingProps', + 'items', + 'renderItem', + 'truncate', + ]), parameters: { viewport: { defaultViewport: 'mobile1', @@ -115,7 +113,6 @@ export const MobileSideNav: Story = { export const RenderItem: Story = { args: { - ...componentDefaults, renderItem: ({ children }) => {children}, items: [ { @@ -135,15 +132,15 @@ export const RenderItem: Story = { }, ], }, - argTypes: { - // This story demos the renderItem prop; removing other props to streamline controls - 'aria-label': { table: { disable: true } }, - heading: { table: { disable: true } }, - headingProps: { table: { disable: true } }, - toggleOpenOnMobile: { table: { disable: true } }, - isOpenOnMobile: { table: { disable: true } }, - mobileBreakpoints: { table: { disable: true } }, - mobileTitle: { table: { disable: true } }, - truncate: { table: { disable: true } }, - }, + // This story demos the renderItem prop; removing other props to streamline controls + argTypes: hideStorybookControls([ + 'aria-label', + 'heading', + 'headingProps', + 'toggleOpenOnMobile', + 'isOpenOnMobile', + 'mobileBreakpoints', + 'mobileTitle', + 'truncate', + ]), }; diff --git a/src/components/text_truncate/text_truncate.stories.tsx b/src/components/text_truncate/text_truncate.stories.tsx index 82efe8a7755..d72d8f8cd90 100644 --- a/src/components/text_truncate/text_truncate.stories.tsx +++ b/src/components/text_truncate/text_truncate.stories.tsx @@ -9,6 +9,10 @@ import React, { useState } from 'react'; import type { Meta, StoryObj } from '@storybook/react'; import { css } from '@emotion/react'; +import { + hideStorybookControls, + disableStorybookControls, +} from '../../../.storybook/utils'; import { EuiHighlight, EuiMark } from '../../components'; @@ -21,17 +25,17 @@ const meta: Meta = { truncationOffset: { if: { arg: 'truncation', neq: 'startEnd' } }, // Should also not show on `middle`, but Storybook doesn't currently support multiple if conditions :( truncationPosition: { if: { arg: 'truncation', eq: 'startEnd' } }, }, + args: { + text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit', + // Component defaults + truncation: 'end', + ellipsis: '…', + }, }; export default meta; type Story = StoryObj; -const componentDefaults = { - text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit', - truncation: 'end', - ellipsis: '…', -} as const; - export const Playground: Story = { render: (props) => (
@@ -39,7 +43,6 @@ export const Playground: Story = {
), args: { - ...componentDefaults, width: 200, }, }; @@ -66,12 +69,9 @@ export const ResizeObserver: Story = { ), args: { - ...componentDefaults, onResize: console.log, }, - argTypes: { - width: { control: false }, - }, + argTypes: disableStorybookControls(['width']), }; export const StartEndAnchorForSearch: Story = { @@ -116,18 +116,17 @@ export const StartEndAnchorForSearch: Story = { ); }, args: { - ...componentDefaults, width: 200, truncation: 'startEnd', truncationPosition: 30, }, - argTypes: { + argTypes: hideStorybookControls([ // Disable uncontrollable props - truncation: { table: { disable: true } }, - truncationPosition: { table: { disable: true } }, + 'truncation', + 'truncationPosition', // Disable props that aren't useful for this this demo - truncationOffset: { table: { disable: true } }, - children: { table: { disable: true } }, - onResize: { table: { disable: true } }, - }, + 'truncationOffset', + 'children', + 'onResize', + ]), }; From 872804bcaf342b4403bb222e6f75766651438bf6 Mon Sep 17 00:00:00 2001 From: Trevor Pierce <1Copenut@users.noreply.github.com> Date: Wed, 4 Oct 2023 08:20:19 -0500 Subject: [PATCH 06/38] [INFRA] Parallelize Buildkite test job, take 3 (#7242) Co-authored-by: Cee Chen --- .../pipelines/pipeline_pull_request_test.yml | 69 ++++++++++++++++- .buildkite/scripts/pipeline_test.sh | 74 +++++++++++++++---- src/components/icon/icon.test.tsx | 13 ++-- 3 files changed, 134 insertions(+), 22 deletions(-) diff --git a/.buildkite/pipelines/pipeline_pull_request_test.yml b/.buildkite/pipelines/pipeline_pull_request_test.yml index af25c625a9f..08118c5a138 100644 --- a/.buildkite/pipelines/pipeline_pull_request_test.yml +++ b/.buildkite/pipelines/pipeline_pull_request_test.yml @@ -1,9 +1,72 @@ # 🏠/.buildkite/pipelines/pipeline_pull_request_test.yml steps: - - agents: + - command: .buildkite/scripts/pipeline_test.sh + label: ":typescript: Linting" + agents: provider: "gcp" - command: .buildkite/scripts/pipeline_test.sh - if: build.branch != "main" # We're skipping testing commits in main for now to maintain parity with previous Jenkins setup + env: + TEST_TYPE: 'lint' + if: build.branch != "main" # This job is triggered by the combined test and deploy docs for every PR + + - command: .buildkite/scripts/pipeline_test.sh + label: ":jest: TS unit tests" + agents: + provider: "gcp" + env: + TEST_TYPE: 'unit:ts' + if: build.branch != "main" + + - command: .buildkite/scripts/pipeline_test.sh + label: ":jest: TSX unit tests on React 16" + agents: + provider: "gcp" + env: + TEST_TYPE: 'unit:tsx:16' + if: build.branch != "main" + + - command: .buildkite/scripts/pipeline_test.sh + label: ":jest: TSX unit tests on React 17" + agents: + provider: "gcp" + env: + TEST_TYPE: 'unit:tsx:17' + if: build.branch != "main" + + - command: .buildkite/scripts/pipeline_test.sh + label: ":jest: TSX unit tests on React 18" + agents: + provider: "gcp" + env: + TEST_TYPE: 'unit:tsx' + if: build.branch != "main" + + - command: .buildkite/scripts/pipeline_test.sh + label: ":cypress: Cypress tests on React 16" + agents: + provider: "gcp" + env: + TEST_TYPE: 'cypress:16' + if: build.branch != "main" + artifact_paths: + - "cypress/screenshots/**/*.png" + + - command: .buildkite/scripts/pipeline_test.sh + label: ":cypress: Cypress tests on React 17" + agents: + provider: "gcp" + env: + TEST_TYPE: 'cypress:17' + if: build.branch != "main" + artifact_paths: + - "cypress/screenshots/**/*.png" + + - command: .buildkite/scripts/pipeline_test.sh + label: ":cypress: Cypress tests on React 18" + agents: + provider: "gcp" + env: + TEST_TYPE: 'cypress:18' + if: build.branch != "main" artifact_paths: - "cypress/screenshots/**/*.png" diff --git a/.buildkite/scripts/pipeline_test.sh b/.buildkite/scripts/pipeline_test.sh index bad0e5ce24a..0a82ff17071 100644 --- a/.buildkite/scripts/pipeline_test.sh +++ b/.buildkite/scripts/pipeline_test.sh @@ -2,17 +2,63 @@ set -euo pipefail -docker run \ - -i --rm \ - --env GIT_COMMITTER_NAME=test \ - --env GIT_COMMITTER_EMAIL=test \ - --env HOME=/tmp \ - --user="$(id -u):$(id -g)" \ - --volume="$(pwd):/app" \ - --workdir=/app \ - docker.elastic.co/eui/ci:5.3 \ - bash -c "/opt/yarn*/bin/yarn \ - && yarn cypress install \ - && yarn lint \ - && yarn test-unit --node-options=--max_old_space_size=2048 \ - && yarn test-cypress --node-options=--max_old_space_size=2048" +DOCKER_OPTIONS=( + -i --rm + --env GIT_COMMITTER_NAME=test + --env GIT_COMMITTER_EMAIL=test + --env HOME=/tmp + --user="$(id -u):$(id -g)" + --volume="$(pwd):/app" + --workdir=/app + docker.elastic.co/eui/ci:5.3 +) + +case $TEST_TYPE in + lint) + echo "[TASK]: Running linters" + DOCKER_OPTIONS+=(bash -c "/opt/yarn*/bin/yarn && yarn lint") + ;; + + unit:ts) + echo "[TASK]: Running .ts and .js unit tests" + DOCKER_OPTIONS+=(bash -c "/opt/yarn*/bin/yarn && yarn test-unit --node-options=--max_old_space_size=2048 --testMatch=non-react") + ;; + + unit:tsx:16) + echo "[TASK]: Running Jest .tsx tests against React 16" + DOCKER_OPTIONS+=(bash -c "/opt/yarn*/bin/yarn && yarn test-unit --node-options=--max_old_space_size=2048 --react-version=16 --testMatch=react") + ;; + + unit:tsx:17) + echo "[TASK]: Running Jest .tsx tests against React 17" + DOCKER_OPTIONS+=(bash -c "/opt/yarn*/bin/yarn && yarn test-unit --node-options=--max_old_space_size=2048 --react-version=17 --testMatch=react") + ;; + + unit:tsx) + echo "[TASK]: Running Jest .tsx tests against React 18" + DOCKER_OPTIONS+=(bash -c "/opt/yarn*/bin/yarn && yarn test-unit --node-options=--max_old_space_size=2048 --testMatch=react") + ;; + + cypress:16) + echo "[TASK]: Running Cypress tests against React 16" + DOCKER_OPTIONS+=(bash -c "/opt/yarn*/bin/yarn && yarn test-cypress --node-options=--max_old_space_size=2048 --react-version=16") + ;; + + cypress:17) + echo "[TASK]: Running Cypress tests against React 17" + DOCKER_OPTIONS+=(bash -c "/opt/yarn*/bin/yarn && yarn test-cypress --node-options=--max_old_space_size=2048 --react-version=17") + ;; + + cypress:18) + echo "[TASK]: Running Cypress tests against React 18" + DOCKER_OPTIONS+=(bash -c "/opt/yarn*/bin/yarn && yarn test-cypress --node-options=--max_old_space_size=2048") + ;; + + *) + echo "[ERROR]: Unknown task" + echo "Exit code: 1" + exit 1 + ;; +esac + +docker run "${DOCKER_OPTIONS[@]}" diff --git a/src/components/icon/icon.test.tsx b/src/components/icon/icon.test.tsx index 85e53b3b239..5080fade0fb 100644 --- a/src/components/icon/icon.test.tsx +++ b/src/components/icon/icon.test.tsx @@ -34,11 +34,14 @@ const testIcon = (props: PropsOf) => async () => { act(() => { render(); }); - await waitFor(() => { - const icon = document.querySelector(`[data-icon-type=${props.type}]`); - expect(icon).toHaveAttribute('data-is-loaded', 'true'); - expect(icon).toMatchSnapshot(); - }); + await waitFor( + () => { + const icon = document.querySelector(`[data-icon-type=${props.type}]`); + expect(icon).toHaveAttribute('data-is-loaded', 'true'); + expect(icon).toMatchSnapshot(); + }, + { timeout: 3000 } // CI will sometimes time out if the icon doesn't load fast enough + ); }; describe('EuiIcon', () => { From 08c7af8ca0d5422e3a1caab513b635ceab965390 Mon Sep 17 00:00:00 2001 From: Cee Chen <549407+cee-chen@users.noreply.github.com> Date: Wed, 4 Oct 2023 09:19:31 -0700 Subject: [PATCH 07/38] Create new EuiTextBlockTruncate component (#7250) --- .../guide_tabbed_page/guide_tabbed_page.tsx | 18 +- src-docs/src/routes.js | 52 +- .../src/views/text_truncate/multi_line.tsx | 25 + .../text_truncate/text_truncate_example.js | 527 ++++++++++-------- src/components/text_truncate/index.ts | 3 + .../text_block_truncate.stories.tsx | 45 ++ .../text_block_truncate.test.tsx | 45 ++ .../text_truncate/text_block_truncate.tsx | 73 +++ upcoming_changelogs/7250.md | 1 + 9 files changed, 524 insertions(+), 265 deletions(-) create mode 100644 src-docs/src/views/text_truncate/multi_line.tsx create mode 100644 src/components/text_truncate/text_block_truncate.stories.tsx create mode 100644 src/components/text_truncate/text_block_truncate.test.tsx create mode 100644 src/components/text_truncate/text_block_truncate.tsx create mode 100644 upcoming_changelogs/7250.md diff --git a/src-docs/src/components/guide_tabbed_page/guide_tabbed_page.tsx b/src-docs/src/components/guide_tabbed_page/guide_tabbed_page.tsx index 78c9fce25ed..576de96f5dd 100644 --- a/src-docs/src/components/guide_tabbed_page/guide_tabbed_page.tsx +++ b/src-docs/src/components/guide_tabbed_page/guide_tabbed_page.tsx @@ -20,7 +20,10 @@ import { } from '../../../../src/components'; import { LanguageSelector, ThemeContext } from '../with_theme'; -import { GuideSection } from '../guide_section/guide_section'; +import { + GuideSection, + GuideSectionProps, +} from '../guide_section/guide_section'; export type GuideTabbedPageProps = PropsWithChildren & CommonProps & { @@ -126,11 +129,20 @@ export const GuideTabbedPage: FunctionComponent = ({ /> ); } else { - const PageComponent = page.page; + let rendered: ReactNode; + + if (page.page) { + const PageComponent = page.page; + rendered = ; + } else { + rendered = page.sections.map((sectionProps: GuideSectionProps) => ( + + )); + } return ( - + {rendered} ); } diff --git a/src-docs/src/routes.js b/src-docs/src/routes.js index 6fdae119ce1..930a20ca705 100644 --- a/src-docs/src/routes.js +++ b/src-docs/src/routes.js @@ -650,31 +650,33 @@ const navigation = [ { name: 'Utilities', items: [ - AccessibilityExample, - AutoSizerExample, - BeaconExample, - ColorPaletteExample, - CopyExample, - UtilityClassesExample, - DelayRenderExample, - ErrorBoundaryExample, - FocusTrapExample, - HighlightAndMarkExample, - HtmlIdGeneratorExample, - InnerTextExample, - I18nExample, - MutationObserverExample, - OutsideClickDetectorExample, - OverlayMaskExample, - PortalExample, - PrettyDurationExample, - ProviderExample, - ResizeObserverExample, - ScrollExample, - TextDiffExample, - TextTruncateExample, - WindowEventExample, - ].map((example) => createExample(example)), + ...[ + AccessibilityExample, + AutoSizerExample, + BeaconExample, + ColorPaletteExample, + CopyExample, + UtilityClassesExample, + DelayRenderExample, + ErrorBoundaryExample, + FocusTrapExample, + HighlightAndMarkExample, + HtmlIdGeneratorExample, + InnerTextExample, + I18nExample, + MutationObserverExample, + OutsideClickDetectorExample, + OverlayMaskExample, + PortalExample, + PrettyDurationExample, + ProviderExample, + ResizeObserverExample, + ScrollExample, + TextDiffExample, + ].map((example) => createExample(example)), + createTabbedPage(TextTruncateExample), + createExample(WindowEventExample), + ], }, { name: 'Package', diff --git a/src-docs/src/views/text_truncate/multi_line.tsx b/src-docs/src/views/text_truncate/multi_line.tsx new file mode 100644 index 00000000000..f341584a839 --- /dev/null +++ b/src-docs/src/views/text_truncate/multi_line.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { css } from '@emotion/react'; +import { faker } from '@faker-js/faker'; + +import { EuiPanel, EuiText, EuiTextBlockTruncate } from '../../../../src'; + +export default () => { + return ( + + + + {faker.lorem.lines(10)} + + + + ); +}; diff --git a/src-docs/src/views/text_truncate/text_truncate_example.js b/src-docs/src/views/text_truncate/text_truncate_example.js index a38a75383b8..a463fba56b9 100644 --- a/src-docs/src/views/text_truncate/text_truncate_example.js +++ b/src-docs/src/views/text_truncate/text_truncate_example.js @@ -9,6 +9,7 @@ import { EuiCode, EuiCallOut, EuiTextTruncate, + EuiTextBlockTruncate, } from '../../../../src/components'; import Truncation from './truncation'; @@ -29,10 +30,13 @@ const renderPropSource = require('!!raw-loader!./render_prop'); import Performance from './performance'; const performanceSource = require('!!raw-loader!./performance'); +import MultiLine from './multi_line'; +const multiLineSource = require('!!raw-loader!./multi_line'); + export const TextTruncateExample = { title: 'Text truncation', isBeta: true, - intro: ( + notice: ( EuiTextTruncate is a beta component that is still undergoing performance investigation. We @@ -40,259 +44,308 @@ export const TextTruncateExample = { per page). ), - sections: [ + pages: [ { - source: [ + title: 'Single line', + sections: [ { - type: GuideSectionTypes.JS, - code: truncationSource, + source: [ + { + type: GuideSectionTypes.JS, + code: truncationSource, + }, + ], + text: ( + <> +

+ EuiTextTruncate provides customizable and + size-aware single line text truncation. +

+

+ The four truncation styles supported are{' '} + start, end,{' '} + startEnd, and middle. + Resize the below demo to see how different truncation styles + respond to dynamic width changes. +

+ + ), + demo: , + props: { EuiTextTruncate }, + snippet: ``, }, - ], - text: ( - <> -

- EuiTextTruncate provides customizable and - size-aware single line text truncation. -

-

- The four truncation styles supported are start,{' '} - end, startEnd, and{' '} - middle. Resize the below demo to see how - different truncation styles respond to dynamic width changes. -

- - ), - demo: , - props: { EuiTextTruncate }, - snippet: ``, - }, - { - text: ( - <> - -

- EuiTextTruncate attempts to mimic the behavior of{' '} - text-overflow: ellipsis as closely as possible, - although there may be edge cases and cross-browser issues, as this - is essentially a{' '} - + - browser implementation - {' '} - we are trying to polyfill. +

+ EuiTextTruncate attempts to mimic the + behavior of text-overflow: ellipsis as + closely as possible, although there may be edge cases and + cross-browser issues, as this is essentially a{' '} + + browser implementation + {' '} + we are trying to polyfill. +

+
    +
  • + Screen readers should ignore the truncated text and only + read out the full text. +
  • +
  • + Sighted mouse users will be able to briefly hover over the + truncated text and read the full text in a native browser + title tooltip. +
  • +
  • + For mouse users, double clicking to select the truncated + line should allow copying the full untruncated text. +
  • +
+
+ + ), + }, + { + title: 'Custom ellipsis', + source: [ + { + type: GuideSectionTypes.JS, + code: ellipsisSource, + }, + ], + text: ( +

+ By default, EuiTextTruncate uses the unicode + character for horizontal ellipis. It can be customized via the{' '} + ellipsis prop as necessary (e.g. for specific + languages, extra punctuation, etc).

-
    -
  • - Screen readers should ignore the truncated text and only read - out the full text. -
  • -
  • - Sighted mouse users will be able to briefly hover over the - truncated text and read the full text in a native browser title - tooltip. -
  • -
  • - For mouse users, double clicking to select the truncated line - should allow copying the full untruncated text. -
  • -
- - - ), - }, - { - title: 'Custom ellipsis', - source: [ + ), + demo: , + props: { EuiTextTruncate }, + snippet: ``, + }, + { + title: 'Truncation offset', + source: [ + { + type: GuideSectionTypes.JS, + code: truncationOffsetSource, + }, + ], + text: ( +

+ The start and end truncation + types support a truncationOffset property that + allows preserving a specified number of characters at either the + start or end of the text. Increase or decrease the number control + below to see the prop in action. +

+ ), + demo: , + props: { EuiTextTruncate }, + snippet: ``, + }, { - type: GuideSectionTypes.JS, - code: ellipsisSource, + title: 'Truncation position', + source: [ + { + type: GuideSectionTypes.JS, + code: truncationPositionSource, + }, + ], + text: ( + <> +

+ The startEnd truncation type supports a{' '} + truncationPosition property. By default,{' '} + startEnd anchors the displayed text to the + middle of the string. However, you may prefer to display a + specific subsection of the full text closer to the start or end, + which this prop allows. +

+

+ This behavior will intelligently detect when positions are near + enough to the start or end of the text to omit leading or + trailing ellipses when necessary. +

+

+ Increase or decrease the number control below to see the prop in + action. +

+ + ), + demo: , + props: { EuiTextTruncate }, + snippet: ``, }, - ], - text: ( -

- By default, EuiTextTruncate uses the unicode - character for horizontal ellipis. It can be customized via the{' '} - ellipsis prop as necessary (e.g. for specific - languages, extra punctuation, etc). -

- ), - demo: , - props: { EuiTextTruncate }, - snippet: ``, - }, - { - title: 'Truncation offset', - source: [ { - type: GuideSectionTypes.JS, - code: truncationOffsetSource, + title: 'Render prop', + source: [ + { + type: GuideSectionTypes.JS, + code: renderPropSource, + }, + ], + text: ( + <> +

+ By default, EuiTextTruncate will automatically + output the calculated truncated string. You can optionally + override this by passing a render prop function to{' '} + children, which allows for more flexible text + rendering. +

+

+ The below example demonstrates a primary use case for the render + prop and the truncationPosition prop. If a + user is searching for a specific word in truncated text, you can + use{' '} + + EuiHighlight or EuiMark + {' '} + to highlight the search term, and passing the index of the found + word to truncationPosition ensures the search + term is always visible to the user. +

+ + + ), + demo: , + props: { EuiTextTruncate }, + snippet: ` + {(text) => {text}} + `, }, - ], - text: ( -

- The start and end truncation - types support a truncationOffset property that - allows preserving a specified number of characters at either the start - or end of the text. Increase or decrease the number control below to - see the prop in action. -

- ), - demo: , - props: { EuiTextTruncate }, - snippet: ``, - }, - { - title: 'Truncation position', - source: [ { - type: GuideSectionTypes.JS, - code: truncationPositionSource, + title: 'Performance', + text: ( + <> +

+ EuiTextTruncate uses a canvas element under the + hood to manipulate text and calculate whether the text width + fits within the available width. Additionally, by default, the + component will include its own resize observer in order to react + to width changes. +

+

+ These functionalities can cause performance issues if the + component is rendered over hundreds of times per page, and we + would strongly recommend using caution when doing so. Several + escape hatches are available for performance improvements: +

+
    + css` + li:not(:last-child) { + margin-block-end: ${euiTheme.size.m}; + } + ` + } + > +
  1. + Pass a width prop to skip initializing a + resize observer for each component instance. For text within a + container of the same width, we would strongly recommend + applying a single resize observer to the parent container and + passing that width to all child{' '} + EuiTextTruncates. Additionally, you may want + to consider{' '} + + throttling + {' '} + any resize observers or width-based logic. +
  2. +
  3. + Use{' '} + + virtualization + {' '} + to reduce the number of rendered elements visible at any given + time. For over hundreds of instances, this will generally be + the most effective solution for performance or rerender + issues. +
  4. +
  5. + If necessary, consider pulling out the underlying{' '} + TruncationUtils and re-using the same + canvas context, as opposed to repeatedly creating new ones. +
  6. +
+ + ), + demo: , + source: [{ type: GuideSectionTypes.TSX, code: performanceSource }], + props: { EuiTextTruncate }, + snippet: ``, }, ], - text: ( - <> -

- The startEnd truncation type supports a{' '} - truncationPosition property. By default,{' '} - startEnd anchors the displayed text to the middle - of the string. However, you may prefer to display a specific - subsection of the full text closer to the start or end, which this - prop allows. -

-

- This behavior will intelligently detect when positions are near - enough to the start or end of the text to omit leading or trailing - ellipses when necessary. -

-

- Increase or decrease the number control below to see the prop in - action. -

- - ), - demo: , - props: { EuiTextTruncate }, - snippet: ``, }, { - title: 'Render prop', - source: [ + title: 'Multi line', + sections: [ { - type: GuideSectionTypes.JS, - code: renderPropSource, + source: [ + { + type: GuideSectionTypes.JS, + code: multiLineSource, + }, + ], + text: ( + <> +

+ EuiTextBlockTruncate allows truncating text + after a set number of wrapping lines. +

+

+ Please note: This component is currently a quick shortcut for + the{' '} + + CSS line-clamp + {' '} + property. This means that truncating at the end of the text is + the default, and there are currently no plans to add JavaScript + customization for this behavior. +

+ + ), + demo: , + props: { EuiTextBlockTruncate }, + snippet: `Hello world`, }, ], - text: ( - <> -

- By default, EuiTextTruncate will automatically - output the calculated truncated string. You can optionally override - this by passing a render prop function to{' '} - children, which allows for more flexible text - rendering. -

-

- The below example demonstrates a primary use case for the render - prop and the truncationPosition prop. If a user - is searching for a specific word in truncated text, you can use{' '} - - EuiHighlight or EuiMark - {' '} - to highlight the search term, and passing the index of the found - word to truncationPosition ensures the search - term is always visible to the user. -

- - - ), - demo: , - props: { EuiTextTruncate }, - snippet: ` - {(text) => {text}} -`, - }, - { - title: 'Performance', - text: ( - <> -

- EuiTextTruncate uses a canvas element under the - hood to manipulate text and calculate whether the text width fits - within the available width. Additionally, by default, the component - will include its own resize observer in order to react to width - changes. -

-

- These functionalities can cause performance issues if the component - is rendered over hundreds of times per page, and we would strongly - recommend using caution when doing so. Several escape hatches are - available for performance improvements: -

-
    - css` - li:not(:last-child) { - margin-block-end: ${euiTheme.size.m}; - } - ` - } - > -
  1. - Pass a width prop to skip initializing a resize - observer for each component instance. For text within a container - of the same width, we would strongly recommend applying a single - resize observer to the parent container and passing that width to - all child EuiTextTruncates. Additionally, you may - want to consider{' '} - - throttling - {' '} - any resize observers or width-based logic. -
  2. -
  3. - Use{' '} - - virtualization - {' '} - to reduce the number of rendered elements visible at any given - time. For over hundreds of instances, this will generally be the - most effective solution for performance or rerender issues. -
  4. -
  5. - If necessary, consider pulling out the underlying{' '} - TruncationUtils and re-using the same canvas - context, as opposed to repeatedly creating new ones. -
  6. -
- - ), - demo: , - source: [{ type: GuideSectionTypes.TSX, code: performanceSource }], - props: { EuiTextTruncate }, - snippet: ``, }, ], }; diff --git a/src/components/text_truncate/index.ts b/src/components/text_truncate/index.ts index a2785e5a6c7..4d5bbcb18b0 100644 --- a/src/components/text_truncate/index.ts +++ b/src/components/text_truncate/index.ts @@ -12,4 +12,7 @@ export type { } from './text_truncate'; export { EuiTextTruncate } from './text_truncate'; +export type { EuiTextBlockTruncateProps } from './text_block_truncate'; +export { EuiTextBlockTruncate } from './text_block_truncate'; + export { TruncationUtils } from './utils'; diff --git a/src/components/text_truncate/text_block_truncate.stories.tsx b/src/components/text_truncate/text_block_truncate.stories.tsx new file mode 100644 index 00000000000..fae93b3b2ca --- /dev/null +++ b/src/components/text_truncate/text_block_truncate.stories.tsx @@ -0,0 +1,45 @@ +/* + * 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 type { Meta, StoryObj } from '@storybook/react'; + +import { EuiPanel } from '../panel'; + +import { + EuiTextBlockTruncate, + EuiTextBlockTruncateProps, +} from './text_block_truncate'; + +const meta: Meta = { + title: 'EuiTextBlockTruncate', + component: EuiTextBlockTruncate, + decorators: [ + (Story) => ( + + + + ), + ], +}; + +export default meta; +type Story = StoryObj; + +export const Playground: Story = { + render: ({ children, ...args }) => ( + +

{children}

+
+ ), + args: { + lines: 3, + children: + "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.", + }, +}; diff --git a/src/components/text_truncate/text_block_truncate.test.tsx b/src/components/text_truncate/text_block_truncate.test.tsx new file mode 100644 index 00000000000..d954c93d6bf --- /dev/null +++ b/src/components/text_truncate/text_block_truncate.test.tsx @@ -0,0 +1,45 @@ +/* + * 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 { render } from '../../test/rtl'; +import { shouldRenderCustomStyles } from '../../test/internal'; + +import { EuiTextBlockTruncate } from './text_block_truncate'; + +describe('EuiTextBlockTruncate', () => { + shouldRenderCustomStyles(); + + it('renders', () => { + const { container } = render( + Hello world + ); + expect(container.firstChild).toMatchInlineSnapshot(` +
+ Hello world +
+ `); + }); + + it('allows cloning styles onto the child element instead of rendering an extra div wrapper', () => { + const { container } = render( + +

Hello world

+
+ ); + expect(container.firstChild).toMatchInlineSnapshot(` +

+ Hello world +

+ `); + }); +}); diff --git a/src/components/text_truncate/text_block_truncate.tsx b/src/components/text_truncate/text_block_truncate.tsx new file mode 100644 index 00000000000..8e91d026e54 --- /dev/null +++ b/src/components/text_truncate/text_block_truncate.tsx @@ -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, { + isValidElement, + FunctionComponent, + HTMLAttributes, + PropsWithChildren, + useMemo, +} from 'react'; +import { css } from '@emotion/react'; +import classNames from 'classnames'; + +import { CommonProps } from '../common'; +import { cloneElementWithCss } from '../../services'; + +const styles = { + euiTextBlockTruncate: css` + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 0; + overflow: hidden; + `, +}; + +export type EuiTextBlockTruncateProps = PropsWithChildren & + CommonProps & + HTMLAttributes & { + /** + * Number of lines of text to truncate to + */ + lines: number; + /** + * Applies styling to the child element instead of rendering a parent wrapper `div`. + * Can only be used when wrapping a *single* child element/tag, and not raw text. + */ + cloneElement?: boolean; + }; + +export const EuiTextBlockTruncate: FunctionComponent< + EuiTextBlockTruncateProps +> = ({ children, className, style, lines, cloneElement, ...rest }) => { + const classes = classNames('euiTextBlockTruncate', className); + + const cssStyles = styles.euiTextBlockTruncate; + + const inlineStyles = useMemo( + () => ({ + WebkitLineClamp: lines, + ...style, + }), + [lines, style] + ); + + if (isValidElement(children) && cloneElement) { + return cloneElementWithCss(children, { + css: cssStyles, + style: { ...children.props.style, ...inlineStyles }, + className: classNames(children.props.className, classes), + }); + } else { + return ( +
+ {children} +
+ ); + } +}; diff --git a/upcoming_changelogs/7250.md b/upcoming_changelogs/7250.md new file mode 100644 index 00000000000..af7320f7e1c --- /dev/null +++ b/upcoming_changelogs/7250.md @@ -0,0 +1 @@ +- Added a new beta `EuiTextBlockTruncate` component for multi-line truncation From 83e3d4ed7922f4fcf1d4dbd2c486e291c6ad38ec Mon Sep 17 00:00:00 2001 From: Cee Chen <549407+cee-chen@users.noreply.github.com> Date: Wed, 4 Oct 2023 09:22:00 -0700 Subject: [PATCH 08/38] [EuiFlyout] Allow push flyouts to have slide in animations (#7239) --- .../collapsible_nav.test.tsx.snap | 4 +- .../collapsible_nav_beta.test.tsx.snap | 4 +- .../flyout/__snapshots__/flyout.test.tsx.snap | 57 ++++------ src/components/flyout/flyout.stories.tsx | 102 ++++++++++++++++++ src/components/flyout/flyout.styles.ts | 18 ++-- src/components/flyout/flyout.test.tsx | 33 ++++-- src/components/flyout/flyout.tsx | 27 +++-- upcoming_changelogs/7239.md | 1 + 8 files changed, 180 insertions(+), 66 deletions(-) create mode 100644 src/components/flyout/flyout.stories.tsx create mode 100644 upcoming_changelogs/7239.md diff --git a/src/components/collapsible_nav/__snapshots__/collapsible_nav.test.tsx.snap b/src/components/collapsible_nav/__snapshots__/collapsible_nav.test.tsx.snap index 0c74dfef51e..16ed32ffd1d 100644 --- a/src/components/collapsible_nav/__snapshots__/collapsible_nav.test.tsx.snap +++ b/src/components/collapsible_nav/__snapshots__/collapsible_nav.test.tsx.snap @@ -273,7 +273,7 @@ exports[`EuiCollapsibleNav props isDocked 1`] = ` data-focus-lock-disabled="disabled" >
+`; + exports[`EuiFlyout props sides left is rendered 1`] = ` Array [
@@ -1047,42 +1068,6 @@ Array [ ] `; -exports[`EuiFlyout props type=push is rendered 1`] = ` -Array [ -
, -
-
- -
-
, -
, -] -`; - exports[`EuiFlyout renders extra screen reader instructions when fixed EuiHeaders headers exist on the page 1`] = ` = { + title: 'EuiFlyout', + component: EuiFlyout, + args: { + // Component defaults + type: 'overlay', + side: 'right', + size: 'm', + paddingSize: 'l', + pushMinBreakpoint: 'l', + closeButtonPosition: 'inside', + hideCloseButton: false, + ownFocus: true, + }, +}; + +export default meta; +type Story = StoryObj; + +const StatefulFlyout = (props: Partial) => { + const [isOpen, setIsOpen] = useState(true); + return ( + <> + setIsOpen(!isOpen)}> + Toggle flyout + + {isOpen && setIsOpen(false)} />} + + ); +}; + +export const Playground: Story = { + render: ({ ...args }) => , +}; + +export const PushFlyouts: Story = { + render: ({ ...args }) => { + const fillerText = ( + +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed eu + condimentum ipsum, nec ornare metus. Sed egestas elit nec placerat + suscipit. Cras pulvinar nisi eget enim sodales fringilla. Aliquam + lobortis lorem at ornare aliquet. Mauris laoreet laoreet mollis. + Pellentesque aliquet tortor dui, non luctus turpis pulvinar vitae. + Nunc ultrices scelerisque erat eu rutrum. Nam at ligula enim. Ut nec + nisl faucibus, euismod neque ut, aliquam nisl. Donec eu ante ut arcu + rutrum blandit nec ac nisl. In elementum id enim vitae aliquam. In + sagittis, neque vitae ultricies interdum, sapien justo efficitur + ligula, sit amet fermentum nisl magna sit amet turpis. Nulla facilisi. + Proin nec viverra mi. Morbi dolor arcu, ornare non consequat et, + viverra dapibus tellus. +

+
+ ); + return ( + <> + + {fillerText} + + {fillerText} + + ); + }, + args: { + type: 'push', + pushAnimation: false, + pushMinBreakpoint: 'xs', + }, + argTypes: hideStorybookControls([ + 'onClose', + 'aria-label', + 'as', + 'closeButtonPosition', + 'closeButtonProps', + 'focusTrapProps', + 'hideCloseButton', + 'includeFixedHeadersInFocusTrap', + 'maskProps', + 'maxWidth', + 'outsideClickCloses', + 'ownFocus', + 'paddingSize', + 'style', + ]), +}; diff --git a/src/components/flyout/flyout.styles.ts b/src/components/flyout/flyout.styles.ts index d24faac1f0d..96dc5c09540 100644 --- a/src/components/flyout/flyout.styles.ts +++ b/src/components/flyout/flyout.styles.ts @@ -158,20 +158,22 @@ export const euiFlyoutStyles = (euiThemeContext: UseEuiTheme) => { overlay: css` ${euiShadowXLarge(euiThemeContext)} `, - push: css` - clip-path: none; - /* Don't animate on loading a docked nav */ - animation-duration: 0s !important; /* stylelint-disable-line declaration-no-important */ - /* Make sure the header shadows are above */ - z-index: ${Number(euiTheme.levels.flyout) - 1}; - `, - pushSide: { + push: { + push: css` + clip-path: none; + /* Make sure the header shadows are above */ + z-index: ${Number(euiTheme.levels.flyout) - 1}; + `, right: css` ${logicalCSS('border-left', euiTheme.border.thick)} `, left: css` ${logicalCSS('border-right', euiTheme.border.thick)} `, + noAnimation: css` + /* Don't animate on loading a docked nav */ + animation-duration: 0s !important; /* stylelint-disable-line declaration-no-important */ + `, }, // Padding diff --git a/src/components/flyout/flyout.test.tsx b/src/components/flyout/flyout.test.tsx index b0b3480e654..316d426b4a0 100644 --- a/src/components/flyout/flyout.test.tsx +++ b/src/components/flyout/flyout.test.tsx @@ -139,15 +139,34 @@ describe('EuiFlyout', () => { }); }); - describe('type=push', () => { - test('is rendered', () => { - const component = mount( - {}} type="push" pushMinBreakpoint="xs" /> + describe('push flyouts', () => { + it('renders', () => { + const { getByTestSubject } = render( + {}} + type="push" + pushMinBreakpoint="xs" + /> ); - expect( - takeMountedSnapshot(component, { hasArrayOutput: true }) - ).toMatchSnapshot(); + expect(getByTestSubject('flyout')).toMatchSnapshot(); + }); + + it('can render with animations', () => { + const { getByTestSubject } = render( + {}} + type="push" + pushMinBreakpoint="xs" + pushAnimation={true} + /> + ); + + expect(getByTestSubject('flyout').className).not.toContain( + 'noAnimation' + ); }); }); diff --git a/src/components/flyout/flyout.tsx b/src/components/flyout/flyout.tsx index b506c50a07b..31f2b5ad76e 100644 --- a/src/components/flyout/flyout.tsx +++ b/src/components/flyout/flyout.tsx @@ -131,6 +131,11 @@ interface _EuiFlyoutProps { * @default l */ pushMinBreakpoint?: EuiBreakpointSize; + /** + * Enables a slide in animation on push flyouts + * @default false + */ + pushAnimation?: boolean; style?: CSSProperties; /** * Object of props passed to EuiFocusTrap. @@ -181,6 +186,7 @@ export const EuiFlyout = forwardRef( type = 'overlay', outsideClickCloses, pushMinBreakpoint = 'l', + pushAnimation = false, focusTrapProps: _focusTrapProps = {}, includeFixedHeadersInFocusTrap = true, 'aria-describedby': _ariaDescribedBy, @@ -216,20 +222,18 @@ export const EuiFlyout = forwardRef( /** * Accomodate for the `isPushed` state by adding padding to the body equal to the width of the element */ - if (type === 'push') { - if (isPushed) { - if (side === 'right') { - document.body.style.paddingRight = `${dimensions.width}px`; - } else if (side === 'left') { - document.body.style.paddingLeft = `${dimensions.width}px`; - } + if (isPushed) { + if (side === 'right') { + document.body.style.paddingRight = `${dimensions.width}px`; + } else if (side === 'left') { + document.body.style.paddingLeft = `${dimensions.width}px`; } } return () => { document.body.classList.remove('euiBody--hasFlyout'); - if (type === 'push') { + if (isPushed) { if (side === 'right') { document.body.style.paddingRight = ''; } else if (side === 'left') { @@ -237,7 +241,7 @@ export const EuiFlyout = forwardRef( } } }; - }, [type, side, dimensions, isPushed]); + }, [side, dimensions, isPushed]); /** * ESC key closes flyout (always?) @@ -268,8 +272,9 @@ export const EuiFlyout = forwardRef( styles.paddingSizes[paddingSize], isEuiFlyoutSizeNamed(size) && styles[size], maxWidth === false && styles.noMaxWidth, - styles[type], - type === 'push' && styles.pushSide[side], + isPushed ? styles.push.push : styles.overlay, + isPushed && styles.push[side], + isPushed && !pushAnimation && styles.push.noAnimation, styles[side], ]; diff --git a/upcoming_changelogs/7239.md b/upcoming_changelogs/7239.md new file mode 100644 index 00000000000..8ef483e31bc --- /dev/null +++ b/upcoming_changelogs/7239.md @@ -0,0 +1 @@ +- Added new `pushAnimation` prop to push `EuiFlyout`s, which enables a slide in animation From 266d6202b81c9aee9232dc0c29ce1d0827b8e8ef Mon Sep 17 00:00:00 2001 From: Cee Chen <549407+cee-chen@users.noreply.github.com> Date: Wed, 4 Oct 2023 09:24:50 -0700 Subject: [PATCH 09/38] [EuiFlexGroup][EuiFlexGrid] Fix `m` gutter size spacing (#7251) --- src/components/flex/flex_grid.styles.ts | 2 +- src/components/flex/flex_group.styles.ts | 2 +- upcoming_changelogs/7251.md | 3 +++ 3 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 upcoming_changelogs/7251.md diff --git a/src/components/flex/flex_grid.styles.ts b/src/components/flex/flex_grid.styles.ts index 329921b8902..2fb5dfd1a0c 100644 --- a/src/components/flex/flex_grid.styles.ts +++ b/src/components/flex/flex_grid.styles.ts @@ -55,7 +55,7 @@ export const euiFlexGridStyles = ( gap: ${euiTheme.size.s}; `, m: css` - gap: ${euiTheme.size.m}; + gap: ${euiTheme.size.base}; `, l: css` gap: ${euiTheme.size.l}; diff --git a/src/components/flex/flex_group.styles.ts b/src/components/flex/flex_group.styles.ts index 1efb18e3318..3abdc304399 100644 --- a/src/components/flex/flex_group.styles.ts +++ b/src/components/flex/flex_group.styles.ts @@ -40,7 +40,7 @@ export const euiFlexGroupStyles = (euiThemeContext: UseEuiTheme) => { gap: ${euiTheme.size.s}; `, m: css` - gap: ${euiTheme.size.m}; + gap: ${euiTheme.size.base}; `, l: css` gap: ${euiTheme.size.l}; diff --git a/upcoming_changelogs/7251.md b/upcoming_changelogs/7251.md new file mode 100644 index 00000000000..117da956a9e --- /dev/null +++ b/upcoming_changelogs/7251.md @@ -0,0 +1,3 @@ +**Bug fixes** + +- Fixed `EuiFlexGroup` and `EuiFlexGrid's `m` gutter size From 7f14f18f65ba2b05014bff89819ae40cf5487dce Mon Sep 17 00:00:00 2001 From: Cee Chen <549407+cee-chen@users.noreply.github.com> Date: Wed, 4 Oct 2023 11:55:10 -0700 Subject: [PATCH 10/38] Update global EUI font size to use configurable theme units (#7182) --- .storybook/preview.tsx | 5 ++- .../views/theme/typography/_typography_js.tsx | 25 +++++++++++ src/components/provider/provider.stories.tsx | 43 +++++++++++++++++++ src/global_styling/reset/global_styles.tsx | 20 ++++++--- upcoming_changelogs/7182.md | 7 +++ 5 files changed, 92 insertions(+), 8 deletions(-) create mode 100644 src/components/provider/provider.stories.tsx create mode 100644 upcoming_changelogs/7182.md diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index af501c4be44..857654a3218 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -48,7 +48,10 @@ import { hideStorybookControls } from './utils'; const preview: Preview = { decorators: [ (Story, context) => ( - +
{ /> + + + We strongly recommend using relative (rem or{' '} + em) units instead of px when + possible{' '} + + } + > +

+ Relative font units respect configured browser default font sizes, + which some users may set to larger than than the 16px default due to, + e.g. visual impairment, monitor size, or personal preference.{' '} + + Read more on accessible text resizing. + +

+
tableLayout="auto" diff --git a/src/components/provider/provider.stories.tsx b/src/components/provider/provider.stories.tsx new file mode 100644 index 00000000000..7eb59fe5767 --- /dev/null +++ b/src/components/provider/provider.stories.tsx @@ -0,0 +1,43 @@ +/* + * 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 type { Meta, StoryObj } from '@storybook/react'; + +import { EuiProvider, EuiProviderProps } from './provider'; + +const meta: Meta> = { + title: 'EuiProvider', + component: EuiProvider, + argTypes: { + colorMode: { + control: 'select', + options: ['light', 'dark', 'inverse', 'LIGHT', 'DARK', 'INVERSE'], + }, + modify: { control: 'object' }, + componentDefaults: { control: 'object' }, + globalStyles: { control: 'boolean' }, + utilityClasses: { control: 'boolean' }, + }, +}; + +export default meta; +type Story = StoryObj>; + +export const FontDefaultUnits: Story = { + render: () => ( + <> + Change `modify.font.defaultUnits` to{' '} + `rem`, `em`, or `px` and then inspect this demo's `html` + CSS + + ), + args: { + modify: { font: { defaultUnits: 'rem' } }, + }, +}; diff --git a/src/global_styling/reset/global_styles.tsx b/src/global_styling/reset/global_styles.tsx index f485bd0199c..55cd8d5cc2c 100644 --- a/src/global_styling/reset/global_styles.tsx +++ b/src/global_styling/reset/global_styles.tsx @@ -37,12 +37,15 @@ export const EuiGlobalStyles = ({}: EuiGlobalStylesProps) => { * This font reset sets all our base font/typography related properties * that are needed to override browser-specific element settings. */ - const fontReset = ` - font-family: ${font.family}; - font-size: ${`${font.scale[font.body.scale] * base}px`}; - line-height: ${base / (font.scale[font.body.scale] * base)}; - font-weight: ${font.weight[font.body.weight]}; - `; + const fontBodyScale = font.scale[font.body.scale]; + const fontReset = { + fontFamily: font.family, + fontSize: `${ + font.defaultUnits === 'px' ? fontBodyScale * base : fontBodyScale + }${font.defaultUnits}`, + lineHeight: base / (fontBodyScale * base), + fontWeight: font.weight[font.body.weight], + }; /** * Final styles @@ -70,7 +73,10 @@ export const EuiGlobalStyles = ({}: EuiGlobalStylesProps) => { input, textarea, select { - ${fontReset} + ${{ + ...fontReset, + fontSize: '1rem', // Inherit from html root + }} } // Chrome has opinionated select:disabled opacity styles that need to be overridden diff --git a/upcoming_changelogs/7182.md b/upcoming_changelogs/7182.md new file mode 100644 index 00000000000..aa34dd7ed82 --- /dev/null +++ b/upcoming_changelogs/7182.md @@ -0,0 +1,7 @@ +**Breaking changes** + +- EUI's global body font-size now respects the `font.defaultUnits` token. This means that the global font size will use the `rem` unit by default, instead of `px`. + +**Accessibility** + +- When using `rem` or `em` font units, EUI now respects, instead of ignoring, browser default font sizes set by end users. From e09776c31e8f068a0643a57c373f7067dd37ab04 Mon Sep 17 00:00:00 2001 From: Cee Chen <549407+cee-chen@users.noreply.github.com> Date: Wed, 4 Oct 2023 19:35:59 -0700 Subject: [PATCH 11/38] [EuiComboBox] Update to dogfood `EuiInputPopover` (#7246) --- src-docs/src/views/popover/input_popover.tsx | 1 + .../__snapshots__/combo_box.test.tsx.snap | 1408 +++++------------ src/components/combo_box/combo_box.spec.tsx | 237 ++- src/components/combo_box/combo_box.test.tsx | 934 ++++++----- src/components/combo_box/combo_box.tsx | 420 ++--- .../combo_box_input/combo_box_input.tsx | 25 +- .../_combo_box_options_list.scss | 20 +- .../combo_box_options_list.tsx | 239 +-- src/components/combo_box/index.ts | 1 - src/components/combo_box/types.ts | 5 - src/components/popover/input_popover.spec.tsx | 52 + src/components/popover/input_popover.tsx | 51 +- src/test/rtl/component_helpers.d.ts | 2 + src/test/rtl/component_helpers.ts | 14 +- upcoming_changelogs/7246.md | 2 + 15 files changed, 1361 insertions(+), 2050 deletions(-) create mode 100644 upcoming_changelogs/7246.md diff --git a/src-docs/src/views/popover/input_popover.tsx b/src-docs/src/views/popover/input_popover.tsx index 1e87b9ee1a3..1b2fd51d14d 100644 --- a/src-docs/src/views/popover/input_popover.tsx +++ b/src-docs/src/views/popover/input_popover.tsx @@ -21,6 +21,7 @@ export default () => { setIsPopoverOpen(false)} + closeOnScroll={true} input={ setIsPopoverOpen(true)} diff --git a/src/components/combo_box/__snapshots__/combo_box.test.tsx.snap b/src/components/combo_box/__snapshots__/combo_box.test.tsx.snap index 6bf8b35fb61..a1e7f5f4028 100644 --- a/src/components/combo_box/__snapshots__/combo_box.test.tsx.snap +++ b/src/components/combo_box/__snapshots__/combo_box.test.tsx.snap @@ -1,1094 +1,430 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`EuiComboBox is rendered 1`] = ` +exports[`EuiComboBox renders 1`] = `
- -
-
- +
+ +
+
+ +
+
`; -exports[`props aria-label attribute is rendered 1`] = ` -
- -
-`; - -exports[`props aria-labelledby attribute is rendered 1`] = ` -
- -
-`; - -exports[`props autoFocus is rendered 1`] = ` -
- -
-`; - -exports[`props custom ID is rendered 1`] = ` -
- -
-`; - -exports[`props delimiter is rendered 1`] = ` -
- -
-`; - -exports[`props full width is rendered 1`] = ` -
- -
-`; - -exports[`props isClearable=false disallows user from clearing input when no options are selected 1`] = ` -
- -
-`; - -exports[`props isClearable=false disallows user from clearing input when options are selected 1`] = ` -
- -
-`; - -exports[`props isDisabled is rendered 1`] = ` -
- -
-`; - -exports[`props option.prepend & option.append renders in pills 1`] = ` -Array [ - - - - - - Pre - - - - 1 - - - - - , - - - - - 2 - - - - Post - - - - - - , -] -`; - -exports[`props option.prepend & option.append renders in single selection 1`] = ` - - - - Pre - - - - 1 - - -`; - -exports[`props option.prepend & option.append renders in the options dropdown 1`] = ` -
-
+exports[`EuiComboBox renders the options list dropdown 1`] = ` + +
- -
+
- - 2 - - - Post - - - - - - + aria-hidden="true" + class="euiFormControlLayoutCustomIcon__icon" + data-euiicon-type="arrowDown" + /> + +
+
+
+
-
-`; - -exports[`props options list is rendered 1`] = ` -
-
- -
-
- -
-
-
-
+ data-focus-guard="true" + style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;" + tabindex="-1" + />
+
+
+
+
- -`; - -exports[`props selectedOptions are rendered 1`] = ` -
- -
-`; - -exports[`props singleSelection is rendered 1`] = ` -
- -
-`; - -exports[`props singleSelection prepend and append is rendered 1`] = ` -
- - - - - - -
-`; - -exports[`props singleSelection selects existing option when opened 1`] = ` -
- - - - - - -
+ `; diff --git a/src/components/combo_box/combo_box.spec.tsx b/src/components/combo_box/combo_box.spec.tsx index 4fb066cbd0c..c9f45a66836 100644 --- a/src/components/combo_box/combo_box.spec.tsx +++ b/src/components/combo_box/combo_box.spec.tsx @@ -10,8 +10,15 @@ /// /// -import React, { useState } from 'react'; -import { EuiComboBox } from './index'; +import React, { FunctionComponent, useState } from 'react'; + +import { EuiFlyout, EuiPopover, EuiButton } from '../index'; + +import { + EuiComboBox, + type EuiComboBoxProps, + type EuiComboBoxOptionOption, +} from './index'; // CI doesn't have access to the Inter font, so we need to manually include it // for truncation font calculations to work correctly @@ -27,7 +34,7 @@ before(() => { }); describe('EuiComboBox', () => { - describe('Focus management', () => { + describe('focus management', () => { it('keeps focus on the input box when clicking a disabled item', () => { cy.realMount( { }); }); + describe('inputPopoverProps', () => { + it('allows setting a minimum popover width', () => { + cy.mount( + {}} + data-test-subj="combobox" + inputPopoverProps={{ + panelMinWidth: 300, + anchorPosition: 'downCenter', + }} + style={{ margin: '0 auto' }} + /> + ); + cy.get('[data-test-subj="comboBoxInput"]').click(); + + cy.get('[data-popover-panel]') + .should('have.css', 'inline-size', '400px') + .should('have.css', 'left', '50px'); + + cy.get('[data-test-subj="combobox"]').then( + ($el) => ($el[0].style.width = '200px') + ); + + cy.get('[data-popover-panel]') + .should('have.css', 'inline-size', '300px') + .should('have.css', 'left', '100px'); + }); + }); + describe('truncation', () => { const sharedProps = { style: { width: 200 }, @@ -194,86 +232,171 @@ describe('EuiComboBox', () => { }); }); }); - - describe('Backspace to delete last pill', () => { - const options = [ + describe('selection', () => { + const defaultOptions: Array> = [ { label: 'Item 1' }, { label: 'Item 2' }, { label: 'Item 3' }, ]; - - const TestComboBox = (...rest) => { - const [selectedOptions, setSelected] = useState([]); - const onChange = (selectedOptions) => { + const StatefulComboBox: FunctionComponent< + Partial> + > = ({ options = defaultOptions, ...rest }) => { + const [selectedOptions, setSelected] = useState([]); + const onChange = (selectedOptions: typeof options) => { setSelected(selectedOptions); }; return ( ); }; - it('does not delete the last pill if there is search text', () => { - cy.realMount(); - cy.get('[data-test-subj=comboBoxSearchInput]').realClick(); - cy.realPress('{downarrow}'); - cy.realPress('Enter'); - cy.realPress('{downarrow}'); - cy.realPress('Enter'); - cy.get('.euiComboBoxPill').should('have.length', 2); + describe('delimiter', () => { + it('selects the option when the delimiter option is typed into the search', () => { + cy.mount(); + cy.get('[data-test-subj="euiComboBoxPill"]').should('not.exist'); - cy.get('[data-test-subj=comboBoxSearchInput]').type('test'); - cy.get('[data-test-subj=comboBoxSearchInput]').realPress('Backspace'); + cy.get('[data-test-subj="comboBoxSearchInput"]').click(); + cy.realType('Item 1,'); - cy.get('[data-test-subj=comboBoxSearchInput]') - .invoke('val') - .should('equal', 'tes'); - cy.get('.euiComboBoxPill').should('have.length', 2); - }); + cy.get('[data-test-subj="euiComboBoxPill"]').should( + 'have.text', + 'Item 1' + ); + cy.get('[data-test-subj="comboBoxSearchInput"]').should( + 'have.value', + '' + ); + }); - it('does not delete the last pill if the input is not active when backspace is pressed', () => { - cy.realMount(); - cy.get('[data-test-subj=comboBoxSearchInput]').realClick(); - cy.realPress('{downarrow}'); - cy.realPress('Enter'); - cy.get('[data-test-subj=comboBoxSearchInput]').type('test'); - cy.realPress('Escape'); - cy.get('.euiComboBoxPill').should('have.length', 1); + it('does nothing if the item if already selected', () => { + cy.mount( + + ); + cy.get('[data-test-subj="euiComboBoxPill"]').should( + 'have.text', + 'Item 1' + ); + + cy.get('[data-test-subj="comboBoxSearchInput"]').click(); + cy.realType('Item 1,'); + + cy.get('[data-test-subj="euiComboBoxPill"]').should('have.length', 1); + cy.get('[data-test-subj="comboBoxSearchInput"]').should( + 'have.value', + 'Item 1,' + ); + cy.contains("Item 1, doesn't match any options"); + }); - cy.realPress(['Shift', 'Tab']); // Should be focused on the first pill's X button - cy.realPress('Backspace'); - cy.get('.euiComboBoxPill').should('have.length', 1); + it('still respects enter to select', () => { + cy.mount(); + cy.get('[data-test-subj="euiComboBoxPill"]').should('not.exist'); - cy.repeatRealPress('Tab', 2); // Should be focused on the clear button - cy.realPress('Backspace'); - cy.get('.euiComboBoxPill').should('have.length', 1); + cy.get('[data-test-subj="comboBoxSearchInput"]').click(); + cy.realType('Item 1'); + cy.realPress('Enter'); + + cy.get('[data-test-subj="euiComboBoxPill"]').should( + 'have.text', + 'Item 1' + ); + }); }); - it('deletes the last pill added when backspace on the input is pressed ', () => { - cy.realMount(); - cy.get('[data-test-subj=comboBoxSearchInput]').realClick(); - cy.realPress('{downarrow}'); - cy.realPress('Enter'); - cy.realPress('{downarrow}'); - cy.realPress('Enter'); - cy.get('.euiComboBoxPill').should('have.length', 2); + describe('backspace to delete last pill', () => { + it('does not delete the last pill if there is search text', () => { + cy.realMount(); + cy.get('[data-test-subj=comboBoxSearchInput]').realClick(); + cy.realPress('{downarrow}'); + cy.realPress('Enter'); + cy.realPress('{downarrow}'); + cy.realPress('Enter'); + cy.get('.euiComboBoxPill').should('have.length', 2); + + cy.get('[data-test-subj=comboBoxSearchInput]').type('test'); + cy.get('[data-test-subj=comboBoxSearchInput]').realPress('Backspace'); + + cy.get('[data-test-subj=comboBoxSearchInput]') + .invoke('val') + .should('equal', 'tes'); + cy.get('.euiComboBoxPill').should('have.length', 2); + }); + + it('does not delete the last pill if the input is not active when backspace is pressed', () => { + cy.realMount(); + cy.get('[data-test-subj=comboBoxSearchInput]').realClick(); + cy.realPress('{downarrow}'); + cy.realPress('Enter'); + cy.get('[data-test-subj=comboBoxSearchInput]').type('test'); + cy.realPress('Escape'); + cy.get('.euiComboBoxPill').should('have.length', 1); + + cy.realPress(['Shift', 'Tab']); // Should be focused on the first pill's X button + cy.realPress('Backspace'); + cy.get('.euiComboBoxPill').should('have.length', 1); + + cy.repeatRealPress('Tab', 2); // Should be focused on the clear button + cy.realPress('Backspace'); + cy.get('.euiComboBoxPill').should('have.length', 1); + }); + + it('deletes the last pill added when backspace on the input is pressed ', () => { + cy.realMount(); + cy.get('[data-test-subj=comboBoxSearchInput]').realClick(); + cy.realPress('{downarrow}'); + cy.realPress('Enter'); + cy.realPress('{downarrow}'); + cy.realPress('Enter'); + cy.get('.euiComboBoxPill').should('have.length', 2); + + cy.get('[data-test-subj=comboBoxSearchInput]').realPress('Backspace'); + cy.get('.euiComboBoxPill').should('have.length', 1); + }); + + it('opens up the selection list again after deleting the active single selection ', () => { + cy.realMount(); + cy.get('[data-test-subj=comboBoxSearchInput]').realClick(); + cy.realPress('{downarrow}'); + cy.realPress('Enter'); - cy.get('[data-test-subj=comboBoxSearchInput]').realPress('Backspace'); - cy.get('.euiComboBoxPill').should('have.length', 1); + cy.realPress('Backspace'); + cy.get('[data-test-subj=comboBoxOptionsList]').should('have.length', 1); + }); }); + }); - it('opens up the selection list again after deleting the active single selection ', () => { - cy.realMount(); - cy.get('[data-test-subj=comboBoxSearchInput]').realClick(); - cy.realPress('{downarrow}'); - cy.realPress('Enter'); + describe('z-index regression testing', () => { + it('displays the dropdown list above any inherited z-indices from parents', () => { + cy.mount( + {}} size="s"> + {}} + button={Toggle popover} + > + + + + ); + cy.wait(500); // Let the flyout finish animating in + cy.get('[data-test-subj=comboBoxSearchInput]').click(); + + cy.get('[data-test-subj="comboBoxOptionsList"]') + .parents('[data-popover-panel]') + .should('have.css', 'z-index', '5000'); - cy.realPress('Backspace'); - cy.get('[data-test-subj=comboBoxOptionsList]').should('have.length', 1); + // Should be able to click the first option without an error + // about the popover or flyout blocking the click + cy.get('[role=option]').click('top'); }); }); }); diff --git a/src/components/combo_box/combo_box.test.tsx b/src/components/combo_box/combo_box.test.tsx index e2e0a6c1bae..e6204b282a0 100644 --- a/src/components/combo_box/combo_box.test.tsx +++ b/src/components/combo_box/combo_box.test.tsx @@ -6,62 +6,37 @@ * Side Public License, v 1. */ -import React, { ReactNode } from 'react'; -import { act, fireEvent } from '@testing-library/react'; -import { shallow, mount } from 'enzyme'; -import { render } from '../../test/rtl'; +import React from 'react'; +import { fireEvent } from '@testing-library/react'; +import { render, showEuiComboBoxOptions } from '../../test/rtl'; import { shouldRenderCustomStyles } from '../../test/internal'; -import { - requiredProps, - findTestSubject, - takeMountedSnapshot, -} from '../../test'; -import { comboBoxKeys } from '../../services'; - -import { EuiComboBox, EuiComboBoxProps } from './combo_box'; -import type { EuiComboBoxOptionOption } from './types'; +import { requiredProps } from '../../test'; -jest.mock('../portal', () => ({ - EuiPortal: ({ children }: { children: ReactNode }) => children, -})); +import { keys } from '../../services'; +import { EuiComboBox } from './combo_box'; +import type { EuiComboBoxOptionOption } from './types'; -interface TitanOption { - 'data-test-subj'?: 'titanOption'; +interface Options { + 'data-test-subj'?: string; label: string; } -const options: TitanOption[] = [ +const options: Options[] = [ { 'data-test-subj': 'titanOption', label: 'Titan', }, - { - label: 'Enceladus', - }, - { - label: 'Mimas', - }, - { - label: 'Dione', - }, - { - label: 'Iapetus', - }, - { - label: 'Phoebe', - }, - { - label: 'Rhea', - }, + { label: 'Enceladus' }, + { label: 'Mimas' }, + { label: 'Dione' }, + { label: 'Iapetus' }, + { label: 'Phoebe' }, + { label: 'Rhea' }, { label: "Pandora is one of Saturn's moons, named for a Titaness of Greek mythology", }, - { - label: 'Tethys', - }, - { - label: 'Hyperion', - }, + { label: 'Tethys' }, + { label: 'Hyperion' }, ]; describe('EuiComboBox', () => { @@ -74,20 +49,17 @@ describe('EuiComboBox', () => { { skip: { parentTest: true }, childProps: ['truncationProps', 'options[0]'], - renderCallback: async ({ getByTestSubject, findAllByTestSubject }) => { - fireEvent.click(getByTestSubject('comboBoxToggleListButton')); - await findAllByTestSubject('truncatedText'); - }, + renderCallback: showEuiComboBoxOptions, } ); - test('is rendered', () => { + it('renders', () => { const { container } = render(); expect(container.firstChild).toMatchSnapshot(); }); - test('supports thousands of options in an options group', () => { + it('supports thousands of options in an options group', () => { // tests for a regression: RangeError: Maximum call stack size exceeded // https://mathiasbynens.be/demo/javascript-argument-count const options: EuiComboBoxOptionOption[] = [{ label: 'test', options: [] }]; @@ -95,508 +67,594 @@ describe('EuiComboBox', () => { options[0].options?.push({ label: `option ${i}` }); } - mount(); + render(); }); -}); -describe('props', () => { - test('options list is rendered', () => { - const component = mount( + it('renders the options list dropdown', async () => { + const { baseElement } = render( ); + await showEuiComboBoxOptions(); - act(() => { - component.setState({ isListOpen: true }); - }); - expect(takeMountedSnapshot(component)).toMatchSnapshot(); + expect(baseElement).toMatchSnapshot(); }); - test('selectedOptions are rendered', () => { - const component = shallow( + it('renders selectedOptions as pills', () => { + const { getAllByTestSubject } = render( ); + const selections = getAllByTestSubject('euiComboBoxPill'); - expect(component).toMatchSnapshot(); + expect(selections).toHaveLength(2); + expect(selections[0]).toHaveTextContent(options[2].label); + expect(selections[1]).toHaveTextContent(options[4].label); }); - test('custom ID is rendered', () => { - const component = shallow( - - ); - - expect(component).toMatchSnapshot(); - }); - - describe('option.prepend & option.append', () => { - const options = [ - { label: '1', prepend: Pre }, - { label: '2', append: Post }, - ]; + describe('props', () => { + describe('option.prepend & option.append', () => { + const options = [ + { label: '1', prepend: Pre }, + { label: '2', append: Post }, + ]; + + it('renders in pills', () => { + const { getByTestSubject, getAllByTestSubject } = render( + + ); + + expect(getByTestSubject('prepend')).toBeInTheDocument(); + expect(getByTestSubject('append')).toBeInTheDocument(); + + expect(getAllByTestSubject('euiComboBoxPill')[0]) + .toMatchInlineSnapshot(` + + + + + + Pre + + + + 1 + + + + + + `); + }); - test('renders in pills', () => { - const { getByTestSubject, getAllByTestSubject } = render( - - ); + test('renders in the options dropdown', async () => { + const { getByTestSubject, getAllByRole } = render( + + ); + await showEuiComboBoxOptions(); + + const dropdown = getByTestSubject('comboBoxOptionsList'); + expect( + dropdown.querySelector('.euiComboBoxOption__prepend') + ).toBeInTheDocument(); + expect( + dropdown.querySelector('.euiComboBoxOption__append') + ).toBeInTheDocument(); + + expect(getAllByRole('option')[0]).toMatchInlineSnapshot(` + + `); + }); - expect(getByTestSubject('prepend')).toBeInTheDocument(); - expect(getByTestSubject('append')).toBeInTheDocument(); - expect(getAllByTestSubject('euiComboBoxPill')).toMatchSnapshot(); + test('renders in single selection', () => { + const { getByTestSubject } = render( + + ); + + expect(getByTestSubject('euiComboBoxPill')).toMatchInlineSnapshot(` + + + + Pre + + + + 1 + + + `); + }); }); - test('renders in the options dropdown', () => { - const component = mount(); + describe('singleSelection', () => { + it('does not show or allow selecting more than one option', async () => { + const onChange = jest.fn(); + + const { getAllByTestSubject } = render( + + ); + const selections = getAllByTestSubject('euiComboBoxPill'); + + expect(selections).toHaveLength(1); + expect(selections[0]).toHaveTextContent('Mimas'); + }); - act(() => { - component.setState({ isListOpen: true }); + it('selects existing option when opened', async () => { + const { getByTestSubject } = render( + + ); + await showEuiComboBoxOptions(); + + const dropdown = getByTestSubject('comboBoxOptionsList'); + const checkedOption = '[data-euiicon-type="check"]'; + + // Should only have 1 checked item + expect(dropdown.querySelectorAll(checkedOption)).toHaveLength(1); + + // The first option should be rendered and have the check + const option = dropdown.querySelector('[data-test-subj="titanOption"]'); + expect(option).toBeTruthy(); + expect(option!.querySelector(checkedOption)).toBeTruthy(); }); - const dropdown = component.find( - 'div[data-test-subj="comboBoxOptionsList"]' - ); - expect(dropdown.find('.euiComboBoxOption__prepend')).toHaveLength(1); - expect(dropdown.find('.euiComboBoxOption__append')).toHaveLength(1); - expect(dropdown.render()).toMatchSnapshot(); + it('renders prepend and append in form layout', () => { + const { container } = render( + + ); + + expect( + container.querySelector('.euiFormControlLayout__prepend') + ).toBeInTheDocument(); + expect( + container.querySelector('.euiFormControlLayout__append') + ).toBeInTheDocument(); + }); + + it('renders as plain text', () => { + const { getByTestSubject } = render( + + ); + + expect(getByTestSubject('euiComboBoxPill').className).toContain( + 'euiComboBoxPill--plainText' + ); + }); }); - test('renders in single selection', () => { - const { getByTestSubject } = render( + test('isDisabled', () => { + const { container, queryByTestSubject, queryByTitle } = render( ); - expect(getByTestSubject('euiComboBoxPill')).toMatchSnapshot(); - }); - }); + expect(container.firstElementChild!.className).toContain('-isDisabled'); + expect(queryByTestSubject('comboBoxSearchInput')).toBeDisabled(); - describe('isClearable=false disallows user from clearing input', () => { - test('when no options are selected', () => { - const component = shallow( - - ); - - expect(component).toMatchSnapshot(); + expect(queryByTestSubject('comboBoxClearButton')).toBeFalsy(); + expect(queryByTestSubject('comboBoxToggleListButton')).toBeFalsy(); + expect( + queryByTitle('Remove Titan from selection in this group') + ).toBeFalsy(); }); - test('when options are selected', () => { - const component = shallow( + test('fullWidth', () => { + // TODO: Should likely be a visual screenshot test + const { container } = render( ); - expect(component).toMatchSnapshot(); + expect(container.innerHTML).toContain('euiFormControlLayout--fullWidth'); + expect(container.innerHTML).toContain('euiComboBox--fullWidth'); + expect(container.innerHTML).toContain( + 'euiComboBox__inputWrap--fullWidth' + ); }); - }); - describe('singleSelection', () => { - test('is rendered', () => { - const component = shallow( + test('autoFocus', () => { + const { getByTestSubject } = render( ); - expect(component).toMatchSnapshot(); - }); - test('selects existing option when opened', () => { - const component = shallow( - + expect(document.activeElement).toBe( + getByTestSubject('comboBoxSearchInput') ); - - component.setState({ isListOpen: true }); - expect(component).toMatchSnapshot(); }); - test('prepend and append is rendered', () => { - const component = shallow( + + test('aria-label / aria-labelledby renders on the input, not on the wrapper', () => { + const { getByTestSubject } = render( ); + const input = getByTestSubject('comboBoxSearchInput'); - component.setState({ isListOpen: true }); - expect(component).toMatchSnapshot(); + expect(input).toHaveAttribute('aria-label', 'Test label'); + expect(input).toHaveAttribute('aria-labelledby', 'test-heading-id'); }); - }); - - test('isDisabled is rendered', () => { - const component = shallow( - - ); - expect(component).toMatchSnapshot(); - }); - - test('full width is rendered', () => { - const component = shallow( - - ); + test('inputRef', () => { + const inputRefCallback = jest.fn(); - expect(component).toMatchSnapshot(); - }); - - test('delimiter is rendered', () => { - const component = shallow( - - ); + const { getByRole } = render( + + ); + expect(inputRefCallback).toHaveBeenCalledTimes(1); - expect(component).toMatchSnapshot(); + expect(getByRole('combobox')).toBe(inputRefCallback.mock.calls[0][0]); + }); }); - test('autoFocus is rendered', () => { - const component = shallow( + it('does not show multiple checkmarks with duplicate labels', async () => { + const options = [ + { label: 'Titan', key: 'titan1' }, + { label: 'Titan', key: 'titan2' }, + { label: 'Tethys' }, + ]; + const { baseElement } = render( ); + await showEuiComboBoxOptions(); - expect(component).toMatchSnapshot(); - }); - - test('aria-label attribute is rendered', () => { - const component = shallow( - + const dropdownOptions = baseElement.querySelectorAll( + '.euiFilterSelectItem' ); - - expect(component).toMatchSnapshot(); + expect( + dropdownOptions[0]!.querySelector('[data-euiicon-type="check"]') + ).toBeFalsy(); + expect( + dropdownOptions[1]!.querySelector('[data-euiicon-type="check"]') + ).toBeTruthy(); }); - test('aria-labelledby attribute is rendered', () => { - const component = shallow( - - ); - - expect(component).toMatchSnapshot(); - }); -}); + describe('behavior', () => { + describe('hitting "Enter"', () => { + it('calls the onCreateOption callback when there is input', () => { + const onCreateOptionHandler = jest.fn(); + + const { getByTestSubject } = render( + + ); + const input = getByTestSubject('comboBoxSearchInput'); + + fireEvent.change(input, { target: { value: 'foo' } }); + fireEvent.keyDown(input, { key: 'Enter' }); + + expect(onCreateOptionHandler).toHaveBeenCalledTimes(1); + expect(onCreateOptionHandler).toHaveBeenCalledWith('foo', options); + }); -test('does not show multiple checkmarks with duplicate labels', () => { - const options = [ - { - label: 'Titan', - key: 'titan1', - }, - { - label: 'Titan', - key: 'titan2', - }, - { - label: 'Tethys', - }, - ]; - const { baseElement, getByTestSubject } = render( - - ); - fireEvent.focus(getByTestSubject('comboBoxSearchInput')); - - const dropdownOptions = baseElement.querySelectorAll('.euiFilterSelectItem'); - expect( - dropdownOptions[0]!.querySelector('[data-euiicon-type="check"]') - ).toBeFalsy(); - expect( - dropdownOptions[1]!.querySelector('[data-euiicon-type="check"]') - ).toBeTruthy(); -}); + it('does not call onCreateOption when there is no input', () => { + const onCreateOptionHandler = jest.fn(); -describe('behavior', () => { - describe('hitting "Enter"', () => { - test('calls the onCreateOption callback when there is input', () => { - const onCreateOptionHandler = jest.fn(); + const { getByTestSubject } = render( + + ); + const input = getByTestSubject('comboBoxSearchInput'); - const component = mount( - - ); - - act(() => { - component.setState({ searchValue: 'foo' }); - }); + fireEvent.keyDown(input, { key: 'Enter' }); - act(() => { - const searchInput = findTestSubject(component, 'comboBoxSearchInput'); - searchInput.simulate('focus'); - searchInput.simulate('keyDown', { key: comboBoxKeys.ENTER }); + expect(onCreateOptionHandler).not.toHaveBeenCalled(); }); - - expect(onCreateOptionHandler).toHaveBeenCalledTimes(1); - expect(onCreateOptionHandler).toHaveBeenNthCalledWith(1, 'foo', options); }); - test("doesn't the onCreateOption callback when there is no input", () => { - const onCreateOptionHandler = jest.fn(); + describe('tabbing off the search input', () => { + it("closes the options list if the user isn't navigating the options", async () => { + const keyDownBubbled = jest.fn(); - const component = mount( - - ); + const { getByTestSubject } = render( +
+ +
+ ); + await showEuiComboBoxOptions(); - const searchInput = findTestSubject(component, 'comboBoxSearchInput'); - searchInput.simulate('focus'); - searchInput.simulate('keyDown', { key: comboBoxKeys.ENTER }); - expect(onCreateOptionHandler).not.toHaveBeenCalled(); - }); - }); + const mockEvent = { key: keys.TAB, shiftKey: true }; + fireEvent.keyDown(getByTestSubject('comboBoxSearchInput'), mockEvent); - describe('tabbing', () => { - test("off the search input closes the options list if the user isn't navigating the options", () => { - const onKeyDownWrapper = jest.fn(); - const component = mount( -
- -
- ); + // If the TAB keydown bubbled up to the wrapper, then a browser DOM would shift the focus + expect(keyDownBubbled).toHaveBeenCalledWith( + expect.objectContaining(mockEvent) + ); + }); - const searchInput = findTestSubject(component, 'comboBoxSearchInput'); - searchInput.simulate('focus'); + it('calls onCreateOption', () => { + const onCreateOptionHandler = jest.fn(); - // Focusing the input should open the options list. - expect(findTestSubject(component, 'comboBoxOptionsList')).toBeDefined(); + const { getByTestSubject } = render( + + ); + const input = getByTestSubject('comboBoxSearchInput'); - // Tab backwards to take focus off the combo box. - searchInput.simulate('keyDown', { - key: comboBoxKeys.TAB, - shiftKey: true, + fireEvent.change(input, { target: { value: 'foo' } }); + fireEvent.blur(input); + + expect(onCreateOptionHandler).toHaveBeenCalledTimes(1); + expect(onCreateOptionHandler).toHaveBeenCalledWith('foo', options); }); - // If the TAB keydown propagated to the wrapper, then a browser DOM would shift the focus - expect(onKeyDownWrapper).toHaveBeenCalledTimes(1); - }); + it('does nothing if the user is navigating the options', async () => { + const keyDownBubbled = jest.fn(); - test('off the search input calls onCreateOption', () => { - const onCreateOptionHandler = jest.fn(); + const { getByTestSubject } = render( +
+ +
+ ); + await showEuiComboBoxOptions(); - const component = mount( - - ); - - const searchInput = findTestSubject(component, 'comboBoxSearchInput'); + // Navigate to an option then tab off + const input = getByTestSubject('comboBoxSearchInput'); + fireEvent.keyDown(input, { key: keys.ARROW_DOWN }); + fireEvent.keyDown(input, { key: keys.TAB }); - act(() => { - component.setState({ searchValue: 'foo' }); - searchInput.simulate('focus'); + // If the TAB keydown did not bubble to the wrapper, then the tab event was prevented + expect(keyDownBubbled).not.toHaveBeenCalled(); }); - - searchInput.simulate('blur'); - - expect(onCreateOptionHandler).toHaveBeenCalledTimes(1); - expect(onCreateOptionHandler).toHaveBeenNthCalledWith(1, 'foo', options); }); - test('off the search input does nothing if the user is navigating the options', () => { - const onKeyDownWrapper = jest.fn(); - const component = mount( -
+ describe('clear button', () => { + it('renders when options are selected', () => { + const { getByTestSubject } = render( -
- ); - - const searchInput = findTestSubject(component, 'comboBoxSearchInput'); - searchInput.simulate('focus'); - - // Focusing the input should open the options list. - expect(findTestSubject(component, 'comboBoxOptionsList')).toBeDefined(); + ); - // Navigate to an option. - searchInput.simulate('keyDown', { key: comboBoxKeys.ARROW_DOWN }); - - // Tab backwards to take focus off the combo box. - searchInput.simulate('keyDown', { - key: comboBoxKeys.TAB, - shiftKey: true, + expect(getByTestSubject('comboBoxClearButton')).toBeInTheDocument(); }); - // If the TAB keydown did not bubble to the wrapper, then the tab event was prevented - expect(onKeyDownWrapper.mock.calls.length).toBe(0); - }); - }); - - describe('clear button', () => { - test('calls onChange callback with empty array', () => { - const onChangeHandler = jest.fn(); - const component = mount( - - ); - - findTestSubject(component, 'comboBoxClearButton').simulate('click'); - expect(onChangeHandler).toHaveBeenCalledTimes(1); - expect(onChangeHandler).toHaveBeenNthCalledWith(1, []); - }); + it('does not render when no options are selected', () => { + const { queryByTestSubject } = render( + + ); - test('focuses the input', () => { - const component = mount( - {}} - /> - ); + expect(queryByTestSubject('comboBoxClearButton')).toBeFalsy(); + }); - findTestSubject(component, 'comboBoxClearButton').simulate('click'); - expect( - findTestSubject(component, 'comboBoxSearchInput').getDOMNode() - ).toBe(document.activeElement); - }); - }); + it('does not render when isClearable is false', () => { + const { queryByTestSubject } = render( + + ); - describe('sortMatchesBy', () => { - const sortMatchesByOptions = [ - { - label: 'Something is Disabled', - }, - ...options, - ]; - test('options "none"', () => { - const component = mount< - EuiComboBox, - EuiComboBoxProps, - { matchingOptions: TitanOption[] } - >(); - - findTestSubject(component, 'comboBoxSearchInput').simulate('change', { - target: { value: 'di' }, + expect(queryByTestSubject('comboBoxClearButton')).toBeFalsy(); }); - expect(component.state('matchingOptions')[0].label).toBe( - 'Something is Disabled' - ); - }); + it('calls the onChange callback with empty array', () => { + const onChangeHandler = jest.fn(); - test('options "startsWith"', () => { - const component = mount< - EuiComboBox, - EuiComboBoxProps, - { matchingOptions: TitanOption[] } - >( - - ); + const { getByTestSubject } = render( + + ); + fireEvent.click(getByTestSubject('comboBoxClearButton')); - findTestSubject(component, 'comboBoxSearchInput').simulate('change', { - target: { value: 'di' }, + expect(onChangeHandler).toHaveBeenCalledTimes(1); + expect(onChangeHandler).toHaveBeenCalledWith([]); }); - expect(component.state('matchingOptions')[0].label).toBe('Dione'); + it('focuses the input', () => { + const { getByTestSubject } = render( + {}} + /> + ); + fireEvent.click(getByTestSubject('comboBoxClearButton')); + + expect(document.activeElement).toBe( + getByTestSubject('comboBoxSearchInput') + ); + }); }); - }); - describe('isCaseSensitive', () => { - const isCaseSensitiveOptions = [ - { - label: 'Case sensitivity', - }, - ]; - - test('options "false"', () => { - const component = mount< - EuiComboBox, - EuiComboBoxProps, - { matchingOptions: TitanOption[] } - >( - - ); - - findTestSubject(component, 'comboBoxSearchInput').simulate('change', { - target: { value: 'case' }, + describe('sortMatchesBy', () => { + const onSearchChange = jest.fn(); + const sortMatchesByOptions = [ + { label: 'Something is Disabled' }, + ...options, + ]; + + test('"none"', () => { + const { getByTestSubject, getAllByRole } = render( + + ); + fireEvent.change(getByTestSubject('comboBoxSearchInput'), { + target: { value: 'di' }, + }); + + const foundOptions = getAllByRole('option'); + expect(foundOptions).toHaveLength(2); + expect(foundOptions[0]).toHaveTextContent('Something is Disabled'); + expect(foundOptions[1]).toHaveTextContent('Dione'); }); - expect(component.state('matchingOptions')[0].label).toBe( - 'Case sensitivity' - ); + test('"startsWith"', () => { + const { getByTestSubject, getAllByRole } = render( + + ); + fireEvent.change(getByTestSubject('comboBoxSearchInput'), { + target: { value: 'di' }, + }); + + const foundOptions = getAllByRole('option'); + expect(foundOptions).toHaveLength(2); + expect(foundOptions[0]).toHaveTextContent('Dione'); + expect(foundOptions[1]).toHaveTextContent('Something is Disabled'); + }); }); - test('options "true"', () => { - const component = mount< - EuiComboBox, - EuiComboBoxProps, - { matchingOptions: TitanOption[] } - >( - - ); - - findTestSubject(component, 'comboBoxSearchInput').simulate('change', { - target: { value: 'case' }, + describe('isCaseSensitive', () => { + const isCaseSensitiveOptions = [{ label: 'Case sensitivity' }]; + + test('false', () => { + const { getByTestSubject, queryAllByRole } = render( + + ); + fireEvent.change(getByTestSubject('comboBoxSearchInput'), { + target: { value: 'case' }, + }); + + expect(queryAllByRole('option')).toHaveLength(1); }); - expect(component.state('matchingOptions').length).toBe(0); + test('true', () => { + const { getByTestSubject, queryAllByRole } = render( + + ); + const input = getByTestSubject('comboBoxSearchInput'); - findTestSubject(component, 'comboBoxSearchInput').simulate('change', { - target: { value: 'Case' }, - }); + fireEvent.change(input, { target: { value: 'case' } }); + expect(queryAllByRole('option')).toHaveLength(0); - expect(component.state('matchingOptions')[0].label).toBe( - 'Case sensitivity' - ); + fireEvent.change(input, { target: { value: 'Case' } }); + expect(queryAllByRole('option')).toHaveLength(1); + }); }); }); - - it('calls the inputRef prop with the input element', () => { - const inputRefCallback = jest.fn(); - - const component = mount< - EuiComboBox, - EuiComboBoxProps, - { matchingOptions: TitanOption[] } - >(); - - expect(inputRefCallback).toHaveBeenCalledTimes(1); - expect(component.find('input[role="combobox"]').getDOMNode()).toBe( - inputRefCallback.mock.calls[0][0] - ); - }); }); diff --git a/src/components/combo_box/combo_box.tsx b/src/components/combo_box/combo_box.tsx index 4d6e431b4dc..ffc5678c75f 100644 --- a/src/components/combo_box/combo_box.tsx +++ b/src/components/combo_box/combo_box.tsx @@ -19,13 +19,11 @@ import React, { } from 'react'; import classNames from 'classnames'; -import { findPopoverPosition, htmlIdGenerator, keys } from '../../services'; -import { getElementZIndex } from '../../services/popover'; +import { htmlIdGenerator, keys } from '../../services'; import { CommonProps } from '../common'; -import { EuiPortal } from '../portal'; +import { EuiInputPopover, EuiInputPopoverProps } from '../popover'; import { EuiI18n } from '../i18n'; import { EuiFormControlLayoutProps } from '../form'; -import { EuiFilterSelectItemClass } from '../filter_group/filter_select_item'; import type { EuiTextTruncateProps } from '../text_truncate'; import { @@ -41,11 +39,9 @@ import { } from './combo_box_input/combo_box_input'; import { EuiComboBoxOptionsListProps } from './combo_box_options_list/combo_box_options_list'; import { - UpdatePositionHandler, OptionHandler, RefInstance, EuiComboBoxOptionOption, - EuiComboBoxOptionsListPosition, EuiComboBoxSingleSelectionShape, } from './types'; import { EuiComboBoxOptionsList } from './combo_box_options_list'; @@ -166,6 +162,13 @@ export interface _EuiComboBoxProps * text will always take precedence. */ truncationProps?: Partial>; + /** + * Allows customizing the underlying EuiInputPopover component + * (except for props that control state). + */ + inputPopoverProps?: Partial< + Omit + >; } /** @@ -193,12 +196,8 @@ interface EuiComboBoxState { activeOptionIndex: number; hasFocus: boolean; isListOpen: boolean; - listElement?: RefInstance; - listPosition: EuiComboBoxOptionsListPosition; - listZIndex: number | undefined; matchingOptions: Array>; searchValue: string; - width: number; } const initialSearchValue = ''; @@ -224,9 +223,6 @@ export class EuiComboBox extends Component< activeOptionIndex: -1, hasFocus: false, isListOpen: false, - listElement: null, - listPosition: 'bottom', - listZIndex: undefined, matchingOptions: getMatchingOptions({ options: this.props.options, selectedOptions: this.props.selectedOptions, @@ -237,131 +233,36 @@ export class EuiComboBox extends Component< sortMatchesBy: this.props.sortMatchesBy, }), searchValue: initialSearchValue, - width: 0, }; - _isMounted = false; rootId = htmlIdGenerator(); // Refs comboBoxRefInstance: RefInstance = null; comboBoxRefCallback: RefCallback = (ref) => { this.comboBoxRefInstance = ref; - - if (this.comboBoxRefInstance) { - const comboBoxBounds = this.comboBoxRefInstance.getBoundingClientRect(); - this.setState({ - width: comboBoxBounds.width, - }); - } }; searchInputRefInstance: RefInstance = null; searchInputRefCallback: RefCallback = (ref) => { this.searchInputRefInstance = ref; - if (this.props.inputRef) this.props.inputRef(ref); + this.props.inputRef?.(ref); }; listRefInstance: RefInstance = null; listRefCallback: RefCallback = (ref) => { - if (this.comboBoxRefInstance) { - // find the zIndex of the combobox relative to the page body - // and use that to depth-position the list box - // adds an extra `100` to provide some defense around neighboring elements' positioning - const listZIndex = - getElementZIndex(this.comboBoxRefInstance, document.body) + 100; - this.setState({ listZIndex }); - } this.listRefInstance = ref; }; - toggleButtonRefInstance: RefInstance = - null; - toggleButtonRefCallback: RefCallback = ( - ref - ) => { - this.toggleButtonRefInstance = ref; - }; - - optionsRefInstances: Array> = []; - optionRefCallback: EuiComboBoxOptionsListProps['optionRef'] = ( - index, - ref - ) => { - this.optionsRefInstances[index] = ref; - }; - openList = () => { this.setState({ isListOpen: true, }); }; - closeList = (event?: Event) => { - if (event && event.target === this.searchInputRefInstance) { - // really long search values / custom entries triggers a scroll event on the input - // which the EuiComboBoxOptionsList passes through here - return; - } - + closeList = () => { this.clearActiveOption(); - this.setState({ - listZIndex: undefined, - isListOpen: false, - }); - }; - - updatePosition: UpdatePositionHandler = ( - listElement = this.state.listElement - ) => { - if (!this._isMounted) { - return; - } - - if (!this.state.isListOpen) { - return; - } - - if (!listElement) { - return; - } - - // it's possible that updateListPosition is called when listElement is becoming visible, but isn't yet - const listElementBounds = listElement.getBoundingClientRect(); - if (listElementBounds.width === 0 || listElementBounds.height === 0) { - return; - } - - if (!this.comboBoxRefInstance) { - return; - } - - const comboBoxBounds = this.comboBoxRefInstance.getBoundingClientRect(); - - const { position, top } = findPopoverPosition({ - allowCrossAxis: false, - anchor: this.comboBoxRefInstance, - popover: listElement, - position: 'bottom', - }) as { position: 'bottom'; top: number }; - - if (this.listRefInstance) { - this.listRefInstance.style.top = `${top}px`; - // listElement doesn't have its width set until after updating the position - // which means the popover service won't know about the correct width - // however, we already know where to position the element - this.listRefInstance.style.left = `${ - comboBoxBounds.left + window.pageXOffset - }px`; - this.listRefInstance.style.width = `${comboBoxBounds.width}px`; - } - - // Cache for future calls. - this.setState({ - listElement, - listPosition: position, - width: comboBoxBounds.width, - }); + this.setState({ isListOpen: false }); }; incrementActiveOptionIndex = (amount: number) => { @@ -526,12 +427,8 @@ export class EuiComboBox extends Component< }; onComboBoxFocus: FocusEventHandler = (event) => { - if (this.props.onFocus) { - this.props.onFocus(event); - } - + this.props.onFocus?.(event); this.openList(); - this.setState({ hasFocus: true }); }; @@ -561,11 +458,8 @@ export class EuiComboBox extends Component< this.comboBoxRefInstance.contains(relatedTarget); if (!focusedInOptionsList && !focusedInInput) { + this.props.onBlur?.(event); this.closeList(); - - if (this.props.onBlur) { - this.props.onBlur(event); - } this.setState({ hasFocus: false }); // If the user tabs away or changes focus to another element, take whatever input they've @@ -637,9 +531,7 @@ export class EuiComboBox extends Component< break; default: - if (this.props.onKeyDown) { - this.props.onKeyDown(event); - } + this.props.onKeyDown?.(event); } }; @@ -669,17 +561,13 @@ export class EuiComboBox extends Component< ? [addedOption] : selectedOptions.concat(addedOption); - if (onChange) { - onChange(changeOptions); - } + onChange?.(changeOptions); this.clearSearchValue(); this.clearActiveOption(); if (!isContainerBlur) { - if (this.searchInputRefInstance) { - this.searchInputRefInstance.focus(); - } + this.searchInputRefInstance?.focus(); } if (singleSelection) { @@ -693,24 +581,17 @@ export class EuiComboBox extends Component< onRemoveOption: OptionHandler = (removedOption) => { const { onChange, selectedOptions } = this.props; - if (onChange) { - onChange(selectedOptions.filter((option) => option !== removedOption)); - } + onChange?.(selectedOptions.filter((option) => option !== removedOption)); this.clearActiveOption(); }; clearSelectedOptions = () => { - const { onChange } = this.props; - if (onChange) { - onChange([]); - } + this.props.onChange?.([]); // Clicking the clear button will also cause it to disappear. This would result in focus // shifting unexpectedly to the body element so we set it to the input which is more reasonable, - if (this.searchInputRefInstance) { - this.searchInputRefInstance.focus(); - } + this.searchInputRefInstance?.focus(); if (!this.state.isListOpen) { this.openList(); @@ -719,9 +600,7 @@ export class EuiComboBox extends Component< onComboBoxClick = () => { // When the user clicks anywhere on the box, enter the interaction state. - if (this.searchInputRefInstance) { - this.searchInputRefInstance.focus(); - } + this.searchInputRefInstance?.focus(); // If the user does this from a state in which an option has focus, then we need to reset it or clear it. if ( @@ -742,22 +621,15 @@ export class EuiComboBox extends Component< }; onOpenListClick = () => { - if (this.searchInputRefInstance) { - this.searchInputRefInstance.focus(); - } + this.searchInputRefInstance?.focus(); + if (!this.state.isListOpen) { this.openList(); } }; onOptionListScroll = () => { - if (this.searchInputRefInstance) { - this.searchInputRefInstance.focus(); - } - }; - - onCloseListClick = () => { - this.closeList(); + this.searchInputRefInstance?.focus(); }; onSearchChange: NonNullable['onChange']> = ( @@ -778,10 +650,6 @@ export class EuiComboBox extends Component< } }; - componentDidMount() { - this._isMounted = true; - } - static getDerivedStateFromProps( nextProps: _EuiComboBoxProps, prevState: EuiComboBoxState @@ -817,76 +685,6 @@ export class EuiComboBox extends Component< return stateUpdate; } - updateMatchingOptionsIfDifferent = ( - newMatchingOptions: Array> - ) => { - const { matchingOptions, activeOptionIndex } = this.state; - const { singleSelection, selectedOptions } = this.props; - - let areOptionsDifferent = false; - - if (matchingOptions.length !== newMatchingOptions.length) { - areOptionsDifferent = true; - } else { - for (let i = 0; i < matchingOptions.length; i++) { - if (matchingOptions[i].label !== newMatchingOptions[i].label) { - areOptionsDifferent = true; - break; - } - } - } - - if (areOptionsDifferent) { - this.optionsRefInstances = []; - let nextActiveOptionIndex = activeOptionIndex; - // ensure that the currently selected single option is active if it is in the matchingOptions - if (Boolean(singleSelection) && selectedOptions.length === 1) { - if (newMatchingOptions.includes(selectedOptions[0])) { - nextActiveOptionIndex = newMatchingOptions.indexOf( - selectedOptions[0] - ); - } - } - - this.setState({ - matchingOptions: newMatchingOptions, - activeOptionIndex: nextActiveOptionIndex, - }); - - if (!newMatchingOptions.length) { - // Prevent endless setState -> componentWillUpdate -> setState loop. - if (this.hasActiveOption()) { - this.clearActiveOption(); - } - } - } - }; - - componentDidUpdate() { - const { options, selectedOptions, singleSelection, sortMatchesBy } = - this.props; - const { searchValue } = this.state; - - // React 16.3 has a bug (fixed in 16.4) where getDerivedStateFromProps - // isn't called after a state change, and we track `searchValue` in state - // instead we need to react to a change in searchValue here - this.updateMatchingOptionsIfDifferent( - getMatchingOptions({ - options, - selectedOptions, - searchValue, - isCaseSensitive: this.props.isCaseSensitive, - isPreFiltered: this.props.async, - showPrevSelected: Boolean(singleSelection), - sortMatchesBy, - }) - ); - } - - componentWillUnmount() { - this._isMounted = false; - } - render() { const { 'data-test-subj': dataTestSubj, @@ -919,6 +717,7 @@ export class EuiComboBox extends Component< append, autoFocus, truncationProps, + inputPopoverProps, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledby, ...rest @@ -927,9 +726,7 @@ export class EuiComboBox extends Component< activeOptionIndex, hasFocus, isListOpen, - listPosition, searchValue, - width, matchingOptions, } = this.state; @@ -966,50 +763,41 @@ export class EuiComboBox extends Component< : undefined; optionsList = ( - - - {(listboxAriaLabel: string) => ( - - )} - - + + {(listboxAriaLabel: string) => ( + + )} + ); } @@ -1029,46 +817,58 @@ export class EuiComboBox extends Component< onBlur={this.onContainerBlur} ref={this.comboBoxRefCallback} > - 0} - id={inputId} - inputRef={this.searchInputRefCallback} - isDisabled={isDisabled} - isListOpen={isListOpen} - noIcon={!!noSuggestions} - onChange={this.onSearchChange} - onClear={ - isClearable && !isDisabled ? this.clearSelectedOptions : undefined + panelPaddingSize="none" + disableFocusTrap={true} + closeOnScroll={true} + {...inputPopoverProps} + isOpen={isListOpen} + closePopover={this.closeList} + input={ + 0} + id={inputId} + inputRef={this.searchInputRefCallback} + isDisabled={isDisabled} + isListOpen={isListOpen} + noIcon={!!noSuggestions} + onChange={this.onSearchChange} + onClear={ + isClearable && !isDisabled + ? this.clearSelectedOptions + : undefined + } + onClick={this.onComboBoxClick} + onCloseListClick={this.closeList} + onFocus={this.onComboBoxFocus} + onOpenListClick={this.onOpenListClick} + onRemoveOption={this.onRemoveOption} + placeholder={placeholder} + rootId={this.rootId} + searchValue={searchValue} + selectedOptions={selectedOptions} + singleSelection={singleSelection} + value={value} + append={singleSelection ? append : undefined} + prepend={singleSelection ? prepend : undefined} + isLoading={isLoading} + isInvalid={markAsInvalid} + autoFocus={autoFocus} + aria-label={ariaLabel} + aria-labelledby={ariaLabelledby} + /> } - onClick={this.onComboBoxClick} - onCloseListClick={this.onCloseListClick} - onFocus={this.onComboBoxFocus} - onOpenListClick={this.onOpenListClick} - onRemoveOption={this.onRemoveOption} - placeholder={placeholder} - rootId={this.rootId} - searchValue={searchValue} - selectedOptions={selectedOptions} - singleSelection={singleSelection} - toggleButtonRef={this.toggleButtonRefCallback} - updatePosition={this.updatePosition} - value={value} - append={singleSelection ? append : undefined} - prepend={singleSelection ? prepend : undefined} - isLoading={isLoading} - isInvalid={markAsInvalid} - autoFocus={autoFocus} - aria-label={ariaLabel} - aria-labelledby={ariaLabelledby} - /> - {optionsList} + > + {optionsList} + ); } diff --git a/src/components/combo_box/combo_box_input/combo_box_input.tsx b/src/components/combo_box/combo_box_input/combo_box_input.tsx index ead4a60c328..004112c7e8e 100644 --- a/src/components/combo_box/combo_box_input/combo_box_input.tsx +++ b/src/components/combo_box/combo_box_input/combo_box_input.tsx @@ -29,7 +29,6 @@ import { EuiComboBoxOptionOption, EuiComboBoxSingleSelectionShape, OptionHandler, - UpdatePositionHandler, } from '../types'; export interface EuiComboBoxInputProps extends CommonProps { @@ -56,7 +55,6 @@ export interface EuiComboBoxInputProps extends CommonProps { selectedOptions: Array>; singleSelection?: boolean | EuiComboBoxSingleSelectionShape; toggleButtonRef?: RefCallback; - updatePosition: UpdatePositionHandler; value?: string; prepend?: EuiFormControlLayoutProps['prepend']; append?: EuiFormControlLayoutProps['append']; @@ -99,12 +97,11 @@ export class EuiComboBoxInput extends Component< this.setState({ inputWidth }); }; - updatePosition = () => { - // Wait a beat for the DOM to update, since we depend on DOM elements' bounds. - requestAnimationFrame(() => { - this.props.updatePosition(); - }); - }; + componentDidUpdate(prevProps: EuiComboBoxInputProps) { + if (prevProps.searchValue !== this.props.searchValue) { + this.updateInputSize(this.props.searchValue); + } + } onFocus: FocusEventHandler = (event) => { this.props.onFocus(event); @@ -145,16 +142,6 @@ export class EuiComboBoxInput extends Component< } }; - componentDidUpdate(prevProps: EuiComboBoxInputProps) { - if (prevProps.searchValue !== this.props.searchValue) { - this.updateInputSize(this.props.searchValue); - - // We need to update the position of everything if the user enters enough input to change - // the size of the input. - this.updatePosition(); - } - } - render() { const { compressed, @@ -176,7 +163,6 @@ export class EuiComboBoxInput extends Component< searchValue, selectedOptions, singleSelection: singleSelectionProp, - toggleButtonRef, value, prepend, append, @@ -289,7 +275,6 @@ export class EuiComboBoxInput extends Component< 'data-test-subj': 'comboBoxToggleListButton', disabled: isDisabled, onClick: isListOpen && !isDisabled ? onCloseListClick : onOpenListClick, - ref: toggleButtonRef, side: 'right', tabIndex: -1, type: 'arrowDown', diff --git a/src/components/combo_box/combo_box_options_list/_combo_box_options_list.scss b/src/components/combo_box/combo_box_options_list/_combo_box_options_list.scss index 46da053fed4..9904605dcca 100644 --- a/src/components/combo_box/combo_box_options_list/_combo_box_options_list.scss +++ b/src/components/combo_box/combo_box_options_list/_combo_box_options_list.scss @@ -2,13 +2,13 @@ * 1. Using specificity to override panel shadow * 2. Prevent really long input from overflowing the container. */ + .euiComboBoxOptionsList { - // Remove transforms from popover panel - transform: none !important; // stylelint-disable-line declaration-no-important - top: 0; + max-height: 200px; // Also used/set in the JS file + overflow: hidden; - .euiFilterSelectItem__content { - margin-block: 0 !important; // stylelint-disable-line declaration-no-important + &__virtualization { + @include euiScrollBar; } /* Kibana FTR affordance - without this, Selenium complains about the overlaid @@ -26,13 +26,3 @@ text-align: center; word-wrap: break-word; } - -.euiComboBoxOptionsList__rowWrap { - padding: 0; - max-height: 200px; // Also used/set in the JS file - overflow: hidden; - - > div { // Targets the element for FixedSizeList that doesn't have a selector - @include euiScrollBar; - } -} diff --git a/src/components/combo_box/combo_box_options_list/combo_box_options_list.tsx b/src/components/combo_box/combo_box_options_list/combo_box_options_list.tsx index 217e19e4d18..f06e5092d00 100644 --- a/src/components/combo_box/combo_box_options_list/combo_box_options_list.tsx +++ b/src/components/combo_box/combo_box_options_list/combo_box_options_list.tsx @@ -6,15 +6,11 @@ * Side Public License, v 1. */ -import React, { - Component, - ComponentProps, - ReactNode, - RefCallback, -} from 'react'; +import React, { Component, ContextType, ReactNode, RefCallback } from 'react'; import classNames from 'classnames'; import { FixedSizeList, + FixedSizeListProps, ListProps, ListChildComponentProps, } from 'react-window'; @@ -28,79 +24,66 @@ import { EuiComboBoxTitle } from './combo_box_title'; import { EuiI18n } from '../../i18n'; import { EuiFilterSelectItem, - EuiFilterSelectItemClass, FilterChecked, } from '../../filter_group/filter_select_item'; import { htmlIdGenerator } from '../../../services'; import { CommonProps } from '../../common'; import { EuiBadge } from '../../badge'; -import { EuiPopoverPanel } from '../../popover/popover_panel'; import { EuiTextTruncate } from '../../text_truncate'; +import { EuiInputPopoverWidthContext } from '../../popover/input_popover'; import type { _EuiComboBoxProps } from '../combo_box'; import { EuiComboBoxOptionOption, - EuiComboBoxOptionsListPosition, EuiComboBoxSingleSelectionShape, OptionHandler, - RefInstance, - UpdatePositionHandler, } from '../types'; -export type EuiComboBoxOptionsListProps = CommonProps & - ComponentProps & { - activeOptionIndex?: number; - areAllOptionsSelected?: boolean; - listboxAriaLabel: string; - /** - * Creates a custom text option. You can use `{searchValue}` inside your string to better customize your text. - * It won't show if there's no onCreateOption. - */ - customOptionText?: string; - fullWidth?: boolean; - getSelectedOptionForSearchValue?: (params: { - isCaseSensitive?: boolean; - searchValue: string; - selectedOptions: any[]; - }) => EuiComboBoxOptionOption | undefined; +export type EuiComboBoxOptionsListProps = CommonProps & { + activeOptionIndex?: number; + areAllOptionsSelected?: boolean; + listboxAriaLabel: string; + /** + * Creates a custom text option. You can use `{searchValue}` inside your string to better customize your text. + * It won't show if there's no onCreateOption. + */ + customOptionText?: string; + fullWidth?: boolean; + getSelectedOptionForSearchValue?: (params: { isCaseSensitive?: boolean; - isLoading?: boolean; - listRef: RefCallback; - matchingOptions: Array>; - onCloseList: (event: Event) => void; - onCreateOption?: ( - searchValue: string, - options: Array> - ) => boolean | void; - onOptionClick?: OptionHandler; - onOptionEnterKey?: OptionHandler; - onScroll?: ListProps['onScroll']; - optionRef: ( - index: number, - node: RefInstance - ) => void; - /** - * Array of EuiComboBoxOptionOption objects. See #EuiComboBoxOptionOption - */ - options: Array>; - position?: EuiComboBoxOptionsListPosition; - renderOption?: ( - option: EuiComboBoxOptionOption, - searchValue: string, - OPTION_CONTENT_CLASSNAME: string - ) => ReactNode; - rootId: ReturnType; - rowHeight: number; - scrollToIndex?: number; searchValue: string; - selectedOptions: Array>; - updatePosition: UpdatePositionHandler; - width: number; - singleSelection?: boolean | EuiComboBoxSingleSelectionShape; - delimiter?: string; - zIndex?: number; - truncationProps?: _EuiComboBoxProps['truncationProps']; - }; + selectedOptions: any[]; + }) => EuiComboBoxOptionOption | undefined; + isCaseSensitive?: boolean; + isLoading?: boolean; + listRef: RefCallback; + matchingOptions: Array>; + onCloseList: (event: Event) => void; + onCreateOption?: ( + searchValue: string, + options: Array> + ) => boolean | void; + onOptionClick?: OptionHandler; + onOptionEnterKey?: OptionHandler; + onScroll?: ListProps['onScroll']; + /** + * Array of EuiComboBoxOptionOption objects. See #EuiComboBoxOptionOption + */ + options: Array>; + renderOption?: ( + option: EuiComboBoxOptionOption, + searchValue: string, + OPTION_CONTENT_CLASSNAME: string + ) => ReactNode; + rootId: ReturnType; + rowHeight: number; + scrollToIndex?: number; + searchValue: string; + selectedOptions: Array>; + singleSelection?: boolean | EuiComboBoxSingleSelectionShape; + delimiter?: string; + truncationProps?: _EuiComboBoxProps['truncationProps']; +}; const hitEnterBadge = ( extends Component< EuiComboBoxOptionsListProps > { - listRefInstance: RefInstance = null; listRef: FixedSizeList | null = null; - listBoxRef: HTMLUListElement | null = null; + + static contextType = EuiInputPopoverWidthContext; + declare context: ContextType; static defaultProps = { 'data-test-subj': '', @@ -124,44 +108,7 @@ export class EuiComboBoxOptionsList extends Component< isCaseSensitive: false, }; - updatePosition = () => { - // Wait a beat for the DOM to update, since we depend on DOM elements' bounds. - requestAnimationFrame(() => { - this.props.updatePosition(this.listRefInstance); - }); - }; - - componentDidMount() { - // Wait a frame, otherwise moving focus from one combo box to another will result in the class - // being removed from the body. - requestAnimationFrame(() => { - document.body.classList.add('euiBody-hasPortalContent'); - }); - this.updatePosition(); - window.addEventListener('resize', this.updatePosition); - - // Firefox will trigger a scroll event in many common situations when the options list div is appended - // to the DOM; in testing it was always within 100ms, but setting a timeout here for 500ms to be safe - setTimeout(() => { - window.addEventListener('scroll', this.closeListOnScroll, { - passive: true, // for better performance as we won't call preventDefault - capture: true, // scroll events don't bubble, they must be captured instead - }); - }, 500); - } - componentDidUpdate(prevProps: EuiComboBoxOptionsListProps) { - const { options, selectedOptions, searchValue } = prevProps; - - // We don't compare matchingOptions because that will result in a loop. - if ( - searchValue !== this.props.searchValue || - options !== this.props.options || - selectedOptions !== this.props.selectedOptions - ) { - this.updatePosition(); - } - if ( this.listRef && typeof this.props.activeOptionIndex !== 'undefined' && @@ -171,44 +118,25 @@ export class EuiComboBoxOptionsList extends Component< } } - componentWillUnmount() { - document.body.classList.remove('euiBody-hasPortalContent'); - window.removeEventListener('resize', this.updatePosition); - window.removeEventListener('scroll', this.closeListOnScroll, { - capture: true, - }); - } - - closeListOnScroll = (event: Event) => { - // Close the list when a scroll event happens, but not if the scroll happened in the options list. - // This mirrors Firefox's approach of auto-closing `select` elements onscroll. - if ( - this.listRefInstance && - event.target && - this.listRefInstance.contains(event.target as Node) === false - ) { - this.props.onCloseList(event); - } - }; - - listRefCallback: RefCallback = (ref) => { - this.props.listRef(ref); - this.listRefInstance = ref; - }; - setListRef = (ref: FixedSizeList | null) => { this.listRef = ref; }; - setListBoxRef = (ref: HTMLUListElement | null) => { - this.listBoxRef = ref; - - if (ref) { - ref.setAttribute('aria-label', this.props.listboxAriaLabel); - ref.setAttribute('id', this.props.rootId('listbox')); - ref.setAttribute('role', 'listbox'); - ref.setAttribute('tabIndex', '0'); - } + ListInnerElement: FixedSizeListProps['innerElementType'] = ({ + children, + ...rest + }) => { + return ( +
+ {children} +
+ ); }; ListRow = ({ data, index, style }: ListChildComponentProps) => { @@ -227,7 +155,6 @@ export class EuiComboBoxOptionsList extends Component< singleSelection, selectedOptions, onOptionClick, - optionRef, activeOptionIndex, renderOption, searchValue, @@ -271,7 +198,6 @@ export class EuiComboBoxOptionsList extends Component< onOptionClick(option); } }} - ref={optionRef.bind(this, index)} isFocused={optionIsFocused} checked={checked} showIcons={singleSelection ? true : false} @@ -383,9 +309,7 @@ export class EuiComboBoxOptionsList extends Component< onOptionClick, onOptionEnterKey, onScroll, - optionRef, options, - position = 'bottom', renderOption, rootId, rowHeight, @@ -393,11 +317,7 @@ export class EuiComboBoxOptionsList extends Component< searchValue, selectedOptions, singleSelection, - updatePosition, - width, delimiter, - zIndex, - style, truncationProps, listboxAriaLabel, ...rest @@ -534,47 +454,34 @@ export class EuiComboBoxOptionsList extends Component< matchingOptions.length < 7 ? matchingOptions.length : 7; const height = numVisibleOptions * (rowHeight + 1); // Add one for the border - // bounded by max-height of euiComboBoxOptionsList__rowWrap + // bounded by max-height of .euiComboBoxOptionsList const boundedHeight = height > 200 ? 200 : height; const optionsList = ( {this.ListRow} ); - /** - * Reusing the EuiPopover__panel classes to help with consistency/maintenance. - * But this should really be converted to user the popover component. - */ - const classes = classNames('euiComboBoxOptionsList'); - return ( - -
- {emptyState || optionsList} -
-
+ {emptyState || optionsList} + ); } } diff --git a/src/components/combo_box/index.ts b/src/components/combo_box/index.ts index e80488c60ac..d85ab3bf70f 100644 --- a/src/components/combo_box/index.ts +++ b/src/components/combo_box/index.ts @@ -12,6 +12,5 @@ export * from './combo_box_input'; export * from './combo_box_options_list'; export type { EuiComboBoxOptionOption, - EuiComboBoxOptionsListPosition, EuiComboBoxSingleSelectionShape, } from './types'; diff --git a/src/components/combo_box/types.ts b/src/components/combo_box/types.ts index 0049587d0e7..45cf3838975 100644 --- a/src/components/combo_box/types.ts +++ b/src/components/combo_box/types.ts @@ -26,15 +26,10 @@ export interface EuiComboBoxOptionOption< truncationProps?: _EuiComboBoxProps['truncationProps']; } -export type UpdatePositionHandler = ( - listElement?: RefInstance -) => void; export type OptionHandler = (option: EuiComboBoxOptionOption) => void; export type RefInstance = T | null; -export type EuiComboBoxOptionsListPosition = 'top' | 'bottom'; - export interface EuiComboBoxSingleSelectionShape { asPlainText?: boolean; } diff --git a/src/components/popover/input_popover.spec.tsx b/src/components/popover/input_popover.spec.tsx index 80fde317f43..2e00958b3fa 100644 --- a/src/components/popover/input_popover.spec.tsx +++ b/src/components/popover/input_popover.spec.tsx @@ -200,4 +200,56 @@ describe('EuiPopover', () => { }); }); }); + + describe('closeOnScroll', () => { + const ScrollAllTheThings = () => { + const [isOpen, setIsOpen] = useState(true); + + return ( +
+ setIsOpen(false)} + input={ + setIsOpen(true)} + rows={1} + defaultValue={`hello\nworld`} + /> + } + > +
+
Popover content
+
+
+
+ ); + }; + + it('closes the popover when the user scrolls outside of the component', () => { + cy.mount(); + cy.wait(500); // Wait for the setTimeout in the useEffect + + // Scrolling the input or popover should not close the popover + cy.get('[data-test-subj="inputWithScroll"]').scrollTo('bottom'); + cy.get('[data-popover-panel]').should('exist'); + + cy.get('[data-test-subj="popoverWithScroll"]').scrollTo('bottom'); + cy.wait(500); // Wait a tick for false positives + cy.get('[data-popover-panel]').should('exist'); + + // Scrolling anywhere else should close the popover + cy.scrollTo('bottom'); + cy.get('[data-popover-panel]').should('not.exist'); + + // Popover should be able to re-opened after close + cy.get('[data-test-subj="inputWithScroll"]').click(); + cy.get('[data-popover-panel]').should('exist'); + }); + }); }); diff --git a/src/components/popover/input_popover.tsx b/src/components/popover/input_popover.tsx index 1d61c2a708e..6fc3c41aa8f 100644 --- a/src/components/popover/input_popover.tsx +++ b/src/components/popover/input_popover.tsx @@ -15,6 +15,7 @@ import React, { useCallback, useMemo, useRef, + createContext, } from 'react'; import { css } from '@emotion/react'; import classnames from 'classnames'; @@ -36,6 +37,10 @@ export interface _EuiInputPopoverProps */ anchorPosition?: 'downLeft' | 'downRight' | 'downCenter'; disableFocusTrap?: boolean; + /** + * Allows automatically closing the input popover on page scroll + */ + closeOnScroll?: boolean; fullWidth?: boolean; input: EuiPopoverProps['button']; inputRef?: EuiPopoverProps['buttonRef']; @@ -52,10 +57,14 @@ export type EuiInputPopoverProps = CommonProps & HTMLAttributes & _EuiInputPopoverProps; +// Used by child components that want to know the parent popover width +export const EuiInputPopoverWidthContext = createContext(0); + export const EuiInputPopover: FunctionComponent = ({ children, className, closePopover, + closeOnScroll = false, disableFocusTrap = false, focusTrapProps, input, @@ -140,6 +149,44 @@ export const EuiInputPopover: FunctionComponent = ({ [disableFocusTrap, closePopover, panelPropsOnKeyDown] ); + /** + * Optional close on scroll behavior + */ + + useEffect(() => { + // When the popover opens, add a scroll listener to the page (& remove it after) + if (closeOnScroll && panelEl) { + // Close the popover, but only if the scroll event occurs outside the input or the popover itself + const closePopoverOnScroll = (event: Event) => { + if (!panelEl || !inputEl || !event.target) return; + const scrollTarget = event.target as Node; + + if ( + panelEl.contains(scrollTarget) === false && + inputEl.contains(scrollTarget) === false + ) { + closePopover(); + } + }; + + // Firefox will trigger a scroll event in many common situations when the options list div is appended + // to the DOM; in testing it was always within 100ms, but setting a timeout here for 500ms to be safe + const timeoutId = setTimeout(() => { + window.addEventListener('scroll', closePopoverOnScroll, { + passive: true, // for better performance as we won't call preventDefault + capture: true, // scroll events don't bubble, they must be captured instead + }); + }, 500); + + return () => { + window.removeEventListener('scroll', closePopoverOnScroll, { + capture: true, + }); + clearTimeout(timeoutId); + }; + } + }, [closeOnScroll, closePopover, panelEl, inputEl]); + return ( = ({ disabled={disableFocusTrap} {...focusTrapProps} > - {children} + + {children} + ); diff --git a/src/test/rtl/component_helpers.d.ts b/src/test/rtl/component_helpers.d.ts index 678cf0ba3dc..13dc28dcb6c 100644 --- a/src/test/rtl/component_helpers.d.ts +++ b/src/test/rtl/component_helpers.d.ts @@ -8,3 +8,5 @@ export declare const waitForEuiPopoverClose: () => Promise; export declare const waitForEuiToolTipVisible: () => Promise; export declare const waitForEuiToolTipHidden: () => Promise; + +export declare const showEuiComboBoxOptions: () => Promise; diff --git a/src/test/rtl/component_helpers.ts b/src/test/rtl/component_helpers.ts index aa37e1dde78..66611572cd6 100644 --- a/src/test/rtl/component_helpers.ts +++ b/src/test/rtl/component_helpers.ts @@ -7,7 +7,8 @@ */ import '@testing-library/jest-dom'; -import { waitFor } from '@testing-library/react'; +import { waitFor, fireEvent } from '@testing-library/react'; +import { screen } from './custom_render'; /** * Ensure the EuiPopover being tested is open/closed before continuing @@ -43,3 +44,14 @@ export const waitForEuiToolTipHidden = async () => const tooltip = document.querySelector('.euiToolTipPopover'); expect(tooltip).toBeNull(); }); + +/** + * Doot doo + */ +export const showEuiComboBoxOptions = async () => { + fireEvent.click(screen.getByTestSubject('comboBoxToggleListButton')); + await waitForEuiPopoverOpen(); + await waitFor(() => { + expect(screen.getByRole('listbox')).toBeInTheDocument(); + }); +}; diff --git a/upcoming_changelogs/7246.md b/upcoming_changelogs/7246.md new file mode 100644 index 00000000000..98cd7e9792f --- /dev/null +++ b/upcoming_changelogs/7246.md @@ -0,0 +1,2 @@ +- Updated `EuiComboBox` to use `EuiInputPopover` under the hood +- Added `inputPopoverProps` to `EuiComboBox`, which allows customizing the underlying popover From 2e257a23f6db6bde632fa634fa0962e5129d243c Mon Sep 17 00:00:00 2001 From: Cee Chen Date: Wed, 4 Oct 2023 20:13:52 -0700 Subject: [PATCH 12/38] Fix failing React 16 snapshot --- .../combo_box/__snapshots__/combo_box.test.tsx.snap | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/components/combo_box/__snapshots__/combo_box.test.tsx.snap b/src/components/combo_box/__snapshots__/combo_box.test.tsx.snap index a1e7f5f4028..43288f4a70e 100644 --- a/src/components/combo_box/__snapshots__/combo_box.test.tsx.snap +++ b/src/components/combo_box/__snapshots__/combo_box.test.tsx.snap @@ -96,6 +96,13 @@ exports[`EuiComboBox renders the options list dropdown 1`] = ` style="inline-size: 2px;" value="" /> + + Combo box. Selected. Combo box input. Type some text or, to display a list of choices, press Down Arrow. To exit the list of choices, press Escape. +
Date: Thu, 5 Oct 2023 10:01:54 -0700 Subject: [PATCH 13/38] Fix failing React 16 test in main --- src/components/combo_box/combo_box.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/combo_box/combo_box.tsx b/src/components/combo_box/combo_box.tsx index ffc5678c75f..acd7ec2d70c 100644 --- a/src/components/combo_box/combo_box.tsx +++ b/src/components/combo_box/combo_box.tsx @@ -481,6 +481,7 @@ export class EuiComboBox extends Component< onKeyDown: KeyboardEventHandler = (event) => { if (this.props.isDisabled) return; + event.persist(); // TODO: Remove once React 16 support is dropped switch (event.key) { case keys.ARROW_UP: event.preventDefault(); From c77af6e5f06db954814dab92ef88baf0a02ce5cb Mon Sep 17 00:00:00 2001 From: Cee Chen Date: Thu, 5 Oct 2023 10:31:24 -0700 Subject: [PATCH 14/38] This time, for real, fix combobox CI failures in main - Apologies for the shenanigans y'all, should've made this a PR and not assumed I knew what I was doing by pushing a quick fix to main --- .../__snapshots__/combo_box.test.tsx.snap | 7 ----- src/components/combo_box/combo_box.test.tsx | 29 ++++++++++++------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/components/combo_box/__snapshots__/combo_box.test.tsx.snap b/src/components/combo_box/__snapshots__/combo_box.test.tsx.snap index 43288f4a70e..a1e7f5f4028 100644 --- a/src/components/combo_box/__snapshots__/combo_box.test.tsx.snap +++ b/src/components/combo_box/__snapshots__/combo_box.test.tsx.snap @@ -96,13 +96,6 @@ exports[`EuiComboBox renders the options list dropdown 1`] = ` style="inline-size: 2px;" value="" /> - - Combo box. Selected. Combo box input. Type some text or, to display a list of choices, press Down Arrow. To exit the list of choices, press Escape. -
{ render(); }); - it('renders the options list dropdown', async () => { - const { baseElement } = render( - - ); - await showEuiComboBoxOptions(); + // React 16 for some reason doesn't snapshot the screen reader text + testOnReactVersion(['17', '18'])( + 'renders the options list dropdown', + async () => { + const { baseElement } = render( + + ); + await showEuiComboBoxOptions(); - expect(baseElement).toMatchSnapshot(); - }); + expect(baseElement).toMatchSnapshot(); + } + ); it('renders selectedOptions as pills', () => { const { getAllByTestSubject } = render( From eb091bdca750c57f4f11408f831d334ee6f102fc Mon Sep 17 00:00:00 2001 From: Cee Chen <549407+cee-chen@users.noreply.github.com> Date: Fri, 6 Oct 2023 11:32:46 -0700 Subject: [PATCH 15/38] [Services] Remove unused key services (#7256) --- .../context_menu/context_menu_panel.tsx | 12 +++---- src/components/popover/popover.tsx | 4 +-- .../accessibility/accessible_click_keys.ts | 15 -------- .../accessibility/cascading_menu_keys.ts | 36 ------------------- src/services/accessibility/combo_box_keys.ts | 26 -------------- src/services/accessibility/index.ts | 3 -- src/services/index.ts | 8 +---- upcoming_changelogs/7256.md | 3 ++ 8 files changed, 12 insertions(+), 95 deletions(-) delete mode 100644 src/services/accessibility/accessible_click_keys.ts delete mode 100644 src/services/accessibility/cascading_menu_keys.ts delete mode 100644 src/services/accessibility/combo_box_keys.ts create mode 100644 upcoming_changelogs/7256.md diff --git a/src/components/context_menu/context_menu_panel.tsx b/src/components/context_menu/context_menu_panel.tsx index 8cacaff10b1..1e6602b2c2c 100644 --- a/src/components/context_menu/context_menu_panel.tsx +++ b/src/components/context_menu/context_menu_panel.tsx @@ -19,7 +19,7 @@ import { tabbable, FocusableElement } from 'tabbable'; import { CommonProps, NoArgCallback, keysOf } from '../common'; import { EuiIcon } from '../icon'; import { EuiResizeObserver } from '../observer/resize_observer'; -import { cascadingMenuKeys } from '../../services'; +import { keys } from '../../services'; import { EuiContextMenuItem, EuiContextMenuItemProps, @@ -164,7 +164,7 @@ export class EuiContextMenuPanel extends Component { document.activeElement === this.backButton || document.activeElement === this.panel) ) { - if (event.key === cascadingMenuKeys.ARROW_LEFT) { + if (event.key === keys.ARROW_LEFT) { if (showPreviousPanel) { event.preventDefault(); event.stopPropagation(); @@ -179,7 +179,7 @@ export class EuiContextMenuPanel extends Component { if (items?.length) { switch (event.key) { - case cascadingMenuKeys.TAB: + case keys.TAB: requestAnimationFrame(() => { // NOTE: document.activeElement is stale if not wrapped in requestAnimationFrame const focusedItemIndex = this.state.menuItems.indexOf( @@ -197,7 +197,7 @@ export class EuiContextMenuPanel extends Component { }); break; - case cascadingMenuKeys.ARROW_UP: + case keys.ARROW_UP: event.preventDefault(); this.focusMenuItem('up'); @@ -206,7 +206,7 @@ export class EuiContextMenuPanel extends Component { } break; - case cascadingMenuKeys.ARROW_DOWN: + case keys.ARROW_DOWN: event.preventDefault(); this.focusMenuItem('down'); @@ -215,7 +215,7 @@ export class EuiContextMenuPanel extends Component { } break; - case cascadingMenuKeys.ARROW_RIGHT: + case keys.ARROW_RIGHT: if (this.props.showNextPanel) { event.preventDefault(); this.props.showNextPanel( diff --git a/src/components/popover/popover.tsx b/src/components/popover/popover.tsx index 0c8b797f79b..d8366f4a6e5 100644 --- a/src/components/popover/popover.tsx +++ b/src/components/popover/popover.tsx @@ -23,7 +23,7 @@ import { CommonProps, NoArgCallback } from '../common'; import { FocusTarget, EuiFocusTrap, EuiFocusTrapProps } from '../focus_trap'; import { - cascadingMenuKeys, + keys, getTransitionTimings, getWaitDuration, performOnFrame, @@ -383,7 +383,7 @@ export class EuiPopover extends Component { }; onKeyDown = (event: KeyboardEvent) => { - if (event.key === cascadingMenuKeys.ESCAPE) { + if (event.key === keys.ESCAPE) { this.onEscapeKey(event as unknown as Event); } }; diff --git a/src/services/accessibility/accessible_click_keys.ts b/src/services/accessibility/accessible_click_keys.ts deleted file mode 100644 index 77b7864e0a1..00000000000 --- a/src/services/accessibility/accessible_click_keys.ts +++ /dev/null @@ -1,15 +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 { ENTER, SPACE } from '../keys'; - -// These keys are used to execute click actions on interactive elements like buttons and links. -export const accessibleClickKeys = { - [ENTER]: 'enter', - [SPACE]: 'space', -}; diff --git a/src/services/accessibility/cascading_menu_keys.ts b/src/services/accessibility/cascading_menu_keys.ts deleted file mode 100644 index 92009245223..00000000000 --- a/src/services/accessibility/cascading_menu_keys.ts +++ /dev/null @@ -1,36 +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. - */ - -/** - * These keys are used for navigating cascading menu UI components. - * - * ARROW_DOWN: Select the next item in the list. - * ARROW_LEFT: Show the previous menu. - * ARROW_RIGHT: Show the next menu for the selected item. - * ARROW_UP: Select the previous item in the list. - * ESC: Deselect the current selection and hide the list. - * TAB: Normal tabbing navigation is still supported. - */ - -import { - ARROW_DOWN, - ARROW_LEFT, - ARROW_RIGHT, - ARROW_UP, - ESCAPE, - TAB, -} from '../keys'; - -export const cascadingMenuKeys = { - ARROW_DOWN, - ARROW_LEFT, - ARROW_RIGHT, - ARROW_UP, - ESCAPE, - TAB, -}; diff --git a/src/services/accessibility/combo_box_keys.ts b/src/services/accessibility/combo_box_keys.ts deleted file mode 100644 index f638a645c95..00000000000 --- a/src/services/accessibility/combo_box_keys.ts +++ /dev/null @@ -1,26 +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. - */ - -/** - * These keys are used for navigating combobox UI components. - * - * ARROW_UP: Select the previous item in the list. - * ARROW_DOWN: Select the next item in the list. - * ENTER / TAB: Complete input with the current selection. - * ESC: Deselect the current selection and hide the list. - */ - -import { ARROW_DOWN, ENTER, ESCAPE, TAB, ARROW_UP } from '../keys'; - -export const comboBoxKeys = { - ARROW_DOWN, - ARROW_UP, - ENTER, - ESCAPE, - TAB, -}; diff --git a/src/services/accessibility/index.ts b/src/services/accessibility/index.ts index 987a02d1a91..390682faaa9 100644 --- a/src/services/accessibility/index.ts +++ b/src/services/accessibility/index.ts @@ -6,7 +6,4 @@ * Side Public License, v 1. */ -export { accessibleClickKeys } from './accessible_click_keys'; -export { cascadingMenuKeys } from './cascading_menu_keys'; -export { comboBoxKeys } from './combo_box_keys'; export { htmlIdGenerator, useGeneratedHtmlId } from './html_id_generator'; diff --git a/src/services/index.ts b/src/services/index.ts index 99ee327b329..42594a27d86 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -8,13 +8,7 @@ // Export all keys under a `keys` named variable import * as keys from './keys'; -export { - accessibleClickKeys, - cascadingMenuKeys, - comboBoxKeys, - htmlIdGenerator, - useGeneratedHtmlId, -} from './accessibility'; +export { htmlIdGenerator, useGeneratedHtmlId } from './accessibility'; export { CENTER_ALIGNMENT, LEFT_ALIGNMENT, RIGHT_ALIGNMENT } from './alignment'; export type { HorizontalAlignment } from './alignment'; export { diff --git a/upcoming_changelogs/7256.md b/upcoming_changelogs/7256.md new file mode 100644 index 00000000000..3159521b9e4 --- /dev/null +++ b/upcoming_changelogs/7256.md @@ -0,0 +1,3 @@ +**Breaking changes** + +- Removed exported `accessibleClickKeys`, `comboBoxKeys`, and `cascadingMenuKeys` services. Use the generic `keys` service instead From dfe2da8208da0095aa9e41e234d0d1a7457a2c69 Mon Sep 17 00:00:00 2001 From: Cee Chen <549407+cee-chen@users.noreply.github.com> Date: Fri, 6 Oct 2023 12:43:32 -0700 Subject: [PATCH 16/38] [EuiFlyout] Memoize various internals to reduce rerenders (#7259) --- src/components/flyout/flyout.tsx | 96 ++++++++++++++++++++------------ upcoming_changelogs/7259.md | 3 + 2 files changed, 63 insertions(+), 36 deletions(-) create mode 100644 upcoming_changelogs/7259.md diff --git a/src/components/flyout/flyout.tsx b/src/components/flyout/flyout.tsx index 31f2b5ad76e..a6ac5deb1fb 100644 --- a/src/components/flyout/flyout.tsx +++ b/src/components/flyout/flyout.tsx @@ -9,6 +9,8 @@ import React, { useEffect, useRef, + useMemo, + useCallback, useState, forwardRef, ComponentPropsWithRef, @@ -187,7 +189,7 @@ export const EuiFlyout = forwardRef( outsideClickCloses, pushMinBreakpoint = 'l', pushAnimation = false, - focusTrapProps: _focusTrapProps = {}, + focusTrapProps: _focusTrapProps, includeFixedHeadersInFocusTrap = true, 'aria-describedby': _ariaDescribedBy, ...rest @@ -246,23 +248,31 @@ export const EuiFlyout = forwardRef( /** * ESC key closes flyout (always?) */ - const onKeyDown = (event: KeyboardEvent) => { - if (!isPushed && event.key === keys.ESCAPE) { - event.preventDefault(); - onClose(event); - } - }; + const onKeyDown = useCallback( + (event: KeyboardEvent) => { + if (!isPushed && event.key === keys.ESCAPE) { + event.preventDefault(); + onClose(event); + } + }, + [onClose, isPushed] + ); /** * Set inline styles */ - let newStyle = style; - if (typeof maxWidth !== 'boolean') { - newStyle = { ...newStyle, ...logicalStyle('max-width', maxWidth) }; - } - if (!isEuiFlyoutSizeNamed(size)) { - newStyle = { ...newStyle, ...logicalStyle('width', size) }; - } + const inlineStyles = useMemo(() => { + const widthStyle = + !isEuiFlyoutSizeNamed(size) && logicalStyle('width', size); + const maxWidthStyle = + typeof maxWidth !== 'boolean' && logicalStyle('max-width', maxWidth); + + return { + ...style, + ...widthStyle, + ...maxWidthStyle, + }; + }, [style, maxWidth, size]); const euiTheme = useEuiTheme(); const styles = euiFlyoutStyles(euiTheme); @@ -280,8 +290,9 @@ export const EuiFlyout = forwardRef( const classes = classnames('euiFlyout', className); - let closeButton; - if (onClose && !hideCloseButton) { + const closeButton = useMemo(() => { + if (hideCloseButton || !onClose) return null; + const closeButtonClasses = classnames( 'euiFlyout__closeButton', closeButtonProps?.className @@ -296,7 +307,7 @@ export const EuiFlyout = forwardRef( closeButtonProps?.css, ]; - closeButton = ( + return ( {(closeAriaLabel: string) => ( ); - } + }, [ + onClose, + hideCloseButton, + closeButtonPosition, + closeButtonProps, + side, + euiTheme, + ]); /* * If not disabled, automatically add fixed EuiHeaders as shards @@ -345,10 +363,13 @@ export const EuiFlyout = forwardRef( } }, [includeFixedHeadersInFocusTrap, resizeRef]); - const focusTrapProps: EuiFlyoutProps['focusTrapProps'] = { - ..._focusTrapProps, - shards: [...fixedHeaders, ...(_focusTrapProps.shards || [])], - }; + const focusTrapProps: EuiFlyoutProps['focusTrapProps'] = useMemo( + () => ({ + ..._focusTrapProps, + shards: [...fixedHeaders, ...(_focusTrapProps?.shards || [])], + }), + [fixedHeaders, _focusTrapProps] + ); /* * Provide meaningful screen reader instructions/details @@ -393,19 +414,22 @@ export const EuiFlyout = forwardRef( * or if `outsideClickCloses={true}` to close on clicks that target * (both mousedown and mouseup) the overlay mask. */ - const onClickOutside = (event: MouseEvent | TouchEvent) => { - // Do not close the flyout for any external click - if (outsideClickCloses === false) return undefined; - if (hasOverlayMask) { - // The overlay mask is present, so only clicks on the mask should close the flyout, regardless of outsideClickCloses - if (event.target === maskRef.current) return onClose(event); - } else { - // No overlay mask is present, so any outside clicks should close the flyout - if (outsideClickCloses === true) return onClose(event); - } - // Otherwise if ownFocus is false and outsideClickCloses is undefined, outside clicks should not close the flyout - return undefined; - }; + const onClickOutside = useCallback( + (event: MouseEvent | TouchEvent) => { + // Do not close the flyout for any external click + if (outsideClickCloses === false) return undefined; + if (hasOverlayMask) { + // The overlay mask is present, so only clicks on the mask should close the flyout, regardless of outsideClickCloses + if (event.target === maskRef.current) return onClose(event); + } else { + // No overlay mask is present, so any outside clicks should close the flyout + if (outsideClickCloses === true) return onClose(event); + } + // Otherwise if ownFocus is false and outsideClickCloses is undefined, outside clicks should not close the flyout + return undefined; + }, + [onClose, hasOverlayMask, outsideClickCloses] + ); let flyout = ( )} role={!isPushed ? 'dialog' : rest.role} diff --git a/upcoming_changelogs/7259.md b/upcoming_changelogs/7259.md new file mode 100644 index 00000000000..9beea48791b --- /dev/null +++ b/upcoming_changelogs/7259.md @@ -0,0 +1,3 @@ +**Bug fixes** + +- Fixed focus trap rerender issues in `EuiFlyout` with memoization From dfe59ce976c5f14b8f76261b4c1022029ce6c24c Mon Sep 17 00:00:00 2001 From: Cee Chen <549407+cee-chen@users.noreply.github.com> Date: Fri, 6 Oct 2023 15:33:11 -0700 Subject: [PATCH 17/38] [EuiBasicTable][EuiInMemoryTable] Add new `truncationText` line API (#7254) --- src-docs/src/views/tables/auto/auto.tsx | 14 +++-- src/components/basic_table/table_types.ts | 12 +++- .../table_row_cell.test.tsx.snap | 16 ++++- src/components/table/table_row_cell.test.tsx | 21 ++++++- src/components/table/table_row_cell.tsx | 63 ++++++++++++------- upcoming_changelogs/7254.md | 1 + 6 files changed, 92 insertions(+), 35 deletions(-) create mode 100644 upcoming_changelogs/7254.md diff --git a/src-docs/src/views/tables/auto/auto.tsx b/src-docs/src/views/tables/auto/auto.tsx index 0344bf816b0..80c861df198 100644 --- a/src-docs/src/views/tables/auto/auto.tsx +++ b/src-docs/src/views/tables/auto/auto.tsx @@ -83,11 +83,12 @@ const columns: Array> = [ { field: 'jobTitle', name: 'Job title', + truncateText: true, }, { field: 'address', name: 'Address', - truncateText: true, + truncateText: { lines: 2 }, }, ]; @@ -147,11 +148,12 @@ const alignButtons: EuiButtonGroupOptionProps[] = [ export default () => { const [tableLayout, setTableLayout] = useState('tableLayoutFixed'); - const [vAlign, setVAlign] = useState('columnVAlignTop'); + const [vAlign, setVAlign] = useState('columnVAlignMiddle'); const [align, setAlign] = useState('columnAlignLeft'); const onTableLayoutChange = (id: string, value: string) => { setTableLayout(id); + columns[4].width = value === 'custom' ? '100px' : undefined; columns[5].width = value === 'custom' ? '20%' : undefined; }; @@ -169,14 +171,16 @@ export default () => { switch (tableLayout) { case 'tableLayoutFixed': - callOutText = 'Address has truncateText set to true'; + callOutText = + 'Job title has truncateText set to true. Address is set to { lines: 2 }'; break; case 'tableLayoutAuto': callOutText = - 'Address has truncateText set to true which is not applied since tableLayout is set to auto'; + 'Job title will not wrap or truncate since tableLayout is set to auto. Address will truncate if necessary'; break; case 'tableLayoutCustom': - callOutText = 'Address has truncateText set to true and width set to 20%'; + callOutText = + 'Job title has a custom column width of 100px. Address has a custom column width of 20%'; break; } diff --git a/src/components/basic_table/table_types.ts b/src/components/basic_table/table_types.ts index 587515bbf58..0bd235f5d3c 100644 --- a/src/components/basic_table/table_types.ts +++ b/src/components/basic_table/table_types.ts @@ -12,7 +12,10 @@ import { Pagination } from './pagination_bar'; import { Action } from './action_types'; import { Primitive } from '../../services/sort/comparators'; import { CommonProps } from '../common'; -import { EuiTableRowCellMobileOptionsShape } from '../table/table_row_cell'; +import { + EuiTableRowCellProps, + EuiTableRowCellMobileOptionsShape, +} from '../table/table_row_cell'; export type ItemId = string | number | ((item: T) => string); export type ItemIdResolved = string | number; @@ -68,9 +71,12 @@ export interface EuiTableFieldDataColumnType */ align?: HorizontalAlignment; /** - * Indicates whether this column should truncate its content when it doesn't fit + * Indicates whether this column should truncate overflowing text content. + * - Set to `true` to enable single-line truncation. + * - To enable multi-line truncation, use a configuration object with `lines` + * set to a number of lines to truncate to. */ - truncateText?: boolean; + truncateText?: EuiTableRowCellProps['truncateText']; mobileOptions?: Omit & { render?: (item: T) => ReactNode; }; diff --git a/src/components/table/__snapshots__/table_row_cell.test.tsx.snap b/src/components/table/__snapshots__/table_row_cell.test.tsx.snap index 5617a87cf89..794bc452819 100644 --- a/src/components/table/__snapshots__/table_row_cell.test.tsx.snap +++ b/src/components/table/__snapshots__/table_row_cell.test.tsx.snap @@ -112,7 +112,21 @@ exports[`truncateText defaults to false 1`] = ` `; -exports[`truncateText is rendered when specified 1`] = ` +exports[`truncateText renders lines configuration 1`] = ` + +
+ +
+ +`; + +exports[`truncateText renders true 1`] = ` diff --git a/src/components/table/table_row_cell.test.tsx b/src/components/table/table_row_cell.test.tsx index 105149bd5f6..1a3ef10ba43 100644 --- a/src/components/table/table_row_cell.test.tsx +++ b/src/components/table/table_row_cell.test.tsx @@ -78,15 +78,32 @@ describe('textOnly', () => { }); describe('truncateText', () => { - test('defaults to false', () => { + it('defaults to false', () => { const { container } = render(); expect(container.firstChild).toMatchSnapshot(); }); - test('is rendered when specified', () => { + it('renders true', () => { const { container } = render(); + expect( + container.querySelector('.euiTableCellContent--truncateText') + ).toBeInTheDocument(); + expect(container.firstChild).toMatchSnapshot(); + }); + + test('renders lines configuration', () => { + const { container } = render( + + ); + + expect( + container.querySelector('.euiTableCellContent--truncateText') + ).not.toBeInTheDocument(); + expect(container.querySelector('.euiTableCellContent__text')).toHaveClass( + 'euiTextBlockTruncate' + ); expect(container.firstChild).toMatchSnapshot(); }); }); diff --git a/src/components/table/table_row_cell.tsx b/src/components/table/table_row_cell.tsx index 748efd9c1e0..546fbd93d81 100644 --- a/src/components/table/table_row_cell.tsx +++ b/src/components/table/table_row_cell.tsx @@ -8,15 +8,15 @@ import React, { CSSProperties, - Fragment, FunctionComponent, ReactElement, ReactNode, TdHTMLAttributes, + useCallback, } from 'react'; import classNames from 'classnames'; -import { CommonProps } from '../common'; +import { CommonProps } from '../common'; import { HorizontalAlignment, LEFT_ALIGNMENT, @@ -24,6 +24,8 @@ import { CENTER_ALIGNMENT, useIsWithinBreakpoints, } from '../../services'; +import { isObject } from '../../services/predicate'; +import { EuiTextBlockTruncate } from '../text_truncate'; import { resolveWidthAsStyle } from './utils'; @@ -42,9 +44,12 @@ interface EuiTableRowCellSharedPropsShape { */ textOnly?: boolean; /** - * Don't allow line breaks within cells + * Indicates whether this column should truncate overflowing text content. + * - Set to `true` to enable single-line truncation. + * - To enable multi-line truncation, use a configuration object with `lines` + * set to a number of lines to truncate to. */ - truncateText?: boolean; + truncateText?: boolean | { lines: number }; width?: CSSProperties['width']; } @@ -138,7 +143,7 @@ export const EuiTableRowCell: FunctionComponent = ({ 'euiTableCellContent--alignRight': align === RIGHT_ALIGNMENT, 'euiTableCellContent--alignCenter': align === CENTER_ALIGNMENT, 'euiTableCellContent--showOnHover': showOnHover, - 'euiTableCellContent--truncateText': truncateText, + 'euiTableCellContent--truncateText': truncateText === true, // We're doing this rigamarole instead of creating `euiTableCellContent--textOnly` for BWC // purposes for the time-being. 'euiTableCellContent--overflowingContent': textOnly !== true, @@ -171,23 +176,33 @@ export const EuiTableRowCell: FunctionComponent = ({ const styleObj = resolveWidthAsStyle(style, widthValue); - function modifyChildren(children: ReactNode) { - let modifiedChildren = children; - - if (textOnly === true) { - modifiedChildren = {children}; - } else if (React.isValidElement(children)) { - modifiedChildren = React.Children.map( - children, - (child: ReactElement) => - React.cloneElement(child, { - className: classNames(child.props.className, childClasses), - }) - ); - } - - return modifiedChildren; - } + const modifyChildren = useCallback( + (children: ReactNode) => { + let modifiedChildren = children; + + if (textOnly === true) { + modifiedChildren = {children}; + } else if (React.isValidElement(children)) { + modifiedChildren = React.Children.map( + children, + (child: ReactElement) => + React.cloneElement(child, { + className: classNames(child.props.className, childClasses), + }) + ); + } + if (isObject(truncateText) && truncateText.lines) { + modifiedChildren = ( + + {modifiedChildren} + + ); + } + + return modifiedChildren; + }, + [childClasses, textOnly, truncateText] + ); const childrenNode = modifyChildren(children); @@ -223,14 +238,14 @@ export const EuiTableRowCell: FunctionComponent = ({ {/* Content depending on mobile render existing */} {mobileOptions.render ? ( - + <>
{modifyChildren(mobileOptions.render)}
{childrenNode}
-
+ ) : (
{childrenNode}
)} diff --git a/upcoming_changelogs/7254.md b/upcoming_changelogs/7254.md new file mode 100644 index 00000000000..f381cbf3119 --- /dev/null +++ b/upcoming_changelogs/7254.md @@ -0,0 +1 @@ +- Updated `EuiBasicTable` and `EuiInMemoryTable` to support multi-line truncation. This can be set via `truncateText.lines` in the `columns` prop. From 6c71d6a3bbfead7cfdcd6e58de014fe731b8bd83 Mon Sep 17 00:00:00 2001 From: Cee Chen <549407+cee-chen@users.noreply.github.com> Date: Mon, 9 Oct 2023 08:38:39 -0700 Subject: [PATCH 18/38] [EuiContextMenu] Fix animation flash when navigating between panels (#7268) --- src/components/context_menu/_context_menu_panel.scss | 4 ++++ upcoming_changelogs/7268.md | 3 +++ 2 files changed, 7 insertions(+) create mode 100644 upcoming_changelogs/7268.md diff --git a/src/components/context_menu/_context_menu_panel.scss b/src/components/context_menu/_context_menu_panel.scss index cefb1938b48..7f21ff1e355 100644 --- a/src/components/context_menu/_context_menu_panel.scss +++ b/src/components/context_menu/_context_menu_panel.scss @@ -10,21 +10,25 @@ &.euiContextMenuPanel-txInLeft { pointer-events: none; animation: euiContextMenuPanelTxInLeft $euiAnimSpeedNormal $euiAnimSlightResistance; + animation-fill-mode: forwards; } &.euiContextMenuPanel-txOutLeft { pointer-events: none; animation: euiContextMenuPanelTxOutLeft $euiAnimSpeedNormal $euiAnimSlightResistance; + animation-fill-mode: forwards; } &.euiContextMenuPanel-txInRight { pointer-events: none; animation: euiContextMenuPanelTxInRight $euiAnimSpeedNormal $euiAnimSlightResistance; + animation-fill-mode: forwards; } &.euiContextMenuPanel-txOutRight { pointer-events: none; animation: euiContextMenuPanelTxOutRight $euiAnimSpeedNormal $euiAnimSlightResistance; + animation-fill-mode: forwards; } } diff --git a/upcoming_changelogs/7268.md b/upcoming_changelogs/7268.md new file mode 100644 index 00000000000..2a4f5d5ff56 --- /dev/null +++ b/upcoming_changelogs/7268.md @@ -0,0 +1,3 @@ +**Bug fixes** + +- Fixed a visual bug with `EuiContextMenu`'s animation between panels From f47ef61459196395ac9766ac90f92ed70e1dfda3 Mon Sep 17 00:00:00 2001 From: Cee Chen <549407+cee-chen@users.noreply.github.com> Date: Mon, 9 Oct 2023 08:39:53 -0700 Subject: [PATCH 19/38] Remove deprecated `EuiColorStops` component (#7262) --- .../color_picker/color_picker_example.js | 28 - .../color_stop_thumb.test.tsx.snap | 137 --- .../__snapshots__/color_stops.test.tsx.snap | 834 ------------------ .../color_stops/color_stop_thumb.styles.ts | 94 -- .../color_stops/color_stop_thumb.test.tsx | 143 --- .../color_stops/color_stop_thumb.tsx | 418 --------- .../color_stops/color_stops.styles.ts | 114 --- .../color_stops/color_stops.test.tsx | 531 ----------- .../color_picker/color_stops/color_stops.tsx | 595 ------------- .../color_picker/color_stops/index.ts | 9 - .../color_picker/color_stops/utils.test.ts | 115 --- .../color_picker/color_stops/utils.ts | 129 --- src/components/color_picker/index.ts | 5 - upcoming_changelogs/7262.md | 3 + 14 files changed, 3 insertions(+), 3152 deletions(-) delete mode 100644 src/components/color_picker/color_stops/__snapshots__/color_stop_thumb.test.tsx.snap delete mode 100644 src/components/color_picker/color_stops/__snapshots__/color_stops.test.tsx.snap delete mode 100644 src/components/color_picker/color_stops/color_stop_thumb.styles.ts delete mode 100644 src/components/color_picker/color_stops/color_stop_thumb.test.tsx delete mode 100644 src/components/color_picker/color_stops/color_stop_thumb.tsx delete mode 100644 src/components/color_picker/color_stops/color_stops.styles.ts delete mode 100644 src/components/color_picker/color_stops/color_stops.test.tsx delete mode 100644 src/components/color_picker/color_stops/color_stops.tsx delete mode 100644 src/components/color_picker/color_stops/index.ts delete mode 100644 src/components/color_picker/color_stops/utils.test.ts delete mode 100644 src/components/color_picker/color_stops/utils.ts create mode 100644 upcoming_changelogs/7262.md diff --git a/src-docs/src/views/color_picker/color_picker_example.js b/src-docs/src/views/color_picker/color_picker_example.js index 2a997acec0c..b349fb8a644 100644 --- a/src-docs/src/views/color_picker/color_picker_example.js +++ b/src-docs/src/views/color_picker/color_picker_example.js @@ -9,8 +9,6 @@ import { EuiColorPaletteDisplay, EuiColorPalettePicker, EuiText, - EuiCallOut, - EuiLink, } from '../../../../src/components'; import { EuiColorPalettePickerPaletteTextProps, @@ -327,32 +325,6 @@ export const ColorPickerExample = { snippet: colorPaletteDisplaySnippet, demo: , }, - { - title: 'Color stops', - isDeprecated: true, - text: ( - -

- EuiColorStops is being deprecated due to low usage - and high maintenance requirements. -

-

- If necessary, we recommend{' '} - - copying the component to your application - - . The component will be permanently removed in October 2023. -

-
- ), - }, { title: 'Format selection', source: [ diff --git a/src/components/color_picker/color_stops/__snapshots__/color_stop_thumb.test.tsx.snap b/src/components/color_picker/color_stops/__snapshots__/color_stop_thumb.test.tsx.snap deleted file mode 100644 index 964ccb2355d..00000000000 --- a/src/components/color_picker/color_stops/__snapshots__/color_stop_thumb.test.tsx.snap +++ /dev/null @@ -1,137 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renders EuiColorStopThumb 1`] = ` -
-
-
-
-`; - -exports[`renders disabled EuiColorStopThumb 1`] = ` -
-
-
-
-`; - -exports[`renders picker-only EuiColorStopThumb 1`] = ` -
-
-
-
-`; - -exports[`renders readOnly EuiColorStopThumb 1`] = ` -
-
-
-
-`; - -exports[`renders swatch-only EuiColorStopThumb 1`] = ` -
-
-
-
-`; diff --git a/src/components/color_picker/color_stops/__snapshots__/color_stops.test.tsx.snap b/src/components/color_picker/color_stops/__snapshots__/color_stops.test.tsx.snap deleted file mode 100644 index f1b0ec7b47d..00000000000 --- a/src/components/color_picker/color_stops/__snapshots__/color_stops.test.tsx.snap +++ /dev/null @@ -1,834 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renders EuiColorStops 1`] = ` -
-

- Test: Color stop picker. Each stop consists of a number and corresponding color value. Use the Down and Up arrow keys to select individual stops. Press the Enter key to create a new stop. -

-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-`; - -exports[`renders compressed EuiColorStops 1`] = ` -
-

- Test: Color stop picker. Each stop consists of a number and corresponding color value. Use the Down and Up arrow keys to select individual stops. Press the Enter key to create a new stop. -

-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-`; - -exports[`renders disabled EuiColorStops 1`] = ` -
-

- Test: Disabled. Color stop picker. Each stop consists of a number and corresponding color value. Use the Down and Up arrow keys to select individual stops. Press the Enter key to create a new stop. -

-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-`; - -exports[`renders empty EuiColorStops 1`] = ` -
-

- Test: Color stop picker. Each stop consists of a number and corresponding color value. Use the Down and Up arrow keys to select individual stops. Press the Enter key to create a new stop. -

-
-
-
-
-
-
-
-
-
-`; - -exports[`renders fixed stop EuiColorStops 1`] = ` -
-

- Test: Color stop picker. Each stop consists of a number and corresponding color value. Use the Down and Up arrow keys to select individual stops. Press the Enter key to create a new stop. -

-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-`; - -exports[`renders free-range EuiColorStops 1`] = ` -
-

- Test: Color stop picker. Each stop consists of a number and corresponding color value. Use the Down and Up arrow keys to select individual stops. Press the Enter key to create a new stop. -

-
-
-
-
-
-
-
-
-
-`; - -exports[`renders fullWidth EuiColorStops 1`] = ` -
-

- Test: Color stop picker. Each stop consists of a number and corresponding color value. Use the Down and Up arrow keys to select individual stops. Press the Enter key to create a new stop. -

-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-`; - -exports[`renders max-only EuiColorStops 1`] = ` -
-

- Test: Color stop picker. Each stop consists of a number and corresponding color value. Use the Down and Up arrow keys to select individual stops. Press the Enter key to create a new stop. -

-
-
-
-
-
-
-
-
-
-`; - -exports[`renders min-only EuiColorStops 1`] = ` -
-

- Test: Color stop picker. Each stop consists of a number and corresponding color value. Use the Down and Up arrow keys to select individual stops. Press the Enter key to create a new stop. -

-
-
-
-
-
-
-
-
-
-`; - -exports[`renders readOnly EuiColorStops 1`] = ` -
-

- Test: Read-only. Color stop picker. Each stop consists of a number and corresponding color value. Use the Down and Up arrow keys to select individual stops. Press the Enter key to create a new stop. -

-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-`; diff --git a/src/components/color_picker/color_stops/color_stop_thumb.styles.ts b/src/components/color_picker/color_stops/color_stop_thumb.styles.ts deleted file mode 100644 index 1dea78f03d2..00000000000 --- a/src/components/color_picker/color_stops/color_stop_thumb.styles.ts +++ /dev/null @@ -1,94 +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 { css } from '@emotion/react'; - -import { UseEuiTheme } from '../../../services'; -import { mathWithUnits } from '../../../global_styling'; -import { - euiRangeVariables, - euiRangeThumbFocus, -} from '../../form/range/range.styles'; -import { euiColorPickerVariables } from '../color_picker.styles'; - -export const euiColorStopThumbStyles = (euiThemeContext: UseEuiTheme) => { - return { - // Base - euiColorStopThumb: css` - &:not(:disabled) { - inset-block-start: 0; - margin-block-start: 0; - pointer-events: auto; - cursor: grab; - - &:active { - cursor: grabbing; - } - } - `, - isPopoverOpen: css` - ${euiRangeThumbFocus(euiThemeContext)} - `, - }; -}; - -export const euiColorStopThumbPopoverStyles = ( - euiThemeContext: UseEuiTheme -) => { - const range = euiRangeVariables(euiThemeContext); - const { euiTheme } = euiThemeContext; - - return { - // Base - euiColorStopThumbPopover: css` - position: absolute; - inset-block-start: 50%; - inline-size: ${range.thumbWidth}; - block-size: ${range.thumbHeight}; - margin-block-start: ${mathWithUnits(range.thumbHeight, (x) => x * -0.5)}; - - .euiColorStopThumbPopover__anchor { - position: absolute; - inline-size: 100%; - block-size: 100%; - - /* Background color can potentially have opacity - Pseudo element placed below the thumb to prevent the track from showing through */ - &::before { - content: ''; - display: block; - position: absolute; - inset-inline-start: 0; - inset-block-start: 0; - block-size: ${range.thumbHeight}; - inline-size: ${range.thumbWidth}; - border-radius: ${range.thumbHeight}; - background: ${euiTheme.colors.emptyShade}; - } - } - `, - isLoadingPanel: css` - /* Overrides a stateful class on EuiPopover -> EuiPanel */ - visibility: hidden !important; /* stylelint-disable-line declaration-no-important */ - `, - hasFocus: css` - z-index: ${range.thumbZIndex}; - `, - }; -}; - -export const euiColorStopStyles = (euiThemeContext: UseEuiTheme) => { - const colorPicker = euiColorPickerVariables(euiThemeContext); - - return { - // Base - euiColorStop: css` - inline-size: ${colorPicker.width}; - `, - }; -}; diff --git a/src/components/color_picker/color_stops/color_stop_thumb.test.tsx b/src/components/color_picker/color_stops/color_stop_thumb.test.tsx deleted file mode 100644 index f32f2eba097..00000000000 --- a/src/components/color_picker/color_stops/color_stop_thumb.test.tsx +++ /dev/null @@ -1,143 +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 { EuiColorStopThumb } from './color_stop_thumb'; - -import { requiredProps } from '../../../test'; -import { shouldRenderCustomStyles } from '../../../test/internal'; -import { render } from '../../../test/rtl'; - -jest.mock('../../portal', () => ({ - EuiPortal: ({ children }: { children: any }) => children, -})); - -const onChange = jest.fn(); - -// Note: Unit/interaction tests can be found in ./color_stops.test - -shouldRenderCustomStyles( - {}} - closePopover={() => {}} - {...requiredProps} - />, - { childProps: ['valueInputProps'] } -); - -test('renders EuiColorStopThumb', () => { - const { container } = render( - {}} - closePopover={() => {}} - {...requiredProps} - /> - ); - expect(container.firstChild).toMatchSnapshot(); -}); - -test('renders swatch-only EuiColorStopThumb', () => { - const { container } = render( - {}} - closePopover={() => {}} - {...requiredProps} - /> - ); - expect(container.firstChild).toMatchSnapshot(); -}); - -test('renders picker-only EuiColorStopThumb', () => { - const { container } = render( - {}} - closePopover={() => {}} - {...requiredProps} - /> - ); - expect(container.firstChild).toMatchSnapshot(); -}); - -test('renders disabled EuiColorStopThumb', () => { - const { container } = render( - {}} - closePopover={() => {}} - {...requiredProps} - /> - ); - expect(container.firstChild).toMatchSnapshot(); -}); - -test('renders readOnly EuiColorStopThumb', () => { - const { container } = render( - {}} - closePopover={() => {}} - {...requiredProps} - /> - ); - expect(container.firstChild).toMatchSnapshot(); -}); diff --git a/src/components/color_picker/color_stops/color_stop_thumb.tsx b/src/components/color_picker/color_stops/color_stop_thumb.tsx deleted file mode 100644 index 271b0ab2aaa..00000000000 --- a/src/components/color_picker/color_stops/color_stop_thumb.tsx +++ /dev/null @@ -1,418 +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, { - FunctionComponent, - CSSProperties, - ReactChild, - useEffect, - useMemo, - useRef, - useState, -} from 'react'; -import classNames from 'classnames'; - -import { CommonProps } from '../../common'; -import { - getPositionFromStop, - getStopFromMouseLocation, - isColorInvalid, - isStopInvalid, -} from './utils'; -import { getChromaColor } from '../utils'; -import { keys, useMouseMove, useEuiTheme } from '../../../services'; - -import { EuiButtonIcon } from '../../button'; -import { EuiColorPicker, EuiColorPickerProps } from '../color_picker'; -import { EuiFlexGroup, EuiFlexItem } from '../../flex'; -import { EuiFieldNumber, EuiFieldNumberProps, EuiFormRow } from '../../form'; -import { EuiI18n } from '../../i18n'; -import { EuiPopover } from '../../popover'; -import { EuiScreenReaderOnly } from '../../accessibility'; -import { EuiSpacer } from '../../spacer'; -import { EuiRangeThumb } from '../../form/range/range_thumb'; - -import { - euiColorStopThumbStyles, - euiColorStopThumbPopoverStyles, - euiColorStopStyles, -} from './color_stop_thumb.styles'; - -export interface ColorStop { - stop: number; - color: string; -} - -interface EuiColorStopThumbProps extends CommonProps, ColorStop { - className?: string; - onChange: (colorStop: ColorStop) => void; - onFocus?: () => void; - onRemove?: () => void; - globalMin: number; - globalMax: number; - localMin: number; - localMax: number; - min?: number; - max?: number; - isRangeMin?: boolean; - isRangeMax?: boolean; - parentRef?: HTMLDivElement | null; - colorPickerMode: EuiColorPickerProps['mode']; - colorPickerShowAlpha?: EuiColorPickerProps['showAlpha']; - colorPickerSwatches?: EuiColorPickerProps['swatches']; - disabled?: boolean; - readOnly?: boolean; - isPopoverOpen: boolean; - openPopover: () => void; - closePopover: () => void; - 'data-index'?: string; - 'aria-valuetext'?: string; - style?: CSSProperties; - valueInputProps?: Partial; -} - -export const EuiColorStopThumb: FunctionComponent = ({ - className, - stop, - color, - onChange, - onFocus, - onRemove, - globalMin, - globalMax, - localMin, - localMax, - min, - max, - isRangeMin = false, - isRangeMax = false, - parentRef, - colorPickerMode, - colorPickerShowAlpha, - colorPickerSwatches, - disabled, - readOnly, - isPopoverOpen, - openPopover, - closePopover, - 'data-index': dataIndex, - 'aria-valuetext': ariaValueText, - style, - valueInputProps = {}, - ...rest -}) => { - const background = useMemo(() => { - const chromaColor = getChromaColor(color, colorPickerShowAlpha); - return chromaColor ? chromaColor.css() : undefined; - }, [color, colorPickerShowAlpha]); - const [hasFocus, setHasFocus] = useState(isPopoverOpen); - const [colorIsInvalid, setColorIsInvalid] = useState( - isColorInvalid(color, colorPickerShowAlpha) - ); - const [stopIsInvalid, setStopIsInvalid] = useState(isStopInvalid(stop)); - const [numberInputRef, setNumberInputRef] = useState( - null - ); - const popoverRef = useRef(null); - - useEffect(() => { - if (isPopoverOpen && popoverRef && popoverRef.current) { - popoverRef.current.positionPopoverFixed(); - } - }, [isPopoverOpen, stop]); - - const getStopFromMouseLocationFn = (location: { x: number; y: number }) => { - // Guard against `null` ref in usage - return getStopFromMouseLocation(location, parentRef!, globalMin, globalMax); - }; - - const getPositionFromStopFn = (stop: ColorStop['stop']) => { - // Guard against `null` ref in usage - return getPositionFromStop(stop, parentRef!, globalMin, globalMax); - }; - - const handleOnRemove = () => { - if (onRemove) { - closePopover(); - onRemove(); - } - }; - - const handleFocus = () => { - setHasFocus(true); - if (onFocus) { - onFocus(); - } - }; - - const setHasFocusTrue = () => setHasFocus(true); - const setHasFocusFalse = () => setHasFocus(false); - - const handleColorChange = (value: ColorStop['color']) => { - setColorIsInvalid(isColorInvalid(value, colorPickerShowAlpha)); - onChange({ stop, color: value }); - }; - - const handleStopChange = (value: ColorStop['stop']) => { - const willBeInvalid = value > localMax || value < localMin; - - if (willBeInvalid) { - if (value > localMax) { - value = localMax; - } - if (value < localMin) { - value = localMin; - } - } - setStopIsInvalid(isStopInvalid(value)); - onChange({ stop: value, color }); - }; - - const handleStopInputChange = (e: React.ChangeEvent) => { - let value = parseFloat(e.target.value); - - const willBeInvalid = value > globalMax || value < globalMin; - - if (willBeInvalid) { - if (value > globalMax && max != null) { - value = globalMax; - } - if (value < globalMin && min != null) { - value = globalMin; - } - } - - setStopIsInvalid(isStopInvalid(value)); - onChange({ stop: value, color }); - }; - - const handlePointerChange = ( - location: { x: number; y: number }, - isFirstInteraction?: boolean - ) => { - if (isFirstInteraction) return; // Prevents change on the initial MouseDown event - if (parentRef == null) { - return; - } - const newStop = getStopFromMouseLocationFn(location); - handleStopChange(newStop); - }; - - const handleKeyDown = (event: React.KeyboardEvent) => { - switch (event.key) { - case keys.ENTER: - event.preventDefault(); - openPopover(); - break; - - case keys.ARROW_LEFT: - event.preventDefault(); - if (readOnly) return; - handleStopChange(stop - 1); - break; - - case keys.ARROW_RIGHT: - event.preventDefault(); - if (readOnly) return; - handleStopChange(stop + 1); - break; - } - }; - - const [handleMouseDown, handleInteraction] = - useMouseMove(handlePointerChange); - - const handleOnMouseDown = (e: React.MouseEvent) => { - if (!readOnly) { - handleMouseDown(e); - } - openPopover(); - }; - - const handleTouchInteraction = (e: React.TouchEvent) => { - if (!readOnly) { - handleInteraction(e); - } - }; - - const handleTouchStart = (e: React.TouchEvent) => { - handleTouchInteraction(e); - if (!isPopoverOpen) { - openPopover(); - } - }; - - const euiTheme = useEuiTheme(); - - const popoverStyles = euiColorStopThumbPopoverStyles(euiTheme); - const cssPopoverStyles = [ - popoverStyles.euiColorStopThumbPopover, - (hasFocus || isPopoverOpen) && popoverStyles.hasFocus, - ]; - - const thumbStyles = euiColorStopThumbStyles(euiTheme); - const cssThumbStyles = [ - thumbStyles.euiColorStopThumb, - isPopoverOpen && thumbStyles.isPopoverOpen, - ]; - - const colorStopStyles = euiColorStopStyles(euiTheme); - const cssColorStopStyles = colorStopStyles.euiColorStop; - - const classes = classNames('euiColorStopPopover', className); - - return ( - - {([buttonAriaLabel, buttonTitle]: ReactChild[]) => { - const ariaLabel = buttonAriaLabel as string; - const title = buttonTitle as string; - return ( - - ); - }} - - } - > -
- -

- -

-
- - - - {([stopLabel, stopErrorMessage]: React.ReactChild[]) => ( - - - - )} - - - {!readOnly && ( - - - - {(removeLabel: string) => ( - - )} - - - - )} - - {!readOnly && } - -
-
- ); -}; diff --git a/src/components/color_picker/color_stops/color_stops.styles.ts b/src/components/color_picker/color_stops/color_stops.styles.ts deleted file mode 100644 index abf000a06e0..00000000000 --- a/src/components/color_picker/color_stops/color_stops.styles.ts +++ /dev/null @@ -1,114 +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 { css } from '@emotion/react'; - -import { UseEuiTheme, darken, brighten, hexToRgb } from '../../../services'; -import { mathWithUnits, euiCanAnimate } from '../../../global_styling'; -import { euiCustomControl } from '../../form/form.styles'; - -import { - euiRangeThumbStyle, - euiRangeVariables, -} from '../../form/range/range.styles'; - -export const euiColorStopsStyles = (euiThemeContext: UseEuiTheme) => { - const range = euiRangeVariables(euiThemeContext); - const { euiTheme, colorMode } = euiThemeContext; - - const isDarkMode = colorMode === 'DARK'; - const stripeColor = isDarkMode - ? brighten(range.trackColor, 0.5) - : darken(range.trackColor, 0.5); - const stripesBackground = `repeating-linear-gradient( - -45deg, - ${range.trackColor}, - ${range.trackColor} 25%, - ${stripeColor} 25%, - ${stripeColor} 50%, - ${range.trackColor} 50% - )`; - - return { - // Base - euiColorStops: css``, - isEnabled: css` - /* Show focus ring on keyboard focus only and not mouse click/drag */ - &:focus { - outline: none; - } - - &:focus-visible { - .euiColorStops__track::after { - box-shadow: 0 0 0 1px - rgba(${hexToRgb(euiTheme.colors.emptyShade).join(', ')}, 0.8), - 0 0 0 3px ${range.focusColor}; - } - } - `, - isDisabled: css``, - isHoverDisabled: css``, - isReadOnly: css``, - isDragging: css` - cursor: grabbing; - `, - euiColorStops__track: css` - &::after { - background: ${stripesBackground}; - background-size: ${euiTheme.size.xs} ${euiTheme.size.xs}; /* Percentage stops and background-size are both needed for Safari to render the gradient at fullWidth correctly */ - } - `, - euiColorStops__addTarget: css` - ${euiCustomControl(euiThemeContext, { type: 'round' })} - ${euiRangeThumbStyle(euiThemeContext)} - position: absolute; - inset-block-start: 0; - block-size: ${range.thumbHeight}; - inline-size: ${range.thumbHeight}; - background-color: ${euiTheme.colors.lightestShade}; - pointer-events: none; - opacity: 0; - border: ${euiTheme.border.width.thin} solid ${euiTheme.colors.darkShade}; - box-shadow: none; - z-index: ${range.thumbZIndex}; - - ${euiCanAnimate} { - transition: opacity ${euiTheme.animation.fast} ease-in; - } - `, - }; -}; - -export const euiColorStopsAddContainerStyles = ( - euiThemeContext: UseEuiTheme -) => { - const range = euiRangeVariables(euiThemeContext); - - return { - euiColorStopsAddContainer: css` - display: block; - position: absolute; - inset-inline-start: 0; - inset-inline-end: 0; - inset-block-start: 50%; - block-size: ${range.thumbHeight}; - margin-block-start: ${mathWithUnits(range.thumbHeight, (x) => x * -0.5)}; - z-index: ${range.thumbZIndex}; - `, - isEnabled: css` - &:hover { - cursor: pointer; - - .euiColorStops__addTarget { - opacity: 0.7; - } - } - `, - isDisabled: css``, - }; -}; diff --git a/src/components/color_picker/color_stops/color_stops.test.tsx b/src/components/color_picker/color_stops/color_stops.test.tsx deleted file mode 100644 index 889d51199f6..00000000000 --- a/src/components/color_picker/color_stops/color_stops.test.tsx +++ /dev/null @@ -1,531 +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 } from 'enzyme'; -import { render } from '../../../test/rtl'; - -import { EuiColorStops } from './color_stops'; - -import { - VISUALIZATION_COLORS, - DEFAULT_VISUALIZATION_COLOR, - keys, -} from '../../../services'; -import { requiredProps, findTestSubject } from '../../../test'; -import { shouldRenderCustomStyles } from '../../../test/internal'; -import { EuiFieldNumber } from '../../form/field_number'; - -jest.mock('../../portal', () => ({ - EuiPortal: ({ children }: { children: any }) => children, -})); - -const onChange = jest.fn(); - -const colorStopsArray = [ - { stop: 0, color: '#FF0000' }, - { stop: 25, color: '#00FF00' }, - { stop: 35, color: '#0000FF' }, -]; - -// Note: A couple tests that would be nice, but can't be accomplished at the moment: -// - Tab to bypass thumbs (tabindex="-1" not respected) -// - Drag to reposition thumb (we can't get real page position info) - -shouldRenderCustomStyles( - -); - -test('renders EuiColorStops', () => { - const { container } = render( - - ); - expect(container.firstChild).toMatchSnapshot(); -}); - -test('renders free-range EuiColorStops', () => { - const { container } = render( - - ); - expect(container.firstChild).toMatchSnapshot(); -}); - -test('renders min-only EuiColorStops', () => { - const { container } = render( - - ); - expect(container.firstChild).toMatchSnapshot(); -}); - -test('renders max-only EuiColorStops', () => { - const { container } = render( - - ); - expect(container.firstChild).toMatchSnapshot(); -}); - -test('renders compressed EuiColorStops', () => { - const { container } = render( - - ); - expect(container.firstChild).toMatchSnapshot(); -}); - -test('renders readOnly EuiColorStops', () => { - const { container } = render( - - ); - expect(container.firstChild).toMatchSnapshot(); -}); - -test('renders fullWidth EuiColorStops', () => { - const { container } = render( - - ); - expect(container.firstChild).toMatchSnapshot(); -}); - -test('renders disabled EuiColorStops', () => { - const { container } = render( - - ); - expect(container.firstChild).toMatchSnapshot(); -}); - -test('renders fixed stop EuiColorStops', () => { - const { container } = render( - - ); - expect(container.firstChild).toMatchSnapshot(); -}); - -test('renders stepped stop EuiColorStops', () => { - const { getByTestSubject } = render( - - ); - - expect( - getByTestSubject('euiRangeHighlightProgress').getAttribute('style') - ).toEqual('margin-inline-start: 0%; inline-size: 100%;'); -}); - -test('renders empty EuiColorStops', () => { - const { container } = render( - - ); - expect(container.firstChild).toMatchSnapshot(); -}); - -test('popover color selector is shown when the thumb is clicked', () => { - const colorStops = mount( - - ); - - findTestSubject(colorStops, 'euiColorStopThumb') - .first() - .simulate('mousedown', { pageX: 0, pageY: 0 }) - .simulate('mouseup', { pageX: 0, pageY: 0 }); - const colorSelector = findTestSubject(colorStops, 'euiColorStopPopover'); - expect(colorSelector.length).toBe(1); -}); - -test('passes value input props to number input', () => { - const colorStops = mount( - - ); - - findTestSubject(colorStops, 'euiColorStopThumb') - .first() - .simulate('mousedown', { pageX: 0, pageY: 0 }) - .simulate('mouseup', { pageX: 0, pageY: 0 }); - const colorSelector = findTestSubject(colorStops, 'euiColorStopPopover'); - expect(colorSelector.find(EuiFieldNumber).prop('append')).toEqual('%'); -}); - -test('stop input updates stops', () => { - const colorStops = mount( - - ); - - findTestSubject(colorStops, 'euiColorStopThumb') - .first() - .simulate('mousedown', { pageX: 0, pageY: 0 }) - .simulate('mouseup', { pageX: 0, pageY: 0 }); - const event = { target: { value: '10' } }; - const inputs = colorStops.find('input[type="number"]'); - expect(inputs.length).toBe(1); - inputs.simulate('change', event); - expect(onChange).toBeCalled(); - expect(onChange).toBeCalledWith( - [ - { color: '#FF0000', stop: 10 }, - { color: '#00FF00', stop: 25 }, - { color: '#0000FF', stop: 35 }, - ], - false - ); -}); - -test('stop input updates stops with error prevention (reset to bounds)', () => { - const colorStops = mount( - - ); - - findTestSubject(colorStops, 'euiColorStopThumb') - .first() - .simulate('mousedown', { pageX: 0, pageY: 0 }) - .simulate('mouseup', { pageX: 0, pageY: 0 }); - const event = { target: { value: '1000' } }; - const inputs = colorStops.find('input[type="number"]'); - inputs.simulate('change', event); - expect(onChange).toBeCalled(); - expect(onChange).toBeCalledWith( - [ - { color: '#FF0000', stop: 100 }, - { color: '#00FF00', stop: 25 }, - { color: '#0000FF', stop: 35 }, - ], - false - ); -}); - -test('hex input updates stops', () => { - const colorStops = mount( - - ); - - findTestSubject(colorStops, 'euiColorStopThumb') - .first() - .simulate('mousedown', { pageX: 0, pageY: 0 }) - .simulate('mouseup', { pageX: 0, pageY: 0 }); - const event = { target: { value: '#FFFFFF' } }; - const inputs = colorStops.find('input[type="text"]'); - expect(inputs.length).toBe(1); - inputs.simulate('change', event); - expect(onChange).toBeCalled(); - expect(onChange).toBeCalledWith( - [ - { color: '#FFFFFF', stop: 0 }, - { color: '#00FF00', stop: 25 }, - { color: '#0000FF', stop: 35 }, - ], - false - ); -}); - -test('hex input updates stops with error', () => { - const colorStops = mount( - - ); - - findTestSubject(colorStops, 'euiColorStopThumb') - .first() - .simulate('mousedown', { pageX: 0, pageY: 0 }) - .simulate('mouseup', { pageX: 0, pageY: 0 }); - const event = { target: { value: '#FFFFF' } }; - const inputs = colorStops.find('input[type="text"]'); - inputs.simulate('change', event); - expect(onChange).toBeCalled(); - expect(onChange).toBeCalledWith( - [ - { color: '#FFFFF', stop: 0 }, - { color: '#00FF00', stop: 25 }, - { color: '#0000FF', stop: 35 }, - ], - true // isInvalid - ); -}); - -test('picker updates stops', () => { - const colorStops = mount( - - ); - - findTestSubject(colorStops, 'euiColorStopThumb') - .first() - .simulate('mousedown', { pageX: 0, pageY: 0 }) - .simulate('mouseup', { pageX: 0, pageY: 0 }); - const swatches = colorStops.find('button.euiColorPicker__swatchSelect'); - expect(swatches.length).toBe(VISUALIZATION_COLORS.length); - swatches.first().simulate('click'); - expect(onChange).toBeCalled(); - expect(onChange).toBeCalledWith( - [ - { color: VISUALIZATION_COLORS[0], stop: 0 }, - { color: '#00FF00', stop: 25 }, - { color: '#0000FF', stop: 35 }, - ], - false - ); -}); - -test('thumb focus changes', () => { - const colorStops = mount( - - ); - - const wrapper = findTestSubject(colorStops, 'euiColorStops'); - const thumbs = findTestSubject(colorStops, 'euiColorStopThumb'); - wrapper.simulate('focus'); - wrapper.simulate('keydown', { - key: keys.ARROW_DOWN, - }); - expect(thumbs.first().getDOMNode()).toEqual(document.activeElement); - thumbs.first().simulate('keydown', { - key: keys.ARROW_DOWN, - }); - expect(thumbs.at(1).getDOMNode()).toEqual(document.activeElement); -}); - -test('thumb direction movement', () => { - const colorStops = mount( - - ); - - const wrapper = findTestSubject(colorStops, 'euiColorStops'); - const thumbs = findTestSubject(colorStops, 'euiColorStopThumb'); - wrapper.simulate('focus'); - wrapper.simulate('keydown', { - key: keys.ARROW_DOWN, - }); - expect(thumbs.first().getDOMNode()).toEqual(document.activeElement); - thumbs.first().simulate('keydown', { - key: keys.ARROW_RIGHT, - }); - expect(onChange).toBeCalledWith( - [ - { color: '#FF0000', stop: 1 }, - { color: '#00FF00', stop: 25 }, - { color: '#0000FF', stop: 35 }, - ], - false - ); - thumbs.first().simulate('keydown', { - key: keys.ARROW_LEFT, - }); - expect(onChange).toBeCalledWith( - [ - { color: '#FF0000', stop: 0 }, - { color: '#00FF00', stop: 25 }, - { color: '#0000FF', stop: 35 }, - ], - false - ); -}); - -test('add new thumb via keyboard', () => { - const colorStops = mount( - - ); - - const wrapper = findTestSubject(colorStops, 'euiColorStops'); - wrapper.simulate('focus'); - wrapper.simulate('keydown', { - key: keys.ENTER, - }); - expect(onChange).toBeCalled(); - expect(onChange).toBeCalledWith( - [ - { color: '#FF0000', stop: 0 }, - { color: '#00FF00', stop: 25 }, - { color: '#0000FF', stop: 35 }, - { color: DEFAULT_VISUALIZATION_COLOR, stop: 45 }, - ], - false - ); -}); - -test('add new thumb via click', () => { - const colorStops = mount( - - ); - - const wrapper = findTestSubject(colorStops, 'euiColorStopsAdd'); - wrapper.simulate('click', { pageX: 45, pageY: 0 }); - expect(onChange).toBeCalled(); - // This is a very odd expectation. - // But we can't get actual page positions in this environment (no getBoundingClientRect) - // So we'll expect the _correct_ color and _incorrect_ stop value (NaN), - // with the `isInvalid` arg _correctly_ true as a result. - expect(onChange).toBeCalledWith( - [ - { color: '#FF0000', stop: 0 }, - { color: '#00FF00', stop: 25 }, - { color: '#0000FF', stop: 35 }, - { color: DEFAULT_VISUALIZATION_COLOR, stop: NaN }, - ], - true // isInvalid - ); -}); diff --git a/src/components/color_picker/color_stops/color_stops.tsx b/src/components/color_picker/color_stops/color_stops.tsx deleted file mode 100644 index 75712428b08..00000000000 --- a/src/components/color_picker/color_stops/color_stops.tsx +++ /dev/null @@ -1,595 +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, { - FunctionComponent, - useCallback, - useEffect, - useMemo, - useState, -} from 'react'; -import classNames from 'classnames'; - -import { CommonProps } from '../../common'; -import { - keys, - DEFAULT_VISUALIZATION_COLOR, - getSteppedGradient, - useEuiTheme, -} from '../../../services'; -import { EuiColorStopThumb, ColorStop } from './color_stop_thumb'; -import { - addStop, - addDefinedStop, - getPositionFromStop, - getStopFromMouseLocation, - isInvalid, - removeStop, -} from './utils'; - -import { EuiColorPickerProps } from '../color_picker'; -import { getChromaColor } from '../utils'; -import { EuiI18n } from '../../i18n'; -import { EuiScreenReaderOnly } from '../../accessibility'; -import { EuiRangeHighlight } from '../../form/range/range_highlight'; -import { EuiRangeTrack } from '../../form/range/range_track'; -import { EuiRangeWrapper } from '../../form/range/range_wrapper'; -import { EuiFieldNumberProps } from '../../form/field_number'; - -import { - euiColorStopsStyles, - euiColorStopsAddContainerStyles, -} from './color_stops.styles'; - -/** - * @deprecated - */ -export interface EuiColorStopsProps extends CommonProps { - addColor?: ColorStop['color']; - /** - * An array of #ColorStop. The stops must be numbers in an ordered range. - */ - colorStops: ColorStop[]; - onChange: (stops?: ColorStop[], isInvalid?: boolean) => void; - fullWidth?: boolean; - disabled?: boolean; - readOnly?: boolean; - invalid?: boolean; - compressed?: boolean; - className?: string; - max?: number; - min?: number; - label: string; - /** - * Specify the type of stops: - * `fixed`: individual color blocks. - * `gradient`: each color fades into the next. - * `stepped`: interpolation between colors with a fixed number of steps. - */ - stopType?: 'fixed' | 'gradient' | 'stepped'; - /** - * Only works when `stopType="stepped"` - */ - stepNumber?: number; - mode?: EuiColorPickerProps['mode']; - swatches?: EuiColorPickerProps['swatches']; - showAlpha?: EuiColorPickerProps['showAlpha']; - /** - * Props passed to the value input field in the color stop popover. - * Can be used to configure functionality like append or prepend. - */ - valueInputProps?: Partial< - Omit< - EuiFieldNumberProps, - | 'inputRef' - | 'compressed' - | 'readOnly' - | 'min' - | 'max' - | 'value' - | 'isInvalid' - | 'onChange' - > - >; -} - -// Because of how the thumbs are rendered in the popover, using ref results in an infinite loop. -// We'll instead use old fashioned namespaced DOM selectors to get references -const STOP_ATTR = 'euiColorStop_'; - -const DEFAULT_MIN = 0; -const DEFAULT_MAX = 100; - -function isTargetAThumb(target: HTMLElement | EventTarget) { - const element = target as HTMLElement; - const attr = element.getAttribute('data-index'); - return attr && attr.indexOf(STOP_ATTR) > -1; -} - -function sortStops(colorStops: ColorStop[]) { - return colorStops - .map((el, index) => { - return { - ...el, - id: index, - }; - }) - .sort((a, b) => a.stop - b.stop); -} - -function getValidStops(colorStops: ColorStop[]) { - return colorStops.map((el) => el.stop).filter((stop) => !isNaN(stop)); -} - -function getRangeMin(colorStops: ColorStop[], min?: number) { - const rangeMin = min || DEFAULT_MIN; - const stops = getValidStops(colorStops); - const first = Math.min(...stops); // https://johnresig.com/blog/fast-javascript-maxmin/ - - if (first < rangeMin) { - if (stops.length === 1) { - return first - DEFAULT_MIN; - } else if (stops.length >= 2) { - return first; - } - } - return DEFAULT_MIN; -} -function getRangeMax(colorStops: ColorStop[], max?: number) { - const rangeMax = max || DEFAULT_MAX; - const stops = getValidStops(colorStops); - const last = Math.max(...stops); // https://johnresig.com/blog/fast-javascript-maxmin/ - - if (last > rangeMax) { - if (stops.length === 1) { - return last + DEFAULT_MAX; - } else if (stops.length >= 2) { - return last; - } - } - return DEFAULT_MAX; -} - -/** - * @deprecated - EuiColorStops is scheduled for deprecation due to low internal usage and high - * maintenance requirements. If necessary, we recommend copying this component into your own application. - * - * The component will be permanently removed in October 2023. - */ -export const EuiColorStops: FunctionComponent = ({ - addColor = DEFAULT_VISUALIZATION_COLOR, - max, - min, - mode = 'default', - colorStops, - onChange, - disabled, - readOnly, - compressed, - fullWidth, - className, - label, - stopType = 'gradient', - stepNumber = 10, - swatches, - showAlpha = false, - valueInputProps, - ...rest -}) => { - const sortedStops = useMemo(() => sortStops(colorStops), [colorStops]); - const rangeMax: number = useMemo(() => { - const result = max != null ? max : getRangeMax(colorStops, max); - const width = max != null ? 0 : Math.round(result * 0.05); - return !isNaN(result) ? result + width : DEFAULT_MAX; - }, [colorStops, max]); - const rangeMin: number = useMemo(() => { - const result = min != null ? min : getRangeMin(colorStops, min); - const width = min != null ? 0 : Math.round(rangeMax * 0.05); - return !isNaN(result) ? result - width : DEFAULT_MIN; - }, [colorStops, min, rangeMax]); - const [hasFocus, setHasFocus] = useState(false); - const [focusedStopIndex, setFocusedStopIndex] = useState(null); - const [openedStopId, setOpenedStopId] = useState(null); - const [wrapperRef, setWrapperRef] = useState(null); - const [addTargetPosition, setAddTargetPosition] = useState(0); - const [isHoverDisabled, setIsHoverDisabled] = useState(false); - const [focusStopOnUpdate, setFocusStopOnUpdate] = useState( - null - ); - - const isNotInteractive = disabled || readOnly; - const isDragging = isHoverDisabled && !isNotInteractive; - const addContainerIsDisabled = isHoverDisabled || isNotInteractive; - - const classes = classNames('euiColorStops', className); - - const euiTheme = useEuiTheme(); - const styles = euiColorStopsStyles(euiTheme); - const cssPopoverStyles = [ - styles.euiColorStops, - !disabled ? styles.isEnabled : styles.isDisabled, - readOnly && styles.isReadOnly, - isDragging && styles.isDragging, - ]; - const cssTrackStyles = [styles.euiColorStops__track]; - const cssAddTargetStyles = [styles.euiColorStops__addTarget]; - - const addContainerStyles = euiColorStopsAddContainerStyles(euiTheme); - const cssAddContainerStyles = [ - addContainerStyles.euiColorStopsAddContainer, - !addContainerIsDisabled - ? addContainerStyles.isEnabled - : addContainerStyles.isDisabled, - ]; - - const getStopFromMouseLocationFn = (location: { x: number; y: number }) => { - // Guard against `null` ref in usage - return getStopFromMouseLocation( - location, - wrapperRef!, - min || rangeMin, - max || rangeMax - ); - }; - - const getPositionFromStopFn = (stop: ColorStop['stop']) => { - // Guard against `null` ref in usage - return getPositionFromStop( - stop, - wrapperRef!, - min || rangeMin, - max || rangeMax - ); - }; - - const handleOnChange = useCallback( - (colorStops: ColorStop[]) => { - onChange(colorStops, isInvalid(colorStops, showAlpha)); - }, - [onChange, showAlpha] - ); - - const onFocusStop = useCallback( - (index: number) => { - if (disabled || !wrapperRef) return; - const toFocus = wrapperRef.querySelector( - `[data-index=${STOP_ATTR}${index}]` - ); - if (toFocus) { - setHasFocus(false); - setFocusedStopIndex(index); - toFocus.focus(); - } - }, - [disabled, wrapperRef] - ); - - useEffect(() => { - if (focusStopOnUpdate !== null) { - const toFocusIndex = sortedStops - .map((el) => el.stop) - .indexOf(focusStopOnUpdate); - const toFocusId = toFocusIndex > -1 ? sortedStops[toFocusIndex].id : null; - onFocusStop(toFocusIndex); - setOpenedStopId(toFocusId); - setFocusStopOnUpdate(null); - } - }, [sortedStops, onFocusStop, setFocusStopOnUpdate, focusStopOnUpdate]); - - const onFocusWrapper = useCallback(() => { - setFocusedStopIndex(null); - if (wrapperRef) { - wrapperRef.focus(); - } - }, [wrapperRef]); - - const setWrapperHasFocus = (e: React.FocusEvent) => { - if (e.target === wrapperRef) { - setHasFocus(true); - } - }; - - const removeWrapperFocus = () => { - setHasFocus(false); - }; - - const onAdd = () => { - const stops = sortedStops.map(({ color, stop }) => { - return { - color, - stop, - }; - }); - const newColorStops = addStop(stops, addColor, max || rangeMax); - - setFocusStopOnUpdate(newColorStops[colorStops.length].stop); - handleOnChange(newColorStops); - }; - - const onRemove = useCallback( - (index: number) => { - const newColorStops = removeStop(colorStops, index); - - onFocusWrapper(); - handleOnChange(newColorStops); - }, - [colorStops, handleOnChange, onFocusWrapper] - ); - - const disableHover = () => { - if (disabled) return; - setIsHoverDisabled(true); - }; - - const enableHover = () => { - if (disabled) return; - setIsHoverDisabled(false); - }; - - const handleAddHover = (e: React.MouseEvent) => { - if (isNotInteractive || !wrapperRef) return; - const stop = getStopFromMouseLocationFn({ x: e.pageX, y: e.pageY }); - const position = getPositionFromStopFn(stop); - - setAddTargetPosition(position); - }; - - const handleAddClick = (e: React.MouseEvent) => { - if (isNotInteractive || isTargetAThumb(e.target) || !wrapperRef) return; - const newStop = getStopFromMouseLocationFn({ x: e.pageX, y: e.pageY }); - const newColorStops = addDefinedStop(colorStops, newStop, addColor); - setFocusStopOnUpdate(newStop); - handleOnChange(newColorStops); - }; - - const handleKeyDown = (event: React.KeyboardEvent) => { - if (disabled) return; - switch (event.key) { - case keys.ESCAPE: - onFocusWrapper(); - break; - - case keys.ENTER: - if (readOnly || !hasFocus) return; - onAdd(); - break; - - case keys.BACKSPACE: - if (readOnly || hasFocus || focusedStopIndex == null) return; - if (isTargetAThumb(event.target)) { - if ( - (min == null && focusedStopIndex === 0) || - (max == null && focusedStopIndex === sortedStops.length - 1) - ) { - return; - } - const index = sortedStops[focusedStopIndex].id; - onRemove(index); - } - break; - - case keys.ARROW_DOWN: - if (event.target === wrapperRef || isTargetAThumb(event.target)) { - event.preventDefault(); - if (focusedStopIndex == null) { - onFocusStop(0); - } else { - const next = - focusedStopIndex === sortedStops.length - 1 - ? focusedStopIndex - : focusedStopIndex + 1; - onFocusStop(next); - } - } - break; - - case keys.ARROW_UP: - if (event.target === wrapperRef || isTargetAThumb(event.target)) { - event.preventDefault(); - if (focusedStopIndex == null) { - onFocusStop(0); - } else { - const next = - focusedStopIndex === 0 ? focusedStopIndex : focusedStopIndex - 1; - onFocusStop(next); - } - } - break; - } - }; - - const thumbs = useMemo(() => { - const handleStopChange = (stop: ColorStop, id: number) => { - const newColorStops = [...colorStops]; - newColorStops.splice(id, 1, stop); - handleOnChange(newColorStops); - }; - return sortedStops.map((colorStop, index) => ( - 1 ? () => onRemove(colorStop.id) : undefined - } - onChange={(stop) => handleStopChange(stop, colorStop.id)} - onFocus={() => setFocusedStopIndex(index)} - parentRef={wrapperRef} - colorPickerMode={mode} - colorPickerShowAlpha={showAlpha} - colorPickerSwatches={swatches} - disabled={disabled} - readOnly={readOnly} - aria-valuetext={`Stop: ${colorStop.stop}, Color: ${colorStop.color} (${ - index + 1 - } of ${colorStops.length})`} - isPopoverOpen={!isDragging && colorStop.id === openedStopId} - openPopover={() => { - setOpenedStopId(colorStop.id); - }} - closePopover={() => { - setOpenedStopId(null); - }} - valueInputProps={valueInputProps} - /> - )); - }, [ - colorStops, - disabled, - handleOnChange, - isDragging, - max, - min, - mode, - onRemove, - openedStopId, - rangeMax, - rangeMin, - readOnly, - showAlpha, - sortedStops, - swatches, - wrapperRef, - valueInputProps, - ]); - - const positions = wrapperRef - ? sortedStops.map(({ stop }) => getPositionFromStopFn(stop)) - : []; - const gradientStop = (colorStop: ColorStop, index: number) => { - const color = getChromaColor(colorStop.color, showAlpha); - const rgba = color ? color.css() : 'transparent'; - if (index === 0) { - return `transparent, transparent ${positions[index]}%, ${rgba} ${positions[index]}%`; - } - return `${rgba} ${positions[index]}%`; - }; - const fixedStop = (colorStop: ColorStop, index: number) => { - if (index === sortedStops.length - 1) { - return gradientStop(colorStop, index); - } else { - return `${gradientStop(colorStop, index)}, ${gradientStop( - colorStop, - index + 1 - )}`; - } - }; - - let gradient: string = ''; - - if (stopType === 'stepped' && positions.length > 0) { - const trailingPercentage = positions[0]; - const endingPercentage = positions[positions.length - 1]; - const steppedColors = getSteppedGradient(colorStops, stepNumber); - let steppedGradient = ''; - const percentage = - (endingPercentage - trailingPercentage) / steppedColors.length; - let percentageSteps = - (endingPercentage - trailingPercentage) / steppedColors.length + - trailingPercentage; - steppedColors.forEach((color) => { - steppedGradient = steppedGradient.concat( - `${color} ${percentageSteps - percentage}% ${percentageSteps}%, ` - ); - percentageSteps = percentageSteps + percentage; - }); - steppedGradient = steppedGradient.substring(0, steppedGradient.length - 2); - gradient = `linear-gradient(to right, transparent ${trailingPercentage}%, ${steppedGradient})`; - } else { - const linearGradient = sortedStops.map( - stopType === 'gradient' ? gradientStop : fixedStop - ); - gradient = `linear-gradient(to right,${linearGradient})`; - } - - return ( - - -

- -

-
- - {(trackWidth) => ( - <> - -
-
-
- {thumbs} - - )} - - - ); -}; diff --git a/src/components/color_picker/color_stops/index.ts b/src/components/color_picker/color_stops/index.ts deleted file mode 100644 index 2ac47a5781c..00000000000 --- a/src/components/color_picker/color_stops/index.ts +++ /dev/null @@ -1,9 +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. - */ - -export { EuiColorStops } from './color_stops'; diff --git a/src/components/color_picker/color_stops/utils.test.ts b/src/components/color_picker/color_stops/utils.test.ts deleted file mode 100644 index 814a0525fc4..00000000000 --- a/src/components/color_picker/color_stops/utils.test.ts +++ /dev/null @@ -1,115 +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 { addStop, addDefinedStop, removeStop, isInvalid } from './utils'; - -const colorStops = [ - { stop: 0, color: '#FF0000' }, - { stop: 25, color: '#00FF00' }, - { stop: 35, color: '#0000FF' }, -]; - -describe('isInvalid', () => { - test('Should not mark valid colorStops as invalid', () => { - expect(isInvalid(colorStops)).toBe(false); - }); - - test('Should mark colorStops missing color as invalid', () => { - const colorStops = [{ stop: 0, color: '' }]; - expect(isInvalid(colorStops)).toBe(true); - }); - - test('Should mark colorStops with invalid color as invalid', () => { - const colorStops = [{ stop: 0, color: 'not color' }]; - expect(isInvalid(colorStops)).toBe(true); - }); - - test('Should mark colorStops missing stop as invalid', () => { - const colorStops = [{ stop: null, color: '#FF0000' }]; - // @ts-ignore Intentionally wrong - expect(isInvalid(colorStops)).toBe(true); - }); - - test('Should mark colorStops with invalid stop as invalid', () => { - const colorStops = [{ stop: 'I am not a number', color: '#FF0000' }]; - // @ts-ignore Intentionally wrong - expect(isInvalid(colorStops)).toBe(true); - }); -}); - -describe('addStop', () => { - test('Should add stop when there is only a single stop', () => { - const colorStops = [{ stop: 0, color: '#FF0000' }]; - expect(addStop(colorStops, '#FF0000', 100)).toEqual([ - { stop: 0, color: '#FF0000' }, - { stop: 1, color: '#FF0000' }, - ]); - }); - - test('Should add stop to end of list', () => { - expect(addStop(colorStops, '#FF0000', 100)).toEqual([ - { stop: 0, color: '#FF0000' }, - { stop: 25, color: '#00FF00' }, - { stop: 35, color: '#0000FF' }, - { stop: 45, color: '#FF0000' }, - ]); - }); - - test('Should add stop below the max if max is taken', () => { - expect( - addStop( - [ - { stop: 0, color: '#FF0000' }, - { stop: 100, color: '#FF0000' }, - ], - '#FF0000', - 100 - ) - ).toEqual([ - { stop: 0, color: '#FF0000' }, - { stop: 100, color: '#FF0000' }, - { stop: 99, color: '#FF0000' }, - ]); - }); -}); - -describe('addDefinedStop', () => { - const colorStops = [{ stop: 0, color: '#FF0000' }]; - test('Should add stop', () => { - expect(addDefinedStop(colorStops, 1)).toEqual([ - { stop: 0, color: '#FF0000' }, - { stop: 1, color: '#6092C0' }, - ]); - }); - - test('Should add stop with a specified color', () => { - expect(addDefinedStop(colorStops, 1, '#FFFFFF')).toEqual([ - { stop: 0, color: '#FF0000' }, - { stop: 1, color: '#FFFFFF' }, - ]); - }); -}); - -describe('removeStop', () => { - test('Should not remove only stop', () => { - const colorStops = [{ stop: 0, color: '#FF0000' }]; - expect(removeStop(colorStops, 0)).toEqual(colorStops); - }); - - test('Should remove stop at index', () => { - const colorStops = [ - { stop: 0, color: '#FF0000' }, - { stop: 25, color: '#00FF00' }, - { stop: 35, color: '#0000FF' }, - ]; - expect(removeStop(colorStops, 1)).toEqual([ - { stop: 0, color: '#FF0000' }, - { stop: 35, color: '#0000FF' }, - ]); - }); -}); diff --git a/src/components/color_picker/color_stops/utils.ts b/src/components/color_picker/color_stops/utils.ts deleted file mode 100644 index f09e07659bf..00000000000 --- a/src/components/color_picker/color_stops/utils.ts +++ /dev/null @@ -1,129 +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 { getEventPosition, getChromaColor } from '../utils'; -import { DEFAULT_VISUALIZATION_COLOR } from '../../../services'; -import { ColorStop } from './color_stop_thumb'; -import { EUI_THUMB_SIZE } from '../../form/range/utils'; - -export const removeStop = (colorStops: ColorStop[], index: number) => { - if (colorStops.length === 1) { - return colorStops; - } - - return [...colorStops.slice(0, index), ...colorStops.slice(index + 1)]; -}; - -export const addDefinedStop = ( - colorStops: ColorStop[], - stop: ColorStop['stop'], - color: ColorStop['color'] = DEFAULT_VISUALIZATION_COLOR -) => { - const newStop = { - stop, - color, - }; - colorStops = [...colorStops, newStop]; - colorStops.sort((a, b) => { - if (a.stop < b.stop) { - return -1; - } - if (a.stop > b.stop) { - return 1; - } - return 0; - }); - return colorStops; -}; - -export const addStop = ( - colorStops: ColorStop[], - color: ColorStop['color'] = DEFAULT_VISUALIZATION_COLOR, - max: number -) => { - const index = colorStops.length ? colorStops.length - 1 : 0; - const stops = colorStops.map((el) => el.stop); - const currentStop = stops[index] != null ? stops[index] : max; - let delta = 1; - if (index !== 0) { - const prevStop = stops[index - 1]; - delta = currentStop - prevStop; - } - - let stop = currentStop + delta; - - if (stop > max) { - stop = max; - } - - // We've reached the max, so start working backwards - while (stops.indexOf(stop) > -1) { - stop--; - } - - const newStop = { - stop, - color, - }; - return [ - ...colorStops.slice(0, index + 1), - newStop, - ...colorStops.slice(index + 1), - ]; -}; - -export const isColorInvalid = (color: string, showAlpha: boolean = false) => { - return getChromaColor(color, showAlpha) == null || color === ''; -}; - -export const isStopInvalid = (stop: ColorStop['stop']) => { - return stop == null || isNaN(stop); -}; - -export const isInvalid = ( - colorStops: ColorStop[], - showAlpha: boolean = false -) => { - return colorStops.some((colorStop) => { - return ( - isColorInvalid(colorStop.color, showAlpha) || - isStopInvalid(colorStop.stop) - ); - }); -}; - -export const calculateScale = (trackWidth: number) => { - const thumbToTrackRatio = EUI_THUMB_SIZE / trackWidth; - return (1 - thumbToTrackRatio) * 100; -}; - -export const getStopFromMouseLocation = ( - location: { x: number; y: number }, - ref: HTMLDivElement, - min: number, - max: number -) => { - const box = getEventPosition(location, ref); - return Math.round((box.left / box.width) * (max - min) + min); -}; - -export const getPositionFromStop = ( - stop: ColorStop['stop'], - ref: HTMLDivElement, - min: number, - max: number -) => { - // For wide implementations, integer percentages can be visually off. - // Use 1 decimal place for more accuracy - return parseFloat( - ( - ((stop - min) / (max - min)) * - calculateScale(ref && ref.clientWidth > 0 ? ref.clientWidth : 100) - ).toFixed(1) - ); -}; diff --git a/src/components/color_picker/index.ts b/src/components/color_picker/index.ts index 633320d6938..eba48810e49 100644 --- a/src/components/color_picker/index.ts +++ b/src/components/color_picker/index.ts @@ -14,11 +14,6 @@ export type { EuiHueProps } from './hue'; export { EuiHue } from './hue'; export type { EuiSaturationProps } from './saturation'; export { EuiSaturation } from './saturation'; -export { EuiColorStops } from './color_stops'; -// TODO: Exporting `EuiColorStopsProps` from `'./color_stops'` -// results in a duplicate d.ts entry that causes build warnings -// and potential downstream TS project failures. -export type { EuiColorStopsProps } from './color_stops/color_stops'; export type { EuiColorPalettePickerProps, EuiColorPalettePickerPaletteProps, diff --git a/upcoming_changelogs/7262.md b/upcoming_changelogs/7262.md new file mode 100644 index 00000000000..96dd9760b85 --- /dev/null +++ b/upcoming_changelogs/7262.md @@ -0,0 +1,3 @@ +**Breaking changes** + +- Removed `EuiColorStops` due to low usage From 19147e40f591cb17ba8e6be00c7272e24bf729b2 Mon Sep 17 00:00:00 2001 From: Cee Chen <549407+cee-chen@users.noreply.github.com> Date: Mon, 9 Oct 2023 08:41:09 -0700 Subject: [PATCH 20/38] Remove deprecated `EuiSuggest` component (#7263) --- src-docs/src/routes.js | 3 - src-docs/src/views/_index.scss | 1 - .../src/views/description_list/playground.js | 4 +- .../views/suggest/_global_filter_group.scss | 24 - .../views/suggest/_global_filter_item.scss | 32 - src-docs/src/views/suggest/_index.scss | 3 - .../src/views/suggest/_saved_queries.scss | 31 - .../src/views/suggest/global_filter_add.js | 54 - .../src/views/suggest/global_filter_bar.js | 36 - .../src/views/suggest/global_filter_form.js | 256 ----- .../src/views/suggest/global_filter_item.js | 184 ---- .../views/suggest/global_filter_options.js | 111 --- src-docs/src/views/suggest/hashtag_popover.js | 94 -- src-docs/src/views/suggest/playground.js | 82 -- src-docs/src/views/suggest/saved_queries.js | 156 --- src-docs/src/views/suggest/suggest.tsx | 94 -- src-docs/src/views/suggest/suggest_example.js | 41 - src-docs/src/views/suggest/suggest_item.tsx | 102 -- .../views/suggest/suggest_item_example.tsx | 58 -- .../super_date_picker_example.js | 4 +- .../super_date_picker_pattern.tsx | 30 +- src/components/index.scss | 1 - src/components/index.ts | 2 - .../__snapshots__/suggest.test.tsx.snap | 922 ------------------ .../__snapshots__/suggest_item.test.tsx.snap | 126 --- src/components/suggest/_index.scss | 5 - src/components/suggest/_suggest_input.scss | 4 - src/components/suggest/_suggest_item.scss | 103 -- src/components/suggest/_variables.scss | 13 - src/components/suggest/index.ts | 13 - src/components/suggest/suggest.a11y.tsx | 81 -- src/components/suggest/suggest.spec.tsx | 130 --- src/components/suggest/suggest.test.tsx | 182 ---- src/components/suggest/suggest.tsx | 355 ------- src/components/suggest/suggest_item.test.tsx | 85 -- src/components/suggest/suggest_item.tsx | 194 ---- src/components/suggest/types.ts | 29 - upcoming_changelogs/7263.md | 3 + 38 files changed, 20 insertions(+), 3628 deletions(-) delete mode 100644 src-docs/src/views/suggest/_global_filter_group.scss delete mode 100644 src-docs/src/views/suggest/_global_filter_item.scss delete mode 100644 src-docs/src/views/suggest/_index.scss delete mode 100644 src-docs/src/views/suggest/_saved_queries.scss delete mode 100644 src-docs/src/views/suggest/global_filter_add.js delete mode 100644 src-docs/src/views/suggest/global_filter_bar.js delete mode 100644 src-docs/src/views/suggest/global_filter_form.js delete mode 100644 src-docs/src/views/suggest/global_filter_item.js delete mode 100644 src-docs/src/views/suggest/global_filter_options.js delete mode 100644 src-docs/src/views/suggest/hashtag_popover.js delete mode 100644 src-docs/src/views/suggest/playground.js delete mode 100644 src-docs/src/views/suggest/saved_queries.js delete mode 100644 src-docs/src/views/suggest/suggest.tsx delete mode 100644 src-docs/src/views/suggest/suggest_example.js delete mode 100644 src-docs/src/views/suggest/suggest_item.tsx delete mode 100644 src-docs/src/views/suggest/suggest_item_example.tsx delete mode 100644 src/components/suggest/__snapshots__/suggest.test.tsx.snap delete mode 100644 src/components/suggest/__snapshots__/suggest_item.test.tsx.snap delete mode 100644 src/components/suggest/_index.scss delete mode 100644 src/components/suggest/_suggest_input.scss delete mode 100644 src/components/suggest/_suggest_item.scss delete mode 100644 src/components/suggest/_variables.scss delete mode 100644 src/components/suggest/index.ts delete mode 100644 src/components/suggest/suggest.a11y.tsx delete mode 100644 src/components/suggest/suggest.spec.tsx delete mode 100644 src/components/suggest/suggest.test.tsx delete mode 100644 src/components/suggest/suggest.tsx delete mode 100644 src/components/suggest/suggest_item.test.tsx delete mode 100644 src/components/suggest/suggest_item.tsx delete mode 100644 src/components/suggest/types.ts create mode 100644 upcoming_changelogs/7263.md diff --git a/src-docs/src/routes.js b/src-docs/src/routes.js index 930a20ca705..7f8e6d7f38f 100644 --- a/src-docs/src/routes.js +++ b/src-docs/src/routes.js @@ -215,8 +215,6 @@ import { StatExample } from './views/stat/stat_example'; import { StepsExample } from './views/steps/steps_example'; -import { SuggestExample } from './views/suggest/suggest_example'; - import { SuperDatePickerExample } from './views/super_date_picker/super_date_picker_example'; import { TableExample } from './views/tables/tables_example'; @@ -609,7 +607,6 @@ const navigation = [ RangeControlExample, SearchBarExample, SelectableExample, - SuggestExample, SuperSelectExample, ].map((example) => createExample(example)), }, diff --git a/src-docs/src/views/_index.scss b/src-docs/src/views/_index.scss index 1648b306b84..0fd9e4dbd8b 100644 --- a/src-docs/src/views/_index.scss +++ b/src-docs/src/views/_index.scss @@ -11,7 +11,6 @@ $guideDemoHighlightColor: transparentize($euiColorPrimary, .9); @import './page_template/page'; @import './notification_event/notification_event'; @import './spacer/spacer'; -@import './suggest/index'; @import './text/text_scaling'; @import './tree_view/tree_view'; diff --git a/src-docs/src/views/description_list/playground.js b/src-docs/src/views/description_list/playground.js index 521dbac181d..13bf9645dec 100644 --- a/src-docs/src/views/description_list/playground.js +++ b/src-docs/src/views/description_list/playground.js @@ -28,10 +28,10 @@ export const descriptionListConfig = () => { return { config: { - componentName: 'EuiSuggest', + componentName: 'EuiDescriptionList', props: propsToUse, scope: { - EuiSuggest: EuiDescriptionList, + EuiDescriptionList: EuiDescriptionList, }, imports: { '@elastic/eui': { diff --git a/src-docs/src/views/suggest/_global_filter_group.scss b/src-docs/src/views/suggest/_global_filter_group.scss deleted file mode 100644 index 9370e8f20e6..00000000000 --- a/src-docs/src/views/suggest/_global_filter_group.scss +++ /dev/null @@ -1,24 +0,0 @@ -.globalFilterGroup__filterBar { - margin-top: $euiSizeXS; -} - -.globalFilterGroup__branch { - padding: $euiSize $euiSize $euiSizeS $euiSizeS; - background-repeat: no-repeat; - background-position: right top; - background-image: url("data:image/svg+xml,%0A%3Csvg width='28px' height='28px' viewBox='0 0 28 28' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='#{hexToRGB($euiColorLightShade)}'%3E%3Crect x='14' y='27' width='14' height='1'%3E%3C/rect%3E%3Crect x='0' y='0' width='1' height='14'%3E%3C/rect%3E%3C/g%3E%3C/svg%3E"); -} - -.globalFilterGroup__wrapper { - overflow: hidden; - transition: height $euiAnimSpeedNormal $euiAnimSlightResistance; -} - -.globalFilterGroup__filterFlexItem { - overflow: hidden; - padding: $euiSizeS; -} - -.globalFilterBar__flexItem { - max-width: calc(100% - #{$euiSizeXS}); // Width minus margin around each flex itm -} diff --git a/src-docs/src/views/suggest/_global_filter_item.scss b/src-docs/src/views/suggest/_global_filter_item.scss deleted file mode 100644 index bfda7254583..00000000000 --- a/src-docs/src/views/suggest/_global_filter_item.scss +++ /dev/null @@ -1,32 +0,0 @@ -.globalFilterItem { - line-height: $euiSizeL + $euiSizeXS; - border: none; - color: $euiTextColor; - - &:not(.globalFilterItem-isDisabled) { - @include euiFormControlDefaultShadow; - } -} - -.globalFilterItem-isDisabled { - background-color: transparentize($euiColorLightShade, .4); - text-decoration: line-through; - font-weight: $euiFontWeightRegular; - font-style: italic; -} - -.globalFilterItem-isPinned { - position: relative; - - &::before { - content: ''; - position: absolute; - top: 0; - bottom: 0; - left: 0; - width: $euiSizeXS; - background-color: $euiColorSuccess; - border-top-left-radius: $euiBorderRadius / 2; - border-bottom-left-radius: $euiBorderRadius / 2; - } -} diff --git a/src-docs/src/views/suggest/_index.scss b/src-docs/src/views/suggest/_index.scss deleted file mode 100644 index b8b21d226f4..00000000000 --- a/src-docs/src/views/suggest/_index.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import './global_filter_group'; -@import './global_filter_item'; -@import './saved_queries'; diff --git a/src-docs/src/views/suggest/_saved_queries.scss b/src-docs/src/views/suggest/_saved_queries.scss deleted file mode 100644 index fcdb4840cab..00000000000 --- a/src-docs/src/views/suggest/_saved_queries.scss +++ /dev/null @@ -1,31 +0,0 @@ -.savedQueriesInput__hideDatepicker { - .euiSuperDatePicker__flexWrapper { - width: 100%; - - > div:nth-of-type(1) { - display: none; - } - } -} - -.savedQueriesInput { - padding-bottom: $euiSizeXL * 6; -} - -.savedQueryManagement__text { - padding: $euiSizeM $euiSizeM ($euiSizeM / 2); -} - -.savedQueryManagement__listWrapper { - // Addition height will ensure one item is "cutoff" to indicate more below the scroll - max-height: $euiFormMaxWidth + $euiSize; - overflow-y: hidden; -} - -.savedQueryManagement__list { - @include euiYScrollWithShadows; - max-height: inherit; // Fixes overflow for applied max-height - // Left/Right padding is calculated to match the left alignment of the - // popover text and buttons - padding: ($euiSizeM / 2) $euiSizeXS !important; // stylelint-disable-line declaration-no-important -} diff --git a/src-docs/src/views/suggest/global_filter_add.js b/src-docs/src/views/suggest/global_filter_add.js deleted file mode 100644 index 83abc7dac7e..00000000000 --- a/src-docs/src/views/suggest/global_filter_add.js +++ /dev/null @@ -1,54 +0,0 @@ -import React, { useState } from 'react'; - -import { - EuiButtonEmpty, - EuiPopover, - EuiPopoverTitle, - EuiFlexGroup, - EuiFlexItem, -} from '../../../../src/components'; - -import GlobalFilterForm from './global_filter_form'; - -export default () => { - const [isPopoverOpen, setIsPopoverOpen] = useState(false); - - const togglePopover = () => { - setIsPopoverOpen(!isPopoverOpen); - }; - - const closePopover = () => { - setIsPopoverOpen(false); - }; - - return ( - - + Add filter - - } - anchorPosition="downCenter" - > - - - Add a filter - - {/* This button should open a modal */} - - Edit as Query DSL - - - - - - - - ); -}; diff --git a/src-docs/src/views/suggest/global_filter_bar.js b/src-docs/src/views/suggest/global_filter_bar.js deleted file mode 100644 index ddb7ce4b4d8..00000000000 --- a/src-docs/src/views/suggest/global_filter_bar.js +++ /dev/null @@ -1,36 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; - -import { EuiBadgeGroup } from '../../../../src/components'; -import GlobalFilterAdd from './global_filter_add'; -import { GlobalFilterItem } from './global_filter_item'; - -export const GlobalFilterBar = ({ filters, className, ...rest }) => { - const classes = classNames('globalFilterBar', className); - - const pinnedFilters = filters - .filter((filter) => filter.isPinned) - .map((filter) => { - return ; - }); - - const unpinnedFilters = filters - .filter((filter) => !filter.isPinned) - .map((filter) => { - return ; - }); - - return ( - - {/* Show pinned filters first and in a specific group */} - {pinnedFilters} - {unpinnedFilters} - - - ); -}; - -GlobalFilterBar.propTypes = { - filters: PropTypes.array, -}; diff --git a/src-docs/src/views/suggest/global_filter_form.js b/src-docs/src/views/suggest/global_filter_form.js deleted file mode 100644 index 1132a77daac..00000000000 --- a/src-docs/src/views/suggest/global_filter_form.js +++ /dev/null @@ -1,256 +0,0 @@ -import React, { useState } from 'react'; -import PropTypes from 'prop-types'; - -import { - EuiFlexGroup, - EuiFlexItem, - EuiButtonEmpty, - EuiFormRow, - EuiComboBox, - EuiButton, - EuiSpacer, - EuiSwitch, - EuiFieldText, -} from '../../../../src/components'; - -const fieldOption = [ - { - label: 'Fields', - isGroupLabelOption: true, - }, - { - label: 'field_1', - }, - { - label: 'field_2', - }, - { - label: 'field_3', - }, - { - label: 'field_4', - }, -]; -const operatorOption = [ - { - label: 'Operators', - isGroupLabelOption: true, - }, - { - label: 'IS', - }, - { - label: 'IS NOT', - }, - { - label: 'IS ONE OF', - }, - { - label: 'EXISTS', - }, -]; -const valueOption = [ - { - label: 'Values', - isGroupLabelOption: true, - }, - { - label: 'Value 1', - }, - { - label: 'Value 2', - }, - { - label: 'Value 3', - }, - { - label: 'Value 4', - }, -]; - -const GlobalFilterForm = (props) => { - const [fieldOptions, setFieldOptions] = useState(fieldOption); - const [operandOptions, setOperandOptions] = useState(operatorOption); - const [valueOptions, setValueOptions] = useState(valueOption); - const [selectedField, setSelectedField] = useState( - props.selectedObject ? props.selectedObject.field : [] - ); - const [selectedOperand, setSelectedOperand] = useState( - props.selectedObject ? props.selectedObject.operand : [] - ); - const [selectedValues, setSelectedValues] = useState( - props.selectedObject ? props.selectedObject.values : [] - ); - const [useCustomLabel, setUseCustomLabel] = useState(false); - const [customLabel, setCustomLabel] = useState(''); - - const onFieldChange = (selectedOptions) => { - // We should only get back either 0 or 1 options. - setSelectedField(selectedOptions); - }; - - const onOperandChange = (selectedOptions) => { - // We should only get back either 0 or 1 options. - setSelectedOperand(selectedOptions); - }; - - const onValuesChange = (selectedOptions) => { - setSelectedValues(selectedOptions); - }; - - const onCustomLabelSwitchChange = (e) => { - setUseCustomLabel(e.target.checked); - }; - - const onFieldSearchChange = (searchValue) => { - setFieldOptions( - fieldOption.filter((option) => - option.label.toLowerCase().includes(searchValue.toLowerCase()) - ) - ); - }; - - const onOperandSearchChange = (searchValue) => { - setOperandOptions( - operatorOption.filter((option) => - option.label.toLowerCase().includes(searchValue.toLowerCase()) - ) - ); - }; - - const onValuesSearchChange = (searchValue) => { - setValueOptions( - valueOption.filter((option) => - option.label.toLowerCase().includes(searchValue.toLowerCase()) - ) - ); - }; - - const resetForm = () => { - setSelectedField([]); - setSelectedOperand([]); - setSelectedValues([]); - setUseCustomLabel(false); - setCustomLabel(''); - }; - - const onCustomLabelChange = (e) => { - setCustomLabel(e.target.value); - }; - - const { onAdd, onCancel, selectedObject, ...rest } = props; - - return ( -
- - - - - - - - - - - - - - - -
- - - -
- - - - - - {useCustomLabel && ( -
- - - - -
- )} - - - - - - - Add - - - - - {selectedObject ? 'Cancel' : 'Reset form'} - - - - - {selectedObject && ( - - Delete - - )} - - -
- ); -}; - -GlobalFilterForm.propTypes = { - onAdd: PropTypes.func.isRequired, - onCancel: PropTypes.func.isRequired, - selectedObject: PropTypes.object, -}; - -export default GlobalFilterForm; diff --git a/src-docs/src/views/suggest/global_filter_item.js b/src-docs/src/views/suggest/global_filter_item.js deleted file mode 100644 index d4c166cc638..00000000000 --- a/src-docs/src/views/suggest/global_filter_item.js +++ /dev/null @@ -1,184 +0,0 @@ -import React, { useState } from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; - -import { - EuiBadge, - EuiPopover, - EuiContextMenu, -} from '../../../../src/components'; -import GlobalFilterForm from './global_filter_form'; - -function flattenPanelTree(tree, array = []) { - array.push(tree); - - if (tree.items) { - tree.items.forEach((item) => { - if (item.panel) { - flattenPanelTree(item.panel, array); - item.panel = item.panel.id; - } - }); - } - - return array; -} - -export const GlobalFilterItem = (props) => { - const [isPopoverOpen, setIsPopoverOpen] = useState(false); - const togglePopover = () => { - setIsPopoverOpen(!isPopoverOpen); - }; - - const closePopover = () => { - setIsPopoverOpen(false); - }; - - const deleteFilter = (e) => { - // Make sure it doesn't also trigger the onclick for the whole badge - e.stopPropagation(); - }; - - const { - className, - id, - field, - operator, // eslint-disable-line no-unused-vars - value, - isDisabled, - isPinned, - isExcluded, - ...rest - } = props; - - const classes = classNames( - 'globalFilterItem', - { - 'globalFilterItem-isDisabled': isDisabled, - 'globalFilterItem-isPinned': isPinned, - 'globalFilterItem-isExcluded': isExcluded, - }, - className - ); - - let prefix = null; - if (isExcluded) { - prefix = NOT ; - } - - let title = `Filter: ${field}: "${value}". Select for more filter actions.`; - if (isPinned) { - title = `Pinned ${title}`; - } else if (isDisabled) { - title = `Disabled ${title}`; - } - - const badge = ( - - {prefix} - {field}: - "{value}" - - ); - - const _createFilterContextMenu = (filter, button) => { - const selectedObject = { - field: [{ label: filter.field }], - operand: [{ label: filter.operator }], - values: [{ label: filter.value }], - }; - - const panelTree = { - id: 0, - items: [ - { - name: `${filter.isPinned ? 'Unpin' : 'Pin across all apps'}`, - icon: 'pin', - onClick: () => { - closePopover(); - }, - }, - { - name: 'Edit filter query', - icon: 'pencil', - panel: { - id: 1, - width: 400, - content: ( -
- -
- ), - }, - }, - { - name: `${filter.isExcluded ? 'Include results' : 'Exclude results'}`, - icon: `${filter.isExcluded ? 'plusInCircle' : 'minusInCircle'}`, - onClick: () => { - closePopover(); - }, - }, - { - name: `${filter.isDisabled ? 'Re-enable' : 'Temporarily disable'}`, - icon: `${filter.isDisabled ? 'eye' : 'eyeClosed'}`, - onClick: () => { - closePopover(); - }, - }, - { - name: 'Delete', - icon: 'trash', - onClick: () => { - closePopover(); - }, - }, - ], - }; - - return ( - - - - ); - }; - - return _createFilterContextMenu(props, badge); -}; - -GlobalFilterItem.propTypes = { - className: PropTypes.string, - id: PropTypes.string.isRequired, - field: PropTypes.string.isRequired, - operator: PropTypes.string.isRequired, - value: PropTypes.string.isRequired, - isDisabled: PropTypes.bool.isRequired, - isPinned: PropTypes.bool.isRequired, - isExcluded: PropTypes.bool.isRequired, -}; diff --git a/src-docs/src/views/suggest/global_filter_options.js b/src-docs/src/views/suggest/global_filter_options.js deleted file mode 100644 index 89a8dd19769..00000000000 --- a/src-docs/src/views/suggest/global_filter_options.js +++ /dev/null @@ -1,111 +0,0 @@ -import React, { useState } from 'react'; - -import { - EuiButtonIcon, - EuiPopover, - EuiContextMenu, - EuiPopoverTitle, -} from '../../../../src/components'; - -function flattenPanelTree(tree, array = []) { - array.push(tree); - - if (tree.items) { - tree.items.forEach((item) => { - if (item.panel) { - flattenPanelTree(item.panel, array); - item.panel = item.panel.id; - } - }); - } - - return array; -} - -export default () => { - const [isPopoverOpen, setPopover] = useState(false); - - const togglePopover = () => { - setPopover(!isPopoverOpen); - }; - - const closePopover = () => { - setPopover(false); - }; - - const panelTree = { - id: 0, - items: [ - { - name: 'Enable all', - icon: 'eye', - onClick: () => { - closePopover(); - }, - }, - { - name: 'Disable all', - icon: 'eyeClosed', - onClick: () => { - closePopover(); - }, - }, - { - name: 'Pin all', - icon: 'pin', - onClick: () => { - closePopover(); - }, - }, - { - name: 'Unpin all', - icon: 'pin', - onClick: () => { - closePopover(); - }, - }, - { - name: 'Invert inclusion', - icon: 'invert', - onClick: () => { - closePopover(); - }, - }, - { - name: 'Invert visibility', - icon: 'eye', - onClick: () => { - closePopover(); - }, - }, - { - name: 'Remove all', - icon: 'trash', - onClick: () => { - closePopover(); - }, - }, - ], - }; - - return ( - - } - anchorPosition="downCenter" - panelPaddingSize="none" - > - Change all filters - - - ); -}; diff --git a/src-docs/src/views/suggest/hashtag_popover.js b/src-docs/src/views/suggest/hashtag_popover.js deleted file mode 100644 index a68d625f2a4..00000000000 --- a/src-docs/src/views/suggest/hashtag_popover.js +++ /dev/null @@ -1,94 +0,0 @@ -import React, { useState } from 'react'; - -import { - EuiButton, - EuiButtonEmpty, - EuiFlexGroup, - EuiFlexItem, - EuiIcon, - EuiListGroup, - EuiListGroupItem, - EuiPopover, - EuiPopoverFooter, - EuiPopoverTitle, - EuiText, -} from '../../../../src/components'; - -export default (props) => { - const [isPopoverOpen, setPopover] = useState(false); - - const togglePopover = () => { - setPopover(!isPopoverOpen); - }; - - const closePopover = () => { - setPopover(false); - }; - - const hashtagButton = ( - - - - ); - - return ( - - SAVED QUERIES -
- -

Save query text and filters that you want to use again.

-
-
- - - - -
- {props.value !== '' ? ( - - - - - Save - - - - - ) : undefined} -
-
- ); -}; diff --git a/src-docs/src/views/suggest/playground.js b/src-docs/src/views/suggest/playground.js deleted file mode 100644 index 5ce0ebab000..00000000000 --- a/src-docs/src/views/suggest/playground.js +++ /dev/null @@ -1,82 +0,0 @@ -import { PropTypes } from 'react-view'; -import { EuiSuggest } from '../../../../src/components'; -import { - propUtilityForPlayground, - generateCustomProps, - createOptionalEnum, - simulateFunction, - dummyFunction, -} from '../../services/playground'; - -export const suggestConfig = () => { - const docgenInfo = Array.isArray(EuiSuggest.__docgenInfo) - ? EuiSuggest.__docgenInfo[0] - : EuiSuggest.__docgenInfo; - const propsToUse = propUtilityForPlayground(docgenInfo.props); - - const suggestions = `[ - { - type: { iconType: 'kqlField', color: 'tint4' }, - label: 'Field sample', - }, - { - type: { iconType: 'kqlValue', color: 'tint0' }, - label: 'Value sample', - }, - { - type: { iconType: 'kqlSelector', color: 'tint2' }, - label: 'Conjunction sample', - }, - { - type: { iconType: 'kqlOperand', color: 'tint1' }, - label: 'Operator sample', - }, - { - type: { iconType: 'search', color: 'tint8' }, - label: 'Recent search', - }, - { - type: { iconType: 'save', color: 'tint3' }, - label: 'Saved search', - }, - ]`; - propsToUse.suggestions = { - ...propsToUse.suggestions, - value: suggestions, - }; - - propsToUse.status = { - ...createOptionalEnum(propsToUse.status), - defaultValue: 'unchanged', - }; - - propsToUse.onItemClick = simulateFunction(propsToUse.onItemClick); - propsToUse.onInputChange = simulateFunction(propsToUse.onInputChange); - propsToUse.onSearchChange = simulateFunction(propsToUse.onSearchChange); - - propsToUse.maxHeight = { - ...propsToUse.maxHeight, - type: PropTypes.String, - }; - - return { - config: { - componentName: 'EuiSuggest', - props: propsToUse, - scope: { - EuiSuggest, - }, - imports: { - '@elastic/eui': { - named: ['EuiSuggest'], - }, - }, - customProps: { - ...generateCustomProps(['suggestions']), - onItemClick: dummyFunction, - onInputChange: dummyFunction, - onSearchChange: dummyFunction, - }, - }, - }; -}; diff --git a/src-docs/src/views/suggest/saved_queries.js b/src-docs/src/views/suggest/saved_queries.js deleted file mode 100644 index 57e61eede71..00000000000 --- a/src-docs/src/views/suggest/saved_queries.js +++ /dev/null @@ -1,156 +0,0 @@ -import React, { useState } from 'react'; - -import { - EuiButtonEmpty, - EuiFlexGroup, - EuiFlexItem, - EuiSuggest, - EuiSuperDatePicker, -} from '../../../../src/components'; - -import { GlobalFilterBar } from './global_filter_bar'; -import GlobalFilterOptions from './global_filter_options'; -import HashtagPopover from './hashtag_popover'; - -const shortDescription = 'This is the description'; - -const sampleItems = [ - { - type: { iconType: 'kqlField', color: 'tint4' }, - label: 'Field sample', - description: shortDescription, - }, - { - type: { iconType: 'kqlValue', color: 'tint0' }, - label: 'Value sample', - description: shortDescription, - }, - { - type: { iconType: 'kqlSelector', color: 'tint2' }, - label: 'Conjunction sample', - description: shortDescription, - }, - { - type: { iconType: 'kqlOperand', color: 'tint1' }, - label: 'Operator sample', - description: shortDescription, - }, - { - type: { iconType: 'search', color: 'tint8' }, - label: 'Recent search', - }, - { - type: { iconType: 'save', color: 'tint3' }, - label: 'Saved search', - }, -]; - -export default () => { - const status = 'unchanged'; - const [value, setValue] = useState(''); - const [hideDatepicker, setHide] = useState(false); - const filters = [ - { - id: 'filter0', - field: '@tags.keyword', - operator: 'IS', - value: 'value', - isDisabled: false, - isPinned: true, - isExcluded: false, - }, - { - id: 'filter1', - field: - 'Filter with a very long title to test if the badge will properly get truncated in the separate set of filter badges that are not quite as long but man does it really need to be long', - operator: 'IS', - value: 'value', - isDisabled: true, - isPinned: false, - isExcluded: false, - }, - { - id: 'filter2', - field: '@tags.keyword', - operator: 'IS NOT', - value: 'value', - isDisabled: false, - isPinned: true, - isExcluded: true, - }, - { - id: 'filter3', - field: '@tags.keyword', - operator: 'IS', - value: 'value', - isDisabled: false, - isPinned: false, - isExcluded: false, - }, - ]; - - const onFieldFocus = () => { - setHide(true); - }; - - const onFieldBlur = () => { - setHide(false); - }; - - const getInputValue = (val) => { - setValue(val); - }; - - const onItemClick = (item) => { - console.log(item); - }; - - const onTimeChange = (dateRange) => { - console.log(dateRange); - }; - - const append = KQL; - - return ( -
- - - } - append={append} - aria-label="Filter" - suggestions={sampleItems} - onItemClick={onItemClick} - onInputChange={getInputValue} - /> - - - - - - - - - - - - - -
- ); -}; diff --git a/src-docs/src/views/suggest/suggest.tsx b/src-docs/src/views/suggest/suggest.tsx deleted file mode 100644 index 2d1da494e5a..00000000000 --- a/src-docs/src/views/suggest/suggest.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import React, { useState } from 'react'; - -import { - EuiRadioGroup, - EuiSuggest, - EuiSpacer, - EuiFormRow, - EuiSuggestionProps, -} from '../../../../src/components'; - -import { EuiSuggestStatus } from '../../../../src/components/suggest/types'; - -import { htmlIdGenerator } from '../../../../src/services'; - -const shortDescription = 'This is the description'; - -const sampleItems = [ - { - type: { iconType: 'kqlField', color: 'tint4' }, - label: 'Field sample', - description: shortDescription, - }, - { - type: { iconType: 'kqlValue', color: 'tint0' }, - label: 'Value sample', - description: shortDescription, - }, - { - type: { iconType: 'kqlSelector', color: 'tint2' }, - label: 'Conjunction sample', - description: shortDescription, - }, - { - type: { iconType: 'kqlOperand', color: 'tint1' }, - label: 'Operator sample', - description: shortDescription, - }, - { - type: { iconType: 'search', color: 'tint8' }, - label: 'Recent search', - }, - { - type: { iconType: 'save', color: 'tint3' }, - label: 'Saved search', - }, -]; - -const idPrefix = htmlIdGenerator()(); - -export default () => { - const radios: Array<{ - id: string; - value: EuiSuggestStatus; - label: string; - }> = [ - { id: `${idPrefix}0`, value: 'unchanged', label: 'No new changes' }, - { id: `${idPrefix}1`, value: 'unsaved', label: 'Not yet saved' }, - { id: `${idPrefix}2`, value: 'saved', label: 'Saved' }, - { id: `${idPrefix}3`, value: 'loading', label: 'Loading' }, - ]; - const [status, setStatus] = useState('unchanged'); - const [radioIdSelected, setSelectedId] = useState(`${idPrefix}0`); - - const onStatusChange = (optionId: string) => { - setSelectedId(optionId); - setStatus(radios.find((x) => x.id === optionId)!.value); - }; - - const onItemClick = (item: EuiSuggestionProps) => { - console.log(item); - }; - - return ( -
- onStatusChange(id)} - /> - - - {}} - onItemClick={onItemClick} - placeholder="Enter query to display suggestions" - suggestions={sampleItems} - /> - -
- ); -}; diff --git a/src-docs/src/views/suggest/suggest_example.js b/src-docs/src/views/suggest/suggest_example.js deleted file mode 100644 index d243a7fe4b1..00000000000 --- a/src-docs/src/views/suggest/suggest_example.js +++ /dev/null @@ -1,41 +0,0 @@ -import React from 'react'; -import { Link } from 'react-router-dom'; - -import { EuiCallOut, EuiLink } from '../../../../src/components'; - -export const SuggestExample = { - title: 'Suggest', - isDeprecated: true, - sections: [ - { - text: ( - -

- EuiSuggest is being deprecated due to low usage and - high overlap with other existing EUI components. -

-

- {' '} - We recommend using{' '} - - EuiSelectable - {' '} - instead, or{' '} - - copying the component to your application - {' '} - for usage if necessary. The component will be permanently removed in - October 2023. -

-
- ), - }, - ], -}; diff --git a/src-docs/src/views/suggest/suggest_item.tsx b/src-docs/src/views/suggest/suggest_item.tsx deleted file mode 100644 index daa49a6147f..00000000000 --- a/src-docs/src/views/suggest/suggest_item.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import React from 'react'; - -import { EuiSuggestItem, EuiSuggest } from '../../../../src/components'; - -const shortDescription = 'This is the description'; - -const sampleItems = [ - { - type: { iconType: 'kqlField', color: 'tint5' }, - label: 'Field sample', - description: shortDescription, - }, - { - type: { iconType: 'kqlValue', color: 'tint0' }, - label: 'Value sample', - description: shortDescription, - }, - { - type: { iconType: 'kqlSelector', color: 'tint3' }, - label: 'Conjunction sample', - description: shortDescription, - }, - { - type: { iconType: 'kqlOperand', color: 'tint1' }, - label: 'Operator sample', - description: shortDescription, - }, - { - type: { iconType: 'search', color: 'tint10' }, - label: 'Recent search', - }, - { - type: { iconType: 'save', color: 'tint7' }, - label: 'Saved query', - }, -]; - -const customWidthItems = [ - { - type: { iconType: 'kqlField', color: 'tint5' }, - label: 'Field sample with label at 30%', - labelWidth: '30', - description: shortDescription, - }, - { - type: { iconType: 'kqlField', color: 'tint5' }, - label: 'Field sample with label at 50%', - labelWidth: '50', - description: shortDescription, - }, - { - type: { iconType: 'kqlField', color: 'tint5' }, - label: 'Field sample with label at 80%', - labelWidth: '80', - description: shortDescription, - }, -]; - -const moreItems = [ - { - type: { iconType: 'kqlValue', color: 'tint0' }, - label: 'Items with no description will expand their label', - }, - { - type: { iconType: 'kqlValue', color: 'tint0' }, - label: 'Item with a description that wraps', - description: - 'This is a long description. Fusce euismod dui eu metus sagittis molestie.', - truncate: false, - }, -]; - -const allItems = sampleItems.concat(customWidthItems).concat(moreItems); - -export default ({ - withInput, - fullWidth, - virtualized, -}: { - withInput: boolean; - fullWidth: boolean; - virtualized: boolean; -}) => ( - <> - {withInput ? ( - {}} - placeholder="Enter query to display suggestions" - isVirtualized={virtualized} - suggestions={allItems} - /> - ) : ( -
- {allItems.map((item, index) => ( - - ))} -
- )} - -); diff --git a/src-docs/src/views/suggest/suggest_item_example.tsx b/src-docs/src/views/suggest/suggest_item_example.tsx deleted file mode 100644 index 78dd78ed28d..00000000000 --- a/src-docs/src/views/suggest/suggest_item_example.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import React, { useState } from 'react'; - -import { EuiSwitch, EuiSuggestItem } from '../../../../src/components'; -import { GuideSection } from '../../components/guide_section/guide_section'; -import { GuideSectionTypes } from '../../components/guide_section/guide_section_types'; - -import SuggestItem from './suggest_item'; -const suggestItemSource = require('!!raw-loader!./suggest_item'); - -export default () => { - const [fullWidth, setFullWidth] = useState(false); - const [withInput, setWithInput] = useState(false); - const [virtualized, setVirtualized] = useState(false); - - return ( - <> - setFullWidth(e.target.checked)} - />{' '} -   - { - const checked = e.target.checked; - setWithInput(checked); - setVirtualized((isSet) => (checked ? isSet : false)); - }} - />{' '} -   - setVirtualized(e.target.checked)} - />{' '} -   - - } - source={[ - { - type: GuideSectionTypes.JS, - code: suggestItemSource, - }, - ]} - props={{ EuiSuggestItem }} - /> - - ); -}; diff --git a/src-docs/src/views/super_date_picker/super_date_picker_example.js b/src-docs/src/views/super_date_picker/super_date_picker_example.js index 95e194be0de..1f3b6b36bed 100644 --- a/src-docs/src/views/super_date_picker/super_date_picker_example.js +++ b/src-docs/src/views/super_date_picker/super_date_picker_example.js @@ -337,8 +337,8 @@ if (!endMoment || !endMoment.isValid()) { The following is a demo pattern of how to layout the{' '} EuiSuperDatePicker alongside Elastic's KQL search bar using{' '} - - EuiSuggest + + EuiSearchBar {' '} and shrinking to fit when the search bar is in focus.

diff --git a/src-docs/src/views/super_date_picker/super_date_picker_pattern.tsx b/src-docs/src/views/super_date_picker/super_date_picker_pattern.tsx index c9eb9200259..663917ca1ed 100644 --- a/src-docs/src/views/super_date_picker/super_date_picker_pattern.tsx +++ b/src-docs/src/views/super_date_picker/super_date_picker_pattern.tsx @@ -5,7 +5,7 @@ import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, - EuiSuggest, + EuiSearchBar, EuiSuperDatePicker, EuiSuperDatePickerProps, OnRefreshChangeProps, @@ -14,13 +14,6 @@ import { EuiOutsideClickDetector, } from '../../../../src'; -const sampleItems = [ - { - type: { iconType: 'kqlField', color: 'tint4' }, - label: 'Field sample', - }, -]; - export default () => { const [recentlyUsedRanges, setRecentlyUsedRanges] = useState< NonNullable @@ -82,15 +75,18 @@ export default () => { onOutsideClick={() => setShowQuickSelectOnly(false)} isDisabled={!showQuickSelectOnly} > - setShowQuickSelectOnly(true)} - onBlur={() => setShowQuickSelectOnly(false)} - prepend={ - - } - append={KQL} - aria-label="Filter using KQL" - suggestions={sampleItems} + setShowQuickSelectOnly(true), + onFocus: () => setShowQuickSelectOnly(true), + onBlur: () => setShowQuickSelectOnly(false), + prepend: ( + + ), + append: KQL, + 'aria-label': 'Filter using KQL', + placeholder: 'type:visualization -dashboard', + }} /> diff --git a/src/components/index.scss b/src/components/index.scss index e916bfccef7..9d2983b50e2 100644 --- a/src/components/index.scss +++ b/src/components/index.scss @@ -15,5 +15,4 @@ @import 'side_nav/index'; @import 'search_bar/index'; @import 'selectable/index'; -@import 'suggest/index'; @import 'table/index'; diff --git a/src/components/index.ts b/src/components/index.ts index 8b1550792ba..8bb0c034309 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -152,8 +152,6 @@ export * from './stat'; export * from './steps'; -export * from './suggest'; - export * from './table'; export * from './token'; diff --git a/src/components/suggest/__snapshots__/suggest.test.tsx.snap b/src/components/suggest/__snapshots__/suggest.test.tsx.snap deleted file mode 100644 index 60eedbc96fb..00000000000 --- a/src/components/suggest/__snapshots__/suggest.test.tsx.snap +++ /dev/null @@ -1,922 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`EuiSuggest is rendered 1`] = ` -
-
-
-
-
-
- - -
- -
-
-

- State: unchanged. - - Use the Up and Down arrow keys to move focus over options. Press Enter to select. Press Escape to collapse options. -

-
-
-
-`; - -exports[`EuiSuggest props append 1`] = ` -
-
-
-
-
-
- - -
- -
- - Appended - -
-

- State: unchanged. - - Use the Up and Down arrow keys to move focus over options. Press Enter to select. Press Escape to collapse options. -

-
-
-
-`; - -exports[`EuiSuggest props isVirtualized 1`] = ` -
-
-
-
-
-
- - -
- -
-
-

- State: unchanged. - - Use the Up and Down arrow keys to move focus over options. Press Enter to select. Press Escape to collapse options. -

-
-
-
-`; - -exports[`EuiSuggest props maxHeight 1`] = ` -
-
-
-
-
-
- - -
- -
-
-

- State: unchanged. - - Use the Up and Down arrow keys to move focus over options. Press Enter to select. Press Escape to collapse options. -

-
-
-
-`; - -exports[`EuiSuggest props options common 1`] = ` -
-
-
-
-
-
- - -
- -
-
-

- State: unchanged. - - Use the Up and Down arrow keys to move focus over options. Press Enter to select. Press Escape to collapse options. -

-
-
-
-`; - -exports[`EuiSuggest props options standard 1`] = ` -
-
-
-
-
-
- - -
- -
-
-

- State: unchanged. - - Use the Up and Down arrow keys to move focus over options. Press Enter to select. Press Escape to collapse options. -

-
-
-
-`; - -exports[`EuiSuggest props remaining EuiFieldSearch props are spread to the search input 1`] = ` -
-
-
-
-
-
- - -
- -
- - -
-
-
-

- State: unchanged. - - Use the Up and Down arrow keys to move focus over options. Press Enter to select. Press Escape to collapse options. -

-
-
-
-`; - -exports[`EuiSuggest props status status: loading is rendered 1`] = ` -
-
-
-
-
-
- - -
- -
- -
-
-
-

- State: loading. - - Use the Up and Down arrow keys to move focus over options. Press Enter to select. Press Escape to collapse options. -

-
-
-
-`; - -exports[`EuiSuggest props status status: saved is rendered 1`] = ` -
-
-
-
-
-
- - -
- -
- - - -
-

- State: saved. - - Use the Up and Down arrow keys to move focus over options. Press Enter to select. Press Escape to collapse options. -

-
-
-
-`; - -exports[`EuiSuggest props status status: unchanged is rendered 1`] = ` -
-
-
-
-
-
- - -
- -
-
-

- State: unchanged. - - Use the Up and Down arrow keys to move focus over options. Press Enter to select. Press Escape to collapse options. -

-
-
-
-`; - -exports[`EuiSuggest props status status: unsaved is rendered 1`] = ` -
-
-
-
-
-
- - -
- -
- - - -
-

- State: unsaved. - - Use the Up and Down arrow keys to move focus over options. Press Enter to select. Press Escape to collapse options. -

-
-
-
-`; - -exports[`EuiSuggest props tooltipContent tooltipContent for status: loading is rendered 1`] = ` -
-
-
-
-
-
- - -
- -
- -
-
-
-

- State: loading. - - Use the Up and Down arrow keys to move focus over options. Press Enter to select. Press Escape to collapse options. -

-
-
-
-`; - -exports[`EuiSuggest props tooltipContent tooltipContent for status: saved is rendered 1`] = ` -
-
-
-
-
-
- - -
- -
- - - -
-

- State: saved. - - Use the Up and Down arrow keys to move focus over options. Press Enter to select. Press Escape to collapse options. -

-
-
-
-`; - -exports[`EuiSuggest props tooltipContent tooltipContent for status: unchanged is rendered 1`] = ` -
-
-
-
-
-
- - -
- -
-
-

- State: unchanged. - - Use the Up and Down arrow keys to move focus over options. Press Enter to select. Press Escape to collapse options. -

-
-
-
-`; - -exports[`EuiSuggest props tooltipContent tooltipContent for status: unsaved is rendered 1`] = ` -
-
-
-
-
-
- - -
- -
- - - -
-

- State: unsaved. - - Use the Up and Down arrow keys to move focus over options. Press Enter to select. Press Escape to collapse options. -

-
-
-
-`; diff --git a/src/components/suggest/__snapshots__/suggest_item.test.tsx.snap b/src/components/suggest/__snapshots__/suggest_item.test.tsx.snap deleted file mode 100644 index c7c43893e38..00000000000 --- a/src/components/suggest/__snapshots__/suggest_item.test.tsx.snap +++ /dev/null @@ -1,126 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`EuiSuggestItem is rendered 1`] = ` - - - - - - Test label - - -`; - -exports[`props item with no description has expanded label is rendered 1`] = ` - - - - - - Charles de Gaulle International Airport - - -`; - -exports[`props labelWidth is 30% is rendered 1`] = ` - - - - - - This is the description - - - This is the description - - -`; - -exports[`props truncate is rendered 1`] = ` - - - - - - This is the description - - - This is the description - - -`; - -exports[`props truncate renders false 1`] = ` - - - - - - This is the description - - - This is the description - - -`; diff --git a/src/components/suggest/_index.scss b/src/components/suggest/_index.scss deleted file mode 100644 index d31182e14d4..00000000000 --- a/src/components/suggest/_index.scss +++ /dev/null @@ -1,5 +0,0 @@ -@import 'variables'; - -@import 'suggest_item'; - -@import 'suggest_input'; diff --git a/src/components/suggest/_suggest_input.scss b/src/components/suggest/_suggest_input.scss deleted file mode 100644 index a135311dafd..00000000000 --- a/src/components/suggest/_suggest_input.scss +++ /dev/null @@ -1,4 +0,0 @@ -.euiSuggestInput__statusIcon { - // stylelint-disable-next-line declaration-no-important - background-color: transparent !important; // Override typical append coloring -} diff --git a/src/components/suggest/_suggest_item.scss b/src/components/suggest/_suggest_item.scss deleted file mode 100644 index 41e5b8284cf..00000000000 --- a/src/components/suggest/_suggest_item.scss +++ /dev/null @@ -1,103 +0,0 @@ -.euiSuggestItem { - display: flex; - align-items: flex-start; - font-size: $euiFontSizeXS; - line-height: $euiSize; - color: $euiTextColor; - flex: 1 1 100%; - max-width: 100%; - - &--truncate { - .euiSuggestItem__label, - .euiSuggestItem__description { - @include euiTextTruncate; - } - } -} - -// When `onClick` is provided, EuiSuggestItem renders as a - ); - } else { - return ( - - {innerContent} - - ); - } -}; diff --git a/src/components/suggest/types.ts b/src/components/suggest/types.ts deleted file mode 100644 index cd01ebd890e..00000000000 --- a/src/components/suggest/types.ts +++ /dev/null @@ -1,29 +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. - */ - -export const ALL_STATUSES = [ - 'unsaved', - 'saved', - 'unchanged', - 'loading', -] as const; -type StatusTuple = typeof ALL_STATUSES; -export type EuiSuggestStatus = StatusTuple[number]; - -export interface _Status { - icon?: string; - color?: string; - tooltip?: string; -} - -export interface _EuiSuggestStatusMap { - unsaved: _Status; - saved: _Status; - unchanged: _Status; - loading: _Status; -} diff --git a/upcoming_changelogs/7263.md b/upcoming_changelogs/7263.md new file mode 100644 index 00000000000..b4c9f2c4567 --- /dev/null +++ b/upcoming_changelogs/7263.md @@ -0,0 +1,3 @@ +**Breaking changes** + +- Removed `EuiSuggest`. We recommend using `EuiSelectable` or `EuiComboBox` instead From edcb655c79146705449f92ae0f355f2af77482fb Mon Sep 17 00:00:00 2001 From: Cee Chen <549407+cee-chen@users.noreply.github.com> Date: Mon, 9 Oct 2023 08:43:20 -0700 Subject: [PATCH 21/38] Remove deprecated `euiHeader` Sass mixin and variables (#7264) --- src/global_styling/mixins/_header.scss | 29 ------------------- src/global_styling/mixins/_index.scss | 1 - src/global_styling/variables/_header.scss | 3 -- src/global_styling/variables/_index.scss | 1 - .../global_styling/mixins/_index.scss | 1 - upcoming_changelogs/7264.md | 3 ++ 6 files changed, 3 insertions(+), 35 deletions(-) delete mode 100644 src/global_styling/mixins/_header.scss delete mode 100644 src/global_styling/variables/_header.scss create mode 100644 upcoming_changelogs/7264.md diff --git a/src/global_styling/mixins/_header.scss b/src/global_styling/mixins/_header.scss deleted file mode 100644 index aae8c7eb1fc..00000000000 --- a/src/global_styling/mixins/_header.scss +++ /dev/null @@ -1,29 +0,0 @@ -@import '../variables/header'; - -@mixin euiHeaderAffordForFixed($headerHeight: $euiHeaderHeightCompensation) { - @warn 'This mixin will shortly be deprecated. Use the CSS variable var(--euiFixedHeadersOffset) instead, which updates dynamically based on the number of fixed headers on the page.'; - - // The `@at-root #{&}` allows for grouping alongside another specific body class. - // When not applied inside of another selector, it simply renders with the single class - @at-root #{&}.euiBody--headerIsFixed { - padding-top: $headerHeight; - - // When the EuiHeader is fixed, we need to account for it in the position of the flyout - &:not(.euiDataGrid__restrictBody) .euiFlyout, - .euiCollapsibleNav { - top: $headerHeight; - height: calc(100% - #{$headerHeight}); - } - - @include euiBreakpoint('m', 'l', 'xl') { - .euiPageSideBar--sticky { - max-height: calc(100vh - #{$headerHeight}); - top: #{$headerHeight}; - } - } - - &:not(.euiDataGrid__restrictBody) .euiOverlayMask[data-relative-to-header='below'] { - top: #{$headerHeight}; - } - } -} diff --git a/src/global_styling/mixins/_index.scss b/src/global_styling/mixins/_index.scss index 1e23b8a2228..0d8915a49d4 100644 --- a/src/global_styling/mixins/_index.scss +++ b/src/global_styling/mixins/_index.scss @@ -8,7 +8,6 @@ @import 'button'; @import 'form'; -@import 'header'; @import 'loading'; @import 'link'; @import 'panel'; diff --git a/src/global_styling/variables/_header.scss b/src/global_styling/variables/_header.scss deleted file mode 100644 index df68553c62e..00000000000 --- a/src/global_styling/variables/_header.scss +++ /dev/null @@ -1,3 +0,0 @@ -$euiHeaderHeight: $euiSizeXXL + $euiSizeS !default; -// Use the following variable in other components to afford for the fixed header -$euiHeaderHeightCompensation: $euiHeaderHeight !default; diff --git a/src/global_styling/variables/_index.scss b/src/global_styling/variables/_index.scss index 80fbed518bb..6971a19d892 100644 --- a/src/global_styling/variables/_index.scss +++ b/src/global_styling/variables/_index.scss @@ -21,7 +21,6 @@ @import 'buttons'; @import 'form'; -@import 'header'; @import 'page'; @import 'panel'; @import 'tool_tip'; diff --git a/src/themes/amsterdam/global_styling/mixins/_index.scss b/src/themes/amsterdam/global_styling/mixins/_index.scss index 986c7aa2391..3d83a4cd823 100644 --- a/src/themes/amsterdam/global_styling/mixins/_index.scss +++ b/src/themes/amsterdam/global_styling/mixins/_index.scss @@ -10,7 +10,6 @@ @import '../../../../global_styling/mixins/button'; @import '../../../../global_styling/mixins/form'; -@import '../../../../global_styling/mixins/header'; @import '../../../../global_styling/mixins/loading'; @import 'link'; @import '../../../../global_styling/mixins/panel'; diff --git a/upcoming_changelogs/7264.md b/upcoming_changelogs/7264.md new file mode 100644 index 00000000000..0ebd8034aa1 --- /dev/null +++ b/upcoming_changelogs/7264.md @@ -0,0 +1,3 @@ +**Breaking changes** + +- Removed `euiHeaderAffordForFixed` Sass mixin, and `$euiHeaderHeight` and `$euiHeaderHeightCompensation` Sass variables. Use the CSS variable `--var(euiFixedHeadersOffset, 0)` instead. From cde5b3efa4b6dabaaa7a6760b561bb104134707b Mon Sep 17 00:00:00 2001 From: Cee Chen <549407+cee-chen@users.noreply.github.com> Date: Mon, 9 Oct 2023 08:44:23 -0700 Subject: [PATCH 22/38] [Tech debt] Remove `testCustomHook` in favor of RTL `renderHook` (#7260) --- .../body/data_grid_row_manager.test.ts | 36 ++- .../data_grid_header_cell.test.tsx.snap | 125 +++------- .../header/data_grid_header_cell.test.tsx | 231 ++++++++++-------- .../body/header/header_is_interactive.test.ts | 27 +- .../controls/display_selector.test.tsx | 8 +- .../controls/fullscreen_selector.test.tsx | 177 ++++---------- .../datagrid/utils/col_widths.test.ts | 88 +++---- src/components/datagrid/utils/focus.test.tsx | 130 ++++------ src/components/datagrid/utils/ref.test.ts | 33 ++- .../datagrid/utils/scrolling.test.tsx | 186 ++++++-------- .../pretty_duration.test.tsx | 31 +-- .../super_date_picker/pretty_interval.test.ts | 45 ++-- .../super_date_picker/time_options.test.tsx | 13 +- .../markdown_format.styles.test.ts | 6 +- .../progress/progress.styles.test.ts | 10 +- src/components/text/text.styles.test.ts | 6 +- src/components/title/title.styles.test.ts | 4 +- src/global_styling/mixins/_color.test.ts | 10 +- src/global_styling/mixins/_padding.test.ts | 6 +- src/global_styling/mixins/_responsive.test.ts | 36 +-- src/global_styling/mixins/_states.test.ts | 10 +- src/global_styling/mixins/_typography.test.ts | 17 +- ...ity.test.tsx.snap => utility.test.ts.snap} | 0 .../{utility.test.tsx => utility.test.ts} | 16 +- src/test/README.md | 15 -- src/test/internal/index.ts | 1 - src/test/internal/test_custom_hook.tsx | 41 ---- 27 files changed, 525 insertions(+), 783 deletions(-) rename src/global_styling/utility/__snapshots__/{utility.test.tsx.snap => utility.test.ts.snap} (100%) rename src/global_styling/utility/{utility.test.tsx => utility.test.ts} (63%) delete mode 100644 src/test/internal/test_custom_hook.tsx diff --git a/src/components/datagrid/body/data_grid_row_manager.test.ts b/src/components/datagrid/body/data_grid_row_manager.test.ts index 83ee562adba..8525afba21c 100644 --- a/src/components/datagrid/body/data_grid_row_manager.test.ts +++ b/src/components/datagrid/body/data_grid_row_manager.test.ts @@ -6,18 +6,17 @@ * Side Public License, v 1. */ -import { testCustomHook } from '../../../test/internal'; +import { renderHook } from '@testing-library/react/pure'; // Pure is important here to preserve state between tests -import { EuiDataGridRowManager } from '../data_grid_types'; import { useRowManager } from './data_grid_row_manager'; describe('row manager', () => { const mockGridRef = { current: document.createElement('div') } as any; describe('getRow', () => { - const { return: rowManager } = testCustomHook(() => + const rowManager = renderHook(() => useRowManager({ innerGridRef: mockGridRef }) - ); + ).result.current; describe('when the row DOM element does not already exist', () => { beforeAll(() => { @@ -109,37 +108,34 @@ describe('row manager', () => { }); describe('rowClasses', () => { - const rowClasses = { - 0: 'hello', - }; - let row0: HTMLDivElement; - let row1: HTMLDivElement; + const mockArgs = { innerGridRef: mockGridRef }; const mockRowArgs = { visibleRowIndex: 99, top: '15px', height: 30 }; - const { return: rowManager, updateHookArgs } = - testCustomHook(useRowManager, { - innerGridRef: mockGridRef, - rowClasses, - }); + const initialRowClasses = { 0: 'hello' }; - beforeAll(() => { - row0 = rowManager.getRow({ ...mockRowArgs, rowIndex: 0 }); - row1 = rowManager.getRow({ ...mockRowArgs, rowIndex: 1 }); + const { result, rerender } = renderHook(useRowManager, { + initialProps: { ...mockArgs, rowClasses: initialRowClasses }, }); + const getRow = (rowIndex: number) => + result.current.getRow({ ...mockRowArgs, rowIndex }); + it('creates rows with the passed gridStyle.rowClasses', () => { - expect(row0.classList.contains('hello')).toBe(true); + expect(getRow(0).classList.contains('hello')).toBe(true); }); it('updates row classes dynamically when gridStyle.rowClasses updates', () => { - updateHookArgs({ rowClasses: { 0: 'world' } }); + rerender({ ...mockArgs, rowClasses: { 0: 'world' } }); + const row0 = getRow(0); expect(row0.classList.contains('hello')).toBe(false); expect(row0.classList.contains('world')).toBe(true); }); it('adds/removes row classes correctly when gridStyle.rowClasses updates', () => { - updateHookArgs({ rowClasses: { 1: 'test' } }); + rerender({ ...mockArgs, rowClasses: { 1: 'test' } }); + const row0 = getRow(0); + const row1 = getRow(1); expect(row0.classList.contains('hello')).toBe(false); expect(row0.classList.contains('world')).toBe(false); diff --git a/src/components/datagrid/body/header/__snapshots__/data_grid_header_cell.test.tsx.snap b/src/components/datagrid/body/header/__snapshots__/data_grid_header_cell.test.tsx.snap index 184f011237e..bf87d4c82e4 100644 --- a/src/components/datagrid/body/header/__snapshots__/data_grid_header_cell.test.tsx.snap +++ b/src/components/datagrid/body/header/__snapshots__/data_grid_header_cell.test.tsx.snap @@ -1,117 +1,58 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`EuiDataGridHeaderCell renders 1`] = ` - - - +
- } - className="eui-fullWidth" - closePopover={[Function]} - display="inline-block" - hasArrow={true} - isOpen={false} - offset={7} - ownFocus={true} - panelPaddingSize="none" - panelProps={ - Object { - "onKeyDown": [Function], - } - } - panelRef={[Function]} - popoverScreenReaderText={ - - } - repositionToCrossAxis={true} - > - , - "onClick": [Function], - "size": "xs", - }, - Object { - "color": "text", - "iconType": "sortLeft", - "isDisabled": false, - "label": , - "onClick": [Function], - "size": "xs", - }, - Object { - "color": "text", - "iconType": "sortRight", - "isDisabled": true, - "label": , - "onClick": [Function], - "size": "xs", - }, - ] - } - /> - +
+
- +
`; diff --git a/src/components/datagrid/body/header/data_grid_header_cell.test.tsx b/src/components/datagrid/body/header/data_grid_header_cell.test.tsx index e04002b1257..109489fedc3 100644 --- a/src/components/datagrid/body/header/data_grid_header_cell.test.tsx +++ b/src/components/datagrid/body/header/data_grid_header_cell.test.tsx @@ -6,10 +6,14 @@ * Side Public License, v 1. */ -import React from 'react'; -import { mount, shallow } from 'enzyme'; -import { testCustomHook } from '../../../../test/internal'; -import { render } from '../../../../test/rtl'; +import React, { ReactNode } from 'react'; +import { fireEvent } from '@testing-library/react'; +import { + render, + renderHook, + waitForEuiPopoverOpen, + waitForEuiPopoverClose, +} from '../../../../test/rtl'; import { DataGridFocusContext } from '../../utils/focus'; import { mockFocusContext } from '../../utils/__mocks__/focus_context'; @@ -38,11 +42,12 @@ describe('EuiDataGridHeaderCell', () => { }; it('renders', () => { - const component = shallow(); - expect(component).toMatchSnapshot(); + const { container } = render(); + expect(container.firstChild).toMatchSnapshot(); }); describe('sorting', () => { + const onSort = () => {}; const columnId = 'test'; const mockSortingArgs = { sorting: undefined, @@ -50,64 +55,77 @@ describe('EuiDataGridHeaderCell', () => { showColumnActions: true, }; - const getRenderedText = (text: React.ReactElement) => - render(

{text}

).container.textContent; + const getRender = (node: ReactNode) => render(<>{node}).container; describe('if the current column is being sorted', () => { it('renders an ascending sort arrow', () => { - const { - return: { sortingArrow }, - } = testCustomHook(useSortingUtils, { - ...mockSortingArgs, - sorting: { columns: [{ id: columnId, direction: 'asc' }] }, - }); + const { sortingArrow } = renderHook(() => + useSortingUtils({ + ...mockSortingArgs, + sorting: { + onSort, + columns: [{ id: columnId, direction: 'asc' }], + }, + }) + ).result.current; - expect(shallow(sortingArrow).prop('data-euiicon-type')).toEqual( - 'sortUp' - ); + expect( + getRender(sortingArrow).querySelector('[data-euiicon-type="sortUp"]') + ).toBeInTheDocument(); }); it('renders a descending sort arrow', () => { - const { - return: { sortingArrow }, - } = testCustomHook(useSortingUtils, { - ...mockSortingArgs, - sorting: { columns: [{ id: columnId, direction: 'desc' }] }, - }); + const { sortingArrow } = renderHook(() => + useSortingUtils({ + ...mockSortingArgs, + sorting: { + onSort, + columns: [{ id: columnId, direction: 'desc' }], + }, + }) + ).result.current; - expect(shallow(sortingArrow).prop('data-euiicon-type')).toEqual( - 'sortDown' - ); + expect( + getRender(sortingArrow).querySelector( + '[data-euiicon-type="sortDown"]' + ) + ).toBeInTheDocument(); }); describe('when only the current column is being sorted', () => { describe('when the header cell has no actions', () => { it('renders aria-sort but not sortingScreenReaderText', () => { - const { - return: { ariaSort, sortingScreenReaderText }, - } = testCustomHook(useSortingUtils, { - ...mockSortingArgs, - sorting: { columns: [{ id: columnId, direction: 'asc' }] }, - showColumnActions: false, - }); + const { ariaSort, sortingScreenReaderText } = renderHook(() => + useSortingUtils({ + ...mockSortingArgs, + sorting: { + onSort, + columns: [{ id: columnId, direction: 'asc' }], + }, + showColumnActions: false, + }) + ).result.current; expect(ariaSort).toEqual('ascending'); - expect(getRenderedText(sortingScreenReaderText)).toEqual(''); + expect(getRender(sortingScreenReaderText)).toHaveTextContent(''); }); }); describe('when the header cell has actions', () => { it('renders aria-sort and sortingScreenReaderText', () => { - const { - return: { ariaSort, sortingScreenReaderText }, - } = testCustomHook(useSortingUtils, { - ...mockSortingArgs, - sorting: { columns: [{ id: columnId, direction: 'desc' }] }, - showColumnActions: true, - }); + const { ariaSort, sortingScreenReaderText } = renderHook(() => + useSortingUtils({ + ...mockSortingArgs, + sorting: { + onSort, + columns: [{ id: columnId, direction: 'desc' }], + }, + showColumnActions: true, + }) + ).result.current; expect(ariaSort).toEqual('descending'); - expect(getRenderedText(sortingScreenReaderText)).toEqual( + expect(getRender(sortingScreenReaderText)).toHaveTextContent( 'Sorted descending.' ); }); @@ -117,50 +135,55 @@ describe('EuiDataGridHeaderCell', () => { describe('if the current column is not being sorted', () => { it('does not render an arrow even if other columns are sorted', () => { - const { - return: { sortingArrow }, - } = testCustomHook(useSortingUtils, { - ...mockSortingArgs, - sorting: { columns: [{ id: 'other', direction: 'desc' }] }, - }); + const { sortingArrow } = renderHook(() => + useSortingUtils({ + ...mockSortingArgs, + sorting: { onSort, columns: [{ id: 'other', direction: 'desc' }] }, + }) + ).result.current; expect(sortingArrow).toBeNull(); }); it('does not render aria-sort or screen reader sorting text', () => { - const { - return: { ariaSort, sortingScreenReaderText }, - } = testCustomHook(useSortingUtils, mockSortingArgs); + const { ariaSort, sortingScreenReaderText } = renderHook(() => + useSortingUtils(mockSortingArgs) + ).result.current; expect(ariaSort).toEqual(undefined); - expect(getRenderedText(sortingScreenReaderText)).toEqual(''); + expect(getRender(sortingScreenReaderText)).toHaveTextContent(''); }); }); describe('when multiple columns are being sorted', () => { it('does not render aria-sort, but renders sorting screen reader text text with a full list of sorted columns', () => { - const { - return: { ariaSort, sortingScreenReaderText }, - getUpdatedState, - updateHookArgs, - } = testCustomHook(useSortingUtils, { - id: 'A', - sorting: { - columns: [ - { id: 'A', direction: 'asc' }, - { id: 'B', direction: 'desc' }, - ], + const { result, rerender } = renderHook(useSortingUtils, { + initialProps: { + ...mockSortingArgs, + id: 'A', + sorting: { + onSort, + columns: [ + { id: 'A', direction: 'asc' }, + { id: 'B', direction: 'desc' }, + ], + }, }, }); - expect(ariaSort).toEqual(undefined); - expect(getRenderedText(sortingScreenReaderText)).toMatchInlineSnapshot( - '"Sorted by A, ascending, then sorted by B, descending."' + expect(result.current.ariaSort).toEqual(undefined); + expect( + getRender(result.current.sortingScreenReaderText) + ).toHaveTextContent( + 'Sorted by A, ascending, then sorted by B, descending.' ); // Branch coverage - updateHookArgs({ + rerender({ + ...mockSortingArgs, + id: 'B', sorting: { + onSort, columns: [ { id: 'B', direction: 'desc' }, { id: 'C', direction: 'asc' }, @@ -169,9 +192,9 @@ describe('EuiDataGridHeaderCell', () => { }, }); expect( - getRenderedText(getUpdatedState().sortingScreenReaderText) - ).toMatchInlineSnapshot( - '"Sorted by B, descending, then sorted by C, ascending, then sorted by A, ascending."' + getRender(result.current.sortingScreenReaderText) + ).toHaveTextContent( + 'Sorted by B, descending, then sorted by C, ascending, then sorted by A, ascending.' ); }); }); @@ -179,69 +202,70 @@ describe('EuiDataGridHeaderCell', () => { describe('resizing', () => { it('renders a resizer', () => { - const component = shallow(); - expect(component.find('EuiDataGridColumnResizer')).toHaveLength(1); + const { getByTestSubject } = render( + + ); + expect(getByTestSubject('dataGridColumnResizer')).toBeInTheDocument(); }); it('does not render a resizer if isResizable is false', () => { - const component = shallow( + const { queryByTestSubject } = render( ); - expect(component.find('EuiDataGridColumnResizer')).toHaveLength(0); + expect( + queryByTestSubject('dataGridColumnResizer') + ).not.toBeInTheDocument(); }); it('does not render a resizer if a column width cannot be found', () => { - const component = shallow( + const { queryByTestSubject } = render( ); - expect(component.find('EuiDataGridColumnResizer')).toHaveLength(0); + expect( + queryByTestSubject('dataGridColumnResizer') + ).not.toBeInTheDocument(); }); }); describe('popover', () => { it('does not render a popover if there are no column actions', () => { - const component = shallow( + const { container } = render( ); - expect(component.find('EuiPopover')).toHaveLength(0); + expect(container.querySelector('.euiPopover')).not.toBeInTheDocument(); }); - it('handles popover open', () => { - const component = mount( + it('handles popover open and close', () => { + const { container } = render( ); - component.find('.euiDataGridHeaderCell__button').simulate('click'); + const toggle = container.querySelector('.euiDataGridHeaderCell__button')!; - expect(component.find('EuiPopover').prop('isOpen')).toEqual(true); + fireEvent.click(toggle); + waitForEuiPopoverOpen(); expect(mockFocusContext.setFocusedCell).toHaveBeenCalledWith([0, -1]); - }); - - it('handles popover close', () => { - const component = shallow(); - (component.find('EuiPopover').prop('closePopover') as Function)(); - expect(component.find('EuiPopover').prop('isOpen')).toEqual(false); + fireEvent.click(toggle); + waitForEuiPopoverClose(); }); describe('keyboard arrow navigation', () => { const { - return: { - panelRef, - panelProps: { onKeyDown }, - }, - } = testCustomHook(usePopoverArrowNavigation); + panelRef, + panelProps: { onKeyDown }, + } = renderHook(usePopoverArrowNavigation).result.current; const mockPanel = document.createElement('div'); mockPanel.setAttribute('tabindex', '-1'); @@ -253,18 +277,19 @@ describe('EuiDataGridHeaderCell', () => { panelRef(mockPanel); const preventDefault = jest.fn(); + const keyDownEvent = { preventDefault } as unknown as React.KeyboardEvent; beforeEach(() => jest.clearAllMocks()); describe('early returns', () => { it('does nothing if the up/down arrow keys are not pressed', () => { - onKeyDown({ key: 'Tab', preventDefault }); + onKeyDown({ ...keyDownEvent, key: 'Tab' }); expect(preventDefault).not.toHaveBeenCalled(); }); it('does nothing if the popover contains no tabbable elements', () => { const emptyDiv = document.createElement('div'); panelRef(emptyDiv); - onKeyDown({ key: 'ArrowDown', preventDefault }); + onKeyDown({ ...keyDownEvent, key: 'ArrowDown' }); expect(preventDefault).not.toHaveBeenCalled(); panelRef(mockPanel); // Reset for other tests @@ -275,7 +300,7 @@ describe('EuiDataGridHeaderCell', () => { beforeEach(() => mockPanel.focus()); it('focuses the first action when the arrow down key is pressed', () => { - onKeyDown({ key: 'ArrowDown', preventDefault }); + onKeyDown({ ...keyDownEvent, key: 'ArrowDown' }); expect(preventDefault).toHaveBeenCalled(); expect( document.activeElement?.getAttribute('data-test-subj') @@ -283,7 +308,7 @@ describe('EuiDataGridHeaderCell', () => { }); it('focuses the last action when the arrow up key is pressed', () => { - onKeyDown({ key: 'ArrowUp', preventDefault }); + onKeyDown({ ...keyDownEvent, key: 'ArrowUp' }); expect(preventDefault).toHaveBeenCalled(); expect( document.activeElement?.getAttribute('data-test-subj') @@ -298,19 +323,19 @@ describe('EuiDataGridHeaderCell', () => { ); it('moves focus to the the next action', () => { - onKeyDown({ key: 'ArrowDown', preventDefault }); + onKeyDown({ ...keyDownEvent, key: 'ArrowDown' }); expect( document.activeElement?.getAttribute('data-test-subj') ).toEqual('second'); - onKeyDown({ key: 'ArrowDown', preventDefault }); + onKeyDown({ ...keyDownEvent, key: 'ArrowDown' }); expect( document.activeElement?.getAttribute('data-test-subj') ).toEqual('last'); }); it('loops focus back to the first action when pressing down on the last action', () => { - onKeyDown({ key: 'ArrowDown', preventDefault }); + onKeyDown({ ...keyDownEvent, key: 'ArrowDown' }); expect( document.activeElement?.getAttribute('data-test-subj') ).toEqual('first'); @@ -323,19 +348,19 @@ describe('EuiDataGridHeaderCell', () => { ); it('moves focus to the previous action', () => { - onKeyDown({ key: 'ArrowUp', preventDefault }); + onKeyDown({ ...keyDownEvent, key: 'ArrowUp' }); expect( document.activeElement?.getAttribute('data-test-subj') ).toEqual('second'); - onKeyDown({ key: 'ArrowUp', preventDefault }); + onKeyDown({ ...keyDownEvent, key: 'ArrowUp' }); expect( document.activeElement?.getAttribute('data-test-subj') ).toEqual('first'); }); it('loops focus back to the last action when pressing up on the first action', () => { - onKeyDown({ key: 'ArrowUp', preventDefault }); + onKeyDown({ ...keyDownEvent, key: 'ArrowUp' }); expect( document.activeElement?.getAttribute('data-test-subj') ).toEqual('last'); diff --git a/src/components/datagrid/body/header/header_is_interactive.test.ts b/src/components/datagrid/body/header/header_is_interactive.test.ts index 835c3688f5d..f79aafb060a 100644 --- a/src/components/datagrid/body/header/header_is_interactive.test.ts +++ b/src/components/datagrid/body/header/header_is_interactive.test.ts @@ -6,8 +6,7 @@ * Side Public License, v 1. */ -import { act } from '@testing-library/react'; -import { testCustomHook } from '../../../../test/internal'; +import { renderHook, act } from '@testing-library/react'; import { useHeaderIsInteractive } from './header_is_interactive'; describe('useHeaderIsInteractive', () => { @@ -22,9 +21,9 @@ describe('useHeaderIsInteractive', () => { describe('initial headerIsInteractive state', () => { it('returns false when there are no interactive children within the header', () => { const [mockGridEl] = createMockGrid(); - const { - return: { headerIsInteractive }, - } = testCustomHook(() => useHeaderIsInteractive(mockGridEl)); + const { headerIsInteractive } = renderHook(() => + useHeaderIsInteractive(mockGridEl) + ).result.current; expect(headerIsInteractive).toEqual(false); }); @@ -33,9 +32,9 @@ describe('useHeaderIsInteractive', () => { const [mockGridEl, mockHeaderEl] = createMockGrid(); mockHeaderEl.appendChild(document.createElement('button')); // Interactive child - const { - return: { headerIsInteractive }, - } = testCustomHook(() => useHeaderIsInteractive(mockGridEl)); + const { headerIsInteractive } = renderHook(() => + useHeaderIsInteractive(mockGridEl) + ).result.current; expect(headerIsInteractive).toEqual(true); }); @@ -49,15 +48,13 @@ describe('useHeaderIsInteractive', () => { mockTarget.setAttribute('data-euigrid-tab-managed', 'true'); mockHeaderEl.appendChild(mockCell); - const { - return: { headerIsInteractive, handleHeaderMutation }, - getUpdatedState, - } = testCustomHook(() => useHeaderIsInteractive(null)); - expect(headerIsInteractive).toEqual(false); + const { result } = renderHook(() => useHeaderIsInteractive(null)); + expect(result.current.headerIsInteractive).toEqual(false); - act(() => handleHeaderMutation([{ target: mockTarget }])); + // @ts-ignore matching production types/data isn't necessary for this test + act(() => result.current.handleHeaderMutation([{ target: mockTarget }])); - expect(getUpdatedState().headerIsInteractive).toEqual(true); + expect(result.current.headerIsInteractive).toEqual(true); }); }); }); diff --git a/src/components/datagrid/controls/display_selector.test.tsx b/src/components/datagrid/controls/display_selector.test.tsx index 40dd9910e5c..3fd034d88bc 100644 --- a/src/components/datagrid/controls/display_selector.test.tsx +++ b/src/components/datagrid/controls/display_selector.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { act } from '@testing-library/react'; import { shallow, mount, ShallowWrapper, ReactWrapper } from 'enzyme'; -import { testCustomHook } from '../../../test/internal'; +import { renderHook } from '../../../test/rtl'; import { EuiDataGridToolBarVisibilityOptions, @@ -486,11 +486,9 @@ describe('useDataGridDisplaySelector', () => { describe('gridStyles', () => { it('returns an object of grid styles with user overrides', () => { const initialStyles = { ...startingStyles, stripes: true }; - const { - return: [, gridStyles], - } = testCustomHook(() => + const [, gridStyles] = renderHook(() => useDataGridDisplaySelector(true, initialStyles, {}) - ); + ).result.current; expect(gridStyles).toMatchInlineSnapshot(` Object { diff --git a/src/components/datagrid/controls/fullscreen_selector.test.tsx b/src/components/datagrid/controls/fullscreen_selector.test.tsx index f0ee04b73a0..f0dd674df29 100644 --- a/src/components/datagrid/controls/fullscreen_selector.test.tsx +++ b/src/components/datagrid/controls/fullscreen_selector.test.tsx @@ -7,169 +7,100 @@ */ import React from 'react'; -import { act } from '@testing-library/react'; -import { shallow } from 'enzyme'; +import { fireEvent } from '@testing-library/react'; +import { render, renderHook, renderHookAct } from '../../../test/rtl'; import { keys } from '../../../services'; -import { testCustomHook } from '../../../test/internal'; + import { useDataGridFullScreenSelector } from './fullscreen_selector'; describe('useDataGridFullScreenSelector', () => { - type ReturnedValues = ReturnType; - describe('isFullScreen state', () => { test('setFullScreen toggles isFullScreen', () => { - const { - return: { isFullScreen, setIsFullScreen }, - getUpdatedState, - } = testCustomHook(() => useDataGridFullScreenSelector()); - - expect(isFullScreen).toEqual(false); - act(() => setIsFullScreen(true)); - expect(getUpdatedState().isFullScreen).toEqual(true); + const { result } = renderHook(() => useDataGridFullScreenSelector()); + + expect(result.current.isFullScreen).toEqual(false); + renderHookAct(() => result.current.setIsFullScreen(true)); + expect(result.current.isFullScreen).toEqual(true); }); }); describe('fullScreenSelector', () => { - it('renders a button that toggles entering fullscreen', () => { - const { - return: { fullScreenSelector }, - } = testCustomHook(() => useDataGridFullScreenSelector()); - const component = shallow(
{fullScreenSelector}
); - - expect(component).toMatchInlineSnapshot(` -
- - - -
- `); - }); - - it('renders a button that toggles exiting fullscreen', () => { - const { - return: { setIsFullScreen }, - getUpdatedState, - } = testCustomHook(() => useDataGridFullScreenSelector()); - act(() => setIsFullScreen(true)); - - const { fullScreenSelector } = getUpdatedState(); - const component = shallow(
{fullScreenSelector}
); - - expect(component).toMatchInlineSnapshot(` -
- - Exit fullscreen - ( - - esc - - ) - - } - delay="long" - display="inlineBlock" - position="top" - > - - -
- `); - }); + it('renders a button that toggles entering and exiting fullscreen', () => { + const { result } = renderHook(() => useDataGridFullScreenSelector()); + const { container, getByTestSubject, rerender } = render( + <>{result.current.fullScreenSelector} + ); + expect( + container.querySelector('[data-euiicon-type="fullScreen"]') + ).toBeInTheDocument(); - it('toggles fullscreen mode on button click', () => { - const { - return: { fullScreenSelector, isFullScreen }, - getUpdatedState, - } = testCustomHook(() => useDataGridFullScreenSelector()); - expect(isFullScreen).toEqual(false); - const component = shallow(
{fullScreenSelector}
); - - act(() => { - component - .find('[data-test-subj="dataGridFullScreenButton"]') - .simulate('click'); + renderHookAct(() => { + fireEvent.click(getByTestSubject('dataGridFullScreenButton')); }); - expect(getUpdatedState().isFullScreen).toEqual(true); + expect(result.current.isFullScreen).toEqual(true); + rerender(<>{result.current.fullScreenSelector}); + + expect( + container.querySelector('[data-euiicon-type="fullScreenExit"]') + ).toBeInTheDocument(); }); }); describe('handleGridKeyDown', () => { + const preventDefault = jest.fn(); + const keyDownEvent = { + preventDefault, + } as unknown as React.KeyboardEvent; + + beforeEach(() => preventDefault.mockReset()); + it('exits fullscreen mode when the Escape key is pressed', () => { - const { - return: { setIsFullScreen }, - getUpdatedState, - } = testCustomHook(() => useDataGridFullScreenSelector()); - act(() => setIsFullScreen(true)); - const { handleGridKeyDown } = getUpdatedState(); + const { result } = renderHook(() => useDataGridFullScreenSelector()); + renderHookAct(() => result.current.setIsFullScreen(true)); - const preventDefault = jest.fn(); - act(() => handleGridKeyDown({ key: keys.ESCAPE, preventDefault } as any)); + renderHookAct(() => + result.current.handleGridKeyDown({ ...keyDownEvent, key: keys.ESCAPE }) + ); expect(preventDefault).toHaveBeenCalled(); - expect(getUpdatedState().isFullScreen).toEqual(false); + expect(result.current.isFullScreen).toEqual(false); }); it('does nothing if fullscreen is not open', () => { - const { - return: { handleGridKeyDown }, - getUpdatedState, - } = testCustomHook(() => useDataGridFullScreenSelector()); + const { result } = renderHook(() => useDataGridFullScreenSelector()); - const preventDefault = jest.fn(); - act(() => handleGridKeyDown({ key: keys.ESCAPE, preventDefault } as any)); + renderHookAct(() => + result.current.handleGridKeyDown({ ...keyDownEvent, key: keys.ESCAPE }) + ); expect(preventDefault).not.toHaveBeenCalled(); - expect(getUpdatedState().isFullScreen).toEqual(false); + expect(result.current.isFullScreen).toEqual(false); }); - it('does nothing if other keys are pressed or fullscreen is not open', () => { - const { - return: { handleGridKeyDown }, - getUpdatedState, - } = testCustomHook(() => useDataGridFullScreenSelector()); + it('does nothing if other keys are pressed', () => { + const { result } = renderHook(() => useDataGridFullScreenSelector()); - const preventDefault = jest.fn(); - act(() => handleGridKeyDown({ key: keys.ENTER, preventDefault } as any)); + renderHookAct(() => + result.current.handleGridKeyDown({ ...keyDownEvent, key: keys.ENTER }) + ); expect(preventDefault).not.toHaveBeenCalled(); - expect(getUpdatedState().isFullScreen).toEqual(false); + expect(result.current.isFullScreen).toEqual(false); }); }); describe('body classes', () => { it('adds and removes a fullscreen class to the document body when fullscreen opens/closes', () => { - const { - return: { setIsFullScreen }, - } = testCustomHook(() => useDataGridFullScreenSelector()); - act(() => setIsFullScreen(true)); + const { setIsFullScreen } = renderHook(() => + useDataGridFullScreenSelector() + ).result.current; + + renderHookAct(() => setIsFullScreen(true)); expect( document.body.classList.contains('euiDataGrid__restrictBody') ).toBe(true); - act(() => setIsFullScreen(false)); + renderHookAct(() => setIsFullScreen(false)); expect( document.body.classList.contains('euiDataGrid__restrictBody') ).toBe(false); diff --git a/src/components/datagrid/utils/col_widths.test.ts b/src/components/datagrid/utils/col_widths.test.ts index 28edd76caf3..a18194d73c4 100644 --- a/src/components/datagrid/utils/col_widths.test.ts +++ b/src/components/datagrid/utils/col_widths.test.ts @@ -6,8 +6,7 @@ * Side Public License, v 1. */ -import { act } from '@testing-library/react'; -import { testCustomHook } from '../../../test/internal'; +import { renderHook, act } from '@testing-library/react'; import { useDefaultColumnWidth, doesColumnHaveAnInitialWidth, @@ -20,7 +19,7 @@ jest.mock('../../../utils', () => ({ IS_JEST_ENVIRONMENT: false })); describe('useDefaultColumnWidth', () => { it('returns null if grid has not yet been initialized (gridWidth = 0)', () => { expect( - testCustomHook(() => useDefaultColumnWidth(0, [], [], [])).return + renderHook(() => useDefaultColumnWidth(0, [], [], [])).result.current ).toEqual(null); }); @@ -30,14 +29,16 @@ describe('useDefaultColumnWidth', () => { { id: 'B', initialWidth: 150 }, ]; expect( - testCustomHook(() => useDefaultColumnWidth(500, [], [], columns)).return + renderHook(() => useDefaultColumnWidth(500, [], [], columns)).result + .current ).toEqual(100); }); it('returns the grid width divided by the number of columns without initialWidths', () => { const columns = [{ id: 'A' }, { id: 'B' }, { id: 'C' }, { id: 'D' }]; expect( - testCustomHook(() => useDefaultColumnWidth(500, [], [], columns)).return + renderHook(() => useDefaultColumnWidth(500, [], [], columns)).result + .current ).toEqual(125); // 500 / 4 }); @@ -45,16 +46,17 @@ describe('useDefaultColumnWidth', () => { const controlColumns = [{ id: 'controlColumn', width: 50 }] as any; const columns = [{ id: 'A' }, { id: 'B' }, { id: 'C' }, { id: 'D' }]; expect( - testCustomHook(() => + renderHook(() => useDefaultColumnWidth(1000, controlColumns, controlColumns, columns) - ).return + ).result.current ).toEqual(225); // 1000 - (50 * 2) / 4 }); it('returns a minimum column width of 100px regardless of grid width / number of columns', () => { const columns = [{ id: 'A' }, { id: 'B' }, { id: 'C' }, { id: 'D' }]; expect( - testCustomHook(() => useDefaultColumnWidth(200, [], [], columns)).return + renderHook(() => useDefaultColumnWidth(200, [], [], columns)).result + .current ).toEqual(100); // Would be 200 / 4 = 50 without a minumum }); }); @@ -88,36 +90,34 @@ describe('useColumnWidths', () => { defaultColumnWidth: 150, onColumnResize: jest.fn(), }; - type ReturnedValues = ReturnType; describe('columnWidths', () => { it('returns a map of column `id`s to `initialWidth`s', () => { - const { - return: { columnWidths }, - } = testCustomHook(() => useColumnWidths(args)); + const { columnWidths } = renderHook(() => useColumnWidths(args)).result + .current; + expect(columnWidths).toEqual({ b: 75 }); }); it('recomputes column widths on columns change', () => { - const { updateHookArgs, getUpdatedState } = - testCustomHook(useColumnWidths, args); - - updateHookArgs({ + const { rerender, result } = renderHook(useColumnWidths, { + initialProps: args, + }); + rerender({ + ...args, columns: [{ id: 'c', initialWidth: 125 }], }); - expect(getUpdatedState().columnWidths).toEqual({ c: 125 }); + + expect(result.current.columnWidths).toEqual({ c: 125 }); }); }); describe('setColumnWidth', () => { it("sets a single column's width in the columnWidths map", () => { - const { - return: { setColumnWidth }, - getUpdatedState, - } = testCustomHook(() => useColumnWidths(args)); + const { result } = renderHook(() => useColumnWidths(args)); - act(() => setColumnWidth('c', 125)); - expect(getUpdatedState().columnWidths).toEqual({ b: 75, c: 125 }); + act(() => result.current.setColumnWidth('c', 125)); + expect(result.current.columnWidths).toEqual({ b: 75, c: 125 }); expect(args.onColumnResize).toHaveBeenCalledWith({ columnId: 'c', width: 125, @@ -128,52 +128,53 @@ describe('useColumnWidths', () => { describe('getColumnWidth', () => { describe('leading control columns', () => { it('returns the width property of the column', () => { - const { - return: { getColumnWidth }, - } = testCustomHook(() => useColumnWidths({ ...args })); + const { getColumnWidth } = renderHook(() => + useColumnWidths({ ...args }) + ).result.current; + expect(getColumnWidth(0)).toEqual(50); }); }); describe('trailing control columns', () => { it('returns the width property of the column', () => { - const { - return: { getColumnWidth }, - } = testCustomHook(() => useColumnWidths({ ...args })); + const { getColumnWidth } = renderHook(() => + useColumnWidths({ ...args }) + ).result.current; + expect(getColumnWidth(3)).toEqual(25); }); }); describe('normal data columns', () => { it('returns the initialWidth property of the column if present and not overridden by user settings', () => { - const { - return: { getColumnWidth }, - } = testCustomHook(() => useColumnWidths({ ...args })); + const { getColumnWidth } = renderHook(() => + useColumnWidths({ ...args }) + ).result.current; + expect(getColumnWidth(1)).toEqual(75); }); it('returns the defaultColumnWidth if no column initialWidth or user width was set', () => { - const { - return: { getColumnWidth }, - } = testCustomHook(() => useColumnWidths({ ...args })); + const { getColumnWidth } = renderHook(() => + useColumnWidths({ ...args }) + ).result.current; + expect(getColumnWidth(2)).toEqual(150); }); it('returns a static 100px width if no default column width was passed', () => { - const { - return: { getColumnWidth }, - } = testCustomHook(() => + const { getColumnWidth } = renderHook(() => useColumnWidths({ ...args, defaultColumnWidth: null }) - ); + ).result.current; + expect(getColumnWidth(2)).toEqual(100); }); }); describe('when all columns are hidden', () => { it('does not error & falls back to the static default width', () => { - const { - return: { getColumnWidth }, - } = testCustomHook(() => + const { getColumnWidth } = renderHook(() => useColumnWidths({ ...args, leadingControlColumns: [], @@ -181,7 +182,8 @@ describe('useColumnWidths', () => { columns: [], defaultColumnWidth: null, }) - ); + ).result.current; + expect(getColumnWidth(0)).toEqual(100); }); }); diff --git a/src/components/datagrid/utils/focus.test.tsx b/src/components/datagrid/utils/focus.test.tsx index 9b2e0db5d37..35f861656b1 100644 --- a/src/components/datagrid/utils/focus.test.tsx +++ b/src/components/datagrid/utils/focus.test.tsx @@ -7,10 +7,8 @@ */ import React from 'react'; -import { act } from '@testing-library/react'; -import { mount } from 'enzyme'; +import { render, renderHook, renderHookAct } from '../../../test/rtl'; import { keys } from '../../../services'; -import { testCustomHook } from '../../../test/internal'; import { DataGridFocusContext, useFocus, @@ -22,37 +20,26 @@ import { } from './focus'; describe('useFocus', () => { - type ReturnValues = ReturnType; const mockArgs = { headerIsInteractive: true, gridItemsRendered: { current: null }, }; describe('onFocusUpdate', () => { - const onFocus = jest.fn(); - const { - return: { onFocusUpdate, setFocusedCell }, - } = testCustomHook(() => useFocus(mockArgs)); - - let cleanupFn: Function; - it("adds a cell's onFocus callback to the internal cellsUpdateFocus map,", () => { - cleanupFn = onFocusUpdate([0, 0], onFocus); - // Note: there's no great way to assert this since cellsUpdateFocus is internal, - // so this is a separate test mostly just to document the intention/behavior - }); + const onFocus = jest.fn(); + const { result } = renderHook(() => useFocus(mockArgs)); + const cleanupFn = result.current.onFocusUpdate([0, 0], onFocus); - it("calls the cell's onFocus callback with true when the cell becomes focused", () => { - act(() => setFocusedCell([0, 0])); + // the mapped onFocus is called with true when the cell becomes focused + renderHookAct(() => result.current.setFocusedCell([0, 0])); expect(onFocus).toHaveBeenCalledWith(true); - }); - it("calls the previous cell's onFocus callback with false when another cell becomes focused", () => { - act(() => setFocusedCell([1, 1])); + // the mapped onFocus is called with false when another cell becomes focused + renderHookAct(() => result.current.setFocusedCell([1, 1])); expect(onFocus).toHaveBeenCalledWith(false); - }); - it('removes the cell from the internal cellsUpdateFocus map as a cleanup function', () => { + // the returned function removes the cell from the internal cellsUpdateFocus map cleanupFn(); // Note: there's no great way to assert this since cellsUpdateFocus is internal, // so this is mostly here to document behavior and for line coverage @@ -60,45 +47,39 @@ describe('useFocus', () => { }); describe('focusedCell / setFocusedCell', () => { - const { - return: { focusedCell, setFocusedCell }, - getUpdatedState, - } = testCustomHook(() => useFocus(mockArgs)); - it('gets and sets the focusedCell state', () => { - expect(focusedCell).toEqual(undefined); - act(() => setFocusedCell([2, 2])); - expect(getUpdatedState().focusedCell).toEqual([2, 2]); + const { result } = renderHook(() => useFocus(mockArgs)); + expect(result.current.focusedCell).toEqual(undefined); + + renderHookAct(() => result.current.setFocusedCell([2, 2])); + expect(result.current.focusedCell).toEqual([2, 2]); }); it('does not update if setFocusedCell is called with the same cell X/Y coordinates', () => { - const focusedCellInMemory = getUpdatedState().focusedCell; - act(() => getUpdatedState().setFocusedCell([2, 2])); - expect(getUpdatedState().focusedCell).toBe(focusedCellInMemory); // Would fail if the exact same array wasn't returned + const { result } = renderHook(() => useFocus(mockArgs)); + renderHookAct(() => result.current.setFocusedCell([2, 2])); + + const focusedCellInMemory = result.current.focusedCell; + renderHookAct(() => result.current.setFocusedCell([2, 2])); + expect(result.current.focusedCell).toBe(focusedCellInMemory); // Would fail if the exact same array wasn't returned }); }); describe('focusFirstVisibleInteractiveCell', () => { describe('when the sticky header is interactive', () => { it('always focuses the first header cell', () => { - const { - return: { focusFirstVisibleInteractiveCell }, - getUpdatedState, - } = testCustomHook(() => + const { result } = renderHook(() => useFocus({ ...mockArgs, headerIsInteractive: true }) ); - act(() => focusFirstVisibleInteractiveCell()); - expect(getUpdatedState().focusedCell).toEqual([0, -1]); + renderHookAct(() => result.current.focusFirstVisibleInteractiveCell()); + expect(result.current.focusedCell).toEqual([0, -1]); }); }); describe('describe when the header is not interactive', () => { it('focuses the first visible data cell in the virtualized grid', () => { - const { - return: { focusFirstVisibleInteractiveCell }, - getUpdatedState, - } = testCustomHook(() => + const { result } = renderHook(() => useFocus({ headerIsInteractive: false, gridItemsRendered: { @@ -110,23 +91,20 @@ describe('useFocus', () => { }) ); - act(() => focusFirstVisibleInteractiveCell()); - expect(getUpdatedState().focusedCell).toEqual([1, 10]); + renderHookAct(() => result.current.focusFirstVisibleInteractiveCell()); + expect(result.current.focusedCell).toEqual([1, 10]); }); it("does nothing if the grid isn't yet rendered", () => { - const { - return: { focusFirstVisibleInteractiveCell }, - getUpdatedState, - } = testCustomHook(() => + const { result } = renderHook(() => useFocus({ headerIsInteractive: false, gridItemsRendered: { current: null }, }) ); - act(() => focusFirstVisibleInteractiveCell()); - expect(getUpdatedState().focusedCell).toEqual(undefined); + renderHookAct(() => result.current.focusFirstVisibleInteractiveCell()); + expect(result.current.focusedCell).toEqual(undefined); }); }); }); @@ -134,9 +112,8 @@ describe('useFocus', () => { describe('setIsFocusedCellInView / focusProps', () => { describe('when no focused child cell is in view', () => { it('renders the grid with tabindex 0 and an onKeyUp event', () => { - const { - return: { focusProps }, - } = testCustomHook(() => useFocus(mockArgs)); + const { focusProps } = renderHook(() => useFocus(mockArgs)).result + .current; expect(focusProps).toEqual({ tabIndex: 0, @@ -151,61 +128,48 @@ describe('useFocus', () => { ); it('focuses into the first visible cell of the grid when the grid is directly tabbed to', () => { - const { - return: { - focusProps: { onKeyUp }, - }, - getUpdatedState, - } = testCustomHook(() => useFocus(mockArgs)); + const { result } = renderHook(() => useFocus(mockArgs)); - act(() => - onKeyUp!({ + renderHookAct(() => + result.current.focusProps.onKeyUp!({ key: keys.TAB, target: mockGrid, currentTarget: mockGrid, } as any) ); - expect(getUpdatedState().focusedCell).toEqual([0, -1]); + expect(result.current.focusedCell).toEqual([0, -1]); }); it('does nothing if not a tab keyup, or if the event was not on the grid itself', () => { - const { - return: { - focusProps: { onKeyUp }, - }, - getUpdatedState, - } = testCustomHook(() => useFocus(mockArgs)); + const { result } = renderHook(() => useFocus(mockArgs)); - act(() => - onKeyUp!({ + renderHookAct(() => + result.current.focusProps.onKeyUp!({ key: keys.ARROW_DOWN, target: mockGrid, currentTarget: mockGrid, } as any) ); - expect(getUpdatedState().focusedCell).toEqual(undefined); + expect(result.current.focusedCell).toEqual(undefined); - act(() => - onKeyUp!({ + renderHookAct(() => + result.current.focusProps.onKeyUp!({ key: keys.TAB, target: someChild, currentTarget: mockGrid, } as any) ); - expect(getUpdatedState().focusedCell).toEqual(undefined); + expect(result.current.focusedCell).toEqual(undefined); }); }); }); describe('when a focused cell is in view', () => { it('renders the grid with tabindex -1 (because the child cell will already have a tabindex 0)', () => { - const { - return: { setIsFocusedCellInView }, - getUpdatedState, - } = testCustomHook(() => useFocus(mockArgs)); + const { result } = renderHook(() => useFocus(mockArgs)); - act(() => setIsFocusedCellInView(true)); - expect(getUpdatedState().focusProps).toEqual({ + renderHookAct(() => result.current.setIsFocusedCellInView(true)); + expect(result.current.focusProps).toEqual({ tabIndex: -1, }); }); @@ -649,7 +613,7 @@ describe('useHeaderFocusWorkaround', () => { it('moves focus down from the header to the first data row if the header becomes uninteractive', () => { const focusedCell = [2, -1]; const setFocusedCell = jest.fn(); - mount( + render( @@ -662,7 +626,7 @@ describe('useHeaderFocusWorkaround', () => { it('does nothing if the focus was not on the header when the header became uninteractive', () => { const focusedCell = [2, 0]; const setFocusedCell = jest.fn(); - mount( + render( diff --git a/src/components/datagrid/utils/ref.test.ts b/src/components/datagrid/utils/ref.test.ts index eaad811867f..6ba0d3926f7 100644 --- a/src/components/datagrid/utils/ref.test.ts +++ b/src/components/datagrid/utils/ref.test.ts @@ -6,15 +6,14 @@ * Side Public License, v 1. */ -import { testCustomHook } from '../../../test/internal'; +import { renderHook } from '@testing-library/react'; import { useCellLocationCheck, useSortPageCheck } from './ref'; // see ref.spec.tsx for E2E useImperativeGridRef tests describe('useCellLocationCheck', () => { - const { - return: { checkCellExists }, - } = testCustomHook(() => useCellLocationCheck(10, 5)); + const { checkCellExists } = renderHook(() => useCellLocationCheck(10, 5)) + .result.current; it("throws an error if the passed rowIndex is higher than the grid's rowCount", () => { expect(() => { @@ -45,9 +44,9 @@ describe('useSortPageCheck', () => { const sortedRowMap: number[] = []; it('returns the passed rowIndex as-is', () => { - const { - return: { findVisibleRowIndex }, - } = testCustomHook(() => useSortPageCheck(pagination, sortedRowMap)); + const { findVisibleRowIndex } = renderHook(() => + useSortPageCheck(pagination, sortedRowMap) + ).result.current; expect(findVisibleRowIndex(5)).toEqual(5); }); @@ -57,11 +56,11 @@ describe('useSortPageCheck', () => { const pagination = undefined; const sortedRowMap = [3, 4, 1, 2, 0]; - it('returns the visibleRowIndex of the passed rowIndex (which is the index of the sortedRowMap)', () => { - const { - return: { findVisibleRowIndex }, - } = testCustomHook(() => useSortPageCheck(pagination, sortedRowMap)); + const { findVisibleRowIndex } = renderHook(() => + useSortPageCheck(pagination, sortedRowMap) + ).result.current; + it('returns the visibleRowIndex of the passed rowIndex (which is the index of the sortedRowMap)', () => { expect(findVisibleRowIndex(0)).toEqual(4); expect(findVisibleRowIndex(1)).toEqual(2); expect(findVisibleRowIndex(2)).toEqual(3); @@ -80,13 +79,13 @@ describe('useSortPageCheck', () => { }; const sortedRowMap: number[] = []; + const { findVisibleRowIndex } = renderHook(() => + useSortPageCheck(pagination, sortedRowMap) + ).result.current; + beforeEach(() => jest.clearAllMocks()); it('calculates what page the row should be on, paginates to that page, and returns the index of the row on that page', () => { - const { - return: { findVisibleRowIndex }, - } = testCustomHook(() => useSortPageCheck(pagination, sortedRowMap)); - expect(findVisibleRowIndex(20)).toEqual(0); // First item on 2nd page expect(pagination.onChangePage).toHaveBeenLastCalledWith(1); @@ -95,10 +94,6 @@ describe('useSortPageCheck', () => { }); it('does not paginate if the user is already on the correct page', () => { - const { - return: { findVisibleRowIndex }, - } = testCustomHook(() => useSortPageCheck(pagination, sortedRowMap)); - expect(findVisibleRowIndex(5)).toEqual(5); expect(pagination.onChangePage).not.toHaveBeenCalled(); }); diff --git a/src/components/datagrid/utils/scrolling.test.tsx b/src/components/datagrid/utils/scrolling.test.tsx index 8546b6cb9e5..e4c95d9583e 100644 --- a/src/components/datagrid/utils/scrolling.test.tsx +++ b/src/components/datagrid/utils/scrolling.test.tsx @@ -7,8 +7,7 @@ */ import React from 'react'; -import { testCustomHook } from '../../../test/internal'; -import { render } from '../../../test/rtl'; +import { render, renderHook } from '../../../test/rtl'; import { useScrollCellIntoView, useScrollBars } from './scrolling'; // see scrolling.spec.tsx for E2E useScroll tests @@ -53,37 +52,35 @@ describe('useScrollCellIntoView', () => { }); it('does nothing if the grid references are unavailable', () => { - const { - return: { scrollCellIntoView }, - } = testCustomHook(() => + const { scrollCellIntoView } = renderHook(() => useScrollCellIntoView({ ...args, gridRef: { current: null }, outerGridRef: { current: null }, }) - ); + ).result.current; + scrollCellIntoView({ rowIndex: 0, colIndex: 0 }); expect(scrollTo).not.toHaveBeenCalled(); }); it('does nothing if the grid does not scroll', () => { - const { - return: { scrollCellIntoView }, - } = testCustomHook(() => + const { scrollCellIntoView } = renderHook(() => useScrollCellIntoView({ ...args, hasGridScrolling: false, }) - ); + ).result.current; + scrollCellIntoView({ rowIndex: 0, colIndex: 0 }); expect(scrollTo).not.toHaveBeenCalled(); }); it('calls scrollToItem if the specified cell is not virtualized', async () => { getCell.mockReturnValue(null); - const { - return: { scrollCellIntoView }, - } = testCustomHook(() => useScrollCellIntoView(args)); + const { scrollCellIntoView } = renderHook(() => useScrollCellIntoView(args)) + .result.current; + await scrollCellIntoView({ rowIndex: 20, colIndex: 5 }); expect(scrollToItem).toHaveBeenCalledWith({ columnIndex: 5, rowIndex: 20 }); }); @@ -94,11 +91,10 @@ describe('useScrollCellIntoView', () => { parentNode: { offsetTop: 50 }, offsetLeft: 50, }); - const { - return: { scrollCellIntoView }, - } = testCustomHook(() => useScrollCellIntoView(args)); - scrollCellIntoView({ rowIndex: 1, colIndex: 1 }); + const { scrollCellIntoView } = renderHook(() => useScrollCellIntoView(args)) + .result.current; + scrollCellIntoView({ rowIndex: 1, colIndex: 1 }); expect(scrollToItem).not.toHaveBeenCalled(); expect(scrollTo).not.toHaveBeenCalled(); }); @@ -116,16 +112,14 @@ describe('useScrollCellIntoView', () => { }; getCell.mockReturnValue(cell); - const { - return: { scrollCellIntoView }, - } = testCustomHook(() => + const { scrollCellIntoView } = renderHook(() => useScrollCellIntoView({ ...args, outerGridRef: { current: { ...args.outerGridRef.current, ...grid } }, }) - ); - scrollCellIntoView({ rowIndex: 1, colIndex: 5 }); + ).result.current; + scrollCellIntoView({ rowIndex: 1, colIndex: 5 }); expect(scrollTo).toHaveBeenCalledWith({ scrollLeft: 50, scrollTop: 0 }); }); }); @@ -142,16 +136,14 @@ describe('useScrollCellIntoView', () => { }; getCell.mockReturnValue(cell); - const { - return: { scrollCellIntoView }, - } = testCustomHook(() => + const { scrollCellIntoView } = renderHook(() => useScrollCellIntoView({ ...args, outerGridRef: { current: { ...args.outerGridRef.current, ...grid } }, }) - ); - scrollCellIntoView({ rowIndex: 1, colIndex: 0 }); + ).result.current; + scrollCellIntoView({ rowIndex: 1, colIndex: 0 }); expect(scrollTo).toHaveBeenCalledWith({ scrollLeft: 0, scrollTop: 0 }); }); @@ -167,16 +159,14 @@ describe('useScrollCellIntoView', () => { }; getCell.mockReturnValue(cell); - const { - return: { scrollCellIntoView }, - } = testCustomHook(() => + const { scrollCellIntoView } = renderHook(() => useScrollCellIntoView({ ...args, outerGridRef: { current: { ...args.outerGridRef.current, ...grid } }, }) - ); - scrollCellIntoView({ rowIndex: 1, colIndex: 1 }); + ).result.current; + scrollCellIntoView({ rowIndex: 1, colIndex: 1 }); expect(scrollTo).toHaveBeenCalledWith({ scrollLeft: 50, scrollTop: 0 }); }); }); @@ -194,60 +184,56 @@ describe('useScrollCellIntoView', () => { it('scrolls the grid down if the bottom side of the cell is out of view', () => { getCell.mockReturnValue(cell); - const { - return: { scrollCellIntoView }, - } = testCustomHook(() => + const { scrollCellIntoView } = renderHook(() => useScrollCellIntoView({ ...args, outerGridRef: { current: { ...args.outerGridRef.current, ...grid } }, }) - ); + ).result.current; + scrollCellIntoView({ rowIndex: 5, colIndex: 0 }); expect(scrollTo).toHaveBeenCalledWith({ scrollTop: 50, scrollLeft: 0 }); }); it('accounts for the sticky bottom footer if present', () => { getCell.mockReturnValue(cell); - const { - return: { scrollCellIntoView }, - } = testCustomHook(() => + const { scrollCellIntoView } = renderHook(() => useScrollCellIntoView({ ...args, outerGridRef: { current: { ...args.outerGridRef.current, ...grid } }, hasStickyFooter: true, footerRowHeight: 25, }) - ); + ).result.current; + scrollCellIntoView({ rowIndex: 5, colIndex: 0 }); expect(scrollTo).toHaveBeenCalledWith({ scrollTop: 75, scrollLeft: 0 }); }); it('makes no vertical adjustments if the cell is a sticky header cell', () => { getCell.mockReturnValue(cell); - const { - return: { scrollCellIntoView }, - } = testCustomHook(() => + const { scrollCellIntoView } = renderHook(() => useScrollCellIntoView({ ...args, outerGridRef: { current: { ...args.outerGridRef.current, ...grid } }, }) - ); + ).result.current; + scrollCellIntoView({ rowIndex: -1, colIndex: 0 }); expect(scrollTo).not.toHaveBeenCalled(); }); it('makes no vertical adjustments if the cell is a sticky footer cell', () => { getCell.mockReturnValue(cell); - const { - return: { scrollCellIntoView }, - } = testCustomHook(() => + const { scrollCellIntoView } = renderHook(() => useScrollCellIntoView({ ...args, outerGridRef: { current: { ...args.outerGridRef.current, ...grid } }, visibleRowCount: 25, hasStickyFooter: true, }) - ); + ).result.current; + scrollCellIntoView({ rowIndex: 25, colIndex: 0 }); expect(scrollTo).not.toHaveBeenCalled(); }); @@ -265,29 +251,27 @@ describe('useScrollCellIntoView', () => { it('scrolls the grid up if the top side of the cell is out of view', () => { getCell.mockReturnValue(cell); - const { - return: { scrollCellIntoView }, - } = testCustomHook(() => + const { scrollCellIntoView } = renderHook(() => useScrollCellIntoView({ ...args, outerGridRef: { current: { ...args.outerGridRef.current, ...grid } }, }) - ); + ).result.current; + scrollCellIntoView({ rowIndex: 1, colIndex: 0 }); expect(scrollTo).toHaveBeenCalledWith({ scrollTop: 50, scrollLeft: 0 }); }); it('accounts for the sticky header', () => { getCell.mockReturnValue(cell); - const { - return: { scrollCellIntoView }, - } = testCustomHook(() => + const { scrollCellIntoView } = renderHook(() => useScrollCellIntoView({ ...args, outerGridRef: { current: { ...args.outerGridRef.current, ...grid } }, headerRowHeight: 30, }) - ); + ).result.current; + scrollCellIntoView({ rowIndex: 1, colIndex: 0 }); expect(scrollTo).toHaveBeenCalledWith({ scrollTop: 20, scrollLeft: 0 }); }); @@ -304,46 +288,42 @@ describe('useScrollCellIntoView', () => { }; getCell.mockReturnValue(cell); - const { - return: { scrollCellIntoView }, - } = testCustomHook(() => + const { scrollCellIntoView } = renderHook(() => useScrollCellIntoView({ ...args, outerGridRef: { current: { ...args.outerGridRef.current, ...grid } }, headerRowHeight: 50, }) - ); - scrollCellIntoView({ rowIndex: 1, colIndex: 0 }); + ).result.current; + scrollCellIntoView({ rowIndex: 1, colIndex: 0 }); expect(scrollTo).toHaveBeenCalledWith({ scrollTop: 50, scrollLeft: 0 }); }); it('makes no vertical adjustments if the cell is a sticky header cell', () => { getCell.mockReturnValue(cell); - const { - return: { scrollCellIntoView }, - } = testCustomHook(() => + const { scrollCellIntoView } = renderHook(() => useScrollCellIntoView({ ...args, outerGridRef: { current: { ...args.outerGridRef.current, ...grid } }, }) - ); + ).result.current; + scrollCellIntoView({ rowIndex: -1, colIndex: 0 }); expect(scrollTo).not.toHaveBeenCalled(); }); it('makes no vertical adjustments if the cell is a sticky footer cell', () => { getCell.mockReturnValue(cell); - const { - return: { scrollCellIntoView }, - } = testCustomHook(() => + const { scrollCellIntoView } = renderHook(() => useScrollCellIntoView({ ...args, outerGridRef: { current: { ...args.outerGridRef.current, ...grid } }, visibleRowCount: 25, hasStickyFooter: true, }) - ); + ).result.current; + scrollCellIntoView({ rowIndex: 25, colIndex: 0 }); expect(scrollTo).not.toHaveBeenCalled(); }); @@ -362,13 +342,11 @@ describe('useScrollBars', () => { describe('scrollBarHeight', () => { it("is derived by the difference between the grid's offsetHeight vs clientHeight", () => { - const { - return: { scrollBarHeight }, - } = testCustomHook(() => + const { scrollBarHeight } = renderHook(() => useScrollBars({ current: { ...mockOuterGrid, clientHeight: 40, offsetHeight: 50 }, }) - ); + ).result.current; expect(scrollBarHeight).toEqual(10); }); @@ -376,13 +354,11 @@ describe('useScrollBars', () => { describe('scrollBarWidth', () => { it('is zero if there is no difference between offsetWidth and clientWidth', () => { - const { - return: { scrollBarWidth }, - } = testCustomHook(() => + const { scrollBarWidth } = renderHook(() => useScrollBars({ current: { ...mockOuterGrid, clientWidth: 40, offsetWidth: 40 }, }) - ); + ).result.current; expect(scrollBarWidth).toEqual(0); }); @@ -390,25 +366,21 @@ describe('useScrollBars', () => { describe('hasVerticalScroll', () => { it("has scrolling overflow if the grid's scrollHeight exceeds its clientHeight", () => { - const { - return: { hasVerticalScroll }, - } = testCustomHook(() => + const { hasVerticalScroll } = renderHook(() => useScrollBars({ current: { ...mockOuterGrid, clientHeight: 50, scrollHeight: 100 }, }) - ); + ).result.current; expect(hasVerticalScroll).toEqual(true); }); it("does not have scrolling overflow if the the grid's scrollHeight is the same as its clientHeight", () => { - const { - return: { hasVerticalScroll }, - } = testCustomHook(() => + const { hasVerticalScroll } = renderHook(() => useScrollBars({ current: { ...mockOuterGrid, clientHeight: 50, scrollHeight: 50 }, }) - ); + ).result.current; expect(hasVerticalScroll).toEqual(false); }); @@ -416,25 +388,21 @@ describe('useScrollBars', () => { describe('hasHorizontalScroll', () => { it("has scrolling overflow if the grid's scrollWidth exceeds its clientWidth", () => { - const { - return: { hasHorizontalScroll }, - } = testCustomHook(() => + const { hasHorizontalScroll } = renderHook(() => useScrollBars({ current: { ...mockOuterGrid, clientWidth: 100, scrollWidth: 200 }, }) - ); + ).result.current; expect(hasHorizontalScroll).toEqual(true); }); it("does not have scrolling overflow if the the grid's scrollWidth is the same as its clientWidth", () => { - const { - return: { hasHorizontalScroll }, - } = testCustomHook(() => + const { hasHorizontalScroll } = renderHook(() => useScrollBars({ current: { ...mockOuterGrid, clientWidth: 200, scrollWidth: 200 }, }) - ); + ).result.current; expect(hasHorizontalScroll).toEqual(false); }); @@ -443,9 +411,7 @@ describe('useScrollBars', () => { describe('scrollBorderOverlay', () => { describe('if the grid does not scroll', () => { it('does not render anything', () => { - const { - return: { scrollBorderOverlay }, - } = testCustomHook(() => + const { scrollBorderOverlay } = renderHook(() => useScrollBars({ current: { ...mockOuterGrid, @@ -455,7 +421,7 @@ describe('useScrollBars', () => { scrollWidth: 200, }, }) - ); + ).result.current; expect(scrollBorderOverlay).toEqual(null); }); @@ -463,9 +429,7 @@ describe('useScrollBars', () => { describe('if the grid does not display borders', () => { it('does not render anything', () => { - const { - return: { scrollBorderOverlay }, - } = testCustomHook(() => + const { scrollBorderOverlay } = renderHook(() => useScrollBars( { current: { @@ -476,7 +440,7 @@ describe('useScrollBars', () => { }, 'none' ) - ); + ).result.current; expect(scrollBorderOverlay).toEqual(null); }); @@ -484,9 +448,7 @@ describe('useScrollBars', () => { describe('if the grid scrolls but has inline scrollbars & no scrollbar width/height', () => { it('renders a single overlay with borders for the outermost grid', () => { - const { - return: { scrollBorderOverlay }, - } = testCustomHook(() => + const { scrollBorderOverlay } = renderHook(() => useScrollBars({ current: { ...mockOuterGrid, @@ -498,7 +460,7 @@ describe('useScrollBars', () => { scrollWidth: 200, }, }) - ); + ).result.current; const { container } = render(<>{scrollBorderOverlay}); expect(container.firstChild).toMatchInlineSnapshot(` @@ -512,9 +474,7 @@ describe('useScrollBars', () => { describe('if the grid scrolls and has scrollbars that take up width/height', () => { it('renders a top border for the bottom scrollbar', () => { - const { - return: { scrollBorderOverlay }, - } = testCustomHook(() => + const { scrollBorderOverlay } = renderHook(() => useScrollBars({ current: { ...mockOuterGrid, @@ -526,7 +486,7 @@ describe('useScrollBars', () => { scrollWidth: 100, }, }) - ); + ).result.current; const { container } = render(<>{scrollBorderOverlay}); expect(container.firstChild).toMatchInlineSnapshot(` @@ -543,9 +503,7 @@ describe('useScrollBars', () => { }); it('renders a left border for the bottom scrollbar', () => { - const { - return: { scrollBorderOverlay }, - } = testCustomHook(() => + const { scrollBorderOverlay } = renderHook(() => useScrollBars({ current: { ...mockOuterGrid, @@ -557,7 +515,7 @@ describe('useScrollBars', () => { scrollWidth: 200, }, }) - ); + ).result.current; const { container } = render(<>{scrollBorderOverlay}); expect(container.firstChild).toMatchInlineSnapshot(` @@ -577,7 +535,7 @@ describe('useScrollBars', () => { it('returns falsey values if outerGridRef is not yet instantiated', () => { expect( - testCustomHook(() => useScrollBars({ current: null })).return + renderHook(() => useScrollBars({ current: null })).result.current ).toEqual({ scrollBarHeight: 0, scrollBarWidth: 0, diff --git a/src/components/date_picker/super_date_picker/pretty_duration.test.tsx b/src/components/date_picker/super_date_picker/pretty_duration.test.tsx index a3818586f4a..92004915bf2 100644 --- a/src/components/date_picker/super_date_picker/pretty_duration.test.tsx +++ b/src/components/date_picker/super_date_picker/pretty_duration.test.tsx @@ -7,8 +7,7 @@ */ import React from 'react'; -import { shallow } from 'enzyme'; -import { testCustomHook } from '../../../test/internal'; +import { render, renderHook } from '../../../test/rtl'; import { usePrettyDuration, @@ -30,9 +29,9 @@ describe('usePrettyDuration', () => { const timeFrom = 'now-15m'; const timeTo = 'now'; expect( - testCustomHook(() => + renderHook(() => usePrettyDuration({ timeFrom, timeTo, quickRanges, dateFormat }) - ).return + ).result.current ).toBe('quick range 15 minutes custom display'); }); @@ -40,9 +39,9 @@ describe('usePrettyDuration', () => { const timeFrom = 'now-16m'; const timeTo = 'now'; expect( - testCustomHook(() => + renderHook(() => usePrettyDuration({ timeFrom, timeTo, quickRanges, dateFormat }) - ).return + ).result.current ).toBe('Last 16 minutes'); }); @@ -50,9 +49,9 @@ describe('usePrettyDuration', () => { const timeFrom = 'now-1M/w'; const timeTo = 'now'; expect( - testCustomHook(() => + renderHook(() => usePrettyDuration({ timeFrom, timeTo, quickRanges, dateFormat }) - ).return + ).result.current ).toBe('Last 1 month rounded to the week'); }); @@ -60,9 +59,9 @@ describe('usePrettyDuration', () => { const timeFrom = 'now'; const timeTo = 'now+16m'; expect( - testCustomHook(() => + renderHook(() => usePrettyDuration({ timeFrom, timeTo, quickRanges, dateFormat }) - ).return + ).result.current ).toBe('Next 16 minutes'); }); @@ -70,16 +69,16 @@ describe('usePrettyDuration', () => { const timeFrom = 'now-17m'; const timeTo = 'now-15m'; expect( - testCustomHook(() => + renderHook(() => usePrettyDuration({ timeFrom, timeTo, quickRanges, dateFormat }) - ).return + ).result.current ).toBe('~ 17 minutes ago to ~ 15 minutes ago'); }); }); describe('PrettyDuration', () => { it('renders the returned string from usePrettyDuration', () => { - const component = shallow( + const { container } = render( { dateFormat={dateFormat} /> ); - expect(component).toMatchInlineSnapshot(` - - Next 15 minutes - - `); + expect(container.firstChild).toMatchInlineSnapshot(`Next 15 minutes`); }); }); diff --git a/src/components/date_picker/super_date_picker/pretty_interval.test.ts b/src/components/date_picker/super_date_picker/pretty_interval.test.ts index fb0d9339966..ad5b563dd8f 100644 --- a/src/components/date_picker/super_date_picker/pretty_interval.test.ts +++ b/src/components/date_picker/super_date_picker/pretty_interval.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { testCustomHook } from '../../../test/internal'; +import { renderHook } from '@testing-library/react'; import { usePrettyInterval } from './pretty_interval'; @@ -17,66 +17,65 @@ const SHORT_HAND = true; describe('usePrettyInterval', () => { test('off', () => { expect( - testCustomHook(() => usePrettyInterval(IS_NOT_PAUSED, 0)).return + renderHook(() => usePrettyInterval(IS_NOT_PAUSED, 0)).result.current ).toBe('Off'); expect( - testCustomHook(() => usePrettyInterval(IS_PAUSED, 1000)).return + renderHook(() => usePrettyInterval(IS_PAUSED, 1000)).result.current ).toBe('Off'); }); test('seconds', () => { expect( - testCustomHook(() => usePrettyInterval(IS_NOT_PAUSED, 1000)).return + renderHook(() => usePrettyInterval(IS_NOT_PAUSED, 1000)).result.current ).toBe('1 second'); expect( - testCustomHook(() => usePrettyInterval(IS_NOT_PAUSED, 15000)).return + renderHook(() => usePrettyInterval(IS_NOT_PAUSED, 15000)).result.current ).toBe('15 seconds'); expect( - testCustomHook(() => usePrettyInterval(IS_NOT_PAUSED, 15000, SHORT_HAND)) - .return + renderHook(() => usePrettyInterval(IS_NOT_PAUSED, 15000, SHORT_HAND)) + .result.current ).toBe('15 s'); }); test('minutes', () => { expect( - testCustomHook(() => usePrettyInterval(IS_NOT_PAUSED, 60000)).return + renderHook(() => usePrettyInterval(IS_NOT_PAUSED, 60000)).result.current ).toBe('1 minute'); expect( - testCustomHook(() => usePrettyInterval(IS_NOT_PAUSED, 1800000)).return + renderHook(() => usePrettyInterval(IS_NOT_PAUSED, 1800000)).result.current ).toBe('30 minutes'); expect( - testCustomHook(() => - usePrettyInterval(IS_NOT_PAUSED, 1800000, SHORT_HAND) - ).return + renderHook(() => usePrettyInterval(IS_NOT_PAUSED, 1800000, SHORT_HAND)) + .result.current ).toBe('30 m'); }); test('hours', () => { expect( - testCustomHook(() => usePrettyInterval(IS_NOT_PAUSED, 3600000)).return + renderHook(() => usePrettyInterval(IS_NOT_PAUSED, 3600000)).result.current ).toBe('1 hour'); expect( - testCustomHook(() => usePrettyInterval(IS_NOT_PAUSED, 43200000)).return + renderHook(() => usePrettyInterval(IS_NOT_PAUSED, 43200000)).result + .current ).toBe('12 hours'); expect( - testCustomHook(() => - usePrettyInterval(IS_NOT_PAUSED, 43200000, SHORT_HAND) - ).return + renderHook(() => usePrettyInterval(IS_NOT_PAUSED, 43200000, SHORT_HAND)) + .result.current ).toBe('12 h'); }); test('days', () => { expect( - testCustomHook(() => usePrettyInterval(IS_NOT_PAUSED, 86400000)).return + renderHook(() => usePrettyInterval(IS_NOT_PAUSED, 86400000)).result + .current ).toBe('1 day'); expect( - testCustomHook(() => usePrettyInterval(IS_NOT_PAUSED, 86400000 * 2)) - .return + renderHook(() => usePrettyInterval(IS_NOT_PAUSED, 86400000 * 2)).result + .current ).toBe('2 days'); expect( - testCustomHook(() => - usePrettyInterval(IS_NOT_PAUSED, 86400000, SHORT_HAND) - ).return + renderHook(() => usePrettyInterval(IS_NOT_PAUSED, 86400000, SHORT_HAND)) + .result.current ).toBe('1 d'); }); }); diff --git a/src/components/date_picker/super_date_picker/time_options.test.tsx b/src/components/date_picker/super_date_picker/time_options.test.tsx index 27c70bb3c17..f256fbbdab0 100644 --- a/src/components/date_picker/super_date_picker/time_options.test.tsx +++ b/src/components/date_picker/super_date_picker/time_options.test.tsx @@ -7,14 +7,13 @@ */ import React from 'react'; -import { shallow } from 'enzyme'; -import { testCustomHook } from '../../../test/internal'; +import { render, renderHook } from '../../../test/rtl'; import { useI18nTimeOptions, RenderI18nTimeOptions } from './time_options'; describe('useI18nTimeOptions', () => { it('returns a series of time options constants/arrays/objects with i18n strings', () => { - const { return: timeOptions } = testCustomHook(useI18nTimeOptions); + const timeOptions = renderHook(useI18nTimeOptions).result.current; expect(timeOptions).toMatchInlineSnapshot(` Object { @@ -188,16 +187,12 @@ describe('useI18nTimeOptions', () => { describe('RenderI18nTimeOptions', () => { it('is a render function that passes the underlying children timeOptions as an arg', () => { - const component = shallow( + const { container } = render( {({ timeUnitsOptions }) => <>{timeUnitsOptions[0].text}} ); - expect(component).toMatchInlineSnapshot(` - - Seconds - - `); + expect(container.firstChild).toMatchInlineSnapshot(`Seconds`); }); }); diff --git a/src/components/markdown_editor/markdown_format.styles.test.ts b/src/components/markdown_editor/markdown_format.styles.test.ts index 656a253e526..6cef488d674 100644 --- a/src/components/markdown_editor/markdown_format.styles.test.ts +++ b/src/components/markdown_editor/markdown_format.styles.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { testCustomHook } from '../../test/internal'; +import { renderHook } from '@testing-library/react'; import { useEuiTheme } from '../../services'; import { TEXT_SIZES } from '../text/text'; @@ -15,9 +15,9 @@ import { euiMarkdownFormatStyles } from './markdown_format.styles'; describe('euiMarkdownFormat text sizes', () => { TEXT_SIZES.forEach((size) => { test(size, () => { - const emotionReturn = testCustomHook(() => + const emotionReturn = renderHook(() => euiMarkdownFormatStyles(useEuiTheme()) - ).return as any; + ).result.current; expect(emotionReturn[size].styles).toMatchSnapshot(); }); }); diff --git a/src/components/progress/progress.styles.test.ts b/src/components/progress/progress.styles.test.ts index c1002f48427..4dda29e4e12 100644 --- a/src/components/progress/progress.styles.test.ts +++ b/src/components/progress/progress.styles.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { testCustomHook } from '../../test/internal'; +import { renderHook } from '@testing-library/react'; import { useEuiTheme } from '../../services'; import { POSITIONS, COLORS } from './progress'; @@ -15,9 +15,9 @@ import { euiProgressStyles } from './progress.styles'; describe('euiProgressStyles', () => { describe('native progress CSS', () => { const isNative = true; - const emotionReturn = testCustomHook(() => + const emotionReturn = renderHook(() => euiProgressStyles(useEuiTheme(), isNative) - ).return as any; + ).result.current; describe('positions', () => { POSITIONS.forEach((position) => { @@ -38,9 +38,9 @@ describe('euiProgressStyles', () => { describe('indeterminate div CSS', () => { const isNative = false; - const emotionReturn = testCustomHook(() => + const emotionReturn = renderHook(() => euiProgressStyles(useEuiTheme(), isNative) - ).return as any; + ).result.current; describe('positions', () => { POSITIONS.forEach((position) => { diff --git a/src/components/text/text.styles.test.ts b/src/components/text/text.styles.test.ts index 9407755296e..d90aa4cd3a8 100644 --- a/src/components/text/text.styles.test.ts +++ b/src/components/text/text.styles.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { testCustomHook } from '../../test/internal'; +import { renderHook } from '@testing-library/react'; import { useEuiTheme } from '../../services'; import { TEXT_SIZES } from './text'; @@ -15,8 +15,8 @@ import { euiTextStyles } from './text.styles'; describe('euiTextStyles sizes', () => { TEXT_SIZES.forEach((size) => { test(size, () => { - const emotionReturn = testCustomHook(() => euiTextStyles(useEuiTheme())) - .return as any; + const emotionReturn = renderHook(() => euiTextStyles(useEuiTheme())) + .result.current; expect(emotionReturn[size].styles).toMatchSnapshot(); }); }); diff --git a/src/components/title/title.styles.test.ts b/src/components/title/title.styles.test.ts index 1428f5e21a1..d270a0ed775 100644 --- a/src/components/title/title.styles.test.ts +++ b/src/components/title/title.styles.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { testCustomHook } from '../../test/internal'; +import { renderHook } from '@testing-library/react'; import { TITLE_SIZES } from './title'; import { useEuiTitle } from './title.styles'; @@ -16,7 +16,7 @@ describe('euiTitle mixin', () => { TITLE_SIZES.forEach((size) => { it(size, () => { expect( - testCustomHook(() => useEuiTitle(size)).return + renderHook(() => useEuiTitle(size)).result.current ).toMatchSnapshot(); }); }); diff --git a/src/global_styling/mixins/_color.test.ts b/src/global_styling/mixins/_color.test.ts index c0b98fb5c67..7f7470c31e5 100644 --- a/src/global_styling/mixins/_color.test.ts +++ b/src/global_styling/mixins/_color.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { testCustomHook } from '../../test/internal'; +import { renderHook } from '@testing-library/react'; import { BACKGROUND_COLORS, useEuiBackgroundColor, @@ -18,16 +18,16 @@ describe('useEuiBackgroundColor mixin returns a calculated background version', BACKGROUND_COLORS.forEach((color) => { it(color, () => { expect( - testCustomHook(() => useEuiBackgroundColor(color)).return + renderHook(() => useEuiBackgroundColor(color)).result.current ).toMatchSnapshot(); }); describe('as transparent', () => { it(color, () => { expect( - testCustomHook(() => + renderHook(() => useEuiBackgroundColor(color, { method: 'transparent' }) - ).return + ).result.current ).toMatchSnapshot(); }); }); @@ -36,7 +36,7 @@ describe('useEuiBackgroundColor mixin returns a calculated background version', }); describe('useEuiBackgroundColorCSS hook returns an object of Emotion background-color properties', () => { - const colors = testCustomHook(useEuiBackgroundColorCSS).return as any; + const colors = renderHook(useEuiBackgroundColorCSS).result.current as any; describe('for each color:', () => { Object.entries(colors).map(([color, cssObj]) => { diff --git a/src/global_styling/mixins/_padding.test.ts b/src/global_styling/mixins/_padding.test.ts index 92b388c33f7..4dcbcfb54a0 100644 --- a/src/global_styling/mixins/_padding.test.ts +++ b/src/global_styling/mixins/_padding.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { testCustomHook } from '../../test/internal'; +import { renderHook } from '@testing-library/react'; import { PADDING_SIZES, useEuiPaddingSize, useEuiPaddingCSS } from './_padding'; import { LOGICAL_SIDES } from '../functions/logicals'; @@ -15,7 +15,7 @@ describe('useEuiPaddingSize returns a static padding value', () => { PADDING_SIZES.forEach((size) => { it(size, () => { expect( - testCustomHook(() => useEuiPaddingSize(size)).return + renderHook(() => useEuiPaddingSize(size)).result.current ).toMatchSnapshot(); }); }); @@ -25,7 +25,7 @@ describe('useEuiPaddingSize returns a static padding value', () => { describe('useEuiPaddingCSS hook returns an object of Emotion padding properties', () => { LOGICAL_SIDES.forEach((side) => { describe(`for side: ${side},`, () => { - const sizes = testCustomHook(() => useEuiPaddingCSS(side)).return as any; + const sizes = renderHook(() => useEuiPaddingCSS(side)).result.current; describe('for each size:', () => { Object.entries(sizes).map(([size, cssObj]) => { diff --git a/src/global_styling/mixins/_responsive.test.ts b/src/global_styling/mixins/_responsive.test.ts index 6c5da7c013f..eb2de4e1cb2 100644 --- a/src/global_styling/mixins/_responsive.test.ts +++ b/src/global_styling/mixins/_responsive.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { testCustomHook } from '../../test/internal'; +import { renderHook } from '@testing-library/react'; import { EuiThemeBreakpoints, _EuiThemeBreakpoint } from '../variables'; import { useEuiBreakpoint, @@ -35,12 +35,12 @@ describe('useEuiBreakpoint', () => { 'returns a media query for two element breakpoint combinations (%s and %s)', (minSize, maxSize) => { expect( - testCustomHook(() => + renderHook(() => useEuiBreakpoint([ minSize as _EuiThemeBreakpoint, maxSize as _EuiThemeBreakpoint, ]) - ).return + ).result.current ).toMatchSnapshot(); } ); @@ -50,7 +50,7 @@ describe('useEuiBreakpoint', () => { EuiThemeBreakpoints.forEach((size) => { test(`(${size})`, () => { expect( - testCustomHook(() => useEuiBreakpoint([size])).return + renderHook(() => useEuiBreakpoint([size])).result.current ).toMatchSnapshot(); }); }); @@ -59,7 +59,7 @@ describe('useEuiBreakpoint', () => { describe('breakpoint size arrays with more than 2 sizes', () => { it('should use the first and last items in the array', () => { expect( - testCustomHook(() => useEuiBreakpoint(['s', 'm', 'l'])).return + renderHook(() => useEuiBreakpoint(['s', 'm', 'l'])).result.current ).toMatchInlineSnapshot( '"@media only screen and (min-width: 575px) and (max-width: 1199px)"' ); @@ -67,7 +67,7 @@ describe('useEuiBreakpoint', () => { it('handles sorting the array if sizes are passed in the wrong order', () => { expect( - testCustomHook(() => useEuiBreakpoint(['l', 's', 'm'])).return + renderHook(() => useEuiBreakpoint(['l', 's', 'm'])).result.current ).toMatchInlineSnapshot( '"@media only screen and (min-width: 575px) and (max-width: 1199px)"' ); @@ -76,13 +76,13 @@ describe('useEuiBreakpoint', () => { it('does not generate a min-width if the min size is xs', () => { expect( - testCustomHook(() => useEuiBreakpoint(['xs', 'm'])).return + renderHook(() => useEuiBreakpoint(['xs', 'm'])).result.current ).toMatchInlineSnapshot('"@media only screen and (max-width: 991px)"'); }); it('skips generating a max-width if the max size is xl', () => { expect( - testCustomHook(() => useEuiBreakpoint(['m', 'xl'])).return + renderHook(() => useEuiBreakpoint(['m', 'xl'])).result.current ).toMatchInlineSnapshot('"@media only screen and (min-width: 768px)"'); }); @@ -91,15 +91,15 @@ describe('useEuiBreakpoint', () => { test('if at least one input is not passed', () => { // @ts-expect-error Source has 0 element(s) but target requires 1 - expect(testCustomHook(() => useEuiBreakpoint([])).return).toEqual( + expect(renderHook(() => useEuiBreakpoint([])).result.current).toEqual( fallbackOutput ); }); test('if a breakpoint key without a corresponding value is passed', () => { - expect(testCustomHook(() => useEuiBreakpoint(['asdf'])).return).toEqual( - fallbackOutput - ); + expect( + renderHook(() => useEuiBreakpoint(['asdf'])).result.current + ).toEqual(fallbackOutput); }); }); }); @@ -109,7 +109,7 @@ describe('euiMinBreakpoint', () => { EuiThemeBreakpoints.slice(1).forEach((size) => { it(`(${size})`, () => { expect( - testCustomHook(() => useEuiMinBreakpoint(size)).return + renderHook(() => useEuiMinBreakpoint(size)).result.current ).toMatchSnapshot(); }); }); @@ -122,14 +122,14 @@ describe('euiMinBreakpoint', () => { it('warns if using min-width on a breakpoint that equals 0px', () => { // This functionally does nothing, hence the warning expect( - testCustomHook(() => useEuiMinBreakpoint('xs')).return + renderHook(() => useEuiMinBreakpoint('xs')).result.current ).toMatchInlineSnapshot('"@media only screen"'); expect(warnSpy).toHaveBeenCalledWith('Invalid min breakpoint size: xs'); }); it('warns if an invalid size is passed', () => { expect( - testCustomHook(() => useEuiMinBreakpoint('asdf')).return + renderHook(() => useEuiMinBreakpoint('asdf')).result.current ).toMatchInlineSnapshot('"@media only screen"'); expect(warnSpy).toHaveBeenCalledWith('Invalid min breakpoint size: asdf'); }); @@ -141,7 +141,7 @@ describe('euiMaxBreakpoint', () => { EuiThemeBreakpoints.slice(1).forEach((size) => { it(`(${size})`, () => { expect( - testCustomHook(() => useEuiMaxBreakpoint(size)).return + renderHook(() => useEuiMaxBreakpoint(size)).result.current ).toMatchSnapshot(); }); }); @@ -154,14 +154,14 @@ describe('euiMaxBreakpoint', () => { it('warns if using max-width on a breakpoint that equals 0px', () => { // This functionally does nothing, hence the warning expect( - testCustomHook(() => useEuiMaxBreakpoint('xs')).return + renderHook(() => useEuiMaxBreakpoint('xs')).result.current ).toMatchInlineSnapshot('"@media only screen"'); expect(warnSpy).toHaveBeenCalledWith('Invalid max breakpoint size: xs'); }); it('warns if an invalid size is passed', () => { expect( - testCustomHook(() => useEuiMaxBreakpoint('asdf')).return + renderHook(() => useEuiMaxBreakpoint('asdf')).result.current ).toMatchInlineSnapshot('"@media only screen"'); expect(warnSpy).toHaveBeenCalledWith('Invalid max breakpoint size: asdf'); }); diff --git a/src/global_styling/mixins/_states.test.ts b/src/global_styling/mixins/_states.test.ts index 53f81f0d677..c00026ca290 100644 --- a/src/global_styling/mixins/_states.test.ts +++ b/src/global_styling/mixins/_states.test.ts @@ -6,26 +6,26 @@ * Side Public License, v 1. */ -import { testCustomHook } from '../../test/internal'; +import { renderHook } from '@testing-library/react'; import { useEuiFocusRing } from './_states'; describe('useEuiFocusRing hook returns a string', () => { describe('for each offset:', () => { it('inset', () => { expect( - testCustomHook(() => useEuiFocusRing('inset')).return + renderHook(() => useEuiFocusRing('inset')).result.current ).toMatchSnapshot(); }); it('outset', () => { expect( - testCustomHook(() => useEuiFocusRing('outset')).return + renderHook(() => useEuiFocusRing('outset')).result.current ).toMatchSnapshot(); }); it('16px', () => { expect( - testCustomHook(() => useEuiFocusRing('16px')).return + renderHook(() => useEuiFocusRing('16px')).result.current ).toMatchSnapshot(); }); }); @@ -33,7 +33,7 @@ describe('useEuiFocusRing hook returns a string', () => { describe('for any color:', () => { it('blue', () => { expect( - testCustomHook(() => useEuiFocusRing('outset', 'blue')).return + renderHook(() => useEuiFocusRing('outset', 'blue')).result.current ).toMatchSnapshot(); }); }); diff --git a/src/global_styling/mixins/_typography.test.ts b/src/global_styling/mixins/_typography.test.ts index 3ce71130925..a76dcb43a89 100644 --- a/src/global_styling/mixins/_typography.test.ts +++ b/src/global_styling/mixins/_typography.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { testCustomHook } from '../../test/internal'; +import { renderHook } from '@testing-library/react'; import { EuiThemeFontScales, EuiThemeFontUnits } from '../variables/typography'; import { @@ -23,7 +23,7 @@ describe('euiFontSize', () => { EuiThemeFontScales.forEach((size) => { test(size, () => { expect( - testCustomHook(() => useEuiFontSize(size, { unit })).return + renderHook(() => useEuiFontSize(size, { unit })).result.current ).toMatchSnapshot(); }); }); @@ -33,13 +33,16 @@ describe('euiFontSize', () => { it('handles the optional customScale property by multiplying it against the passed scale', () => { expect( - testCustomHook(() => useEuiFontSize('m', { customScale: 'xs' })).return + renderHook(() => useEuiFontSize('m', { customScale: 'xs' })).result + .current ).toMatchSnapshot({}, 'm scale with xs customScale'); expect( - testCustomHook(() => useEuiFontSize('l', { customScale: 'xxs' })).return + renderHook(() => useEuiFontSize('l', { customScale: 'xxs' })).result + .current ).toMatchSnapshot({}, 'l scale with xxs customScale'); expect( - testCustomHook(() => useEuiFontSize('s', { customScale: 'xl' })).return + renderHook(() => useEuiFontSize('s', { customScale: 'xl' })).result + .current ).toMatchSnapshot({}, 's scale with xl customScale'); }); }); @@ -62,6 +65,8 @@ describe('euiTextTruncate', () => { describe('euiNumberFormat', () => { it('returns a string of CSS text', () => { - expect(testCustomHook(() => useEuiNumberFormat()).return).toMatchSnapshot(); + expect( + renderHook(() => useEuiNumberFormat()).result.current + ).toMatchSnapshot(); }); }); diff --git a/src/global_styling/utility/__snapshots__/utility.test.tsx.snap b/src/global_styling/utility/__snapshots__/utility.test.ts.snap similarity index 100% rename from src/global_styling/utility/__snapshots__/utility.test.tsx.snap rename to src/global_styling/utility/__snapshots__/utility.test.ts.snap diff --git a/src/global_styling/utility/utility.test.tsx b/src/global_styling/utility/utility.test.ts similarity index 63% rename from src/global_styling/utility/utility.test.tsx rename to src/global_styling/utility/utility.test.ts index 7a5a1725dce..a28ff43c999 100644 --- a/src/global_styling/utility/utility.test.tsx +++ b/src/global_styling/utility/utility.test.ts @@ -6,20 +6,18 @@ * Side Public License, v 1. */ -import { testCustomHook } from '../../test/internal'; +import { renderHook } from '@testing-library/react'; import { useEuiTheme } from '../../services'; import { globalStyles } from './utility'; describe('global utility styles', () => { - const useTestHook = () => { - const euiTheme = useEuiTheme(); - return globalStyles(euiTheme); - }; - it('generates static global styles', () => { - const rawStyles = (testCustomHook(useTestHook) as any).return.styles; + const { result } = renderHook(() => { + const euiTheme = useEuiTheme(); + return globalStyles(euiTheme); + }); // Make Emotion's minification a little less annoying to read - const globalStyles = rawStyles.replace(/}\.eui-/g, '}\n.eui-'); - expect(globalStyles).toMatchSnapshot(); + const styles = result.current.styles.replace(/}\.eui-/g, '}\n.eui-'); + expect(styles).toMatchSnapshot(); }); }); diff --git a/src/test/README.md b/src/test/README.md index f76faa2119f..e8d174509b2 100644 --- a/src/test/README.md +++ b/src/test/README.md @@ -42,18 +42,3 @@ expect( }) ).toMatchSnapshot(); ``` - -### testCustomHook - -Use this function to execute a custom hook and access its return value, which can be passed to `expect` for verification. The function also returns a `getUpdatedState` method which will always return the hook's current state, in case some interaction with the react component causes an update. - -```js -const { - return: hookReturnValue, - getUpdatedState, -} = testCustomHook(() => useCellPopover()); - -expect(hookReturnValue).to.equal(myExpectedValue); -doSomeActions(); -expect(getUpdatedState()).to.equal(myExpectedValue); -``` diff --git a/src/test/internal/index.ts b/src/test/internal/index.ts index ec088111b19..434f848588c 100644 --- a/src/test/internal/index.ts +++ b/src/test/internal/index.ts @@ -7,5 +7,4 @@ */ export * from './render_custom_styles'; -export * from './test_custom_hook'; export * from './react_version'; diff --git a/src/test/internal/test_custom_hook.tsx b/src/test/internal/test_custom_hook.tsx deleted file mode 100644 index 5db54cf2298..00000000000 --- a/src/test/internal/test_custom_hook.tsx +++ /dev/null @@ -1,41 +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 } from 'enzyme'; - -export const HookWrapper = (props: { hook?: Function }) => { - const { hook: _, ...args } = props; - const hook = props.hook ? props.hook(args) : undefined; - // @ts-ignore the actual div is irrelevant, we just need to inspect the prop for return values - return
; -}; - -export const testCustomHook = ( - hook?: Function, - args?: unknown -): { - return: T; - getUpdatedState: () => T; - updateHookArgs: (args: unknown) => void; -} => { - const wrapper = mount(); - - const updateHookArgs = (args: any) => wrapper.setProps(args); - - const getHookReturn = (): T => { - wrapper.update(); - return wrapper.find('div').prop('hook'); - }; - - return { - return: getHookReturn(), - getUpdatedState: getHookReturn, // Allows consuming tests to get most recent values - updateHookArgs, // Allows consuming tests to pass updated hook arguments (objects only, no tuples) - }; -}; From 5b09a1882c45c2270b086c054ad206721eac2c7c Mon Sep 17 00:00:00 2001 From: Cee Chen <549407+cee-chen@users.noreply.github.com> Date: Mon, 9 Oct 2023 10:42:24 -0700 Subject: [PATCH 23/38] [EuiCollapsibleNavItem] Prevent DOM errors about `accordionProps` being passed to non-accordions (#7269) --- .../collapsible_nav_item/collapsible_nav_item.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/collapsible_nav_beta/collapsible_nav_item/collapsible_nav_item.tsx b/src/components/collapsible_nav_beta/collapsible_nav_item/collapsible_nav_item.tsx index 0b786eeaded..e11f8733151 100644 --- a/src/components/collapsible_nav_beta/collapsible_nav_item/collapsible_nav_item.tsx +++ b/src/components/collapsible_nav_beta/collapsible_nav_item/collapsible_nav_item.tsx @@ -115,6 +115,7 @@ const EuiCollapsibleNavItemDisplay: FunctionComponent< icon, iconProps, items, + accordionProps, // Ensure this isn't spread to non-accordions children, // Ensure children isn't spread ...props }) => { @@ -133,6 +134,7 @@ const EuiCollapsibleNavItemDisplay: FunctionComponent< From db34093993fcafe8b92b33644eb221bb63aa7391 Mon Sep 17 00:00:00 2001 From: 1Copenut Date: Mon, 9 Oct 2023 13:10:02 -0500 Subject: [PATCH 24/38] update i18ntokens --- i18ntokens.json | 338 ++++++-------------------------------- i18ntokens_changelog.json | 57 +++++++ 2 files changed, 109 insertions(+), 286 deletions(-) diff --git a/i18ntokens.json b/i18ntokens.json index 57ffd236887..dc1c650e032 100644 --- a/i18ntokens.json +++ b/i18ntokens.json @@ -647,132 +647,6 @@ }, "filepath": "src/components/color_picker/color_picker.tsx" }, - { - "token": "euiColorStopThumb.buttonAriaLabel", - "defString": "Press the Enter key to modify this stop. Press Escape to focus the group", - "highlighting": "string", - "loc": { - "start": { - "line": 289, - "column": 8, - "index": 7845 - }, - "end": { - "line": 298, - "column": 9, - "index": 8162 - } - }, - "filepath": "src/components/color_picker/color_stops/color_stop_thumb.tsx" - }, - { - "token": "euiColorStopThumb.buttonTitle", - "defString": "Click to edit, drag to reposition", - "highlighting": "string", - "loc": { - "start": { - "line": 289, - "column": 8, - "index": 7845 - }, - "end": { - "line": 298, - "column": 9, - "index": 8162 - } - }, - "filepath": "src/components/color_picker/color_stops/color_stop_thumb.tsx" - }, - { - "token": "euiColorStopThumb.screenReaderAnnouncement", - "defString": "A popup with a color stop edit form opened.\n Tab forward to cycle through form controls or press\n escape to close this popup.", - "highlighting": "string", - "loc": { - "start": { - "line": 340, - "column": 12, - "index": 9548 - }, - "end": { - "line": 345, - "column": 14, - "index": 9808 - } - }, - "filepath": "src/components/color_picker/color_stops/color_stop_thumb.tsx" - }, - { - "token": "euiColorStopThumb.stopLabel", - "defString": "Stop value", - "highlighting": "string", - "loc": { - "start": { - "line": 350, - "column": 12, - "index": 9948 - }, - "end": { - "line": 356, - "column": 13, - "index": 10177 - } - }, - "filepath": "src/components/color_picker/color_stops/color_stop_thumb.tsx" - }, - { - "token": "euiColorStopThumb.stopErrorMessage", - "defString": "Value is out of range", - "highlighting": "string", - "loc": { - "start": { - "line": 350, - "column": 12, - "index": 9948 - }, - "end": { - "line": 356, - "column": 13, - "index": 10177 - } - }, - "filepath": "src/components/color_picker/color_stops/color_stop_thumb.tsx" - }, - { - "token": "euiColorStopThumb.removeLabel", - "defString": "Remove this stop", - "highlighting": "string", - "loc": { - "start": { - "line": 382, - "column": 16, - "index": 11269 - }, - "end": { - "line": 385, - "column": 17, - "index": 11396 - } - }, - "filepath": "src/components/color_picker/color_stops/color_stop_thumb.tsx" - }, - { - "token": "euiColorStops.screenReaderAnnouncement", - "defString": "{label}: {readOnly} {disabled} Color stop picker. Each stop consists of a number and corresponding color value. Use the Down and Up arrow keys to select individual stops. Press the Enter key to create a new stop.", - "highlighting": "string", - "loc": { - "start": { - "line": 543, - "column": 10, - "index": 16200 - }, - "end": { - "line": 551, - "column": 12, - "index": 16680 - } - }, - "filepath": "src/components/color_picker/color_stops/color_stops.tsx" - }, { "token": "euiHue.label", "defString": "Select the HSV color mode 'hue' value", @@ -851,14 +725,14 @@ "highlighting": "string", "loc": { "start": { - "line": 415, + "line": 335, "column": 12, - "index": 12237 + "index": 9466 }, "end": { - "line": 418, + "line": 338, "column": 14, - "index": 12360 + "index": 9589 } }, "filepath": "src/components/combo_box/combo_box_options_list/combo_box_options_list.tsx" @@ -869,14 +743,14 @@ "highlighting": "string", "loc": { "start": { - "line": 428, + "line": 348, "column": 16, - "index": 12799 + "index": 10028 }, "end": { - "line": 432, + "line": 352, "column": 18, - "index": 13030 + "index": 10259 } }, "filepath": "src/components/combo_box/combo_box_options_list/combo_box_options_list.tsx" @@ -887,14 +761,14 @@ "highlighting": "string", "loc": { "start": { - "line": 447, + "line": 367, "column": 16, - "index": 13459 + "index": 10688 }, "end": { - "line": 453, + "line": 373, "column": 18, - "index": 13732 + "index": 10961 } }, "filepath": "src/components/combo_box/combo_box_options_list/combo_box_options_list.tsx" @@ -905,14 +779,14 @@ "highlighting": "string", "loc": { "start": { - "line": 482, + "line": 402, "column": 20, - "index": 14723 + "index": 11952 }, "end": { - "line": 488, + "line": 408, "column": 22, - "index": 15021 + "index": 12250 } }, "filepath": "src/components/combo_box/combo_box_options_list/combo_box_options_list.tsx" @@ -923,14 +797,14 @@ "highlighting": "string", "loc": { "start": { - "line": 499, + "line": 419, "column": 12, - "index": 15225 + "index": 12454 }, "end": { - "line": 503, + "line": 423, "column": 14, - "index": 15446 + "index": 12675 } }, "filepath": "src/components/combo_box/combo_box_options_list/combo_box_options_list.tsx" @@ -941,14 +815,14 @@ "highlighting": "string", "loc": { "start": { - "line": 510, + "line": 430, "column": 10, - "index": 15565 + "index": 12794 }, "end": { - "line": 513, + "line": 433, "column": 12, - "index": 15705 + "index": 12934 } }, "filepath": "src/components/combo_box/combo_box_options_list/combo_box_options_list.tsx" @@ -959,14 +833,14 @@ "highlighting": "string", "loc": { "start": { - "line": 519, + "line": 439, "column": 10, - "index": 15818 + "index": 13047 }, "end": { - "line": 522, + "line": 442, "column": 12, - "index": 15961 + "index": 13190 } }, "filepath": "src/components/combo_box/combo_box_options_list/combo_box_options_list.tsx" @@ -977,14 +851,14 @@ "highlighting": "string", "loc": { "start": { - "line": 970, - "column": 10, - "index": 28029 + "line": 767, + "column": 8, + "index": 22243 }, "end": { - "line": 973, - "column": 11, - "index": 28154 + "line": 770, + "column": 9, + "index": 22362 } }, "filepath": "src/components/combo_box/combo_box.tsx" @@ -4703,14 +4577,14 @@ "highlighting": "string", "loc": { "start": { - "line": 295, + "line": 311, "column": 8, - "index": 9503 + "index": 9875 }, "end": { - "line": 295, + "line": 311, "column": 78, - "index": 9573 + "index": 9945 } }, "filepath": "src/components/flyout/flyout.tsx" @@ -4721,14 +4595,14 @@ "highlighting": "string", "loc": { "start": { - "line": 359, + "line": 385, "column": 12, - "index": 11856 + "index": 12431 }, "end": { - "line": 362, + "line": 388, "column": 14, - "index": 12065 + "index": 12640 } }, "filepath": "src/components/flyout/flyout.tsx" @@ -4739,14 +4613,14 @@ "highlighting": "string", "loc": { "start": { - "line": 364, + "line": 390, "column": 12, - "index": 12094 + "index": 12669 }, "end": { - "line": 367, + "line": 393, "column": 14, - "index": 12266 + "index": 12841 } }, "filepath": "src/components/flyout/flyout.tsx" @@ -4757,14 +4631,14 @@ "highlighting": "string", "loc": { "start": { - "line": 370, + "line": 396, "column": 12, - "index": 12337 + "index": 12912 }, "end": { - "line": 373, + "line": 399, "column": 14, - "index": 12524 + "index": 13099 } }, "filepath": "src/components/flyout/flyout.tsx" @@ -6055,12 +5929,12 @@ "start": { "line": 687, "column": 16, - "index": 20276 + "index": 20250 }, "end": { "line": 690, "column": 18, - "index": 20470 + "index": 20444 } }, "filepath": "src/components/popover/popover.tsx" @@ -6893,114 +6767,6 @@ }, "filepath": "src/components/steps/step_strings.tsx" }, - { - "token": "euiSuggest.stateSavedTooltip", - "defString": "Saved.", - "highlighting": "string", - "loc": { - "start": { - "line": 210, - "column": 39, - "index": 5033 - }, - "end": { - "line": 213, - "column": 3, - "index": 5167 - } - }, - "filepath": "src/components/suggest/suggest.tsx" - }, - { - "token": "euiSuggest.stateUnsavedTooltip", - "defString": "Changes have not been saved.", - "highlighting": "string", - "loc": { - "start": { - "line": 210, - "column": 39, - "index": 5033 - }, - "end": { - "line": 213, - "column": 3, - "index": 5167 - } - }, - "filepath": "src/components/suggest/suggest.tsx" - }, - { - "token": "euiSuggest.stateLoading", - "defString": "State: loading.", - "highlighting": "string", - "loc": { - "start": { - "line": 236, - "column": 67, - "index": 5883 - }, - "end": { - "line": 244, - "column": 3, - "index": 6124 - } - }, - "filepath": "src/components/suggest/suggest.tsx" - }, - { - "token": "euiSuggest.stateSaved", - "defString": "State: saved.", - "highlighting": "string", - "loc": { - "start": { - "line": 236, - "column": 67, - "index": 5883 - }, - "end": { - "line": 244, - "column": 3, - "index": 6124 - } - }, - "filepath": "src/components/suggest/suggest.tsx" - }, - { - "token": "euiSuggest.stateUnsaved", - "defString": "State: unsaved.", - "highlighting": "string", - "loc": { - "start": { - "line": 236, - "column": 67, - "index": 5883 - }, - "end": { - "line": 244, - "column": 3, - "index": 6124 - } - }, - "filepath": "src/components/suggest/suggest.tsx" - }, - { - "token": "euiSuggest.stateUnchanged", - "defString": "State: unchanged.", - "highlighting": "string", - "loc": { - "start": { - "line": 236, - "column": 67, - "index": 5883 - }, - "end": { - "line": 244, - "column": 3, - "index": 6124 - } - }, - "filepath": "src/components/suggest/suggest.tsx" - }, { "token": "euiTableSortMobile.sorting", "defString": "Sorting", diff --git a/i18ntokens_changelog.json b/i18ntokens_changelog.json index 5ce5286fb2c..3c34a0048f0 100644 --- a/i18ntokens_changelog.json +++ b/i18ntokens_changelog.json @@ -1,4 +1,61 @@ [ + { + "version": "89.0.0", + "changes": [ + { + "token": "euiColorStopThumb.buttonAriaLabel", + "changeType": "deleted" + }, + { + "token": "euiColorStopThumb.buttonTitle", + "changeType": "deleted" + }, + { + "token": "euiColorStopThumb.screenReaderAnnouncement", + "changeType": "deleted" + }, + { + "token": "euiColorStopThumb.stopLabel", + "changeType": "deleted" + }, + { + "token": "euiColorStopThumb.stopErrorMessage", + "changeType": "deleted" + }, + { + "token": "euiColorStopThumb.removeLabel", + "changeType": "deleted" + }, + { + "token": "euiColorStops.screenReaderAnnouncement", + "changeType": "deleted" + }, + { + "token": "euiSuggest.stateSavedTooltip", + "changeType": "deleted" + }, + { + "token": "euiSuggest.stateUnsavedTooltip", + "changeType": "deleted" + }, + { + "token": "euiSuggest.stateLoading", + "changeType": "deleted" + }, + { + "token": "euiSuggest.stateSaved", + "changeType": "deleted" + }, + { + "token": "euiSuggest.stateUnsaved", + "changeType": "deleted" + }, + { + "token": "euiSuggest.stateUnchanged", + "changeType": "deleted" + } + ] + }, { "version": "88.5.0", "changes": [ From 52a9666d9df4b05c11e1ec86ec33445252a88265 Mon Sep 17 00:00:00 2001 From: 1Copenut Date: Mon, 9 Oct 2023 13:10:03 -0500 Subject: [PATCH 25/38] Updated changelog. --- CHANGELOG.md | 26 ++++++++++++++++++++++++++ upcoming_changelogs/7182.md | 7 ------- upcoming_changelogs/7239.md | 1 - upcoming_changelogs/7246.md | 2 -- upcoming_changelogs/7250.md | 1 - upcoming_changelogs/7251.md | 3 --- upcoming_changelogs/7254.md | 1 - upcoming_changelogs/7256.md | 3 --- upcoming_changelogs/7259.md | 3 --- upcoming_changelogs/7262.md | 3 --- upcoming_changelogs/7263.md | 3 --- upcoming_changelogs/7264.md | 3 --- upcoming_changelogs/7268.md | 3 --- 13 files changed, 26 insertions(+), 33 deletions(-) delete mode 100644 upcoming_changelogs/7182.md delete mode 100644 upcoming_changelogs/7239.md delete mode 100644 upcoming_changelogs/7246.md delete mode 100644 upcoming_changelogs/7250.md delete mode 100644 upcoming_changelogs/7251.md delete mode 100644 upcoming_changelogs/7254.md delete mode 100644 upcoming_changelogs/7256.md delete mode 100644 upcoming_changelogs/7259.md delete mode 100644 upcoming_changelogs/7262.md delete mode 100644 upcoming_changelogs/7263.md delete mode 100644 upcoming_changelogs/7264.md delete mode 100644 upcoming_changelogs/7268.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 13c4d05a593..d065d5ad916 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,29 @@ +## [`89.0.0`](https://github.com/elastic/eui/tree/v89.0.0) + +- Added new `pushAnimation` prop to push `EuiFlyout`s, which enables a slide in animation ([#7239](https://github.com/elastic/eui/pull/7239)) +- Updated `EuiComboBox` to use `EuiInputPopover` under the hood ([#7246](https://github.com/elastic/eui/pull/7246)) +- Added `inputPopoverProps` to `EuiComboBox`, which allows customizing the underlying popover ([#7246](https://github.com/elastic/eui/pull/7246)) +- Added a new beta `EuiTextBlockTruncate` component for multi-line truncation ([#7250](https://github.com/elastic/eui/pull/7250)) +- Updated `EuiBasicTable` and `EuiInMemoryTable` to support multi-line truncation. This can be set via `truncateText.lines` in the `columns` prop. ([#7254](https://github.com/elastic/eui/pull/7254)) + +**Bug fixes** + +- Fixed `EuiFlexGroup` and `EuiFlexGrid's `m` gutter size ([#7251](https://github.com/elastic/eui/pull/7251)) +- Fixed focus trap rerender issues in `EuiFlyout` with memoization ([#7259](https://github.com/elastic/eui/pull/7259)) +- Fixed a visual bug with `EuiContextMenu`'s animation between panels ([#7268](https://github.com/elastic/eui/pull/7268)) + +**Breaking changes** + +- EUI's global body font-size now respects the `font.defaultUnits` token. This means that the global font size will use the `rem` unit by default, instead of `px`. ([#7182](https://github.com/elastic/eui/pull/7182)) +- Removed exported `accessibleClickKeys`, `comboBoxKeys`, and `cascadingMenuKeys` services. Use the generic `keys` service instead ([#7256](https://github.com/elastic/eui/pull/7256)) +- Removed `EuiColorStops` due to low usage ([#7262](https://github.com/elastic/eui/pull/7262)) +- Removed `EuiSuggest`. We recommend using `EuiSelectable` or `EuiComboBox` instead ([#7263](https://github.com/elastic/eui/pull/7263)) +- Removed `euiHeaderAffordForFixed` Sass mixin, and `$euiHeaderHeight` and `$euiHeaderHeightCompensation` Sass variables. Use the CSS variable `--var(euiFixedHeadersOffset, 0)` instead. ([#7264](https://github.com/elastic/eui/pull/7264)) + +**Accessibility** + +- When using `rem` or `em` font units, EUI now respects, instead of ignoring, browser default font sizes set by end users. ([#7182](https://github.com/elastic/eui/pull/7182)) + ## [`88.5.4`](https://github.com/elastic/eui/tree/v88.5.4) - This release contains internal changes to a beta component needed by Kibana. diff --git a/upcoming_changelogs/7182.md b/upcoming_changelogs/7182.md deleted file mode 100644 index aa34dd7ed82..00000000000 --- a/upcoming_changelogs/7182.md +++ /dev/null @@ -1,7 +0,0 @@ -**Breaking changes** - -- EUI's global body font-size now respects the `font.defaultUnits` token. This means that the global font size will use the `rem` unit by default, instead of `px`. - -**Accessibility** - -- When using `rem` or `em` font units, EUI now respects, instead of ignoring, browser default font sizes set by end users. diff --git a/upcoming_changelogs/7239.md b/upcoming_changelogs/7239.md deleted file mode 100644 index 8ef483e31bc..00000000000 --- a/upcoming_changelogs/7239.md +++ /dev/null @@ -1 +0,0 @@ -- Added new `pushAnimation` prop to push `EuiFlyout`s, which enables a slide in animation diff --git a/upcoming_changelogs/7246.md b/upcoming_changelogs/7246.md deleted file mode 100644 index 98cd7e9792f..00000000000 --- a/upcoming_changelogs/7246.md +++ /dev/null @@ -1,2 +0,0 @@ -- Updated `EuiComboBox` to use `EuiInputPopover` under the hood -- Added `inputPopoverProps` to `EuiComboBox`, which allows customizing the underlying popover diff --git a/upcoming_changelogs/7250.md b/upcoming_changelogs/7250.md deleted file mode 100644 index af7320f7e1c..00000000000 --- a/upcoming_changelogs/7250.md +++ /dev/null @@ -1 +0,0 @@ -- Added a new beta `EuiTextBlockTruncate` component for multi-line truncation diff --git a/upcoming_changelogs/7251.md b/upcoming_changelogs/7251.md deleted file mode 100644 index 117da956a9e..00000000000 --- a/upcoming_changelogs/7251.md +++ /dev/null @@ -1,3 +0,0 @@ -**Bug fixes** - -- Fixed `EuiFlexGroup` and `EuiFlexGrid's `m` gutter size diff --git a/upcoming_changelogs/7254.md b/upcoming_changelogs/7254.md deleted file mode 100644 index f381cbf3119..00000000000 --- a/upcoming_changelogs/7254.md +++ /dev/null @@ -1 +0,0 @@ -- Updated `EuiBasicTable` and `EuiInMemoryTable` to support multi-line truncation. This can be set via `truncateText.lines` in the `columns` prop. diff --git a/upcoming_changelogs/7256.md b/upcoming_changelogs/7256.md deleted file mode 100644 index 3159521b9e4..00000000000 --- a/upcoming_changelogs/7256.md +++ /dev/null @@ -1,3 +0,0 @@ -**Breaking changes** - -- Removed exported `accessibleClickKeys`, `comboBoxKeys`, and `cascadingMenuKeys` services. Use the generic `keys` service instead diff --git a/upcoming_changelogs/7259.md b/upcoming_changelogs/7259.md deleted file mode 100644 index 9beea48791b..00000000000 --- a/upcoming_changelogs/7259.md +++ /dev/null @@ -1,3 +0,0 @@ -**Bug fixes** - -- Fixed focus trap rerender issues in `EuiFlyout` with memoization diff --git a/upcoming_changelogs/7262.md b/upcoming_changelogs/7262.md deleted file mode 100644 index 96dd9760b85..00000000000 --- a/upcoming_changelogs/7262.md +++ /dev/null @@ -1,3 +0,0 @@ -**Breaking changes** - -- Removed `EuiColorStops` due to low usage diff --git a/upcoming_changelogs/7263.md b/upcoming_changelogs/7263.md deleted file mode 100644 index b4c9f2c4567..00000000000 --- a/upcoming_changelogs/7263.md +++ /dev/null @@ -1,3 +0,0 @@ -**Breaking changes** - -- Removed `EuiSuggest`. We recommend using `EuiSelectable` or `EuiComboBox` instead diff --git a/upcoming_changelogs/7264.md b/upcoming_changelogs/7264.md deleted file mode 100644 index 0ebd8034aa1..00000000000 --- a/upcoming_changelogs/7264.md +++ /dev/null @@ -1,3 +0,0 @@ -**Breaking changes** - -- Removed `euiHeaderAffordForFixed` Sass mixin, and `$euiHeaderHeight` and `$euiHeaderHeightCompensation` Sass variables. Use the CSS variable `--var(euiFixedHeadersOffset, 0)` instead. diff --git a/upcoming_changelogs/7268.md b/upcoming_changelogs/7268.md deleted file mode 100644 index 2a4f5d5ff56..00000000000 --- a/upcoming_changelogs/7268.md +++ /dev/null @@ -1,3 +0,0 @@ -**Bug fixes** - -- Fixed a visual bug with `EuiContextMenu`'s animation between panels From c096142810209cf07bb8ee60d7baad41abcdb5e4 Mon Sep 17 00:00:00 2001 From: 1Copenut Date: Mon, 9 Oct 2023 13:10:04 -0500 Subject: [PATCH 26/38] 89.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ea4704ddff0..fbb333bcbde 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@elastic/eui", "description": "Elastic UI Component Library", - "version": "88.5.4", + "version": "89.0.0", "license": "SEE LICENSE IN LICENSE.txt", "main": "lib", "module": "es", From f2cb63456f2edd11aa84a896490f84932f8ec38e Mon Sep 17 00:00:00 2001 From: Nick Partridge Date: Mon, 9 Oct 2023 13:26:29 -0700 Subject: [PATCH 27/38] fix: color errors due to `fillColor` changes in `v54.0.0` (#7271) --- .../views/elastic_charts/accessibility_sunburst.js | 2 +- src-docs/src/views/elastic_charts/pie.js | 8 ++++---- src-docs/src/views/elastic_charts/pie_example.js | 2 +- src-docs/src/views/elastic_charts/pie_slices.js | 6 +++--- src-docs/src/views/elastic_charts/treemap.js | 14 ++++++++------ 5 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src-docs/src/views/elastic_charts/accessibility_sunburst.js b/src-docs/src/views/elastic_charts/accessibility_sunburst.js index 3f412e6d0f7..dccdb76e719 100644 --- a/src-docs/src/views/elastic_charts/accessibility_sunburst.js +++ b/src-docs/src/views/elastic_charts/accessibility_sunburst.js @@ -61,7 +61,7 @@ export default () => { { groupByRollup: ({ fruit }) => fruit, shape: { - fillColor: ({ sortIndex }) => + fillColor: (key, sortIndex) => vizColors[sortIndex % vizColors.length], }, }, diff --git a/src-docs/src/views/elastic_charts/pie.js b/src-docs/src/views/elastic_charts/pie.js index 89ab22b2848..390557af492 100644 --- a/src-docs/src/views/elastic_charts/pie.js +++ b/src-docs/src/views/elastic_charts/pie.js @@ -56,8 +56,8 @@ export default () => { { groupByRollup: (d) => d.status, shape: { - fillColor: (d) => - euiChartTheme.theme.colors.vizColors[d.sortIndex], + fillColor: (key, sortIndex) => + euiChartTheme.theme.colors.vizColors[sortIndex], }, }, ]} @@ -95,8 +95,8 @@ export default () => { { groupByRollup: (d) => d.language, shape: { - fillColor: (d) => - euiChartTheme.theme.colors.vizColors[d.sortIndex], + fillColor: (key, sortIndex) => + euiChartTheme.theme.colors.vizColors[sortIndex], }, }, ]} diff --git a/src-docs/src/views/elastic_charts/pie_example.js b/src-docs/src/views/elastic_charts/pie_example.js index 8b2bc087d92..eb0ae03d7b0 100644 --- a/src-docs/src/views/elastic_charts/pie_example.js +++ b/src-docs/src/views/elastic_charts/pie_example.js @@ -217,7 +217,7 @@ const euiChartTheme = isDarkTheme ? EUI_CHARTS_THEME_DARK : EUI_CHARTS_THEME_LIG { groupByRollup: d => d.category, shape: { - fillColor: d => euiChartTheme.theme.colors.vizColors[d.sortIndex], + fillColor: (key, sortIndex) => euiChartTheme.theme.colors.vizColors[sortIndex], }, }, ]} diff --git a/src-docs/src/views/elastic_charts/pie_slices.js b/src-docs/src/views/elastic_charts/pie_slices.js index 639cdc00994..e6a48e1fd21 100644 --- a/src-docs/src/views/elastic_charts/pie_slices.js +++ b/src-docs/src/views/elastic_charts/pie_slices.js @@ -167,8 +167,8 @@ export default () => { { groupByRollup: (d) => d.browser, shape: { - fillColor: (d) => - euiChartTheme.theme.colors.vizColors[d.sortIndex], + fillColor: (key, sortIndex) => + euiChartTheme.theme.colors.vizColors[sortIndex], }, }, ]} @@ -308,7 +308,7 @@ export default () => { { groupByRollup: d => d.browser, shape: { - fillColor: d => euiChartTheme.theme.colors.vizColors[d.sortIndex], + fillColor: (key, sortIndex) => euiChartTheme.theme.colors.vizColors[sortIndex], }, }, ]} diff --git a/src-docs/src/views/elastic_charts/treemap.js b/src-docs/src/views/elastic_charts/treemap.js index daab0de712b..8d78a23d8b1 100644 --- a/src-docs/src/views/elastic_charts/treemap.js +++ b/src-docs/src/views/elastic_charts/treemap.js @@ -68,14 +68,15 @@ export default () => { { groupByRollup: (d) => d.vizType, shape: { - fillColor: (d) => groupedPalette[d.sortIndex * 3], + fillColor: (key, sortIndex) => + groupedPalette[sortIndex * 3], }, }, { groupByRollup: (d) => d.issueType, shape: { - fillColor: (d) => - groupedPalette[d.parent.sortIndex * 3 + d.sortIndex + 1], + fillColor: (key, sortIndex, { parent }) => + groupedPalette[parent.sortIndex * 3 + sortIndex + 1], }, }, ]} @@ -101,7 +102,8 @@ export default () => { { groupByRollup: (d) => d.vizType, shape: { - fillColor: (d) => groupedPalette[d.sortIndex * 3], + fillColor: (key, sortIndex) => + groupedPalette[sortIndex * 3], }, fillLabel: { valueFormatter: () => '', @@ -111,8 +113,8 @@ export default () => { { groupByRollup: (d) => d.issueType, shape: { - fillColor: (d) => - groupedPalette[d.parent.sortIndex * 3 + d.sortIndex], + fillColor: (key, sortIndex, { parent }) => + groupedPalette[parent.sortIndex * 3 + sortIndex], }, }, ]} From cff6f41349f8912573bd41998b4411bfa2d82740 Mon Sep 17 00:00:00 2001 From: Cee Chen <549407+cee-chen@users.noreply.github.com> Date: Mon, 9 Oct 2023 15:59:02 -0700 Subject: [PATCH 28/38] [EuiDataGrid] Update to use `EuiTextBreakTruncate` + cell DOM & CSS cleanup (#7255) --- .../__snapshots__/data_grid.test.tsx.snap | 780 ++++++++---------- .../datagrid/_data_grid_data_row.scss | 111 +-- src/components/datagrid/_mixins.scss | 15 - .../data_grid_body_custom.test.tsx.snap | 104 +-- .../data_grid_body_virtualized.test.tsx.snap | 60 +- .../data_grid_cell.test.tsx.snap | 50 +- .../datagrid/body/data_grid_cell.test.tsx | 68 +- .../datagrid/body/data_grid_cell.tsx | 199 +++-- .../body/data_grid_cell_actions.test.tsx | 17 +- .../datagrid/body/data_grid_cell_actions.tsx | 13 +- .../body/data_grid_cell_popover.spec.tsx | 78 ++ .../datagrid/utils/__mocks__/row_heights.ts | 6 +- .../datagrid/utils/row_heights.test.ts | 42 +- src/components/datagrid/utils/row_heights.ts | 35 +- 14 files changed, 763 insertions(+), 815 deletions(-) diff --git a/src/components/datagrid/__snapshots__/data_grid.test.tsx.snap b/src/components/datagrid/__snapshots__/data_grid.test.tsx.snap index 85841317fe0..1955c97a301 100644 --- a/src/components/datagrid/__snapshots__/data_grid.test.tsx.snap +++ b/src/components/datagrid/__snapshots__/data_grid.test.tsx.snap @@ -724,25 +724,21 @@ exports[`EuiDataGrid rendering renders additional toolbar controls 1`] = ` tabindex="-1" >
-
- 0, A -
- + 0, A
+
-
- 0, B -
- + 0, B
+
-
- 1, A -
- + 1, A
+
-
- 1, B -
- + 1, B
+
-
- 2, A -
- + 2, A
+
-
- 2, B -
- + 2, B
+
@@ -1251,25 +1227,21 @@ exports[`EuiDataGrid rendering renders control columns 1`] = ` data-focus-lock-disabled="disabled" >
-
- 0 -
- + 0
+
-
- 0, A -
- + 0, A
+
-
- 0, B -
- + 0, B
+
-
- 0 -
- + 0
+
-
- 1 -
- + 1
+
-
- 1, A -
- + 1, A
+
-
- 1, B -
- + 1, B
+
-
- 1 -
- + 1
+
-
- 2 -
- + 2
+
-
- 2, A -
- + 2, A
+
-
- 2, B -
- + 2, B
+
-
- 2 -
- + 2
+
-
- 0, A -
- + 0, A
+
-
- 0, B -
- + 0, B
+
-
- 1, A -
- + 1, A
+
-
- 1, B -
- + 1, B
+
-
- 2, A -
- + 2, A
+
-
- 2, B -
- + 2, B
+
@@ -2489,25 +2393,21 @@ exports[`EuiDataGrid rendering renders with common and div attributes 1`] = ` tabindex="-1" >
-
- 0, A -
- + 0, A
+
-
- 0, B -
- + 0, B
+
-
- 1, A -
- + 1, A
+
-
- 1, B -
- + 1, B
+
-
- 2, A -
- + 2, A
+
-
- 2, B -
- + 2, B
+
diff --git a/src/components/datagrid/_data_grid_data_row.scss b/src/components/datagrid/_data_grid_data_row.scss index cfaa2a9095b..5a6869fe66c 100644 --- a/src/components/datagrid/_data_grid_data_row.scss +++ b/src/components/datagrid/_data_grid_data_row.scss @@ -38,22 +38,6 @@ animation-delay: $euiAnimSpeedNormal; animation-fill-mode: forwards; } - /* - * For some incredibly bizarre reason, Safari doesn't correctly update the flex - * width of the content (when rows are an undefined height/single flex row), - * which causes the action icons to overlap & makes the content less readable. - * This workaround "animation" forces a rerender of the flex content width - * - * TODO: Remove this workaround once https://bugs.webkit.org/show_bug.cgi?id=258539 is resolved - */ - .euiDataGridRowCell__expandContent { - animation-name: euiDataGridCellActionsSafariWorkaround; - animation-duration: 1000ms; // I don't know why the duration matters or why it being longer works more consistently 🥲 - animation-delay: $euiAnimSpeedNormal + $euiAnimSpeedExtraFast; // Wait for above animation to finish - animation-iteration-count: 1; - animation-fill-mode: forwards; - animation-timing-function: linear; - } } // On focus, directly show action buttons (without animation) @@ -95,23 +79,6 @@ &.euiDataGridRowCell--capitalize { text-transform: capitalize; } - - .euiDataGridRowCell__definedHeight { - @include euiTextBreakWord; - flex-grow: 1; - } - - // We only truncate if the cell is not a control column. - &:not(.euiDataGridRowCell--controlColumn) { - .euiDataGridRowCell__content, - .euiDataGridRowCell__truncate, - &.euiDataGridRowCell__truncate, - .euiDataGridRowCell__expandContent { - @include euiTextTruncate; - overflow: hidden; - white-space: nowrap; - } - } } .euiDataGridRowCell__popover { @@ -130,39 +97,51 @@ @include euiBottomShadow; // TODO: Convert to euiShadowMedium() in Emotion } -.euiDataGridRowCell__expandFlex { - position: relative; // for positioning expand button +.euiDataGridRowCell__contentWrapper { + position: relative; // Needed for .euiDataGridRowCell__actions--overlay + height: 100%; + overflow: hidden; +} + +.euiDataGridRowCell__defaultHeight { display: flex; align-items: baseline; - height: 100%; + max-width: 100%; + + .euiDataGridRowCell__content { + flex-grow: 1; + } + + .euiDataGridRowCell__actions { + flex-grow: 0; + } .euiDataGridRowCell--controlColumn & { + height: 100%; align-items: center; } } -.euiDataGridRowCell__expandContent { - flex-grow: 1; -} - -.euiDataGridRowCell__contentByHeight { - flex-grow: 1; - height: 100%; +.euiDataGridRowCell__numericalHeight { + // Without this rule, popover anchors content that overflows off the page + [data-euiportal], + .euiPopover, + .euiPopover__anchor { + height: 100%; + } } // Cell actions -.euiDataGridRowCell__expandActions { +.euiDataGridRowCell__actions { display: flex; -} -@include euiDataGridRowCellActions($definedHeight: false) { - flex-grow: 0; -} -@include euiDataGridRowCellActions($definedHeight: true) { - background-color: $euiColorEmptyShade; - position: absolute; - right: 0; - top: 0; - padding: $euiDataGridCellPaddingM 0; + + &--overlay { + position: absolute; + right: 0; + top: 0; + padding: $euiDataGridCellPaddingM 0; + background-color: $euiColorEmptyShade; + } } .euiDataGridRowCell__actionButtonIcon { @@ -181,20 +160,20 @@ // Row stripes @include euiDataGridStyles(stripes) { .euiDataGridRow--striped { - @include euiDataGridRowCellActions($definedHeight: true) { + &, + .euiDataGridRowCell__actions--overlay { background-color: $euiColorLightestShade; } - background-color: $euiColorLightestShade; } } // Row highlights @include euiDataGridStyles(rowHoverHighlight) { .euiDataGridRow:hover { - @include euiDataGridRowCellActions($definedHeight: true) { + &, + .euiDataGridRowCell__actions--overlay { background-color: $euiColorHighlight; } - background-color: $euiColorHighlight; } } @@ -240,10 +219,11 @@ // Compressed density grids - height tweaks @include euiDataGridStyles(fontSizeSmall, paddingSmall) { - @include euiDataGridRowCellActions($definedHeight: true) { + .euiDataGridRowCell__actions--overlay { padding: ($euiDataGridCellPaddingS / 2) 0; } - @include euiDataGridRowCellActions($definedHeight: false) { + + .euiDataGridRowCell__defaultHeight .euiDataGridRowCell__actions { transform: translateY(1px); } } @@ -259,14 +239,3 @@ width: $euiSizeM; } } -@keyframes euiDataGridCellActionsSafariWorkaround { - from { - width: 100%; - flex-basis: 100%; - } - - to { - width: auto; - flex-basis: auto; - } -} diff --git a/src/components/datagrid/_mixins.scss b/src/components/datagrid/_mixins.scss index 1a70c21cc97..4303730770c 100644 --- a/src/components/datagrid/_mixins.scss +++ b/src/components/datagrid/_mixins.scss @@ -82,18 +82,3 @@ $euiDataGridStyles: ( @content; } } - -@mixin euiDataGridRowCellActions($definedHeight: false) { - @if $definedHeight { - // Defined heights are cells with row heights of auto, lineCount, or a static height - // that set the __contentByHeight class - .euiDataGridRowCell__contentByHeight + .euiDataGridRowCell__expandActions { - @content; - } - } @else { - // Otherwise, an undefined height (single flex row) will set __expandContent - .euiDataGridRowCell__expandContent + .euiDataGridRowCell__expandActions { - @content; - } - } -} diff --git a/src/components/datagrid/body/__snapshots__/data_grid_body_custom.test.tsx.snap b/src/components/datagrid/body/__snapshots__/data_grid_body_custom.test.tsx.snap index bce634a36b1..029ed6665a9 100644 --- a/src/components/datagrid/body/__snapshots__/data_grid_body_custom.test.tsx.snap +++ b/src/components/datagrid/body/__snapshots__/data_grid_body_custom.test.tsx.snap @@ -134,25 +134,21 @@ exports[`EuiDataGridBodyCustomRender treats \`renderCustomGridBody\` as a render tabindex="-1" >
-
- hello -
- + hello
+
-
- world -
- + world
+
@@ -206,25 +198,21 @@ exports[`EuiDataGridBodyCustomRender treats \`renderCustomGridBody\` as a render tabindex="-1" >
-
- lorem -
- + lorem
+
-
- ipsum -
- + ipsum
+
diff --git a/src/components/datagrid/body/__snapshots__/data_grid_body_virtualized.test.tsx.snap b/src/components/datagrid/body/__snapshots__/data_grid_body_virtualized.test.tsx.snap index 2e2fa824dc3..7251f9857ab 100644 --- a/src/components/datagrid/body/__snapshots__/data_grid_body_virtualized.test.tsx.snap +++ b/src/components/datagrid/body/__snapshots__/data_grid_body_virtualized.test.tsx.snap @@ -135,27 +135,23 @@ exports[`EuiDataGridBodyVirtualized renders 1`] = ` tabindex="-1" >
-
- - cell content - -
- + + cell content +
+
-
- - cell content - -
- + + cell content +
+
diff --git a/src/components/datagrid/body/__snapshots__/data_grid_cell.test.tsx.snap b/src/components/datagrid/body/__snapshots__/data_grid_cell.test.tsx.snap index 3282d55d703..019cda14347 100644 --- a/src/components/datagrid/body/__snapshots__/data_grid_cell.test.tsx.snap +++ b/src/components/datagrid/body/__snapshots__/data_grid_cell.test.tsx.snap @@ -49,38 +49,34 @@ exports[`EuiDataGridCell renders 1`] = ` tabindex="-1" >
-
-
- - -
+
+ +
-
+
`; diff --git a/src/components/datagrid/body/data_grid_cell.test.tsx b/src/components/datagrid/body/data_grid_cell.test.tsx index 6aa83e8677b..0271566e937 100644 --- a/src/components/datagrid/body/data_grid_cell.test.tsx +++ b/src/components/datagrid/body/data_grid_cell.test.tsx @@ -718,19 +718,61 @@ describe('EuiDataGridCell', () => { }); }); - it('renders certain classes/styles if rowHeightOptions is passed', () => { - const component = mount( - - ); + describe('renders certain classes/styles based on rowHeightOptions', () => { + const props = { ...requiredProps, renderCellValue: () => null }; - expect( - component.find('.euiDataGridRowCell__contentByHeight').exists() - ).toBe(true); + test('default', () => { + const component = mount( + + ); + + expect( + component.find('.euiDataGridRowCell__defaultHeight').exists() + ).toBe(true); + expect(component.find('.eui-textTruncate').exists()).toBe(true); + }); + + test('auto', () => { + const component = mount( + + ); + + expect(component.find('.euiDataGridRowCell__autoHeight').exists()).toBe( + true + ); + expect(component.find('.eui-textBreakWord').exists()).toBe(true); + }); + + test('numerical', () => { + const component = mount( + + ); + + expect( + component.find('.euiDataGridRowCell__numericalHeight').exists() + ).toBe(true); + expect(component.find('.eui-textBreakWord').exists()).toBe(true); + }); + + test('lineCount', () => { + const component = mount( + + ); + + expect( + component.find('.euiDataGridRowCell__lineCountHeight').exists() + ).toBe(true); + expect(component.find('.eui-textBreakWord').exists()).toBe(true); + expect(component.find('.euiTextBlockTruncate').exists()).toBe(true); + }); }); }); diff --git a/src/components/datagrid/body/data_grid_cell.tsx b/src/components/datagrid/body/data_grid_cell.tsx index 0cdb33969fa..23ec939abb3 100644 --- a/src/components/datagrid/body/data_grid_cell.tsx +++ b/src/components/datagrid/body/data_grid_cell.tsx @@ -17,6 +17,7 @@ import React, { KeyboardEvent, memo, MutableRefObject, + ReactNode, } from 'react'; import { createPortal } from 'react-dom'; import { tabbable } from 'tabbable'; @@ -24,6 +25,7 @@ import { keys } from '../../../services'; import { EuiScreenReaderOnly } from '../../accessibility'; import { EuiFocusTrap } from '../../focus_trap'; import { EuiI18n } from '../../i18n'; +import { EuiTextBlockTruncate } from '../../text_truncate'; import { hasResizeObserver } from '../../observer/resize_observer/resize_observer'; import { DataGridFocusContext } from '../utils/focus'; import { RowHeightVirtualizationUtils } from '../utils/row_heights'; @@ -34,6 +36,7 @@ import { EuiDataGridCellValueElementProps, EuiDataGridCellValueProps, EuiDataGridCellPopoverElementProps, + EuiDataGridRowHeightOption, } from '../data_grid_types'; import { EuiDataGridCellActions, @@ -46,69 +49,108 @@ const EuiDataGridCellContent: FunctionComponent< EuiDataGridCellValueProps & { setCellProps: EuiDataGridCellValueElementProps['setCellProps']; setCellContentsRef: EuiDataGridCell['setCellContentsRef']; + setPopoverAnchorRef: MutableRefObject; isExpanded: boolean; - isDefinedHeight: boolean; + isControlColumn: boolean; isFocused: boolean; ariaRowIndex: number; + rowHeight?: EuiDataGridRowHeightOption; + cellHeightType: string; + cellActions?: ReactNode; } > = memo( ({ renderCellValue, column, setCellContentsRef, - rowHeightsOptions, + setPopoverAnchorRef, rowIndex, colIndex, ariaRowIndex, + rowHeight, rowHeightUtils, - isDefinedHeight, + isControlColumn, isFocused, + cellHeightType, + cellActions, ...rest }) => { // React is more permissible than the TS types indicate const CellElement = renderCellValue as JSXElementConstructor; - return ( - <> -
- { + setCellContentsRef(el); + setPopoverAnchorRef.current = + cellHeightType === 'default' + ? // Default height cells need the popover to be anchored on the wrapper, + // in order for the popover to centered on the full cell width (as content + // width is affected by the width of cell actions) + (el?.parentElement as HTMLDivElement) + : // Numerical height cells need the popover anchor to be below the wrapper + // class, in order to set height: 100% on the portalled popover div wrappers + el; + }} + data-datagrid-cellcontent + className={classes} + > + +
+ ); + if (cellHeightType === 'lineCount' && !isControlColumn) { + const lines = rowHeightUtils!.getLineCount(rowHeight)!; + cellContent = ( + + {cellContent} + + ); + } + + const screenReaderText = ( + +
- - - - +

+ + ); + + return ( +
+ {cellContent} + {screenReaderText} + {cellActions} +
); } ); @@ -651,33 +693,52 @@ export class EuiDataGridCell extends Component< } }; - const isDefinedHeight = !!rowHeightUtils?.getRowHeightOption( + const rowHeight = rowHeightUtils?.getRowHeightOption( rowIndex, rowHeightsOptions ); + const cellHeightType = + rowHeightUtils?.getHeightType(rowHeight) || 'default'; const cellContentProps = { ...rest, setCellProps: this.setCellProps, column, columnType, + cellHeightType, isExpandable, isExpanded: popoverIsOpen, isDetails: false, isFocused: this.state.isFocused, setCellContentsRef: this.setCellContentsRef, - rowHeightsOptions, + setPopoverAnchorRef: this.popoverAnchorRef, + rowHeight, rowHeightUtils, - isDefinedHeight, + isControlColumn: cellClasses.includes( + 'euiDataGridRowCell--controlColumn' + ), ariaRowIndex, }; - const anchorClass = 'euiDataGridRowCell__expandFlex'; - const expandClass = isDefinedHeight - ? 'euiDataGridRowCell__contentByHeight' - : 'euiDataGridRowCell__expandContent'; + const cellActions = showCellActions && ( + { + if (popoverIsOpen) { + closeCellPopover(); + } else { + openCellPopover({ rowIndex: visibleRowIndex, colIndex }); + } + }} + /> + ); - let innerContent = ( + const cellContent = isExpandable ? ( + + ) : ( -
-
- -
-
+
); - if (isExpandable) { - innerContent = ( -
-
- -
- {showCellActions && ( - { - if (popoverIsOpen) { - closeCellPopover(); - } else { - openCellPopover({ rowIndex: visibleRowIndex, colIndex }); - } - }} - /> - )} -
- ); - } - - const content = ( + const cell = (
- {innerContent} + {cellContent}
); return rowManager && !IS_JEST_ENVIRONMENT ? createPortal( - content, + cell, rowManager.getRow({ rowIndex, visibleRowIndex, @@ -757,6 +790,6 @@ export class EuiDataGridCell extends Component< height: style!.height as number, // comes in as an integer from react-window }) ) - : content; + : cell; } } diff --git a/src/components/datagrid/body/data_grid_cell_actions.test.tsx b/src/components/datagrid/body/data_grid_cell_actions.test.tsx index e14f235f60c..151f3003814 100644 --- a/src/components/datagrid/body/data_grid_cell_actions.test.tsx +++ b/src/components/datagrid/body/data_grid_cell_actions.test.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { shallow } from 'enzyme'; +import { render } from '../../../test/rtl'; import { EuiDataGridColumnCellAction } from '../data_grid_types'; import { @@ -24,6 +25,7 @@ describe('EuiDataGridCellActions', () => { onExpandClick: jest.fn(), rowIndex: 0, colIndex: 0, + cellHeightType: 'default', }; it('renders an expand button', () => { @@ -31,7 +33,7 @@ describe('EuiDataGridCellActions', () => { expect(component).toMatchInlineSnapshot(`
{ expect(component).toMatchInlineSnapshot(`
{ `); }); + it('renders with overlay positioning for non default height cells', () => { + const { container } = render( + + ); + + // TODO: Switch to `.toHaveStyle({ position: 'absolute' })` once on Emotion + expect(container.firstChild).toHaveClass( + 'euiDataGridRowCell__actions--overlay' + ); + }); + describe('visible cell actions limit', () => { it('by default, does not render more than the first two primary cell actions', () => { const component = shallow( diff --git a/src/components/datagrid/body/data_grid_cell_actions.tsx b/src/components/datagrid/body/data_grid_cell_actions.tsx index 21bac608c36..bc3ed204dfe 100644 --- a/src/components/datagrid/body/data_grid_cell_actions.tsx +++ b/src/components/datagrid/body/data_grid_cell_actions.tsx @@ -18,17 +18,20 @@ import { EuiButtonIcon, EuiButtonIconProps } from '../../button/button_icon'; import { EuiButtonEmpty, EuiButtonEmptyProps } from '../../button/button_empty'; import { EuiFlexGroup, EuiFlexItem } from '../../flex'; import { EuiPopoverFooter } from '../../popover'; +import classNames from 'classnames'; export const EuiDataGridCellActions = ({ onExpandClick, column, rowIndex, colIndex, + cellHeightType, }: { onExpandClick: () => void; column?: EuiDataGridColumn; rowIndex: number; colIndex: number; + cellHeightType: string; }) => { // Note: The cell expand button/expansion popover is *always* rendered if // column.cellActions is present (regardless of column.isExpandable). @@ -91,11 +94,11 @@ export const EuiDataGridCellActions = ({ ); }, [column, colIndex, rowIndex]); - return ( -
- {[...additionalButtons, expandButton]} -
- ); + const classes = classNames('euiDataGridRowCell__actions', { + 'euiDataGridRowCell__actions--overlay': cellHeightType !== 'default', + }); + + return
{[...additionalButtons, expandButton]}
; }; export const EuiDataGridCellPopoverActions = ({ diff --git a/src/components/datagrid/body/data_grid_cell_popover.spec.tsx b/src/components/datagrid/body/data_grid_cell_popover.spec.tsx index deca8599bc1..0c4dd05fb04 100644 --- a/src/components/datagrid/body/data_grid_cell_popover.spec.tsx +++ b/src/components/datagrid/body/data_grid_cell_popover.spec.tsx @@ -130,4 +130,82 @@ describe('EuiDataGridCellPopover', () => { cy.get('.euiDataGridRowCell__popover.hello.world').should('exist'); }); + + describe('popover anchor/positioning', () => { + const props = { + ...baseProps, + rowCount: 1, + renderCellValue: ({ columnId }) => { + if (columnId === 'A') { + return 'short text'; + } else { + return 'Very long text that should get cut off because it is so long'; + } + }, + }; + + const openCellPopover = (id: string) => { + cy.get( + `[data-gridcell-row-index="0"][data-gridcell-column-id="${id}"]` + ).realClick(); + cy.realPress('Enter'); + }; + + it('default row height', () => { + cy.realMount(); + + openCellPopover('B'); + cy.get('[data-test-subj="euiDataGridExpansionPopover"]') + .should('have.css', 'left', '24.5px') + .should('have.css', 'top') + .and('match', /^(104|103)px/); // CI is off by 1 px + }); + + it('lineCount row height', () => { + cy.realMount( + + ); + openCellPopover('B'); + + cy.get('[data-test-subj="euiDataGridExpansionPopover"]') + .should('have.css', 'left', '24.5px') + .should('have.css', 'top') + .and('match', /^(127|126)px/); // CI is off by 1 px + }); + + it('numerical row height', () => { + cy.realMount( + + ); + openCellPopover('B'); + + // Should not be anchored to the bottom of the overflowing text + cy.get('[data-test-subj="euiDataGridExpansionPopover"]') + .should('have.css', 'left', '24.5px') + .should('have.css', 'top') + .and('match', /^(106|105)px/); // CI is off by 1 px + }); + + it('auto row height', () => { + cy.realMount( + + ); + + openCellPopover('B'); + cy.get('[data-test-subj="euiDataGridExpansionPopover"]') + .should('have.css', 'left', '24.5px') + .should('have.css', 'top') + .and('match', /^(151|150)px/); // CI is off by 1 px + + // The shorter cell content should not have the same top position + openCellPopover('A'); + cy.get('[data-test-subj="euiDataGridExpansionPopover"]') + .should('have.css', 'left', '19px') + .should('have.css', 'top') + .and('match', /^(103|102)px/); // CI is off by 1 px + }); + }); }); diff --git a/src/components/datagrid/utils/__mocks__/row_heights.ts b/src/components/datagrid/utils/__mocks__/row_heights.ts index 0528c89d2cb..e06b4957d9d 100644 --- a/src/components/datagrid/utils/__mocks__/row_heights.ts +++ b/src/components/datagrid/utils/__mocks__/row_heights.ts @@ -24,11 +24,7 @@ export const RowHeightUtils = jest.fn().mockImplementation(() => { const rowHeightUtilsMock: RowHeightUtilsPublicAPI = { cacheStyles: jest.fn(), - getStylesForCell: jest.fn(() => ({ - wordWrap: 'break-word', - wordBreak: 'break-word', - flexGrow: 1, - })), + getHeightType: jest.fn(rowHeightUtils.getHeightType), isAutoHeight: jest.fn(() => false), setRowHeight: jest.fn(), pruneHiddenColumnHeights: jest.fn(), diff --git a/src/components/datagrid/utils/row_heights.test.ts b/src/components/datagrid/utils/row_heights.test.ts index 643ac31ef1a..a7b8c832a73 100644 --- a/src/components/datagrid/utils/row_heights.test.ts +++ b/src/components/datagrid/utils/row_heights.test.ts @@ -184,34 +184,22 @@ describe('RowHeightUtils', () => { }); }); }); + }); - describe('getStylesForCell (returns inline CSS styles based on height config)', () => { - describe('auto height', () => { - it('returns empty styles object', () => { - expect( - rowHeightUtils.getStylesForCell({ defaultHeight: 'auto' }, 0) - ).toEqual({}); - }); - }); - - describe('lineCount height', () => { - it('returns line-clamp CSS', () => { - expect( - rowHeightUtils.getStylesForCell( - { defaultHeight: { lineCount: 5 } }, - 0 - ) - ).toEqual(expect.objectContaining({ WebkitLineClamp: 5 })); - }); - }); - - describe('numeric heights', () => { - it('returns default CSS', () => { - expect( - rowHeightUtils.getStylesForCell({ defaultHeight: 34 }, 0) - ).toEqual({ height: '100%', overflow: 'hidden' }); - }); - }); + describe('getHeightType', () => { + it('returns a string enum based on rowHeightsOptions', () => { + expect(rowHeightUtils.getHeightType(undefined)).toEqual('default'); + expect(rowHeightUtils.getHeightType('auto')).toEqual('auto'); + expect(rowHeightUtils.getHeightType({ lineCount: 3 })).toEqual( + 'lineCount' + ); + expect(rowHeightUtils.getHeightType({ lineCount: 0 })).toEqual( + 'lineCount' + ); + expect(rowHeightUtils.getHeightType({ height: 100 })).toEqual( + 'numerical' + ); + expect(rowHeightUtils.getHeightType(100)).toEqual('numerical'); }); }); diff --git a/src/components/datagrid/utils/row_heights.ts b/src/components/datagrid/utils/row_heights.ts index 3aed6933c4b..0c82650e390 100644 --- a/src/components/datagrid/utils/row_heights.ts +++ b/src/components/datagrid/utils/row_heights.ts @@ -7,7 +7,6 @@ */ import { - CSSProperties, MutableRefObject, useCallback, useContext, @@ -107,31 +106,21 @@ export class RowHeightUtils { }; } - getStylesForCell = ( - rowHeightsOptions: EuiDataGridRowHeightsOptions, - rowIndex: number - ): CSSProperties => { - const height = this.getRowHeightOption(rowIndex, rowHeightsOptions); + /** + * Height types + */ - if (height === AUTO_HEIGHT) { - return {}; + getHeightType = (option?: EuiDataGridRowHeightOption) => { + if (option == null) { + return 'default'; } - - const lineCount = this.getLineCount(height); - if (lineCount) { - return { - WebkitLineClamp: lineCount, - display: '-webkit-box', - WebkitBoxOrient: 'vertical', - height: '100%', - overflow: 'hidden', - }; + if (option === AUTO_HEIGHT) { + return 'auto'; } - - return { - height: '100%', - overflow: 'hidden', - }; + if (this.getLineCount(option) != null) { + return 'lineCount'; + } + return 'numerical'; }; /** From b1c8582daddda4ab24f4c099fd15ae05209f1531 Mon Sep 17 00:00:00 2001 From: Cee Chen <549407+cee-chen@users.noreply.github.com> Date: Tue, 10 Oct 2023 09:45:35 -0700 Subject: [PATCH 29/38] [Emotion] Reduce CSS browser prefixing to supported browsers only (#7272) --- package.json | 1 + src-docs/src/components/codesandbox/link.js | 3 +- src-docs/src/views/app_context.js | 3 + .../src/views/provider/provider_example.js | 44 +- .../src/views/provider/provider_styles.tsx | 9 +- .../__snapshots__/bottom_bar.test.tsx.snap | 50 +- .../button/__snapshots__/button.test.tsx.snap | 2 +- .../__snapshots__/button_empty.test.tsx.snap | 2 +- .../__snapshots__/button_icon.test.tsx.snap | 2 +- .../collapsible_nav_group.test.tsx.snap | 2 +- .../__snapshots__/control_bar.test.tsx.snap | 2 +- .../overlay_mask/overlay_mask.test.tsx | 2 +- .../__snapshots__/page_template.test.tsx.snap | 2 +- .../page_bottom_bar.test.tsx.snap | 20 +- .../portal/__snapshots__/portal.test.tsx.snap | 4 +- src/components/provider/provider.test.tsx | 12 +- src/components/provider/provider.tsx | 15 +- src/services/emotion/css.test.ts | 36 ++ src/services/emotion/css.ts | 25 + src/services/emotion/index.ts | 1 + src/services/emotion/prefixer.test.tsx | 567 ++++++++++++++++++ src/services/emotion/prefixer.ts | 120 ++++ .../__snapshots__/provider.test.tsx.snap | 12 +- src/services/theme/provider.tsx | 15 +- src/services/theme/utils.ts | 2 +- upcoming_changelogs/7272.md | 3 + yarn.lock | 5 + 27 files changed, 875 insertions(+), 86 deletions(-) create mode 100644 src/services/emotion/css.test.ts create mode 100644 src/services/emotion/css.ts create mode 100644 src/services/emotion/prefixer.test.tsx create mode 100644 src/services/emotion/prefixer.ts create mode 100644 upcoming_changelogs/7272.md diff --git a/package.json b/package.json index fbb333bcbde..a706480cee8 100644 --- a/package.json +++ b/package.json @@ -150,6 +150,7 @@ "@types/react-dom": "^18.2.6", "@types/react-is": "^17.0.3", "@types/react-router-dom": "^5.3.3", + "@types/stylis": "^4.2.1", "@types/testing-library__jest-dom": "^5.14.3", "@types/url-parse": "^1.4.8", "@types/uuid": "^8.3.0", diff --git a/src-docs/src/components/codesandbox/link.js b/src-docs/src/components/codesandbox/link.js index ef9d3297ea2..214a8aae8de 100644 --- a/src-docs/src/components/codesandbox/link.js +++ b/src-docs/src/components/codesandbox/link.js @@ -169,12 +169,13 @@ import '@elastic/charts/dist/theme_only_${colorMode}.css';` import React from 'react'; import { createRoot } from 'react-dom/client'; import createCache from '@emotion/cache'; -import { EuiProvider } from '@elastic/eui'; +import { EuiProvider, euiStylisPrefixer } from '@elastic/eui'; import { Demo } from './demo'; const cache = createCache({ key: 'codesandbox', + stylisPlugins: [euiStylisPrefixer], container: document.querySelector('meta[name="emotion-styles"]'), }); cache.compat = true; diff --git a/src-docs/src/views/app_context.js b/src-docs/src/views/app_context.js index 94495219134..1e38535ec8b 100644 --- a/src-docs/src/views/app_context.js +++ b/src-docs/src/views/app_context.js @@ -8,6 +8,7 @@ import { translateUsingPseudoLocale } from '../services'; import { getLocale } from '../store'; import { EuiContext, EuiProvider } from '../../../src/components'; +import { euiStylisPrefixer } from '../../../src/services'; import { EUI_THEMES } from '../../../src/themes'; import favicon16Prod from '../images/favicon/prod/favicon-16x16.png'; @@ -19,11 +20,13 @@ import favicon96Dev from '../images/favicon/dev/favicon-96x96.png'; const generalEmotionCache = createCache({ key: 'css', + stylisPlugins: [euiStylisPrefixer], container: document.querySelector('meta[name="emotion-styles"]'), }); generalEmotionCache.compat = true; const utilityCache = createCache({ key: 'util', + stylisPlugins: [euiStylisPrefixer], container: document.querySelector('meta[name="emotion-styles-utility"]'), }); diff --git a/src-docs/src/views/provider/provider_example.js b/src-docs/src/views/provider/provider_example.js index c01f8a9b524..3d2d7b488c2 100644 --- a/src-docs/src/views/provider/provider_example.js +++ b/src-docs/src/views/provider/provider_example.js @@ -96,21 +96,41 @@ export const ProviderExample = { {''} -

- @emotion/cache and style injection location -

+

@emotion/cache customization

- In the case that your app has its own static stylesheet,{' '} - @emotion styles may not be injected into the - correct location in the {''}, causing - unintentional overrides or unapplied styles.{' '} - - The @emotion/cache library + The{' '} + + @emotion/cache library {' '} - provides configuration options that help with specifying the - injection location. We recommend using {''}{' '} - tags to achieve this. + provides extra configuration options for EUI's CSS-in-JS behavior:

+
    +
  • + Browser prefixing: By default, EUI uses CSS + browser prefixes based on our{' '} + + supported browsers matrix + {' '} + (latest evergreen only). Should you need to customize this, you + can pass in your own prefix plugin via the{' '} + stylisPlugins option. + +
  • +
  • + Injection location: In the case that your app has + its own static stylesheet, Emotion's styles may not be injected + into the correct location in the {''}, + causing unintentional overrides or unapplied styles. You can use + the container option and{' '} + {''} tags to achieve this. +
  • +
diff --git a/src-docs/src/views/provider/provider_styles.tsx b/src-docs/src/views/provider/provider_styles.tsx index 9b4ca730d41..c32fdbfe1f9 100644 --- a/src-docs/src/views/provider/provider_styles.tsx +++ b/src-docs/src/views/provider/provider_styles.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { EuiCodeBlock, EuiSpacer, useEuiTheme } from '../../../../src'; +import { EuiCodeBlock, useEuiTheme } from '../../../../src'; export default () => { const { colorMode } = useEuiTheme(); @@ -22,18 +22,17 @@ export default () => { `} - - {`// App.js -import { EuiProvider } from '@elastic/eui' +import { EuiProvider, euiStylisPrefixer } from '@elastic/eui' import createCache from '@emotion/cache'; const euiCache = createCache({ key: 'eui', + stylisPlugins: [euiStylisPrefixer], container: document.querySelector('meta[name="eui-style-insert"]'), }); -cache.compat = true; +euiCache.compat = true; {/* Content */} diff --git a/src/components/bottom_bar/__snapshots__/bottom_bar.test.tsx.snap b/src/components/bottom_bar/__snapshots__/bottom_bar.test.tsx.snap index fe8ec160fdd..d890fdb1121 100644 --- a/src/components/bottom_bar/__snapshots__/bottom_bar.test.tsx.snap +++ b/src/components/bottom_bar/__snapshots__/bottom_bar.test.tsx.snap @@ -6,12 +6,12 @@ exports[`EuiBottomBar is rendered 1`] = ` >
@@ -38,12 +38,12 @@ exports[`EuiBottomBar props affordForDisplacement can be false 1`] = ` >

`; diff --git a/src/components/control_bar/__snapshots__/control_bar.test.tsx.snap b/src/components/control_bar/__snapshots__/control_bar.test.tsx.snap index a943f8ee0b8..2a29d3c7006 100644 --- a/src/components/control_bar/__snapshots__/control_bar.test.tsx.snap +++ b/src/components/control_bar/__snapshots__/control_bar.test.tsx.snap @@ -49,7 +49,7 @@ exports[`EuiControlBar is rendered 1`] = `

@@ -34,7 +34,7 @@ exports[`_EuiPageBottomBar paddingSize l is rendered 1`] = `

Content diff --git a/src/components/provider/provider.test.tsx b/src/components/provider/provider.test.tsx index 84fc7a9c96e..0f61e2d61e2 100644 --- a/src/components/provider/provider.test.tsx +++ b/src/components/provider/provider.test.tsx @@ -66,14 +66,22 @@ describe('EuiProvider', () => { ) as HTMLStyleElement; }; - it('uses a default cache from Emotion when configured without a cache', () => { - render(); + it('uses a default fallback cache with EUI prefixing when one is not passed', () => { + render( + +
+ + ); expect(emotionCache.key).toEqual('css'); expect(getStyleByCss('html').dataset.emotion).toEqual('css-global'); expect(getStyleByCss('.eui-displayBlock').dataset.emotion).toEqual( 'css-global' ); + // The below CSS would have prefixes if the default `@emotion/css` cache were used + expect(getStyleByCss('test-no-cache').textContent).toMatchInlineSnapshot( + `".css-1b3dqg7-test-no-cache{display:flex;}"` + ); }); it('applies the cache to all styles', () => { diff --git a/src/components/provider/provider.tsx b/src/components/provider/provider.tsx index c9e39613e70..69403787511 100644 --- a/src/components/provider/provider.tsx +++ b/src/components/provider/provider.tsx @@ -7,13 +7,8 @@ */ import React, { PropsWithChildren } from 'react'; -import { cache as fallbackCache, EmotionCache } from '@emotion/css'; +import type { EmotionCache } from '@emotion/css'; -import { - EuiGlobalStyles, - EuiGlobalStylesProps, -} from '../../global_styling/reset/global_styles'; -import { EuiUtilityClasses } from '../../global_styling/utility/utility'; import { EuiThemeProvider, EuiThemeProviderProps, @@ -21,7 +16,15 @@ import { CurrentEuiBreakpointProvider, } from '../../services'; import { emitEuiProviderWarning } from '../../services/theme/warning'; +import { cache as fallbackCache } from '../../services/emotion/css'; + +import { + EuiGlobalStyles, + EuiGlobalStylesProps, +} from '../../global_styling/reset/global_styles'; +import { EuiUtilityClasses } from '../../global_styling/utility/utility'; import { EuiThemeAmsterdam } from '../../themes'; + import { EuiCacheProvider } from './cache'; import { EuiProviderNestedCheck, useIsNestedEuiProvider } from './nested'; import { diff --git a/src/services/emotion/css.test.ts b/src/services/emotion/css.test.ts new file mode 100644 index 00000000000..2e2bab76422 --- /dev/null +++ b/src/services/emotion/css.test.ts @@ -0,0 +1,36 @@ +/* + * 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 { css, cx } from './css'; + +describe('custom EUI Emotion instance', () => { + it("creates a vanilla JS className that contains styles with EUI's custom configuration", () => { + const test = css` + display: flex; + `; + expect(test).toEqual('css-vyoujf'); + + // Should not have any extra browser prefixes, per EUI's configuration + const styleOutput = document.head.querySelector('style[data-emotion]')!; + expect(styleOutput.textContent).toEqual('.css-vyoujf{display:flex;}'); + }); + + // NOTE: Currently, custom Emotion instances do *not* merge css auto labels + // @see https://github.com/emotion-js/emotion/issues/3113 + it('correctly merges css with labels', () => { + const test1 = css` + label: hello; + color: red; + `; + const test2 = css` + label: world; + background-color: blue; + `; + expect(cx(test1, test2)).toEqual('css-4dyepw-hello-world'); + }); +}); diff --git a/src/services/emotion/css.ts b/src/services/emotion/css.ts new file mode 100644 index 00000000000..0c3193996bf --- /dev/null +++ b/src/services/emotion/css.ts @@ -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 createEmotion from '@emotion/css/create-instance'; + +import { euiStylisPrefixer } from './prefixer'; + +/** + * This custom instance is needed for internal EUI components to call + * `@emotion/css` with EUI's custom prefixer plugin + * @see https://emotion.sh/docs/@emotion/css#custom-instances + * + * NOTE: Usage is currently being beta tested internally, + * and is not yet intended to be a public export + */ +export const { css, cx, cache } = createEmotion({ + key: 'css', + stylisPlugins: [euiStylisPrefixer], + speedy: false, +}); diff --git a/src/services/emotion/index.ts b/src/services/emotion/index.ts index d7a50a60227..2fb45487285 100644 --- a/src/services/emotion/index.ts +++ b/src/services/emotion/index.ts @@ -7,3 +7,4 @@ */ export * from './clone_element'; +export * from './prefixer'; diff --git a/src/services/emotion/prefixer.test.tsx b/src/services/emotion/prefixer.test.tsx new file mode 100644 index 00000000000..867e611290e --- /dev/null +++ b/src/services/emotion/prefixer.test.tsx @@ -0,0 +1,567 @@ +/* + * 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, { FunctionComponent, PropsWithChildren } from 'react'; +import { render } from '@testing-library/react'; +import { css, keyframes } from '@emotion/react'; +import { cache as defaultEmotionCache } from '@emotion/css'; +import createCache from '@emotion/cache'; + +import { EuiProvider } from '../../components/provider'; + +import { euiStylisPrefixer } from './prefixer'; + +describe('euiStylisPrefixer', () => { + const cacheWithPrefixer = createCache({ + key: 'test', + stylisPlugins: [euiStylisPrefixer], + }); + + const wrapper: FunctionComponent = ({ children }) => ( + + {children} + + ); + + const getStyleCss = (label: string) => { + const styleEl = Array.from( + document.querySelectorAll('style[data-emotion]') + ).find((el) => el?.textContent?.includes(label)) as HTMLStyleElement; + if (!styleEl) return; + + // Make output styles a little easier to read + return styleEl + .textContent!.replace('{', ' {\n') + .replace(/;/g, ';\n') + .replace(/:/g, ': '); + }; + + describe('does prefix', () => { + test('user-select', () => { + render( +
, + { wrapper } + ); + expect(getStyleCss('user-select')).toMatchInlineSnapshot(` + ".test-8c1x7t-user-select { + -webkit-user-select: none; + user-select: none; + }" + `); + }); + + test('text-decoration', () => { + render( +
, + { wrapper } + ); + expect(getStyleCss('text-decoration')).toMatchInlineSnapshot(` + ".test-5idn3j-text-decoration { + -webkit-text-decoration: line-through dashed blue; + text-decoration: line-through dashed blue; + text-decoration-line: underline overline; + text-decoration-style: wavy; + text-decoration-color: red; + text-decoration-skip: objects; + }" + `); + }); + + test('text-size-adjust', () => { + render( +
, + { wrapper } + ); + expect(getStyleCss('text-size-adjust')).toMatchInlineSnapshot(` + ".test-15dfadm { + -webkit-text-size-adjust: 80%; + text-size-adjust: 80%; + }" + `); + }); + + test('box-decoration-break', () => { + render( +
, + { wrapper } + ); + expect(getStyleCss('box-decoration-break')).toMatchInlineSnapshot(` + ".test-m64wfr-box-decoration-break { + -webkit-box-decoration-break: slice; + box-decoration-break: slice; + }" + `); + }); + + describe('mask CSS', () => { + render( +
, + { wrapper } + ); + expect(getStyleCss('mask-css')).toMatchInlineSnapshot(` + ".test-16l1rpr-mask-css { + -webkit-mask: url(mask.svg); + mask: url(mask.svg); + -webkit-mask-image: linear-gradient(rgba(0, 0, 0, 1), transparent); + mask-image: linear-gradient(rgba(0, 0, 0, 1), transparent); + -webkit-mask-clip: border-box; + mask-clip: border-box; + -webkit-mask-origin: padding-box; + mask-origin: padding-box; + -webkit-mask-composite: subtract; + mask-composite: subtract; + -webkit-mask-mode: alpha; + mask-mode: alpha; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-repeat: repeat-x; + mask-repeat: repeat-x; + -webkit-mask-size: contain; + mask-size: contain; + }" + `); + }); + + test('background-clip text', () => { + render( +
, + { wrapper } + ); + expect(getStyleCss('background-clip')).toMatchInlineSnapshot(` + ".test-9vijyk-background-clip { + background-clip: content-box; + -webkit-background-clip: text; + background-clip: text; + }" + `); + }); + + test('print-color-adjust', () => { + render( +
, + { wrapper } + ); + expect(getStyleCss('print-color-adjust')).toMatchInlineSnapshot(` + ".test-cx4oo-print-color-adjust { + -webkit-print-color-adjust: economy; + print-color-adjust: economy; + }" + `); + }); + + test('max-content, min-content, fit-content, and stretch sizing values', () => { + render( +
, + { wrapper } + ); + expect(getStyleCss('intrinsic-extrensic-sizing')).toMatchInlineSnapshot(` + ".test-tsfq3u-intrinsic-extrensic-sizing { + height: max-content; + width: min-content; + max-inline-size: fit-content; + min-block-size: -webkit-fill-available; + min-block-size: -moz-available; + min-block-size: stretch; + }" + `); + }); + }); + + describe('does not prefix', () => { + test('flex CSS', () => { + render( +
, + { wrapper } + ); + + expect(getStyleCss('no-flex-prefixes')).toMatchInlineSnapshot(` + ".test-1467bft-no-flex-prefixes { + display: flex; + display: inline-flex; + align-items: center; + align-content: center; + align-self: center; + justify-content: center; + flex-shrink: 0; + flex-grow: 0; + flex-basis: 100%; + flex: 1; + flex-direction: column; + order: 2; + }" + `); + }); + + test('transform & transition CSS', () => { + render( +
, + { wrapper } + ); + + expect(getStyleCss('no-transform-prefixes')).toMatchInlineSnapshot(` + ".test-1oc6c8-no-transform-prefixes { + transform: translateY(-1px); + transition: transform 2s linear; + }" + `); + }); + + test('animation CSS', () => { + const testAnimation = keyframes` + from { opacity: 0; } + to { opacity: 1; } + `; + render( +
, + { wrapper } + ); + + expect(getStyleCss('no-animation-prefixes')).toMatchInlineSnapshot(` + ".test-1aw2200-no-animation-prefixes { + animation: animation-1flhruc; + animation-name: test; + animation-delay: 1s; + animation-direction: reverse; + animation-duration: 50ms; + animation-fill-mode: both; + animation-iteration-count: infinite; + animation-play-state: paused; + animation-timing-function: ease-in-out; + }" + `); + expect(getStyleCss('@keyframes')).toBeTruthy(); + expect(getStyleCss('@-webkit-keyframes')).toBeFalsy(); + }); + + test('position sticky', () => { + render( +
, + { wrapper } + ); + + expect(getStyleCss('no-position-sticky-prefix')).toMatchInlineSnapshot(` + ".test-11x1k54-no-position-sticky-prefix { + position: sticky; + }" + `); + }); + + test('writing mode CSS', () => { + render( +
, + { wrapper } + ); + + expect(getStyleCss('no-writing-mode-prefixes')).toMatchInlineSnapshot(` + ".test-1d4iba8-no-writing-mode-prefixes { + writing-mode: vertical-lr; + writing-mode: vertical-rl; + writing-mode: horizontal-tb; + }" + `); + }); + + test('inline logical properties CSS', () => { + render( +
, + { wrapper } + ); + expect(getStyleCss('no-logical-properties-prefixes')) + .toMatchInlineSnapshot(` + ".test-11697m3-no-logical-properties-prefixes { + padding-inline-start: 1rem; + margin-inline-end: 2em; + }" + `); + }); + + test('columns CSS', () => { + render( +
, + { wrapper } + ); + + expect(getStyleCss('no-columns-prefixes')).toMatchInlineSnapshot(` + ".test-1eossg4-no-columns-prefixes { + columns: 3; + column-count: 5; + column-fill: balance; + column-gap: 10px; + column-width: 20px; + column-span: all; + column-rule: blue dotted 2px; + }" + `); + }); + + test('misc text effect CSS', () => { + render( +
, + { wrapper } + ); + expect(getStyleCss('no-misc-text-effect-prefixes')) + .toMatchInlineSnapshot(` + ".test-29xaas-no-misc-text-effect-prefixes { + appearance: none; + hyphens: auto; + cursor: grab; + }" + `); + }); + + test('misc filter/effect/image CSS', () => { + render( +
, + { wrapper } + ); + + expect(getStyleCss('no-filter-prefixes')).toMatchInlineSnapshot(` + ".test-8ziglt-no-filter-prefixes { + filter: grayscale(1); + clip-path: url(#foo); + backface-visibility: visible; + background: image-set( + linear-gradient(blue, white) 1x, + linear-gradient(blue, green) 2x + ); + }" + `); + }); + + test('misc selectors', () => { + render( +
, + { wrapper } + ); + + expect(getStyleCss('::-moz-placeholder')).toBeFalsy(); + expect(getStyleCss(':-moz-read-only')).toBeFalsy(); + expect(getStyleCss(':-moz-read-write')).toBeFalsy(); + }); + }); + + describe('default Emotion cache', () => { + it('prefixes extra CSS that the EUI plugin does not', () => { + render( + +
+ + ); + + expect(getStyleCss('test-default-cache')).toMatchInlineSnapshot(` + ".css-tfft1m-test-default-cache { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-padding-start: 1rem; + padding-inline-start: 1rem; + -webkit-margin-end: 2em; + margin-inline-end: 2em; + -webkit-animation: something; + animation: something; + -webkit-transform: translateY(-1px); + -moz-transform: translateY(-1px); + -ms-transform: translateY(-1px); + transform: translateY(-1px); + -webkit-transition: -webkit-transform 2s linear; + transition: transform 2s linear; + position: -webkit-sticky; + position: sticky; + -webkit-writing-mode: vertical-rl; + -ms-writing-mode: tb-rl; + writing-mode: vertical-rl; + -webkit-column-count: 2; + column-count: 2; + block-size: -webkit-max-content; + block-size: -moz-max-content; + block-size: max-content; + -webkit-filter: blur(5px); + filter: blur(5px); + cursor: -webkit-grab; + cursor: grab; + }" + `); + expect(getStyleCss('::-moz-placeholder')).toBeTruthy(); + }); + }); +}); diff --git a/src/services/emotion/prefixer.ts b/src/services/emotion/prefixer.ts new file mode 100644 index 00000000000..c1c6259c5a4 --- /dev/null +++ b/src/services/emotion/prefixer.ts @@ -0,0 +1,120 @@ +/* + * 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 { + charat, + DECLARATION, + hash, + indexof, + MOZ, + replace, + strlen, + WEBKIT, + type Element, +} from 'stylis'; + +// This is a heavily modified version of Emotion's default `prefixer` plugin +// (mostly removing unnecessary prefixes), which is in turn a modified version +// of stylis's default prefixer. +// @see https://github.com/emotion-js/emotion/blob/main/packages/cache/src/prefixer.js +/* eslint-disable prefer-template */ + +/** + * This is a stylis plugin which handles auto-prefixing CSS output by Emotion. + * + * *Please note*: EUI/Elastic targets latest evergreen browsers for support only. + * @see https://www.elastic.co/support/matrix#matrix_browsers + */ +export const euiStylisPrefixer = (element: Element) => { + if (element.length > -1) + if (!element.return) + switch (element.type) { + case DECLARATION: + element.return = prefix(element.value, element.length); + break; + } +}; + +const prefix = (value: Element['value'], length: Element['length']): string => { + switch (hash(value, length)) { + /** + * `-webkit` prefixes + */ + // user-select - https://caniuse.com/mdn-css_properties_user-select - needed by Safari + case 4246: + // text-decoration - https://caniuse.com/text-decoration - iOS Safari is the main one that needs this + case 5572: + // text-size-adjust - https://caniuse.com/text-size-adjust - iOS Safari + case 2756: + // box-decoration-break - https://caniuse.com/css-boxdecorationbreak - Chrome & Safari + case 3005: + // mask, mask-image, mask-(mode|clip|size), mask-(repeat|origin), mask-position, mask-composite - Chrome + case 6391: + case 5879: + case 5623: + case 6135: + case 4599: + case 4855: + // print-color-adjust - https://caniuse.com/css-color-adjust - Chrome + case 2282: + return WEBKIT + value + value; + + // background-clip - https://caniuse.com/background-clip-text - Chrome, only for `text` value + case 4215: + if (~indexof(value, 'text')) { + return WEBKIT + value + value; + } + + /** + * Intrinsic/extrinsic sizing value prefixes + * `stretch` alternatives needed by Chrome & Firefox - https://caniuse.com/intrinsic-width + */ + // (min|max)?(width|height|inline-size|block-size) + case 8116: + case 7059: + case 5753: + case 5535: + case 5445: + case 5701: + case 4933: + case 4677: + case 5533: + case 5789: + case 5021: + case 4765: + // stretch, max-content, min-content, fill-available + if (strlen(value) - 1 - length > 6) + switch (charat(value, length + 1)) { + // (f)ill-available + case 102: + if (~indexof(value, 'fill-available')) { + return replace( + value, + /(.+:)(.+)-([^]+)/, + '$1' + + WEBKIT + + '$2-$3' + + '$1' + + MOZ + + (charat(value, length + 3) === 108 ? '$3' : '$2-$3') + ); + } + // (s)tretch + case 115: + if (~indexof(value, 'stretch')) { + return ( + prefix(replace(value, 'stretch', 'fill-available'), length) + + value + ); + } + } + break; + } + + return value; +}; diff --git a/src/services/theme/__snapshots__/provider.test.tsx.snap b/src/services/theme/__snapshots__/provider.test.tsx.snap index 47336d01d76..5d8598c2489 100644 --- a/src/services/theme/__snapshots__/provider.test.tsx.snap +++ b/src/services/theme/__snapshots__/provider.test.tsx.snap @@ -2,7 +2,7 @@ exports[`EuiThemeProvider CSS variables allows child components to set non-global theme CSS variables 1`] = ` `; @@ -11,7 +11,7 @@ exports[`EuiThemeProvider nested EuiThemeProviders allows avoiding the extra spa Top-level provider
clone provider color onto div
@@ -23,7 +23,7 @@ exports[`EuiThemeProvider nested EuiThemeProviders allows customizing the span w Top-level provider Nested @@ -36,15 +36,15 @@ exports[`EuiThemeProvider nested EuiThemeProviders renders with a span wrapper t Top-level provider Nested Double nested Triple nested diff --git a/src/services/theme/provider.tsx b/src/services/theme/provider.tsx index 0005f356d9f..76fab79de5b 100644 --- a/src/services/theme/provider.tsx +++ b/src/services/theme/provider.tsx @@ -16,13 +16,12 @@ import React, { PropsWithChildren, HTMLAttributes, } from 'react'; -import classNames from 'classnames'; -import { css } from '@emotion/css'; import { Global, type CSSObject } from '@emotion/react'; import isEqual from 'lodash/isEqual'; import type { CommonProps } from '../../components/common'; import { cloneElementWithCss } from '../emotion'; +import { css, cx } from '../emotion/css'; import { EuiSystemContext, @@ -160,7 +159,7 @@ export const EuiThemeProvider = ({ ? false : bodyColor !== theme.colors.text, colorClassName: css` - label: euiColorMode-${_colorMode}; + label: euiColorMode-${_colorMode || colorMode}; color: ${theme.colors.text}; `, setGlobalCSSVariables: isGlobalTheme @@ -177,6 +176,7 @@ export const EuiThemeProvider = ({ isGlobalTheme, bodyColor, _colorMode, + colorMode, setGlobalCSSVariables, globalCSSVariables, setThemeCSSVariables, @@ -191,7 +191,7 @@ export const EuiThemeProvider = ({ const { cloneElement, className, ...rest } = wrapperProps || {}; const props = { ...rest, - className: classNames(className, nestedThemeContext.colorClassName), + className: cx(className, nestedThemeContext.colorClassName), }; // Condition avoids rendering an empty Emotion selector if no // theme-specific CSS variables have been set by child components @@ -202,14 +202,11 @@ export const EuiThemeProvider = ({ if (cloneElement) { return cloneElementWithCss(children, { ...props, - className: classNames(children.props.className, props.className), + className: cx(children.props.className, props.className), }); } else { return ( - + {children} ); diff --git a/src/services/theme/utils.ts b/src/services/theme/utils.ts index 089f985a6ee..c7c9c5216d0 100644 --- a/src/services/theme/utils.ts +++ b/src/services/theme/utils.ts @@ -39,7 +39,7 @@ export const isInverseColorMode = ( /** * Returns the color mode configured in the current EuiThemeProvider. * Returns the parent color mode if none is explicity set. - * @param {string} coloMode - `light`, `dark`, or `inverse` + * @param {string} colorMode - `light`, `dark`, or `inverse` * @param {string} parentColorMode - `LIGHT` or `DARK`; used as the fallback */ export const getColorMode = ( diff --git a/upcoming_changelogs/7272.md b/upcoming_changelogs/7272.md new file mode 100644 index 00000000000..021502d52d9 --- /dev/null +++ b/upcoming_changelogs/7272.md @@ -0,0 +1,3 @@ +**CSS-in-JS conversions** + +- Reduced default CSS prefixes generated by Emotion to only browsers supported by EUI (latest evergreen browsers). This can be customized by passing your own Emotion cache to `EuiProvider`. diff --git a/yarn.lock b/yarn.lock index 895eec82a7d..b811b178a9d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5219,6 +5219,11 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw== +"@types/stylis@^4.2.1": + version "4.2.1" + resolved "https://registry.yarnpkg.com/@types/stylis/-/stylis-4.2.1.tgz#867fcb0f81719d9ecef533fdda03e32083b959f6" + integrity sha512-OSaMrXUKxVigGlKRrET39V2xdhzlztQ9Aqumn1WbCBKHOi9ry7jKSd7rkyj0GzmWaU960Rd+LpOFpLfx5bMQAg== + "@types/testing-library__jest-dom@^5.14.3", "@types/testing-library__jest-dom@^5.9.1": version "5.14.5" resolved "https://registry.yarnpkg.com/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.5.tgz#d113709c90b3c75fdb127ec338dad7d5f86c974f" From 36bd3eee7e161341767efd1bb4eaa63af65e8a96 Mon Sep 17 00:00:00 2001 From: Trevor Pierce <1Copenut@users.noreply.github.com> Date: Tue, 10 Oct 2023 12:00:50 -0500 Subject: [PATCH 30/38] [BUG] Fix broken docs link for EuiProvider props. (#7273) Co-authored-by: Cee Chen <549407+cee-chen@users.noreply.github.com> --- src-docs/src/views/provider/provider_example.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src-docs/src/views/provider/provider_example.js b/src-docs/src/views/provider/provider_example.js index 3d2d7b488c2..cd7e33ba3ee 100644 --- a/src-docs/src/views/provider/provider_example.js +++ b/src-docs/src/views/provider/provider_example.js @@ -141,8 +141,8 @@ export const ProviderExample = { utility properties on the{' '} cache prop to further define where specific styles should be inserted. See{' '} - the props documentation{' '} - for details. + the props documentation for + details.

From d8d4a3aea0b3e302a9b5f76901bfbeaf6e842991 Mon Sep 17 00:00:00 2001 From: Cee Chen <549407+cee-chen@users.noreply.github.com> Date: Wed, 11 Oct 2023 18:36:39 -0700 Subject: [PATCH 31/38] [Docs] Convert all Elastic Charts docs pages to Typescript (#7277) Co-authored-by: nickofthyme --- ...ity_bullet.js => accessibility_bullet.tsx} | 14 +-- .../elastic_charts/accessibility_example.js | 6 +- ...sunburst.js => accessibility_sunburst.tsx} | 12 +- .../{category_chart.js => category_chart.tsx} | 64 ++++------- .../elastic_charts/{data.js => data.tsx} | 0 .../elastic_charts/{goal.js => goal.tsx} | 24 ++-- .../metric/metric_chart_example.js | 10 +- .../metric/metric_chart_grid.tsx | 1 - .../metric/metric_chart_grid_column.tsx | 1 - .../metric/metric_chart_grid_row.tsx | 1 - .../metric/metric_chart_no_data.tsx | 2 - .../metric/metric_chart_overview.tsx | 1 - .../metric/metric_chart_progress_bar.tsx | 2 - .../metric/metric_chart_resizing.tsx | 1 - .../metric/metric_chart_single_value.tsx | 1 - .../metric/metric_chart_trend.tsx | 1 - .../views/elastic_charts/{pie.js => pie.tsx} | 82 ++++++++------ .../{pie_alts.js => pie_alts.tsx} | 29 +++-- .../src/views/elastic_charts/pie_example.js | 4 +- .../{pie_slices.js => pie_slices.tsx} | 40 +++---- .../elastic_charts/{shared.js => shared.tsx} | 104 +++++++++--------- .../elastic_charts/{sizes.js => sizes.tsx} | 75 +++++++------ .../{sparklines.js => sparklines.tsx} | 9 +- .../elastic_charts/sparklines_example.js | 6 +- .../{texture.js => texture.tsx} | 6 +- .../{theming.js => theming.tsx} | 38 +++---- ...categorical.js => theming_categorical.tsx} | 89 +++++++-------- .../views/elastic_charts/theming_example.js | 2 +- .../{time_chart.js => time_chart.tsx} | 44 ++++---- .../{treemap.js => treemap.tsx} | 14 +-- 30 files changed, 332 insertions(+), 351 deletions(-) rename src-docs/src/views/elastic_charts/{accessibility_bullet.js => accessibility_bullet.tsx} (91%) rename src-docs/src/views/elastic_charts/{accessibility_sunburst.js => accessibility_sunburst.tsx} (86%) rename src-docs/src/views/elastic_charts/{category_chart.js => category_chart.tsx} (86%) rename src-docs/src/views/elastic_charts/{data.js => data.tsx} (100%) rename src-docs/src/views/elastic_charts/{goal.js => goal.tsx} (85%) rename src-docs/src/views/elastic_charts/{pie.js => pie.tsx} (64%) rename src-docs/src/views/elastic_charts/{pie_alts.js => pie_alts.tsx} (93%) rename src-docs/src/views/elastic_charts/{pie_slices.js => pie_slices.tsx} (90%) rename src-docs/src/views/elastic_charts/{shared.js => shared.tsx} (58%) rename src-docs/src/views/elastic_charts/{sizes.js => sizes.tsx} (86%) rename src-docs/src/views/elastic_charts/{sparklines.js => sparklines.tsx} (94%) rename src-docs/src/views/elastic_charts/{texture.js => texture.tsx} (95%) rename src-docs/src/views/elastic_charts/{theming.js => theming.tsx} (79%) rename src-docs/src/views/elastic_charts/{theming_categorical.js => theming_categorical.tsx} (84%) rename src-docs/src/views/elastic_charts/{time_chart.js => time_chart.tsx} (84%) rename src-docs/src/views/elastic_charts/{treemap.js => treemap.tsx} (88%) diff --git a/src-docs/src/views/elastic_charts/accessibility_bullet.js b/src-docs/src/views/elastic_charts/accessibility_bullet.tsx similarity index 91% rename from src-docs/src/views/elastic_charts/accessibility_bullet.js rename to src-docs/src/views/elastic_charts/accessibility_bullet.tsx index ea958ae4318..01e386a7b7c 100644 --- a/src-docs/src/views/elastic_charts/accessibility_bullet.js +++ b/src-docs/src/views/elastic_charts/accessibility_bullet.tsx @@ -34,14 +34,14 @@ export default () => { spectrum = colorPalette([spectrum[4], euiPaletteGray(5)[4]], 5).reverse(); } - const colorMap = { - '0': spectrum[0], - '100': spectrum[1], - '125': spectrum[2], - '150': spectrum[3], - '250': spectrum[4], + const colorMap: Record = { + 0: spectrum[0], + 100: spectrum[1], + 125: spectrum[2], + 150: spectrum[3], + 250: spectrum[4], }; - const bandFillColor = (x) => colorMap[x]; + const bandFillColor = (x: number) => colorMap[x]; return ( <> diff --git a/src-docs/src/views/elastic_charts/accessibility_example.js b/src-docs/src/views/elastic_charts/accessibility_example.js index 3cdfe7be46b..4ad80adb94a 100644 --- a/src-docs/src/views/elastic_charts/accessibility_example.js +++ b/src-docs/src/views/elastic_charts/accessibility_example.js @@ -209,7 +209,7 @@ export const ElasticChartsAccessibilityExample = { `, source: [ { - type: GuideSectionTypes.JS, + type: GuideSectionTypes.TSX, code: SunburstSource, }, ], @@ -283,7 +283,7 @@ export const ElasticChartsAccessibilityExample = { demo: , source: [ { - type: GuideSectionTypes.JS, + type: GuideSectionTypes.TSX, code: TextureMultiSeriesChartSource, }, ], @@ -327,7 +327,7 @@ export const ElasticChartsAccessibilityExample = { demo: , source: [ { - type: GuideSectionTypes.JS, + type: GuideSectionTypes.TSX, code: BulletChartSource, }, ], diff --git a/src-docs/src/views/elastic_charts/accessibility_sunburst.js b/src-docs/src/views/elastic_charts/accessibility_sunburst.tsx similarity index 86% rename from src-docs/src/views/elastic_charts/accessibility_sunburst.js rename to src-docs/src/views/elastic_charts/accessibility_sunburst.tsx index dccdb76e719..0f395cb97fc 100644 --- a/src-docs/src/views/elastic_charts/accessibility_sunburst.js +++ b/src-docs/src/views/elastic_charts/accessibility_sunburst.tsx @@ -19,9 +19,10 @@ export default () => { const euiChartTheme = isDarkTheme ? EUI_CHARTS_THEME_DARK : EUI_CHARTS_THEME_LIGHT; - const { vizColors } = euiChartTheme.theme.colors; + const { vizColors } = euiChartTheme.theme.colors!; - const data = [ + type Data = { fruit: string; count: number }; + const data: Data[] = [ { fruit: 'Apple', count: 100 }, { fruit: 'Banana', count: 50 }, { fruit: 'Tomato', count: 25 }, @@ -54,15 +55,16 @@ export default () => { ariaTableCaption="For the chart representation, after Clementine (22) individual results are not labelled as the segments become too small" /> count} layers={[ { - groupByRollup: ({ fruit }) => fruit, + groupByRollup: ({ fruit }: Data) => fruit, shape: { - fillColor: (key, sortIndex) => - vizColors[sortIndex % vizColors.length], + fillColor: (_, sortIndex) => + vizColors![sortIndex % vizColors!.length], }, }, ]} diff --git a/src-docs/src/views/elastic_charts/category_chart.js b/src-docs/src/views/elastic_charts/category_chart.tsx similarity index 86% rename from src-docs/src/views/elastic_charts/category_chart.js rename to src-docs/src/views/elastic_charts/category_chart.tsx index b7e8dadb94e..2ba9a70b991 100644 --- a/src-docs/src/views/elastic_charts/category_chart.js +++ b/src-docs/src/views/elastic_charts/category_chart.tsx @@ -27,6 +27,7 @@ import { ChartTypeCard, MultiChartCard, CHART_COMPONENTS, + type ChartType, ChartCard, } from './shared'; @@ -38,33 +39,8 @@ export default () => { const [rotated, setRotated] = useState(true); const [ordered, setOrdered] = useState(true); const [formatted, setFormatted] = useState(false); - const [chartType, setChartType] = useState('BarSeries'); + const [chartType, setChartType] = useState('BarSeries'); const [valueLabels, setValueLabels] = useState(false); - const onMultiChange = (multiObject) => { - const { multi, stacked } = multiObject; - setMulti(multi); - setStacked(stacked); - }; - - const onRotatedChange = (e) => { - setRotated(e.target.checked); - }; - - const onOrderedChange = (e) => { - setOrdered(e.target.checked); - }; - - const onFormatChange = (e) => { - setFormatted(e.target.checked); - }; - - const onChartTypeChange = (chartType) => { - setChartType(chartType); - }; - - const onValueLabelsChange = (e) => { - setValueLabels(e.target.checked); - }; const isDarkTheme = colorMode === 'DARK'; const theme = isDarkTheme @@ -83,7 +59,7 @@ export default () => { ...theme, barSeriesStyle: { displayValue: { - ...theme.barSeriesStyle.displayValue, + ...theme.barSeriesStyle?.displayValue, offsetX: rotated ? 4 : 0, offsetY: rotated ? 0 : -4, ...(multi && stacked @@ -91,15 +67,15 @@ export default () => { alignment: { vertical: 'middle', horizontal: 'center', - }, + } as const, } : { alignment: rotated ? { - vertical: 'middle', + vertical: 'middle' as const, } : { - horizontal: 'center', + horizontal: 'center' as const, }, }), }, @@ -169,7 +145,7 @@ const customTheme = { `; - const removeEmptyLines = (string) => string.replace(/(^[ \t]*\n)/gm, ''); + const removeEmptyLines = (string: string) => + string.replace(/(^[ \t]*\n)/gm, ''); const textToCopy = valueLabels ? `${chartVariablesForValueLabels} @@ -216,12 +193,12 @@ ${removeEmptyLines(chartConfigurationToCopy)}` yAccessors={['count']} splitSeriesAccessors={multi ? ['issueType'] : undefined} stackAccessors={stacked ? ['issueType'] : undefined} - displayValueSettings={valueLabels && displayValueSettings} + displayValueSettings={valueLabels ? displayValueSettings : undefined} /> setOrdered(e.target.checked)} /> setRotated(e.target.checked)} /> @@ -271,21 +248,26 @@ ${removeEmptyLines(chartConfigurationToCopy)}` setFormatted(e.target.checked)} /> - type="Although we recommend only bar charts, categorical" - onChange={onChartTypeChange} + onChange={(chartType) => setChartType(chartType)} disabled /> - + { + setMulti(multi); + setStacked(stacked); + }} + /> @@ -297,7 +279,7 @@ ${removeEmptyLines(chartConfigurationToCopy)}` setValueLabels(e.target.checked)} /> diff --git a/src-docs/src/views/elastic_charts/data.js b/src-docs/src/views/elastic_charts/data.tsx similarity index 100% rename from src-docs/src/views/elastic_charts/data.js rename to src-docs/src/views/elastic_charts/data.tsx diff --git a/src-docs/src/views/elastic_charts/goal.js b/src-docs/src/views/elastic_charts/goal.tsx similarity index 85% rename from src-docs/src/views/elastic_charts/goal.js rename to src-docs/src/views/elastic_charts/goal.tsx index b834d2308e4..5edcac5b28f 100644 --- a/src-docs/src/views/elastic_charts/goal.js +++ b/src-docs/src/views/elastic_charts/goal.tsx @@ -1,13 +1,13 @@ -import React, { useContext } from 'react'; +import React from 'react'; import { Chart, Settings, Goal } from '@elastic/charts'; import { EuiSpacer, EuiTitle, EuiCodeBlock } from '../../../../src/components'; import { htmlIdGenerator, + useEuiTheme, useIsWithinBreakpoints, euiPalettePositive, } from '../../../../src/services'; import { EuiFlexGrid, EuiFlexItem } from '../../../../src/components/flex'; -import { ThemeContext } from '../../components'; import { EUI_CHARTS_THEME_DARK, @@ -15,21 +15,20 @@ import { } from '../../../../src/themes/charts/themes'; export const GoalChart = () => { + const { colorMode } = useEuiTheme(); const id = htmlIdGenerator('goal')(); - const themeContext = useContext(ThemeContext); - const isDarkTheme = themeContext.theme.includes('dark'); + + const isDarkTheme = colorMode === 'DARK'; const euiChartTheme = isDarkTheme ? EUI_CHARTS_THEME_DARK : EUI_CHARTS_THEME_LIGHT; - const euiGoalConfig = euiChartTheme.euiGoalConfig; - const isDesktop = useIsWithinBreakpoints(['l', 'xl']); const bandLabels = ['', 'freezing', 'cold', 'warm', 'hot']; const bands = [-10, 0, 15, 25, 40]; const spectrum = euiPalettePositive(5); - const opacityMapHex = { + const opacityMapHex: Record = { '-10': spectrum[0], '0': spectrum[1], '15': spectrum[2], @@ -40,9 +39,9 @@ export const GoalChart = () => { const colorMapTheme = bands.reduce((acc, band) => { acc[band] = opacityMapHex[band]; return acc; - }, {}); + }, {} as Record); - const bandFillColor = (x) => colorMapTheme[x]; + const bandFillColor = (x: number) => colorMapTheme[x]; return ( @@ -55,9 +54,11 @@ export const GoalChart = () => { ariaLabelledBy={id} ariaDescription="This goal chart has a target of 22." ariaUseDefaultSummary={false} - theme={euiChartTheme} + theme={euiChartTheme.theme} /> { labelMinor="Celsius" centralMajor="12" centralMinor="" - config={{ ...euiGoalConfig, angleStart: Math.PI, angleEnd: 0 }} + angleStart={Math.PI} + angleEnd={0} bandLabels={bandLabels} /> diff --git a/src-docs/src/views/elastic_charts/metric/metric_chart_example.js b/src-docs/src/views/elastic_charts/metric/metric_chart_example.js index 8e1891d9292..690f391676f 100644 --- a/src-docs/src/views/elastic_charts/metric/metric_chart_example.js +++ b/src-docs/src/views/elastic_charts/metric/metric_chart_example.js @@ -465,9 +465,11 @@ export const MetricChartExample = { { title: 'No Data', text: ( -

- Various situations could lead to an uncertain state. We designed two{' '} - empty states that should cover most of those cases: + <> +

+ Various situations could lead to an uncertain state. We designed two{' '} + empty states that should cover most of those cases: +

  • When an applied filter makes the metric uncomputable (missing @@ -483,7 +485,7 @@ export const MetricChartExample = { component won't be rendered, and an empty box is rendered.
-

+ ), demo: , }, diff --git a/src-docs/src/views/elastic_charts/metric/metric_chart_grid.tsx b/src-docs/src/views/elastic_charts/metric/metric_chart_grid.tsx index 4508de3f285..e8fb53581fd 100644 --- a/src-docs/src/views/elastic_charts/metric/metric_chart_grid.tsx +++ b/src-docs/src/views/elastic_charts/metric/metric_chart_grid.tsx @@ -149,7 +149,6 @@ export default () => { const chartBaseTheme = isDarkTheme ? DARK_THEME : LIGHT_THEME; return ( - {/* @ts-ignore @elastic/charts typings are not yet compatible with React 18 */} diff --git a/src-docs/src/views/elastic_charts/metric/metric_chart_grid_column.tsx b/src-docs/src/views/elastic_charts/metric/metric_chart_grid_column.tsx index 5e1810534fa..d5c6bff2679 100644 --- a/src-docs/src/views/elastic_charts/metric/metric_chart_grid_column.tsx +++ b/src-docs/src/views/elastic_charts/metric/metric_chart_grid_column.tsx @@ -22,7 +22,6 @@ export default () => { const chartBaseTheme = isDarkTheme ? DARK_THEME : LIGHT_THEME; return ( - {/* @ts-ignore @elastic/charts typings are not yet compatible with React 18 */} { const chartBaseTheme = isDarkTheme ? DARK_THEME : LIGHT_THEME; return ( - {/* @ts-ignore @elastic/charts typings are not yet compatible with React 18 */} { No Data - {/* @ts-ignore @elastic/charts typings are not yet compatible with React 18 */} { Filtered Out - {/* @ts-ignore @elastic/charts typings are not yet compatible with React 18 */} { })); return ( - {/* @ts-ignore @elastic/charts typings are not yet compatible with React 18 */} { paddingSize="none" style={{ overflow: 'hidden', width: '200px' }} > - {/* @ts-ignore @elastic/charts typings are not yet compatible with React 18 */} { paddingSize="none" style={{ overflow: 'hidden', width: '200px' }} > - {/* @ts-ignore @elastic/charts typings are not yet compatible with React 18 */} { paddingSize="none" style={{ overflow: 'hidden', height: '100%', width: '100%' }} > - {/* @ts-ignore @elastic/charts typings are not yet compatible with React 18 */} { - {/* @ts-ignore @elastic/charts typings are not yet compatible with React 18 */} { return ( - {/* @ts-ignore @elastic/charts typings are not yet compatible with React 18 */} { const { colorMode } = useEuiTheme(); const htmlId = htmlIdGenerator(); @@ -28,6 +60,10 @@ export default () => { ? EUI_CHARTS_THEME_DARK : EUI_CHARTS_THEME_LIGHT; + const themeOverrides: PartialTheme = { + partition: { emptySizeRatio: 0.4 }, + }; + return (
@@ -37,27 +73,21 @@ export default () => { - + d.count} layers={[ { - groupByRollup: (d) => d.status, + groupByRollup: (d: (typeof STATUS_DATA)[0]) => d.status, shape: { - fillColor: (key, sortIndex) => - euiChartTheme.theme.colors.vizColors[sortIndex], + fillColor: (_, sortIndex) => + euiChartTheme.theme.colors!.vizColors![sortIndex], }, }, ]} @@ -74,33 +104,19 @@ export default () => { Number(d.percent)} valueFormatter={() => ''} layers={[ { - groupByRollup: (d) => d.language, + groupByRollup: (d: (typeof LANGUAGE_DATA)[0]) => d.language, shape: { - fillColor: (key, sortIndex) => - euiChartTheme.theme.colors.vizColors[sortIndex], + fillColor: (_, sortIndex) => + euiChartTheme.theme.colors!.vizColors![sortIndex], }, }, ]} - emptySizeRatio={0.4} clockwiseSectors={false} /> diff --git a/src-docs/src/views/elastic_charts/pie_alts.js b/src-docs/src/views/elastic_charts/pie_alts.tsx similarity index 93% rename from src-docs/src/views/elastic_charts/pie_alts.js rename to src-docs/src/views/elastic_charts/pie_alts.tsx index bfbd3d7bd41..9446eac4e6b 100644 --- a/src-docs/src/views/elastic_charts/pie_alts.js +++ b/src-docs/src/views/elastic_charts/pie_alts.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { ReactNode, useState } from 'react'; import groupBy from 'lodash/groupBy'; import mapValues from 'lodash/mapValues'; import orderBy from 'lodash/orderBy'; @@ -53,7 +53,7 @@ export default () => { ); if (formatted) { color = [ - euiPaletteForTemperature()[0], + euiPaletteForTemperature(0)[0], euiPaletteGray(5)[isDarkTheme ? 4 : 0], ]; } @@ -77,15 +77,15 @@ export default () => { data = orderBy(DATASET, 'issueType', 'desc'); const sortedData = sortBy(data, [ - ({ vizType }) => totals[vizType], + ({ vizType }: (typeof DATASET)[0]) => totals[vizType], ]).reverse(); data = sortedData; } } - const tickFormat = (tick) => { + const tickFormat = (tick: string) => { if (formatted) { - return `${Number(tick * 100).toFixed(0)}%`; + return `${(Number(tick) * 100).toFixed(0)}%`; } else if (!grouped && String(tick).length > 1) { return String(tick).substring(0, String(tick).length - 3); } else { @@ -95,7 +95,7 @@ export default () => { let isMisleadingChart = false; let isBadChart = false; - let description = + let description: ReactNode = 'This chart is a good alternative to the standard multi-tier pie (or sunburst) chart. It clearly represents the actual values while maintaining visual comparison.'; let title = 'Good alternative'; @@ -139,12 +139,12 @@ export default () => { @@ -152,7 +152,7 @@ export default () => { id="left-axis" position={rotated ? 'bottom' : 'left'} tickFormat={tickFormat} - showGridLines + gridLine={{ visible: true }} /> @@ -177,12 +177,12 @@ export default () => { @@ -190,7 +190,7 @@ export default () => { id="left-axis" position={rotated ? 'bottom' : 'left'} tickFormat={tickFormat} - showGridLines + gridLine={{ visible: true }} /> @@ -213,7 +213,6 @@ export default () => { @@ -279,7 +278,7 @@ export default () => { xAccessor="${usesRainData ? 'season' : 'vizType'}" yAccessors={[${usesRainData ? "'days'" : "'count'"}]} splitSeriesAccessors={[${usesRainData ? "'precipitation'" : "'issueType'"}]} - ${formatted ? 'stackAsPercentage={true}' : ''} + ${formatted ? 'stackMode="percentage"' : ''} ${ stacked ? `stackAccessors={[${ @@ -299,7 +298,7 @@ export default () => { /> { const [sliceOrderIdSelected, setSliceOrderIdSelected] = useState( sliceOrderRadios[0].id ); - const [sliceOrderConfig, setSliceOrderConfig] = useState({ + const [sliceOrderConfig, setSliceOrderConfig] = useState<{ + clockwiseSectors?: boolean; + specialFirstInnermostSector?: boolean; + }>({ clockwiseSectors: false, }); const [sliceOrderConfigText, setSliceOrderConfigText] = useState( @@ -77,20 +80,15 @@ export default () => { pieTypeRadios[0].id ); - const [numSlices, setNumSlices] = useState('3'); + const [numSlices, setNumSlices] = useState(3); const [grouped, setGrouped] = useState(true); const [showLegend, setShowLegend] = useState(false); const [showValues, setShowValues] = useState(true); - const onNumChartsChange = (e) => { - setNumSlices(e.target.value); - }; - - const onSliceOrderChange = (optionId) => { - const sliceOrderLabel = sliceOrderRadios.find( - ({ id }) => id === optionId - ).label; + const onSliceOrderChange = (optionId: string) => { + const sliceOrderLabel = sliceOrderRadios.find(({ id }) => id === optionId)! + .label!; if (sliceOrderLabel.includes('Counter')) { setSliceOrderConfig({ clockwiseSectors: false }); setSliceOrderConfigText('clockwiseSectors={false}'); @@ -106,10 +104,6 @@ export default () => { setSliceOrderIdSelected(optionId); }; - const onGroupChange = (e) => { - setGrouped(e.target.checked); - }; - const isBadChart = numSlices > 5 && !grouped; const isComplicatedChart = false; @@ -142,7 +136,7 @@ export default () => { const themeOverrides = { partition: { - emptySizeRatio: pieTypeIdSelected.includes('Donut') && 0.4, + emptySizeRatio: pieTypeIdSelected.includes('Donut') ? 0.4 : undefined, }, }; @@ -165,10 +159,10 @@ export default () => { valueGetter={showValues ? 'percent' : undefined} layers={[ { - groupByRollup: (d) => d.browser, + groupByRollup: (d: (typeof BROWSER_DATA_2019)[0]) => d.browser, shape: { - fillColor: (key, sortIndex) => - euiChartTheme.theme.colors.vizColors[sortIndex], + fillColor: (_, sortIndex) => + euiChartTheme.theme.colors!.vizColors![sortIndex], }, }, ]} @@ -182,14 +176,13 @@ export default () => { + { - setPieTypeIdSelected(id); - }} + onChange={(id) => setPieTypeIdSelected(id)} buttonSize="compressed" isFullWidth /> @@ -231,7 +224,7 @@ export default () => { max={10} showTicks value={numSlices} - onChange={onNumChartsChange} + onChange={(e) => setNumSlices(Number(e.currentTarget.value))} levels={[ { min: 1, max: 5.5, color: 'success' }, { min: 5.5, max: 10, color: 'danger' }, @@ -244,7 +237,7 @@ export default () => { setGrouped(e.target.checked)} disabled={numSlices <= 5} /> @@ -253,7 +246,6 @@ export default () => { Partition supports the specialized slice order with{' '} diff --git a/src-docs/src/views/elastic_charts/shared.js b/src-docs/src/views/elastic_charts/shared.tsx similarity index 58% rename from src-docs/src/views/elastic_charts/shared.js rename to src-docs/src/views/elastic_charts/shared.tsx index 8fd26f31c61..f55223badc7 100644 --- a/src-docs/src/views/elastic_charts/shared.js +++ b/src-docs/src/views/elastic_charts/shared.tsx @@ -1,8 +1,13 @@ -import React, { useState } from 'react'; -import PropTypes from 'prop-types'; +import React, { + FunctionComponent, + PropsWithChildren, + ReactNode, + useState, +} from 'react'; import { EuiBadge, EuiRadioGroup, + type EuiRadioGroupOption, EuiSpacer, EuiSwitch, EuiPanel, @@ -10,18 +15,19 @@ import { EuiTitle, } from '../../../../src/components'; import { BarSeries, LineSeries, AreaSeries } from '@elastic/charts'; -import euiPackage from '../../../../package'; +import euiPackage from '../../../../package.json'; const { devDependencies } = euiPackage; export const chartsVersion = - devDependencies['@elastic/charts'].match(/\d+\.\d+\.\d+/)[0]; + devDependencies['@elastic/charts'].match(/\d+\.\d+\.\d+/)![0]; export const CHART_COMPONENTS = { BarSeries: BarSeries, LineSeries: LineSeries, AreaSeries: AreaSeries, }; +export type ChartType = keyof typeof CHART_COMPONENTS; export const ExternalBadge = () => { return ( @@ -40,26 +46,43 @@ export const ExternalBadge = () => { ); }; -export const ChartCard = ({ title, description, children }) => { +export const ChartCard: FunctionComponent< + PropsWithChildren & { + title: ReactNode; + description?: ReactNode; + } +> = ({ title, description, children }) => { return ( {title} - -

{description}

-
- + {description && ( + <> + +

{description}

+
+ + + )} {children}
); }; -export const ChartTypeCard = (props) => { +type ChartTypeCardProps = { + type: string; + mixed?: 'enabled' | 'disabled'; + onChange: [Mixed] extends [{ mixed: true }] + ? (chartType: ChartType | 'Mixed') => void + : (chartType: ChartType) => void; + disabled?: boolean; +}; +export const ChartTypeCard = (props: ChartTypeCardProps) => { const idPrefix = 'chartType'; - const toggleButtonsIcons = [ + const toggleButtonsIcons: EuiRadioGroupOption[] = [ { id: `${idPrefix}0`, label: 'BarSeries', @@ -76,12 +99,11 @@ export const ChartTypeCard = (props) => { const [toggleIdSelected, setToggleIdSelectd] = useState(`${idPrefix}0`); - const onChartTypeChange = (optionId) => { + const onChartTypeChange = (optionId: string) => { setToggleIdSelectd(optionId); - const chartType = toggleButtonsIcons.find( - ({ id }) => id === optionId - ).label; + const chartType = toggleButtonsIcons.find(({ id }) => id === optionId)! + .label as ChartType; props.onChange(chartType); }; @@ -109,58 +131,42 @@ export const ChartTypeCard = (props) => { ); }; -ChartTypeCard.propTypes = { - onChange: PropTypes.func.isRequired, - mixed: PropTypes.oneOf(['enabled', 'disabled', true, false]), - disabled: PropTypes.bool, -}; - -export const MultiChartCard = (props) => { +export const MultiChartCard: FunctionComponent<{ + onChange: ({ multi, stacked }: { multi: boolean; stacked: boolean }) => void; +}> = ({ onChange }) => { const [multi, setMulti] = useState(false); const [stacked, setStacked] = useState(false); - const onMultiChange = (e) => { - const isStacked = e.target.checked ? stacked : false; - - setMulti(e.target.checked); - setStacked(isStacked); - - props.onChange({ - multi: e.target.checked, - stacked, - }); - }; - - const onStackedChange = (e) => { - setStacked(e.target.checked); - - props.onChange({ multi: multi, stacked: e.target.checked }); - }; return ( { + const isStacked = e.target.checked ? stacked : false; + + setMulti(e.target.checked); + setStacked(isStacked); + + onChange({ + multi: e.target.checked, + stacked, + }); + }} /> { + setStacked(e.target.checked); + onChange({ multi: multi, stacked: e.target.checked }); + }} disabled={!multi} /> ); }; - -MultiChartCard.propTypes = { - /** - * Returns (multi:boolean, stacked:boolean) - */ - onChange: PropTypes.func.isRequired, -}; diff --git a/src-docs/src/views/elastic_charts/sizes.js b/src-docs/src/views/elastic_charts/sizes.tsx similarity index 86% rename from src-docs/src/views/elastic_charts/sizes.js rename to src-docs/src/views/elastic_charts/sizes.tsx index ba09040f6f6..50729c2dfca 100644 --- a/src-docs/src/views/elastic_charts/sizes.js +++ b/src-docs/src/views/elastic_charts/sizes.tsx @@ -3,12 +3,16 @@ import moment from 'moment'; import { Chart, Settings, + type SettingsProps, Tooltip, + type TooltipProps, Axis, + type AxisProps, timeFormatter, niceTimeFormatByDay, LineAnnotation, BarSeries, + type PointerValue, } from '@elastic/charts'; import { @@ -33,14 +37,33 @@ import { formatDate, dateFormatAliases, withEuiTheme, + type WithEuiThemeProps, } from '../../../../src/services'; import { MultiChartCard, ChartCard } from './shared'; import { TIME_DATA, TIME_DATA_2 } from './data'; -class Sizes extends Component { - constructor(props) { +type State = { + multi: boolean; + stacked: boolean; + width: number; + data1: typeof TIME_DATA; + data2: typeof TIME_DATA_2; + tooltipProps?: TooltipProps; + legendPosition?: SettingsProps['legendPosition']; + xAxisTitle?: AxisProps['title']; + xAxisFormatter?: AxisProps['tickFormat']; + xAxisStyle?: AxisProps['style']; + yAxisStyle?: AxisProps['style']; + changeDescription?: string; +}; +class Sizes extends Component { + smallSize: number; + mediumSize: number; + xsmallSize: number; + + constructor(props: WithEuiThemeProps) { super(props); this.mediumSize = 50; @@ -60,37 +83,11 @@ class Sizes extends Component { this.changePropsBasedOnWidth(100); }; - onStackedChange = (e) => { - this.setState({ - stacked: e.target.checked, - }); - }; - - onMultiChange = (multiObject) => { - this.setState({ - ...multiObject, - }); - }; - - onChartTypeChange = (optionId) => { - this.setState({ - toggleIdSelected: optionId, - }); - }; - - onWidthChartsChange = (e) => { - this.setState({ - width: e.target.value, - }); - - this.changePropsBasedOnWidth(e.target.value); - }; - - changePropsBasedOnWidth = (width) => { + changePropsBasedOnWidth = (width: number) => { const data1 = TIME_DATA.slice(); const data2 = TIME_DATA_2.slice(); let tooltipProps; - let legendPosition = 'right'; + let legendPosition: SettingsProps['legendPosition'] = 'right'; const xAxisFormatter = timeFormatter(niceTimeFormatByDay(1)); let xAxisTitle = `${formatDate(data1[0][0], dateFormatAliases.date)}`; let xAxisStyle; @@ -103,7 +100,7 @@ class Sizes extends Component { } if (width < this.mediumSize) { - const headerFormatter = (tooltipData) => { + const headerFormatter = (tooltipData: PointerValue) => { return `${formatDate( tooltipData.value, dateFormatAliases.shortDateTime @@ -184,7 +181,7 @@ class Sizes extends Component { if (width < this.xsmallSize) { annotation = ( - + this.setState({ ...multiObject })} + /> { + const width = Number(e.currentTarget.value); + this.setState({ width }); + this.changePropsBasedOnWidth(width); + }} aria-label="Width of panel" /> @@ -347,7 +350,7 @@ class Sizes extends Component { position="bottom" title={'${xAxisTitle}'} tickFormat={timeFormatter(niceTimeFormatByDay(1))} - showGridLines={false} + gridLine={{ visible: false }} style={${JSON.stringify(xAxisStyle)}} /> { > - + + { > - + + { > - + + Settings.showLegend = false
  • - - Settings.tooltip = "none" - + Tooltip.type = "none"
  • diff --git a/src-docs/src/views/elastic_charts/texture.js b/src-docs/src/views/elastic_charts/texture.tsx similarity index 95% rename from src-docs/src/views/elastic_charts/texture.js rename to src-docs/src/views/elastic_charts/texture.tsx index c275ba4fdd1..63500bd13be 100644 --- a/src-docs/src/views/elastic_charts/texture.js +++ b/src-docs/src/views/elastic_charts/texture.tsx @@ -1,7 +1,6 @@ import React from 'react'; import { Chart, - CurveType, BarSeries, AreaSeries, Settings, @@ -82,7 +81,6 @@ export default () => { }} stackAccessors={['yes']} data={SAMPLE_SMALL_DATA} - curve={CurveType.CURVE_MONOTONE_X} /> { areaSeriesStyle={{ area: { opacity: 0.05, - shape: 'circle', texture: { opacity: 1, shape: 'circle', @@ -111,8 +108,9 @@ export default () => { data={SAMPLE_SMALL_DATA_2} /> Number(d).toFixed(2)} />
    diff --git a/src-docs/src/views/elastic_charts/theming.js b/src-docs/src/views/elastic_charts/theming.tsx similarity index 79% rename from src-docs/src/views/elastic_charts/theming.js rename to src-docs/src/views/elastic_charts/theming.tsx index 0732d7108a5..8810a9e814d 100644 --- a/src-docs/src/views/elastic_charts/theming.js +++ b/src-docs/src/views/elastic_charts/theming.tsx @@ -46,27 +46,21 @@ const paletteData = { euiPaletteGray, }; -const paletteNames = Object.keys(paletteData); +const palettes = Object.entries(paletteData).map(([paletteName, palette]) => { + return { + value: paletteName, + title: paletteName, + palette: + palette === euiPaletteColorBlind + ? euiPaletteColorBlind({ sortBy: 'natural' }) + : palette(10), + type: 'fixed' as const, + }; +}); export default () => { const { colorMode } = useEuiTheme(); - const palettes = paletteNames.map((paletteName, index) => { - const options = - index > 0 - ? 10 - : { - sortBy: 'natural', - }; - - return { - value: paletteName, - title: paletteName, - palette: paletteData[paletteNames[index]](options), - type: 'fixed', - }; - }); - const [barPalette, setBarPalette] = useState('euiPaletteColorBlind'); /** @@ -84,14 +78,12 @@ export default () => { ? EUI_CHARTS_THEME_DARK.theme : EUI_CHARTS_THEME_LIGHT.theme; - const barPaletteIndex = paletteNames.findIndex((item) => item === barPalette); - const customTheme = - barPaletteIndex > 0 + barPalette !== 'euiPaletteColorBlind' ? [ { colors: { - vizColors: paletteData[paletteNames[barPaletteIndex]](5), + vizColors: paletteData[barPalette as keyof typeof paletteData](5), }, }, theme, @@ -119,11 +111,11 @@ export default () => { yAccessors={['y']} color={['black']} /> - + Number(d).toFixed(2)} />
    diff --git a/src-docs/src/views/elastic_charts/theming_categorical.js b/src-docs/src/views/elastic_charts/theming_categorical.tsx similarity index 84% rename from src-docs/src/views/elastic_charts/theming_categorical.js rename to src-docs/src/views/elastic_charts/theming_categorical.tsx index 3f6e76f0027..8ffef39feb2 100644 --- a/src-docs/src/views/elastic_charts/theming_categorical.js +++ b/src-docs/src/views/elastic_charts/theming_categorical.tsx @@ -21,7 +21,7 @@ import { EuiTitle, } from '../../../../src/components'; -import { CHART_COMPONENTS, ChartCard } from './shared'; +import { CHART_COMPONENTS, type ChartType, ChartCard } from './shared'; import { euiPaletteColorBlind, euiPalettePositive, @@ -29,6 +29,7 @@ import { euiPaletteGray, useEuiTheme, } from '../../../../src/services'; +import type { EuiPalette } from '../../../../src/services/color/eui_palettes'; export default () => { const { colorMode } = useEuiTheme(); @@ -60,12 +61,14 @@ export default () => { colorTypeRadios[0].id ); const [colorType, setColorType] = useState(colorTypeRadios[0].label); - const [numCharts, setNumCharts] = useState('3'); - const [data, setData] = useState([]); + const [numCharts, setNumCharts] = useState(3); + const [data, setData] = useState>( + [] + ); const [dataString, setDataString] = useState('[{x: 1, y: 5.5, g: 0}]'); - const [vizColors, setVizColors] = useState(); - const [vizColorsString, setVizColorsString] = useState(); - const [chartType, setChartType] = useState('LineSeries'); + const [vizColors, setVizColors] = useState(); + const [vizColorsString, setVizColorsString] = useState(''); + const [chartType, setChartType] = useState('LineSeries'); useEffect(() => { createCategoryChart(3); @@ -76,28 +79,7 @@ export default () => { ? EUI_CHARTS_THEME_DARK.theme : EUI_CHARTS_THEME_LIGHT.theme; - const onNumChartsChange = (e) => { - updateCorrectChart(Number(e.target.value), colorType); - setNumCharts(e.target.value); - }; - - const onColorTypeChange = (optionId) => { - const colorType = colorTypeRadios.find(({ id }) => id === optionId).label; - updateCorrectChart(Number(numCharts), colorType); - setColorType(colorType); - setColorTypeIdSelected(optionId); - }; - - const onGroupChange = (e) => { - const colorType = e.target.checked - ? 'Grouped' - : colorTypeRadios.find(({ id }) => id === colorTypeIdSelected).label; - updateCorrectChart(Number(numCharts), colorType); - setGrouped(e.target.checked); - setColorType(colorType); - }; - - const updateCorrectChart = (numCharts, chartType) => { + const updateCorrectChart = (numCharts: number, chartType: string) => { switch (chartType) { case 'Categorical': createCategoryChart(numCharts); @@ -121,7 +103,7 @@ export default () => { } }; - const createCategoryChart = (numCharts) => { + const createCategoryChart = (numCharts: number) => { const dg = new DataGenerator(); const data = dg.generateGroupedSeries(20, numCharts).map((item) => { item.g = `Categorical ${item.g.toUpperCase()}`; @@ -131,11 +113,11 @@ export default () => { setData(data); setDataString("[{x: 1, y: 5.5, g: 'Categorical 1'}]"); setVizColors(undefined); - setVizColorsString(undefined); + setVizColorsString(''); setChartType('LineSeries'); }; - const createQuantityChart = (numCharts) => { + const createQuantityChart = (numCharts: number) => { const vizColors = euiPalettePositive(numCharts); // convert series labels to percentages @@ -159,7 +141,7 @@ export default () => { setChartType('BarSeries'); }; - const createTrendChart = (numCharts) => { + const createTrendChart = (numCharts: number) => { const vizColors = euiPaletteForStatus(numCharts); // convert series labels to better/worse @@ -194,7 +176,7 @@ export default () => { setChartType('BarSeries'); }; - const createHighlightChart = (numCharts) => { + const createHighlightChart = (numCharts: number) => { const vizColors = euiPaletteGray(numCharts); vizColors[vizColors.length - 1] = highlightColor; @@ -271,7 +253,7 @@ export default () => { return item; }); - const isOdd = index % 2; + const isOdd = index % 2 === 0; const chart = ( { theme={customTheme} showLegend={showLegend} legendPosition="right" - showLegendDisplayValue={false} + showLegendExtra={false} /> {charts} Number(d).toFixed(2)} />
    @@ -357,7 +339,14 @@ export default () => { compressed options={colorTypeRadios} idSelected={grouped ? colorTypeRadios[0].id : colorTypeIdSelected} - onChange={onColorTypeChange} + onChange={(optionId) => { + const colorType = colorTypeRadios.find( + ({ id }) => id === optionId + )!.label; + updateCorrectChart(Number(numCharts), colorType); + setColorType(colorType); + setColorTypeIdSelected(optionId); + }} disabled={grouped} />
    @@ -381,7 +370,11 @@ export default () => { showTicks value={grouped ? '2' : numCharts} disabled={grouped} - onChange={onNumChartsChange} + onChange={(e) => { + const numCharts = Number(e.currentTarget.value); + updateCorrectChart(numCharts, colorType); + setNumCharts(numCharts); + }} levels={[ { min: 1, max: 5.5, color: 'success' }, { min: 5.5, max: 10, color: 'danger' }, @@ -401,7 +394,17 @@ export default () => { { + const colorType = e.target.checked + ? 'Grouped' + : colorTypeRadios.find( + ({ id }) => id === colorTypeIdSelected + )!.label; + + updateCorrectChart(Number(numCharts), colorType); + setGrouped(e.target.checked); + setColorType(colorType); + }} />
    @@ -425,7 +428,7 @@ export default () => { theme={${customColorsString}} showLegend={${showLegend}} legendPosition="right" - showLegendDisplayValue={false} + showLegendExtra={false} /> <${chartType} id="bars" @@ -439,12 +442,12 @@ export default () => { Number(d).toFixed(2)} /> `} diff --git a/src-docs/src/views/elastic_charts/theming_example.js b/src-docs/src/views/elastic_charts/theming_example.js index ce86740d63e..9c48b22e2ad 100644 --- a/src-docs/src/views/elastic_charts/theming_example.js +++ b/src-docs/src/views/elastic_charts/theming_example.js @@ -43,7 +43,7 @@ export const ElasticChartsThemingExample = { title: 'Theming via EUI', source: [ { - type: GuideSectionTypes.JS, + type: GuideSectionTypes.TSX, code: themingSource, }, ], diff --git a/src-docs/src/views/elastic_charts/time_chart.js b/src-docs/src/views/elastic_charts/time_chart.tsx similarity index 84% rename from src-docs/src/views/elastic_charts/time_chart.js rename to src-docs/src/views/elastic_charts/time_chart.tsx index 0c18e1c67bd..7b325b442f0 100644 --- a/src-docs/src/views/elastic_charts/time_chart.js +++ b/src-docs/src/views/elastic_charts/time_chart.tsx @@ -35,6 +35,7 @@ import { TIME_DATA, TIME_DATA_2 } from './data'; import { ChartTypeCard, CHART_COMPONENTS, + type ChartType, MultiChartCard, ChartCard, } from './shared'; @@ -44,29 +45,17 @@ export default () => { const [multi, setMulti] = useState(false); const [stacked, setStacked] = useState(false); - const [chartType, setChartType] = useState('BarSeries'); - - const onMultiChange = (multiObject) => { - const { multi, stacked } = multiObject; - setMulti(multi); - setStacked(stacked); - }; - - const onChartTypeChange = (chartType) => { - setChartType(chartType); - }; + const [chartType, setChartType] = useState('BarSeries'); const isDarkTheme = colorMode === 'DARK'; const theme = isDarkTheme ? EUI_CHARTS_THEME_DARK.theme : EUI_CHARTS_THEME_LIGHT.theme; - let ChartType = CHART_COMPONENTS[chartType]; - let ChartType2 = CHART_COMPONENTS[chartType]; - if (chartType === 'Mixed') { - ChartType = BarSeries; - ChartType2 = LineSeries; - } + const ChartType = + chartType === 'Mixed' ? BarSeries : CHART_COMPONENTS[chartType]; + const ChartType2 = + chartType === 'Mixed' ? LineSeries : CHART_COMPONENTS[chartType]; const isBadChart = chartType === 'LineSeries' && stacked; @@ -109,13 +98,13 @@ export default () => { id="bottom-axis" position="bottom" tickFormat={timeFormatter(niceTimeFormatByDay(1))} - showGridLines={chartType !== 'BarSeries'} - tickPadding={0} + gridLine={{ visible: chartType !== 'BarSeries' }} + style={{ tickLine: { padding: 0 } }} /> Number(d).toFixed(2)} /> @@ -124,15 +113,20 @@ export default () => { - type="Time series" - onChange={onChartTypeChange} + onChange={(chartType) => setChartType(chartType)} mixed={multi ? 'enabled' : 'disabled'} /> - + { + setMulti(multi); + setStacked(stacked); + }} + /> @@ -180,12 +174,12 @@ export default () => { id="bottom-axis" position="bottom" tickFormat={timeFormatter(niceTimeFormatByDay(1))} - ${chartType !== 'BarSeries' ? 'showGridLines' : ''} + gridLine={{ visible: ${chartType !== 'BarSeries'} }} /> Number(d).toFixed(2)} /> `} diff --git a/src-docs/src/views/elastic_charts/treemap.js b/src-docs/src/views/elastic_charts/treemap.tsx similarity index 88% rename from src-docs/src/views/elastic_charts/treemap.js rename to src-docs/src/views/elastic_charts/treemap.tsx index 8d78a23d8b1..0a1728e6015 100644 --- a/src-docs/src/views/elastic_charts/treemap.js +++ b/src-docs/src/views/elastic_charts/treemap.tsx @@ -15,6 +15,7 @@ import { import { euiPaletteColorBlind, useEuiTheme } from '../../../../src/services'; import { GITHUB_DATASET_MOD } from './data'; +type DataType = (typeof GITHUB_DATASET_MOD)[0]; export default () => { const { colorMode } = useEuiTheme(); @@ -59,21 +60,20 @@ export default () => { valueAccessor={(d) => d.count} layers={[ { - groupByRollup: (d) => d.total, + groupByRollup: (d: DataType) => d.total, shape: { - fillColor: euiChartTheme.theme.partition.sectorLineStroke, + fillColor: euiChartTheme.theme.partition!.sectorLineStroke!, }, - hideInLegend: true, }, { - groupByRollup: (d) => d.vizType, + groupByRollup: (d: DataType) => d.vizType, shape: { fillColor: (key, sortIndex) => groupedPalette[sortIndex * 3], }, }, { - groupByRollup: (d) => d.issueType, + groupByRollup: (d: DataType) => d.issueType, shape: { fillColor: (key, sortIndex, { parent }) => groupedPalette[parent.sortIndex * 3 + sortIndex + 1], @@ -100,7 +100,7 @@ export default () => { topGroove={0} layers={[ { - groupByRollup: (d) => d.vizType, + groupByRollup: (d: DataType) => d.vizType, shape: { fillColor: (key, sortIndex) => groupedPalette[sortIndex * 3], @@ -111,7 +111,7 @@ export default () => { }, }, { - groupByRollup: (d) => d.issueType, + groupByRollup: (d: DataType) => d.issueType, shape: { fillColor: (key, sortIndex, { parent }) => groupedPalette[parent.sortIndex * 3 + sortIndex], From 3af30c6b767792ec632fa2149842b37ce237e276 Mon Sep 17 00:00:00 2001 From: Jason Stoltzfus Date: Thu, 12 Oct 2023 06:51:14 -0400 Subject: [PATCH 32/38] Update contributing README (#7278) Co-authored-by: Cee Chen <549407+cee-chen@users.noreply.github.com> --- wiki/contributing-to-eui/README.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/wiki/contributing-to-eui/README.md b/wiki/contributing-to-eui/README.md index 25307fb46df..d6f07c52d3e 100644 --- a/wiki/contributing-to-eui/README.md +++ b/wiki/contributing-to-eui/README.md @@ -6,9 +6,15 @@ If there isn't an associated feature request or bug report in EUI's backlog yet, ## Process -### How we assign work and define our roadmap +### Who can contribute? -EUI is built primarily by employees of Elastic. We try to do this in the open as much as possible, but do utilize closed meetings and other planning tools to dictate our longer term roadmap. We try to transcribe the decisions from these discussions in the form of specifications to Github issues for transparency. In general, once on Github, any issue can be worked on by the community. We usually reserve larger projects or ones that are core to our roadmap or design to be done internally. In these cases we mark these issues as `assigned` to a person using Github. We do not, as a policy, assign issues to community members. If you find an issue that is not assigned, assume that you are welcome to work on it and can submit a pull request. Feel free to leave a comment to mark intent and avoid conflict. +EUI is built primarily by and for employees of Elastic. We align our features and roadmap with the needs of our products internally. + +While EUI's primary customer is Kibana and other Elastic products, open source is a part of our DNA at Elastic, and commmunity contributions from outside of Elastic are welcome. These contributions are typically reviewed and merged on a best-effort basis and must generally align with the overall objectives of this project. + +In general, once on Github, any issue can be worked on by the community. If you find an issue that is not assigned, assume that you are welcome to work on it and can submit a pull request. We recommend that you leave us a comment indicating your intent before starting work to avoid potential conflict. We do not, as a policy, assign issues to community members and we usually reserve larger projects or ones that are core to our roadmap or design to be done internally. + +Our best PRs tend to come from existing users whom have a challenge they are attempting to overcome and wish to help us solve it. If you are new to open source and looking for a good project to start making contributions, this project may not be a good fit. We have an extensive backlog in which you'll likely encounter outdated issues and issues lacking the appropriate context to get started. ### How to ensure the timely review of pull requests From 2de79ad4d0f346176ddee693c326961be500e48f Mon Sep 17 00:00:00 2001 From: Trevor Pierce <1Copenut@users.noreply.github.com> Date: Fri, 13 Oct 2023 11:41:00 -0500 Subject: [PATCH 33/38] Bump Node to `v18.18.1`; Remove puppeteer (#7274) Co-authored-by: Cee Chen --- .../pipelines/pipeline_pull_request_test.yml | 3 + .buildkite/scripts/pipeline_test.sh | 8 +- .nvmrc | 2 +- package.json | 8 +- scripts/a11y-testing.js | 106 --- scripts/deploy/build_docs | 2 +- scripts/docker-ci/Dockerfile | 32 +- scripts/test-a11y-docker.js | 4 +- scripts/test-docker.js | 4 +- src/components/delay_hide/delay_hide.tsx | 2 +- .../automated-accessibility-testing.md | 25 +- yarn.lock | 640 +++++++++--------- 12 files changed, 348 insertions(+), 488 deletions(-) delete mode 100644 scripts/a11y-testing.js diff --git a/.buildkite/pipelines/pipeline_pull_request_test.yml b/.buildkite/pipelines/pipeline_pull_request_test.yml index 08118c5a138..418acd54bb5 100644 --- a/.buildkite/pipelines/pipeline_pull_request_test.yml +++ b/.buildkite/pipelines/pipeline_pull_request_test.yml @@ -50,6 +50,7 @@ steps: if: build.branch != "main" artifact_paths: - "cypress/screenshots/**/*.png" + - "cypress/videos/**/*.mp4" - command: .buildkite/scripts/pipeline_test.sh label: ":cypress: Cypress tests on React 17" @@ -60,6 +61,7 @@ steps: if: build.branch != "main" artifact_paths: - "cypress/screenshots/**/*.png" + - "cypress/videos/**/*.mp4" - command: .buildkite/scripts/pipeline_test.sh label: ":cypress: Cypress tests on React 18" @@ -70,3 +72,4 @@ steps: if: build.branch != "main" artifact_paths: - "cypress/screenshots/**/*.png" + - "cypress/videos/**/*.mp4" diff --git a/.buildkite/scripts/pipeline_test.sh b/.buildkite/scripts/pipeline_test.sh index 0a82ff17071..0ab290d4ed1 100644 --- a/.buildkite/scripts/pipeline_test.sh +++ b/.buildkite/scripts/pipeline_test.sh @@ -10,7 +10,7 @@ DOCKER_OPTIONS=( --user="$(id -u):$(id -g)" --volume="$(pwd):/app" --workdir=/app - docker.elastic.co/eui/ci:5.3 + docker.elastic.co/eui/ci:5.5 ) case $TEST_TYPE in @@ -41,17 +41,17 @@ case $TEST_TYPE in cypress:16) echo "[TASK]: Running Cypress tests against React 16" - DOCKER_OPTIONS+=(bash -c "/opt/yarn*/bin/yarn && yarn test-cypress --node-options=--max_old_space_size=2048 --react-version=16") + DOCKER_OPTIONS+=(bash -c "/opt/yarn*/bin/yarn && yarn cypress install && yarn test-cypress --node-options=--max_old_space_size=2048 --react-version=16") ;; cypress:17) echo "[TASK]: Running Cypress tests against React 17" - DOCKER_OPTIONS+=(bash -c "/opt/yarn*/bin/yarn && yarn test-cypress --node-options=--max_old_space_size=2048 --react-version=17") + DOCKER_OPTIONS+=(bash -c "/opt/yarn*/bin/yarn && yarn cypress install && yarn test-cypress --node-options=--max_old_space_size=2048 --react-version=17") ;; cypress:18) echo "[TASK]: Running Cypress tests against React 18" - DOCKER_OPTIONS+=(bash -c "/opt/yarn*/bin/yarn && yarn test-cypress --node-options=--max_old_space_size=2048") + DOCKER_OPTIONS+=(bash -c "/opt/yarn*/bin/yarn && yarn cypress install && yarn test-cypress --node-options=--max_old_space_size=2048") ;; *) diff --git a/.nvmrc b/.nvmrc index 4a1f488b6c3..f6610cade82 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -18.17.1 +18.18.1 diff --git a/package.json b/package.json index a706480cee8..b12aaa7a153 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "main": "lib", "module": "es", "types": "eui.d.ts", - "docker_image": "18.17.1", + "docker_image": "18.18.1", "engines": { "node": "16.x || 18.x || >=20.0" }, @@ -32,17 +32,14 @@ "test": "yarn lint && yarn test-unit", "test-ci": "yarn test && yarn test-cypress", "test-unit": "node ./scripts/test-unit", - "test-a11y": "node ./scripts/a11y-testing", "test-staged": "yarn lint && node scripts/test-staged.js", "test-cypress": "node ./scripts/test-cypress", "test-cypress-dev": "yarn test-cypress --dev", "test-cypress-a11y": "yarn test-cypress --a11y", "combine-test-coverage": "sh ./scripts/combine-coverage.sh", - "start-test-server": "BABEL_MODULES=false NODE_ENV=puppeteer NODE_OPTIONS=--max-old-space-size=4096 webpack-dev-server --config src-docs/webpack.config.js --port 9999", "yo-component": "yo ./generator-eui/app/component.js", "update-token-changelog": "node ./scripts/update-token-changelog.js", "update-changelog-manual": "node -e \"require('./scripts/update-changelog').manualChangelog('${npm_config_release}')\"", - "start-test-server-and-a11y-test": "cross-env WAIT_ON_TIMEOUT=600000 start-server-and-test start-test-server http-get://localhost:9999 test-a11y", "yo-doc": "yo ./generator-eui/app/documentation.js", "yo-changelog": "yo ./generator-eui/changelog/index.js", "release": "node ./scripts/release.js", @@ -99,7 +96,6 @@ "vfile": "^4.2.0" }, "devDependencies": { - "@axe-core/puppeteer": "^4.4.2", "@babel/cli": "^7.21.5", "@babel/core": "^7.21.8", "@babel/plugin-proposal-class-properties": "^7.18.6", @@ -145,7 +141,6 @@ "@types/classnames": "^2.3.1", "@types/enzyme": "^3.10.5", "@types/jest": "^24.0.6", - "@types/node": "^10.17.5", "@types/react": "^18.2.14", "@types/react-dom": "^18.2.6", "@types/react-is": "^17.0.3", @@ -222,7 +217,6 @@ "prettier": "^2.8.8", "process": "^0.11.10", "prop-types": "^15.6.0", - "puppeteer": "^5.5.0", "raw-loader": "^4.0.1", "react": "^18.2.0", "react-16": "npm:react@^16.14.0", diff --git a/scripts/a11y-testing.js b/scripts/a11y-testing.js deleted file mode 100644 index b24125830d4..00000000000 --- a/scripts/a11y-testing.js +++ /dev/null @@ -1,106 +0,0 @@ -const chalk = require('chalk'); -const puppeteer = require('puppeteer'); -const { AxePuppeteer } = require('@axe-core/puppeteer'); - -const docsPages = async (root, page) => { - const pagesToSkip = [ - `${root}#/display/aspect-ratio`, // Has issues with the embedded audio player - `${root}#/layout/accordion`, // Has an issue with ARIA attributes - `${root}#/templates/page-template` // Has multiple `main` elements that we don't want to remove for bad copy/paste code - ]; - - return [ - root, - ...(await page.$$eval('nav a', (anchors) => anchors.map((a) => a.href))), - ].filter((link) => !pagesToSkip.includes(link)); -}; - -const printResult = (violations) => { - const violationData = violations.map( - ({ id, impact, description, nodes }) => ({ - id, - impact, - description, - nodes: nodes.length - })); - console.table(violationData); -} - -(async () => { - let totalViolationsCount = 0; - let root = 'http://localhost:9999/'; - let browser; - let page; - - try { - browser = await puppeteer.launch({ args: ['--no-sandbox'] }); - page = await browser.newPage(); - - await page.setBypassCSP(true); - } catch (e) { - console.log(chalk.red('Failed to setup puppeteer')); - console.log(e); - process.exit(1); - } - - try { - await page.goto(root); - } catch (e) { - root = 'http://localhost:8030/'; - try { - await page.goto(root); - } catch (e) { - console.log( - chalk.red( - 'No local server found. Expecting localhost:9999 or localhost:8030 to resolve.' - ) - ); - process.exit(1); - } - } - const links = await docsPages(root, page); - - for (const link of links) { - await page.goto(link); - - const { violations } = await new AxePuppeteer(page) - .options({ - runOnly: { - type: 'tag', - values: ['section508', 'wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'] - }, - rules: { - 'color-contrast': { enabled: false }, - 'scrollable-region-focusable': { enabled: false }, - 'frame-title': { enabled: false }, // axe reports 18 page violations, but no