From 14f129957c78208e97ca4387f15127aca6500087 Mon Sep 17 00:00:00 2001 From: Lene Gadewoll Date: Wed, 3 Apr 2024 17:37:03 +0200 Subject: [PATCH 01/18] refactor(storybook): updates utils - updates internal helper _updateArgTypes to handle multiple control config entries at once + adds additional util enableToggleFunctionControls --- .storybook/utils.ts | 110 ++++++++++++++++++++++++++++++++------------ 1 file changed, 81 insertions(+), 29 deletions(-) diff --git a/.storybook/utils.ts b/.storybook/utils.ts index 2c1bfa6336f..839cfc02d27 100644 --- a/.storybook/utils.ts +++ b/.storybook/utils.ts @@ -11,6 +11,7 @@ */ import type { Args, ArgTypes, Meta, Preview, StoryObj } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; type StorybookConfig = Meta | StoryObj | Preview; @@ -27,10 +28,12 @@ export const hideStorybookControls = ( config: StorybookConfig, propNames: Array ): StorybookConfig => { - const updatedConfig = _updateArgTypes(config, propNames, { - key: 'table', - value: { disable: true }, - }); + const updatedConfig = _updateArgTypes(config, propNames, [ + { + key: 'table', + value: { disable: true }, + }, + ]); return updatedConfig; }; @@ -49,10 +52,12 @@ export const disableStorybookControls = ( config: StorybookConfig, propNames: Array ): StorybookConfig => { - const updatedConfig = _updateArgTypes(config, propNames, { - key: 'control', - value: false, - }); + const updatedConfig = _updateArgTypes(config, propNames, [ + { + key: 'control', + value: false, + }, + ]); return updatedConfig; }; @@ -72,11 +77,44 @@ export const moveStorybookControlsToCategory = ( propNames: Array, category = 'Additional' ): StorybookConfig => { - const updatedConfig = _updateArgTypes(config, propNames, { - key: 'table', - value: { category }, + const updatedConfig = _updateArgTypes(config, propNames, [ + { + key: 'table', + value: { category }, + }, + ]); + + return updatedConfig; +}; + +/** + * Configures passed argTypes to be setup as toggle control + * which fires a Storybook action when enabled. + * Should be used for function props only. + * + * Can be used for preview (Preview), component (Meta) or story (Story) + * context by passing the config object for either. Use after defining + * the specific config to be able to pass the config to this util. + * + * @returns the mutated config + */ +export const enableFunctionToggleControls = ( + config: StorybookConfig, + propNames: Array +) => { + const setAction = (propName: string | number) => ({ + true: action(propName.toString()), + false: undefined, }); + const updatedConfig = _updateArgTypes(config, propNames, [ + { key: 'control', value: 'boolean' }, + { + key: 'mapping', + value: setAction, + }, + ]); + return updatedConfig; }; @@ -112,29 +150,43 @@ export const hidePanel = { const _updateArgTypes = ( config: StorybookConfig, propNames: Array, - { - key, - value, - }: { key: string; value: Record | boolean } + controls: Array<{ + key: string; + value: + | Record + | boolean + | string + | ((propName: any) => Record); + }> ): StorybookConfig => { const currentArgTypes = config.argTypes as Partial>; const newArgTypes = { ...currentArgTypes }; for (const propName of propNames) { - const currentArgTypeValue = newArgTypes?.[propName] ?? ({} as Args); - const currentControlValue = currentArgTypeValue.hasOwnProperty(key) - ? currentArgTypeValue[key] - : ({} as Record); - - const newValue = - typeof value === 'object' && typeof currentArgTypeValue[key] === 'object' - ? { ...currentControlValue, ...value } - : value; - - newArgTypes[propName] = { - ...currentArgTypeValue, - [key]: newValue, - }; + for (const { key, value } of controls) { + const currentArgTypeValue = newArgTypes?.[propName] ?? ({} as Args); + const currentControlValue = currentArgTypeValue.hasOwnProperty(key) + ? currentArgTypeValue[key] + : ({} as Record); + + let newValue = value; + + if (typeof value === 'function') { + newValue = value(propName); + } + + if ( + typeof value === 'object' && + typeof currentArgTypeValue[key] === 'object' + ) { + newValue = { ...currentControlValue, ...value }; + } + + newArgTypes[propName] = { + ...currentArgTypeValue, + [key]: newValue, + }; + } } config.argTypes = newArgTypes; From 33dac8dc875a6c24358e6abd8f0ebda4202ff5ed Mon Sep 17 00:00:00 2001 From: Lene Gadewoll Date: Wed, 3 Apr 2024 17:39:16 +0200 Subject: [PATCH 02/18] docs(storybook): add/update stories - updates PageHeader story + adds PageHeaderContent and PageTempalte stories - adds type for page directions prop to be reused --- .../page/page_header/page_header.stories.tsx | 106 +++++++++++--- .../page_header_content.stories.tsx | 76 ++++++++++ .../page_template/outer/page_outer.tsx | 5 +- .../page_template/page_template.stories.tsx | 136 ++++++++++++++++++ 4 files changed, 300 insertions(+), 23 deletions(-) create mode 100644 src/components/page/page_header/page_header_content.stories.tsx create mode 100644 src/components/page_template/page_template.stories.tsx diff --git a/src/components/page/page_header/page_header.stories.tsx b/src/components/page/page_header/page_header.stories.tsx index f48ad74436d..2fd58e1ed35 100644 --- a/src/components/page/page_header/page_header.stories.tsx +++ b/src/components/page/page_header/page_header.stories.tsx @@ -9,6 +9,10 @@ import React from 'react'; import type { Meta, StoryObj } from '@storybook/react'; +import { + hideStorybookControls, + moveStorybookControlsToCategory, +} from '../../../../.storybook/utils'; import { EuiButton } from '../../button'; import { EuiPageHeader, EuiPageHeaderProps } from '../page_header'; @@ -33,9 +37,55 @@ const meta: Meta = { }, }; +moveStorybookControlsToCategory( + meta, + [ + 'pageTitle', + 'pageTitleProps', + 'iconType', + 'iconProps', + 'breadcrumbs', + 'breadcrumbProps', + 'tabs', + 'tabsProps', + 'description', + 'responsive', + 'alignItems', + 'rightSideItems', + 'rightSideGroupProps', + 'children', + ], + 'EuiPageHeaderContent props' +); + export default meta; type Story = StoryObj; +const tabs = [ + { + label: 'Tab 1', + isSelected: true, + }, + { + label: 'Tab 2', + }, +]; + +const breadcrumbs = [ + { + text: 'Breadcrumb 1', + href: '#', + }, + { + text: 'Breadcrumb 2', + href: '#', + }, + { + text: 'Current', + href: '#', + }, +]; + export const Playground: Story = { args: { pageTitle: 'Page title', @@ -46,28 +96,40 @@ export const Playground: Story = { Add something, Do something, ], - tabs: [ - { - label: 'Tab 1', - isSelected: true, - }, - { - label: 'Tab 2', - }, - ], - breadcrumbs: [ - { - text: 'Breadcrumb 1', - href: '#', - }, - { - text: 'Breadcrumb 2', - href: '#', - }, - { - text: 'Current', - href: '#', - }, + tabs, + breadcrumbs, + }, +}; + +export const RestrictWidth: Story = { + args: { + pageTitle: 'Page title', + iconType: 'logoKibana', + description: 'Example of a description.', + bottomBorder: 'extended', + rightSideItems: [ + Add something, + Do something, ], + tabs, + breadcrumbs, + restrictWidth: 500, }, }; +// This story displays the restrictWidth functionality; removing other content props to prevent confusion +hideStorybookControls(RestrictWidth, [ + 'pageTitle', + 'pageTitleProps', + 'iconType', + 'iconProps', + 'breadcrumbs', + 'breadcrumbProps', + 'tabs', + 'tabsProps', + 'description', + 'responsive', + 'alignItems', + 'rightSideItems', + 'rightSideGroupProps', + 'children', +]); diff --git a/src/components/page/page_header/page_header_content.stories.tsx b/src/components/page/page_header/page_header_content.stories.tsx new file mode 100644 index 00000000000..cd305101d04 --- /dev/null +++ b/src/components/page/page_header/page_header_content.stories.tsx @@ -0,0 +1,76 @@ +/* + * 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 { hideStorybookControls } from '../../../../.storybook/utils'; +import { EuiButton } from '../../button'; +import { + EuiPageHeaderContent, + EuiPageHeaderContentProps, +} from './page_header_content'; + +const meta: Meta = { + title: 'Layout/EuiPage/EuiPageHeader/EuiPageHeaderContent', + component: EuiPageHeaderContent, + argTypes: { + alignItems: { + control: 'select', + options: ['center', 'bottom', 'top', 'stretch', undefined], + }, + }, + args: { + // Component defaults + paddingSize: 'none', + responsive: true, + restrictWidth: false, + alignItems: undefined, + bottomBorder: false, + }, +}; +hideStorybookControls(meta, ['aria-label']); + +export default meta; +type Story = StoryObj; + +export const Playground: Story = { + args: { + pageTitle: 'Page title', + iconType: 'logoKibana', + description: 'Example of a description.', + bottomBorder: false, + rightSideItems: [ + Add something, + Do something, + ], + tabs: [ + { + label: 'Tab 1', + isSelected: true, + }, + { + label: 'Tab 2', + }, + ], + breadcrumbs: [ + { + text: 'Breadcrumb 1', + href: '#', + }, + { + text: 'Breadcrumb 2', + href: '#', + }, + { + text: 'Current', + href: '#', + }, + ], + }, +}; diff --git a/src/components/page_template/outer/page_outer.tsx b/src/components/page_template/outer/page_outer.tsx index b61aad83ad3..05e7a325ef5 100644 --- a/src/components/page_template/outer/page_outer.tsx +++ b/src/components/page_template/outer/page_outer.tsx @@ -12,6 +12,9 @@ import { useEuiTheme, useIsWithinBreakpoints } from '../../../services'; import { _EuiThemeBreakpoint } from '../../../global_styling'; import { euiPageOuterStyles } from './page_outer.styles'; +export const PAGE_DIRECTIONS = ['row', 'column'] as const; +type PageDirections = (typeof PAGE_DIRECTIONS)[number]; + export interface _EuiPageOuterProps extends CommonProps, HTMLAttributes { @@ -24,7 +27,7 @@ export interface _EuiPageOuterProps * Changes the `flex-direction` property. * Flip to `column` when not including a sidebar. */ - direction?: 'row' | 'column'; + direction?: PageDirections; /** * When direction is `row`, it will flip to `column` when within these breakpoints. */ diff --git a/src/components/page_template/page_template.stories.tsx b/src/components/page_template/page_template.stories.tsx new file mode 100644 index 00000000000..004a3e5eff7 --- /dev/null +++ b/src/components/page_template/page_template.stories.tsx @@ -0,0 +1,136 @@ +/* + * 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 { + hideStorybookControls, + moveStorybookControlsToCategory, +} from '../../../.storybook/utils'; +import { PAGE_DIRECTIONS } from './outer/page_outer'; +import { EuiPageTemplate, EuiPageTemplateProps } from './page_template'; +import { EuiButton } from '../button'; +import { EuiText } from '../text'; + +const headerContent = ( + Button]} + description="header description" + tabs={[ + { label: 'Tab 1', isSelected: true }, + { + label: 'Tab 2', + }, + ]} + /> +); +const sectionContent = ( + Section content +); +const sidebarContent = ( + Sidebar content +); +const bottomBarContent = ( + + BottomBar content + +); +const emptyPromptContent = ( + Empty prompt!} + footer={Button} + > + EmptyPromp content + +); + +const comboContent = ( + <> + + + + Stack EuiPageTemplate sections and headers to create your custom + content order. + + + + {headerContent} + {sectionContent} + {bottomBarContent} + +); + +const meta: Meta = { + title: 'Templates/EuiPageTemplate', + component: EuiPageTemplate, + argTypes: { + bottomBorder: { + control: 'radio', + options: [undefined, true, false, 'extended'], + }, + panelled: { control: 'radio', options: [undefined, true, false] }, + direction: { + control: 'radio', + options: [undefined, ...PAGE_DIRECTIONS], + }, + component: { control: 'text' }, + contentBorder: { control: 'radio', options: [undefined, true, false] }, + children: { + control: 'select', + options: [ + 'Combo', + 'Header', + 'Section', + 'Sidebar', + 'BottomBar', + 'EmptyPrompt', + ], + mapping: { + Combo: comboContent, + Header: headerContent, + Section: sectionContent, + Sidebar: sidebarContent, + BottomBar: bottomBarContent, + EmptyPrompt: emptyPromptContent, + }, + }, + }, + args: { + minHeight: '460px', + responsive: ['xs', 's'], + paddingSize: 'l', + grow: true, + restrictWidth: true, + component: 'main', + }, +}; +moveStorybookControlsToCategory( + meta, + ['minHeight', 'grow', 'direction', 'responsive'], + 'Outer props' +); +moveStorybookControlsToCategory( + meta, + ['contentBorder', 'component', 'mainProps'], + 'Inner props' +); +hideStorybookControls(meta, ['aria-label']); + +export default meta; +type Story = StoryObj; + +export const Playground: Story = { + args: { + children: 'Combo', + }, + // using render() over args to ensure dynamic update on prop changes + render: (args) => , +}; From f01839ed9473a5e83b7c205643fdaade19ad68ac Mon Sep 17 00:00:00 2001 From: Lene Gadewoll Date: Wed, 3 Apr 2024 17:42:47 +0200 Subject: [PATCH 03/18] docs(storybook): adds Pagination stories --- .../pagination/pagination.stories.tsx | 35 ++++++++++ .../pagination/pagination_button.stories.tsx | 69 +++++++++++++++++++ .../pagination_button_arrow.stories.tsx | 39 +++++++++++ 3 files changed, 143 insertions(+) create mode 100644 src/components/pagination/pagination.stories.tsx create mode 100644 src/components/pagination/pagination_button.stories.tsx create mode 100644 src/components/pagination/pagination_button_arrow.stories.tsx diff --git a/src/components/pagination/pagination.stories.tsx b/src/components/pagination/pagination.stories.tsx new file mode 100644 index 00000000000..6c691a8cc40 --- /dev/null +++ b/src/components/pagination/pagination.stories.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { Meta, StoryObj } from '@storybook/react'; + +import { enableFunctionToggleControls } from '../../../.storybook/utils'; +import { EuiPagination, EuiPaginationProps } from './pagination'; + +const meta: Meta = { + title: 'Navigation/EuiPagination/EuiPagination', + component: EuiPagination, + args: { + activePage: 0, + pageCount: 1, + responsive: ['xs', 's'], + compressed: false, + }, +}; +enableFunctionToggleControls(meta, ['onPageClick']); + +export default meta; +type Story = StoryObj; + +export const Playground: Story = {}; +export const PageCount: Story = { + args: { + activePage: 5, + pageCount: 10, + }, +}; diff --git a/src/components/pagination/pagination_button.stories.tsx b/src/components/pagination/pagination_button.stories.tsx new file mode 100644 index 00000000000..c0fa9f1d15b --- /dev/null +++ b/src/components/pagination/pagination_button.stories.tsx @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { Meta, StoryObj } from '@storybook/react'; + +import { + disableStorybookControls, + enableFunctionToggleControls, + hideStorybookControls, + moveStorybookControlsToCategory, +} from '../../../.storybook/utils'; +import { + EuiPaginationButton, + EuiPaginationButtonProps, +} from './pagination_button'; + +const meta: Meta = { + title: 'Navigation/EuiPagination/EuiPaginationButton', + component: EuiPaginationButton, + argTypes: { + iconType: { control: 'text' }, + target: { control: 'text' }, + }, +}; +enableFunctionToggleControls(meta, ['onClick']); +moveStorybookControlsToCategory( + meta, + [ + 'buttonRef', + 'contentProps', + 'color', + 'flush', + 'href', + 'iconSide', + 'iconSize', + 'iconType', + 'isActive', + 'isDisabled', + 'isLoading', + 'isSelected', + 'onClick', + 'rel', + 'size', + 'target', + 'textProps', + 'type', + ], + 'EuiButtonEmpty props' +); +disableStorybookControls(meta, ['buttonRef']); +hideStorybookControls(meta, ['aria-label']); + +export default meta; +type Story = StoryObj; + +export const Playground: Story = { + args: { + pageIndex: 0, + isActive: false, + isDisabled: false, + isSelected: false, + isLoading: false, + }, +}; diff --git a/src/components/pagination/pagination_button_arrow.stories.tsx b/src/components/pagination/pagination_button_arrow.stories.tsx new file mode 100644 index 00000000000..b2ceadd0c67 --- /dev/null +++ b/src/components/pagination/pagination_button_arrow.stories.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { Meta, StoryObj } from '@storybook/react'; + +import { + disableStorybookControls, + enableFunctionToggleControls, + hideStorybookControls, + moveStorybookControlsToCategory, +} from '../../../.storybook/utils'; +import { + EuiPaginationButtonArrow, + Props as EuiPaginationButtonArrowProps, +} from './pagination_button_arrow'; + +const meta: Meta = { + title: 'Navigation/EuiPagination/EuiPaginationButtonArrow', + component: EuiPaginationButtonArrow, +}; +enableFunctionToggleControls(meta, ['onClick']); +moveStorybookControlsToCategory(meta, ['buttonRef'], 'EuiButtonEmpty props'); +disableStorybookControls(meta, ['buttonRef']); +hideStorybookControls(meta, ['aria-label']); + +export default meta; +type Story = StoryObj; + +export const Playground: Story = { + args: { + type: 'next', + disabled: false, + }, +}; From 4318eb91027760540481b4a0e453da37c0cb9b98 Mon Sep 17 00:00:00 2001 From: Lene Gadewoll Date: Wed, 3 Apr 2024 17:43:12 +0200 Subject: [PATCH 04/18] docs(storybook): add EuiPanel stories --- src/components/panel/panel.stories.tsx | 47 ++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 src/components/panel/panel.stories.tsx diff --git a/src/components/panel/panel.stories.tsx b/src/components/panel/panel.stories.tsx new file mode 100644 index 00000000000..03e3296a637 --- /dev/null +++ b/src/components/panel/panel.stories.tsx @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { Meta, StoryObj } from '@storybook/react'; + +import { + disableStorybookControls, + enableFunctionToggleControls, +} from '../../../.storybook/utils'; +import { EuiPanel, EuiPanelProps } from './panel'; + +const meta: Meta = { + title: 'Layout/EuiPanel', + component: EuiPanel, + argTypes: { + element: { + options: [undefined, 'div', 'button'], + }, + ['aria-label']: { + if: { arg: 'onClick', eq: true }, + }, + }, + args: { + paddingSize: 'm', + borderRadius: 'm', + color: 'plain', + hasShadow: true, + hasBorder: false, + grow: true, + }, +}; +enableFunctionToggleControls(meta, ['onClick']); +disableStorybookControls(meta, ['panelRef']); + +export default meta; +type Story = StoryObj; + +export const Playground: Story = { + args: { + children: 'Panel content', + }, +}; From 0425b2dd7fdbe9fa532c8f464828d8da57adcdef Mon Sep 17 00:00:00 2001 From: Lene Gadewoll Date: Thu, 4 Apr 2024 18:57:51 +0200 Subject: [PATCH 05/18] docs(storybook): cleanup and PR feedback --- src/components/page_template/page_template.stories.tsx | 2 +- src/components/pagination/pagination.stories.tsx | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/page_template/page_template.stories.tsx b/src/components/page_template/page_template.stories.tsx index 004a3e5eff7..05983cd7725 100644 --- a/src/components/page_template/page_template.stories.tsx +++ b/src/components/page_template/page_template.stories.tsx @@ -13,8 +13,8 @@ import { hideStorybookControls, moveStorybookControlsToCategory, } from '../../../.storybook/utils'; -import { PAGE_DIRECTIONS } from './outer/page_outer'; import { EuiPageTemplate, EuiPageTemplateProps } from './page_template'; +import { PAGE_DIRECTIONS } from './outer/page_outer'; import { EuiButton } from '../button'; import { EuiText } from '../text'; diff --git a/src/components/pagination/pagination.stories.tsx b/src/components/pagination/pagination.stories.tsx index 6c691a8cc40..b9c061daaaa 100644 --- a/src/components/pagination/pagination.stories.tsx +++ b/src/components/pagination/pagination.stories.tsx @@ -26,8 +26,7 @@ enableFunctionToggleControls(meta, ['onPageClick']); export default meta; type Story = StoryObj; -export const Playground: Story = {}; -export const PageCount: Story = { +export const Playground: Story = { args: { activePage: 5, pageCount: 10, From a741710cadb1e2ea073e5acd20cc0c5ae322365c Mon Sep 17 00:00:00 2001 From: Lene Gadewoll Date: Thu, 4 Apr 2024 19:00:33 +0200 Subject: [PATCH 06/18] refactor(storybook): improve utils - updates enableFunctionToggleControls to overwrite global actions behaviour and set a default for function props + add unit tests for enableFunctionToggleControls --- .storybook/utils.test.ts | 71 ++++++++++++++++++++++++++++++++++++++-- .storybook/utils.ts | 28 +++++++++++++++- 2 files changed, 96 insertions(+), 3 deletions(-) diff --git a/.storybook/utils.test.ts b/.storybook/utils.test.ts index c8d8ee9db33..8b82bbda465 100644 --- a/.storybook/utils.test.ts +++ b/.storybook/utils.test.ts @@ -6,11 +6,14 @@ * Side Public License, v 1. */ -import { +import * as utils from './utils'; + +const { hideStorybookControls, disableStorybookControls, moveStorybookControlsToCategory, -} from './utils'; + enableFunctionToggleControls, +} = utils; describe('hideStorybookControls', () => { it('updates the provided config with the expected `argTypes` object when passed prop name strings', () => { @@ -198,3 +201,67 @@ describe('moveStorybookControlsToCategory', () => { ]); }); }); + +describe('enableFunctionToggleControls', () => { + it('updates the provided config with the expected `argTypes` object when passed function prop name strings', () => { + expect(enableFunctionToggleControls({ argTypes: {} }, ['onClick'])).toEqual( + { + args: { + onClick: false, + }, + argTypes: { + onClick: { + control: 'boolean', + mapping: { false: undefined, true: expect.any(Function) }, + }, + }, + parameters: { actions: { argTypesRegex: null } }, + } + ); + }); + + it('merges existing and new `argTypes` objects correctly', () => { + type TestProps = { hello: boolean; onHello: () => {} }; + + expect( + enableFunctionToggleControls( + { + args: { hello: true }, + argTypes: { + isDisabled: { control: { type: 'boolean' } }, + }, + }, + ['onHello'] + ) + ).toEqual({ + args: { + hello: true, + onHello: false, + }, + argTypes: { + isDisabled: { control: { type: 'boolean' } }, + onHello: { + control: 'boolean', + mapping: { false: undefined, true: expect.any(Function) }, + }, + }, + parameters: { actions: { argTypesRegex: null } }, + }); + }); + + it('throws a typescript error if a generic is passed and the prop names do not match', () => { + type TestProps = { hello: boolean; onHello: () => {} }; + + // No typescript error + enableFunctionToggleControls({ argTypes: {} }, [ + 'hello', + 'onHello', + ]); + enableFunctionToggleControls({ argTypes: {} }, [ + 'hello', + 'onHello', + // @ts-expect-error - will fail `yarn lint` if a TS error is *not* produced + 'error', + ]); + }); +}); diff --git a/.storybook/utils.ts b/.storybook/utils.ts index 839cfc02d27..f94ce9820e3 100644 --- a/.storybook/utils.ts +++ b/.storybook/utils.ts @@ -107,7 +107,19 @@ export const enableFunctionToggleControls = ( false: undefined, }); - const updatedConfig = _updateArgTypes(config, propNames, [ + /* Sets the default value for the passed function prop. + This is needed to ensure the coolean control is set and + to prevent additional clicks. + NOTE: This ahs to happen before the argTypes are updated */ + config.args = propNames.reduce( + (acc, propName) => ({ + ...acc, + [propName]: false, + }), + config.args + ); + + let updatedConfig = _updateArgTypes(config, propNames, [ { key: 'control', value: 'boolean' }, { key: 'mapping', @@ -115,6 +127,20 @@ export const enableFunctionToggleControls = ( }, ]); + updatedConfig = { + ...updatedConfig, + /* Overwrites global parameters.actions setting in preview.tsx which enables + actions on function props starting with "on[Name]" by default. This is needed + to ensure the default "false" state is actually false. */ + parameters: { + ...updatedConfig.parameters, + actions: { + ...updatedConfig.parameters?.actions, + argTypesRegex: null, + }, + }, + }; + return updatedConfig; }; From 3c8c5d65c7ba67798aa4cd708a9a24e3c13f6ef8 Mon Sep 17 00:00:00 2001 From: Lene Gadewoll Date: Thu, 4 Apr 2024 19:02:15 +0200 Subject: [PATCH 07/18] docs: fix typo --- .storybook/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.storybook/utils.ts b/.storybook/utils.ts index f94ce9820e3..4ad21897e9f 100644 --- a/.storybook/utils.ts +++ b/.storybook/utils.ts @@ -110,7 +110,7 @@ export const enableFunctionToggleControls = ( /* Sets the default value for the passed function prop. This is needed to ensure the coolean control is set and to prevent additional clicks. - NOTE: This ahs to happen before the argTypes are updated */ + NOTE: This has to happen before the argTypes are updated */ config.args = propNames.reduce( (acc, propName) => ({ ...acc, From 8b704cfda662cdf3d81c9c6b35745cf70371069d Mon Sep 17 00:00:00 2001 From: Lene Gadewoll Date: Fri, 5 Apr 2024 09:47:20 +0200 Subject: [PATCH 08/18] refactor(storybook): update enableFunctionToggleControls default prop value to true --- .storybook/utils.test.ts | 4 ++-- .storybook/utils.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.storybook/utils.test.ts b/.storybook/utils.test.ts index 8b82bbda465..20201423aeb 100644 --- a/.storybook/utils.test.ts +++ b/.storybook/utils.test.ts @@ -207,7 +207,7 @@ describe('enableFunctionToggleControls', () => { expect(enableFunctionToggleControls({ argTypes: {} }, ['onClick'])).toEqual( { args: { - onClick: false, + onClick: true, }, argTypes: { onClick: { @@ -236,7 +236,7 @@ describe('enableFunctionToggleControls', () => { ).toEqual({ args: { hello: true, - onHello: false, + onHello: true, }, argTypes: { isDisabled: { control: { type: 'boolean' } }, diff --git a/.storybook/utils.ts b/.storybook/utils.ts index 4ad21897e9f..ecb91fa4cb0 100644 --- a/.storybook/utils.ts +++ b/.storybook/utils.ts @@ -114,7 +114,7 @@ export const enableFunctionToggleControls = ( config.args = propNames.reduce( (acc, propName) => ({ ...acc, - [propName]: false, + [propName]: true, }), config.args ); From e6f52754edf59827222d23af33ee30315c89df23 Mon Sep 17 00:00:00 2001 From: Lene Gadewoll Date: Fri, 5 Apr 2024 09:48:27 +0200 Subject: [PATCH 09/18] docs(storybook) PR feedback - updates restrictWidth control to select = adds additional stories for EuiPageTemplate --- .../page/page_header/page_header.stories.tsx | 42 ++--------- .../page_template/page_template.stories.tsx | 74 ++++++++++++++++--- 2 files changed, 70 insertions(+), 46 deletions(-) diff --git a/src/components/page/page_header/page_header.stories.tsx b/src/components/page/page_header/page_header.stories.tsx index 2fd58e1ed35..20dfd51d79d 100644 --- a/src/components/page/page_header/page_header.stories.tsx +++ b/src/components/page/page_header/page_header.stories.tsx @@ -9,10 +9,7 @@ import React from 'react'; import type { Meta, StoryObj } from '@storybook/react'; -import { - hideStorybookControls, - moveStorybookControlsToCategory, -} from '../../../../.storybook/utils'; +import { moveStorybookControlsToCategory } from '../../../../.storybook/utils'; import { EuiButton } from '../../button'; import { EuiPageHeader, EuiPageHeaderProps } from '../page_header'; @@ -27,6 +24,10 @@ const meta: Meta = { pageTitleProps: { control: 'object' }, breadcrumbProps: { control: 'object' }, tabsProps: { control: 'object' }, + restrictWidth: { + control: 'select', + options: [true, false, 500, 900, 1800, '25%', '50%', '75%'], + }, }, args: { // Component defaults @@ -100,36 +101,3 @@ export const Playground: Story = { breadcrumbs, }, }; - -export const RestrictWidth: Story = { - args: { - pageTitle: 'Page title', - iconType: 'logoKibana', - description: 'Example of a description.', - bottomBorder: 'extended', - rightSideItems: [ - Add something, - Do something, - ], - tabs, - breadcrumbs, - restrictWidth: 500, - }, -}; -// This story displays the restrictWidth functionality; removing other content props to prevent confusion -hideStorybookControls(RestrictWidth, [ - 'pageTitle', - 'pageTitleProps', - 'iconType', - 'iconProps', - 'breadcrumbs', - 'breadcrumbProps', - 'tabs', - 'tabsProps', - 'description', - 'responsive', - 'alignItems', - 'rightSideItems', - 'rightSideGroupProps', - 'children', -]); diff --git a/src/components/page_template/page_template.stories.tsx b/src/components/page_template/page_template.stories.tsx index 05983cd7725..d96ba6e0c76 100644 --- a/src/components/page_template/page_template.stories.tsx +++ b/src/components/page_template/page_template.stories.tsx @@ -13,12 +13,14 @@ import { hideStorybookControls, moveStorybookControlsToCategory, } from '../../../.storybook/utils'; -import { EuiPageTemplate, EuiPageTemplateProps } from './page_template'; -import { PAGE_DIRECTIONS } from './outer/page_outer'; import { EuiButton } from '../button'; import { EuiText } from '../text'; +import { EuiPageHeaderProps } from '../page/page_header'; +import { EuiPageSectionProps } from '../page/page_section'; +import { EuiPageTemplate, EuiPageTemplateProps } from './page_template'; +import { PAGE_DIRECTIONS } from './outer/page_outer'; -const headerContent = ( +const headerContent = (props?: EuiPageHeaderProps) => ( ); -const sectionContent = ( - Section content +const sectionContent = (props?: EuiPageSectionProps) => ( + Section content ); -const sidebarContent = ( - Sidebar content +const sidebarContent = (props?: Partial) => ( + Sidebar content ); const bottomBarContent = ( @@ -62,8 +65,8 @@ const comboContent = ( - {headerContent} - {sectionContent} + {headerContent()} + {sectionContent()} {bottomBarContent} ); @@ -83,6 +86,10 @@ const meta: Meta = { }, component: { control: 'text' }, contentBorder: { control: 'radio', options: [undefined, true, false] }, + restrictWidth: { + control: 'select', + options: [true, false, 500, 900, 1800, '25%', '50%', '75%'], + }, children: { control: 'select', options: [ @@ -134,3 +141,52 @@ export const Playground: Story = { // using render() over args to ensure dynamic update on prop changes render: (args) => , }; + +export const WithSidebar: Story = { + render: (args) => ( + + {sidebarContent(args)} + {headerContent(args)} + + + + Stack EuiPageTemplate sections and headers to create your custom + content order. + + + + {sectionContent(args)} + {bottomBarContent} + + ), +}; +hideStorybookControls(WithSidebar, [ + 'children', + 'direction', + 'grow', + 'minHeight', + 'responsive', + 'component', + 'contentBorder', + 'mainProps', +]); + +export const WithEmptyPrompt: Story = { + render: (args) => ( + + {sidebarContent(args)} + {headerContent(args)} + {emptyPromptContent} + + ), +}; +hideStorybookControls(WithEmptyPrompt, [ + 'children', + 'direction', + 'grow', + 'minHeight', + 'responsive', + 'component', + 'contentBorder', + 'mainProps', +]); From 659e96438ffa1d0aa1bf0e0e71bfed4ad68763ea Mon Sep 17 00:00:00 2001 From: Lene Gadewoll Date: Fri, 5 Apr 2024 18:06:03 +0200 Subject: [PATCH 10/18] docs(storybook) use fullscreen layout for EuiPageTemplate stories --- src/components/page_template/page_template.stories.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/page_template/page_template.stories.tsx b/src/components/page_template/page_template.stories.tsx index d96ba6e0c76..d864c3ac080 100644 --- a/src/components/page_template/page_template.stories.tsx +++ b/src/components/page_template/page_template.stories.tsx @@ -74,6 +74,9 @@ const comboContent = ( const meta: Meta = { title: 'Templates/EuiPageTemplate', component: EuiPageTemplate, + parameters: { + layout: 'fullscreen', + }, argTypes: { bottomBorder: { control: 'radio', From a3e312d8632a5ca1cb6bf278661b59b15e3c0bf8 Mon Sep 17 00:00:00 2001 From: Cee Chen Date: Sun, 7 Apr 2024 12:14:25 -0700 Subject: [PATCH 11/18] EuiPageTemplate cleanup --- .../page_template/page_template.stories.tsx | 178 +++++++----------- 1 file changed, 71 insertions(+), 107 deletions(-) diff --git a/src/components/page_template/page_template.stories.tsx b/src/components/page_template/page_template.stories.tsx index d864c3ac080..195d3667276 100644 --- a/src/components/page_template/page_template.stories.tsx +++ b/src/components/page_template/page_template.stories.tsx @@ -13,37 +13,53 @@ import { hideStorybookControls, moveStorybookControlsToCategory, } from '../../../.storybook/utils'; +import { EuiSkeletonText } from '../skeleton'; import { EuiButton } from '../button'; import { EuiText } from '../text'; -import { EuiPageHeaderProps } from '../page/page_header'; -import { EuiPageSectionProps } from '../page/page_section'; + import { EuiPageTemplate, EuiPageTemplateProps } from './page_template'; import { PAGE_DIRECTIONS } from './outer/page_outer'; -const headerContent = (props?: EuiPageHeaderProps) => ( +const headerContent = ( Button]} - description="header description" - tabs={[ - { label: 'Tab 1', isSelected: true }, - { - label: 'Tab 2', - }, - ]} - {...props} + description={ + + } + tabs={[{ label: 'Tab 1', isSelected: true }, { label: 'Tab 2' }]} /> ); -const sectionContent = (props?: EuiPageSectionProps) => ( - Section content +const sectionContent = ( + <> + + + + Stack EuiPageTemplate sections and headers to create your custom + content order. + + + + + + + ); -const sidebarContent = (props?: Partial) => ( - Sidebar content +const sidebarContent = ( + + + ); const bottomBarContent = ( - BottomBar content + ); const emptyPromptContent = ( @@ -51,26 +67,10 @@ const emptyPromptContent = ( title={Empty prompt!} footer={Button} > - EmptyPromp content + ); -const comboContent = ( - <> - - - - Stack EuiPageTemplate sections and headers to create your custom - content order. - - - - {headerContent()} - {sectionContent()} - {bottomBarContent} - -); - const meta: Meta = { title: 'Templates/EuiPageTemplate', component: EuiPageTemplate, @@ -89,29 +89,6 @@ const meta: Meta = { }, component: { control: 'text' }, contentBorder: { control: 'radio', options: [undefined, true, false] }, - restrictWidth: { - control: 'select', - options: [true, false, 500, 900, 1800, '25%', '50%', '75%'], - }, - children: { - control: 'select', - options: [ - 'Combo', - 'Header', - 'Section', - 'Sidebar', - 'BottomBar', - 'EmptyPrompt', - ], - mapping: { - Combo: comboContent, - Header: headerContent, - Section: sectionContent, - Sidebar: sidebarContent, - BottomBar: bottomBarContent, - EmptyPrompt: emptyPromptContent, - }, - }, }, args: { minHeight: '460px', @@ -139,57 +116,44 @@ type Story = StoryObj; export const Playground: Story = { args: { - children: 'Combo', + children: 'With everything', + }, + argTypes: { + restrictWidth: { + control: 'select', + options: [true, false, 500, 900, 1800, '25%', '50%', '75%'], + }, + children: { + control: 'select', + description: + 'For quicker testing, use the selection control to the right to select several examples of common EuiPageTemplate layouts', + options: [ + 'With everything', + 'Without sidebar', + 'Without header', + 'Without bottom bar', + 'With empty prompt content', + ], + mapping: { + 'With everything': [ + sidebarContent, + headerContent, + sectionContent, + bottomBarContent, + ], + 'Without sidebar': [headerContent, sectionContent, bottomBarContent], + 'Without header': [sidebarContent, sectionContent, bottomBarContent], + 'Without bottom bar': [sidebarContent, headerContent, sectionContent], + 'With empty prompt content': [ + sidebarContent, + headerContent, + emptyPromptContent, + bottomBarContent, + ], + }, + }, }, // using render() over args to ensure dynamic update on prop changes - render: (args) => , -}; - -export const WithSidebar: Story = { - render: (args) => ( - - {sidebarContent(args)} - {headerContent(args)} - - - - Stack EuiPageTemplate sections and headers to create your custom - content order. - - - - {sectionContent(args)} - {bottomBarContent} - - ), -}; -hideStorybookControls(WithSidebar, [ - 'children', - 'direction', - 'grow', - 'minHeight', - 'responsive', - 'component', - 'contentBorder', - 'mainProps', -]); - -export const WithEmptyPrompt: Story = { - render: (args) => ( - - {sidebarContent(args)} - {headerContent(args)} - {emptyPromptContent} - - ), + // Cee TODO: This doesn't appear to work for the `paddingSize` and `bottomBorder` props + // render: ({ ...args }) => , }; -hideStorybookControls(WithEmptyPrompt, [ - 'children', - 'direction', - 'grow', - 'minHeight', - 'responsive', - 'component', - 'contentBorder', - 'mainProps', -]); From 256c55cbb956bde4a844727e3a3b51f852c359f0 Mon Sep 17 00:00:00 2001 From: Lene Gadewoll Date: Wed, 10 Apr 2024 13:47:01 +0200 Subject: [PATCH 12/18] docs(storybook): PR feedback - removes skeleton in favor of string to ensure valid HTML + adds parent= on bottom bar to ensure last position on DOM updates --- .../page_template/page_template.stories.tsx | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/components/page_template/page_template.stories.tsx b/src/components/page_template/page_template.stories.tsx index 195d3667276..f21c8a2db88 100644 --- a/src/components/page_template/page_template.stories.tsx +++ b/src/components/page_template/page_template.stories.tsx @@ -25,12 +25,7 @@ const headerContent = ( iconType="logoElastic" pageTitle="Page title" rightSideItems={[Button]} - description={ - - } + description="Page header example description" tabs={[{ label: 'Tab 1', isSelected: true }, { label: 'Tab 2' }]} /> ); @@ -58,7 +53,9 @@ const sidebarContent = ( ); const bottomBarContent = ( - + // adding parent="" here to prevent using a portal and to ensure the + // last position on changes when the parent does not change + ); From 16e3294ff38a3b3a34c4d914ea4ac93d6e95ac48 Mon Sep 17 00:00:00 2001 From: Cee Chen Date: Wed, 10 Apr 2024 12:40:57 -0700 Subject: [PATCH 13/18] Fix child props not correctly updating from `TemplateContext` Turns out the context needed a parent `{}` reference that updates on prop update, which the `useContext()`+setting child properties directly was messing up. Instantiating a new object value via `useMemo` solves the issue + opinionated syntax cleanup while here (use fewer functions - not sure why they needed to be functions??) and destructure more --- .../page_template/page_template.stories.tsx | 4 +- .../page_template/page_template.tsx | 103 ++++++++---------- 2 files changed, 49 insertions(+), 58 deletions(-) diff --git a/src/components/page_template/page_template.stories.tsx b/src/components/page_template/page_template.stories.tsx index f21c8a2db88..b97f43be46b 100644 --- a/src/components/page_template/page_template.stories.tsx +++ b/src/components/page_template/page_template.stories.tsx @@ -53,9 +53,7 @@ const sidebarContent = ( ); const bottomBarContent = ( - // adding parent="" here to prevent using a portal and to ensure the - // last position on changes when the parent does not change - + ); diff --git a/src/components/page_template/page_template.tsx b/src/components/page_template/page_template.tsx index 9f9b4b959f4..70fe06feeda 100644 --- a/src/components/page_template/page_template.tsx +++ b/src/components/page_template/page_template.tsx @@ -12,6 +12,7 @@ import React, { FunctionComponent, HTMLAttributes, useContext, + useMemo, } from 'react'; import classNames from 'classnames'; @@ -100,8 +101,6 @@ export const _EuiPageTemplate: FunctionComponent = ({ minHeight = '460px', ...rest }) => { - const templateContext = useContext(TemplateContext); - // Used as a target to insert the bottom bar component const pageInnerId = useGeneratedHtmlId({ prefix: 'EuiPageTemplateInner', @@ -112,43 +111,6 @@ export const _EuiPageTemplate: FunctionComponent = ({ const sections: React.ReactElement[] = []; const sidebar: React.ReactElement[] = []; - const getBottomBorder = () => { - if (bottomBorder !== undefined) { - return bottomBorder; - } else { - return sidebar.length ? true : 'extended'; - } - }; - - const getHeaderProps = () => ({ - restrictWidth, - paddingSize, - bottomBorder: getBottomBorder(), - }); - - const getSectionProps = (): EuiPageSectionProps => ({ - restrictWidth, - paddingSize, - color: panelled === false ? 'transparent' : 'plain', - }); - - const getSideBarProps = () => ({ - paddingSize, - responsive, - }); - - const getBottomBarProps = () => ({ - restrictWidth, - paddingSize, - // pageInnerId may contain colons that are parsed as pseudo-elements if not escaped - parent: `#${pageInnerId.replaceAll(':', '\\:')}`, - }); - - const innerPanelled = () => panelled ?? Boolean(sidebar.length > 0); - - const innerBordered = () => - contentBorder !== undefined ? contentBorder : Boolean(sidebar.length > 0); - React.Children.toArray(children).forEach((child, index) => { if (!React.isValidElement(child)) return; // Skip non-components @@ -156,10 +118,12 @@ export const _EuiPageTemplate: FunctionComponent = ({ child.type === EuiPageSidebar || child.props.__EMOTION_TYPE_PLEASE_DO_NOT_USE__ === EuiPageSidebar ) { + const sidebarProps = { paddingSize, responsive }; + sidebar.push( React.cloneElement(child, { key: `sidebar${index}`, - ...getSideBarProps(), + ...sidebarProps, // Allow their props overridden by appending the child props spread at the end ...child.props, }) @@ -178,13 +142,42 @@ export const _EuiPageTemplate: FunctionComponent = ({ ...rest.style, }; - templateContext.header = getHeaderProps(); - templateContext.section = getSectionProps(); - templateContext.emptyPrompt = { - panelled: innerPanelled() ? true : panelled, - grow: true, - }; - templateContext.bottomBar = getBottomBarProps(); + const innerPanelled = panelled ?? Boolean(sidebar.length > 0); + const innerBordered = contentBorder ?? Boolean(sidebar.length > 0); + const headerBottomBorder = bottomBorder ?? sidebar.length ? true : 'extended'; + + const templateContext = useMemo(() => { + return { + header: { + restrictWidth, + paddingSize, + bottomBorder: headerBottomBorder, + }, + section: { + restrictWidth, + paddingSize, + color: panelled === false ? 'transparent' : 'plain', + grow: true, + }, + emptyPrompt: { + panelled: innerPanelled ? true : panelled, + grow: true, + }, + bottomBar: { + restrictWidth, + paddingSize, + // pageInnerId may contain colons that are parsed as pseudo-elements if not escaped + parent: `#${pageInnerId.replaceAll(':', '\\:')}`, + }, + }; + }, [ + pageInnerId, + restrictWidth, + paddingSize, + panelled, + innerPanelled, + headerBottomBorder, + ]); return ( @@ -200,8 +193,8 @@ export const _EuiPageTemplate: FunctionComponent = ({ {...mainProps} component={component} id={pageInnerId} - border={innerBordered()} - panelled={innerPanelled()} + border={innerBordered} + panelled={innerPanelled} responsive={responsive} > {sections} @@ -212,23 +205,23 @@ export const _EuiPageTemplate: FunctionComponent = ({ }; const _EuiPageSection: FunctionComponent = (props) => { - const templateContext = useContext(TemplateContext); + const { section } = useContext(TemplateContext); - return ; + return ; }; const _EuiPageHeader: FunctionComponent = (props) => { - const templateContext = useContext(TemplateContext); + const { header } = useContext(TemplateContext); - return ; + return ; }; const _EuiPageEmptyPrompt: FunctionComponent<_EuiPageEmptyPromptProps> = ( props ) => { - const templateContext = useContext(TemplateContext); + const { emptyPrompt } = useContext(TemplateContext); - return ; + return ; }; const _EuiPageBottomBar: FunctionComponent<_EuiPageBottomBarProps> = ( From 23bce58cf0415f9877903de55628ccf19181a722 Mon Sep 17 00:00:00 2001 From: Cee Chen Date: Wed, 10 Apr 2024 13:06:36 -0700 Subject: [PATCH 14/18] More memoization & syntax cleanup --- .../page_template/page_template.tsx | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/components/page_template/page_template.tsx b/src/components/page_template/page_template.tsx index 70fe06feeda..7d928fb26a8 100644 --- a/src/components/page_template/page_template.tsx +++ b/src/components/page_template/page_template.tsx @@ -37,7 +37,7 @@ import { import { _EuiPageRestrictWidth } from '../page/_restrict_width'; import { _EuiPageBottomBorder } from '../page/_bottom_border'; import { useGeneratedHtmlId } from '../../services'; -import { logicalStyle } from '../../global_styling'; +import { logicalStyles } from '../../global_styling'; import { CommonProps } from '../common'; export const TemplateContext = createContext({ @@ -99,6 +99,7 @@ export const _EuiPageTemplate: FunctionComponent = ({ // Outer props className, minHeight = '460px', + style, ...rest }) => { // Used as a target to insert the bottom bar component @@ -133,14 +134,16 @@ export const _EuiPageTemplate: FunctionComponent = ({ } }); - const _minHeight = grow ? `max(${minHeight}, 100vh)` : minHeight; - const classes = classNames('euiPageTemplate', className); - const pageStyle = { - ...logicalStyle('min-height', _minHeight), - ...logicalStyle('padding-top', offset ?? 'var(--euiFixedHeadersOffset, 0)'), - ...rest.style, - }; + const pageStyle = useMemo( + () => + logicalStyles({ + minHeight: grow ? `max(${minHeight}, 100vh)` : minHeight, + paddingTop: offset ?? 'var(--euiFixedHeadersOffset, 0)', + ...style, + }), + [minHeight, grow, offset, style] + ); const innerPanelled = panelled ?? Boolean(sidebar.length > 0); const innerBordered = contentBorder ?? Boolean(sidebar.length > 0); From f7d61cece34a4181099bd00ab14425aba54b961b Mon Sep 17 00:00:00 2001 From: Cee Chen Date: Wed, 10 Apr 2024 13:11:52 -0700 Subject: [PATCH 15/18] Refacator `EuiPageTemplate.Sidebar` to use context instead of `react.cloneElement` - cleaner and matches existing behavior for other subcomponents - consumers who render `` will see broken-ish behavior, but they shouldn't be doing this in any case --- .../page_template/page_template.tsx | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/src/components/page_template/page_template.tsx b/src/components/page_template/page_template.tsx index 7d928fb26a8..418b6fe32df 100644 --- a/src/components/page_template/page_template.tsx +++ b/src/components/page_template/page_template.tsx @@ -33,6 +33,7 @@ import { EuiPageSection, EuiPageSectionProps, EuiPageSidebar, + EuiPageSidebarProps, } from '../page'; import { _EuiPageRestrictWidth } from '../page/_restrict_width'; import { _EuiPageBottomBorder } from '../page/_bottom_border'; @@ -41,6 +42,7 @@ import { logicalStyles } from '../../global_styling'; import { CommonProps } from '../common'; export const TemplateContext = createContext({ + sidebar: {}, section: {}, header: {}, emptyPrompt: {}, @@ -112,23 +114,14 @@ export const _EuiPageTemplate: FunctionComponent = ({ const sections: React.ReactElement[] = []; const sidebar: React.ReactElement[] = []; - React.Children.toArray(children).forEach((child, index) => { + React.Children.toArray(children).forEach((child) => { if (!React.isValidElement(child)) return; // Skip non-components if ( - child.type === EuiPageSidebar || - child.props.__EMOTION_TYPE_PLEASE_DO_NOT_USE__ === EuiPageSidebar + child.type === _EuiPageSidebar || + child.props.__EMOTION_TYPE_PLEASE_DO_NOT_USE__ === _EuiPageSidebar ) { - const sidebarProps = { paddingSize, responsive }; - - sidebar.push( - React.cloneElement(child, { - key: `sidebar${index}`, - ...sidebarProps, - // Allow their props overridden by appending the child props spread at the end - ...child.props, - }) - ); + sidebar.push(child); } else { sections.push(child); } @@ -151,6 +144,10 @@ export const _EuiPageTemplate: FunctionComponent = ({ const templateContext = useMemo(() => { return { + sidebar: { + paddingSize, + responsive, + }, header: { restrictWidth, paddingSize, @@ -176,6 +173,7 @@ export const _EuiPageTemplate: FunctionComponent = ({ }, [ pageInnerId, restrictWidth, + responsive, paddingSize, panelled, innerPanelled, @@ -207,6 +205,12 @@ export const _EuiPageTemplate: FunctionComponent = ({ ); }; +const _EuiPageSidebar: FunctionComponent = (props) => { + const { sidebar } = useContext(TemplateContext); + + return ; +}; + const _EuiPageSection: FunctionComponent = (props) => { const { section } = useContext(TemplateContext); @@ -236,7 +240,7 @@ const _EuiPageBottomBar: FunctionComponent<_EuiPageBottomBarProps> = ( }; export const EuiPageTemplate = Object.assign(_EuiPageTemplate, { - Sidebar: EuiPageSidebar, + Sidebar: _EuiPageSidebar, Header: _EuiPageHeader, Section: _EuiPageSection, BottomBar: _EuiPageBottomBar, From 58cc0c43e988b72ad38917c1bd29df97cbf67e34 Mon Sep 17 00:00:00 2001 From: Cee Chen Date: Wed, 10 Apr 2024 13:25:57 -0700 Subject: [PATCH 16/18] More memoization - children iteration - not sure how useful this is since `children` is very difficult to memoize on --- .../page_template/page_template.tsx | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/src/components/page_template/page_template.tsx b/src/components/page_template/page_template.tsx index 418b6fe32df..75c532bbb0c 100644 --- a/src/components/page_template/page_template.tsx +++ b/src/components/page_template/page_template.tsx @@ -111,21 +111,25 @@ export const _EuiPageTemplate: FunctionComponent = ({ }); // Sections include page header - const sections: React.ReactElement[] = []; - const sidebar: React.ReactElement[] = []; - - React.Children.toArray(children).forEach((child) => { - if (!React.isValidElement(child)) return; // Skip non-components - - if ( - child.type === _EuiPageSidebar || - child.props.__EMOTION_TYPE_PLEASE_DO_NOT_USE__ === _EuiPageSidebar - ) { - sidebar.push(child); - } else { - sections.push(child); - } - }); + const [sidebar, sections] = useMemo(() => { + const sidebar: React.ReactElement[] = []; + const sections: React.ReactElement[] = []; + + React.Children.toArray(children).forEach((child) => { + if (!React.isValidElement(child)) return; // Skip non-components + + if ( + child.type === _EuiPageSidebar || + child.props.__EMOTION_TYPE_PLEASE_DO_NOT_USE__ === _EuiPageSidebar + ) { + sidebar.push(child); + } else { + sections.push(child); + } + }); + + return [sidebar, sections]; + }, [children]); const classes = classNames('euiPageTemplate', className); const pageStyle = useMemo( From 315abfeaddbe31a6eb9d211b4ef70465776ed083 Mon Sep 17 00:00:00 2001 From: Cee Chen Date: Wed, 10 Apr 2024 13:32:04 -0700 Subject: [PATCH 17/18] Fix ternary shenanigans :facepalm: i'm a senior developer errybody --- src/components/page_template/page_template.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/page_template/page_template.tsx b/src/components/page_template/page_template.tsx index 75c532bbb0c..1c8819da37f 100644 --- a/src/components/page_template/page_template.tsx +++ b/src/components/page_template/page_template.tsx @@ -144,7 +144,8 @@ export const _EuiPageTemplate: FunctionComponent = ({ const innerPanelled = panelled ?? Boolean(sidebar.length > 0); const innerBordered = contentBorder ?? Boolean(sidebar.length > 0); - const headerBottomBorder = bottomBorder ?? sidebar.length ? true : 'extended'; + const headerBottomBorder = + bottomBorder ?? (sidebar.length ? true : 'extended'); const templateContext = useMemo(() => { return { From ecb2221167f7ec3d1d143843e0e70cc56db2820d Mon Sep 17 00:00:00 2001 From: Cee Chen Date: Thu, 11 Apr 2024 09:08:23 -0700 Subject: [PATCH 18/18] changelog --- changelogs/upcoming/7648.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/upcoming/7648.md diff --git a/changelogs/upcoming/7648.md b/changelogs/upcoming/7648.md new file mode 100644 index 00000000000..4159ce0d2b2 --- /dev/null +++ b/changelogs/upcoming/7648.md @@ -0,0 +1,4 @@ +**Bug fixes** + +- Fixed an `EuiPageTemplate` bug where prop updates would not cascade down to child sections + - To cascade props down to the sidebar, `EuiPageTemplate` now explicitly requires using the `EuiPageTemplate.Sidebar` rather than `EuiPageSidebar`