diff --git a/src/components/popover/input_popover.stories.tsx b/src/components/popover/input_popover.stories.tsx new file mode 100644 index 000000000000..0a466e613d78 --- /dev/null +++ b/src/components/popover/input_popover.stories.tsx @@ -0,0 +1,125 @@ +/* + * 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, { useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; + +import { + disableStorybookControls, + enableFunctionToggleControls, + moveStorybookControlsToCategory, +} from '../../../.storybook/utils'; +import { EuiFieldText } from '../form'; +import { EuiInputPopover, EuiInputPopoverProps } from './input_popover'; + +const meta: Meta = { + title: 'Layout/EuiInputPopover', + component: EuiInputPopover, + args: { + anchorPosition: 'downLeft', + attachToAnchor: true, + repositionToCrossAxis: false, + display: 'block', + panelPaddingSize: 's', + closeOnScroll: false, + ownFocus: false, + disableFocusTrap: false, + fullWidth: false, + panelMinWidth: 0, + offset: undefined, + buffer: undefined, + hasDragDrop: false, + panelClassName: '', + popoverScreenReaderText: '', + }, +}; +moveStorybookControlsToCategory( + meta, + [ + 'attachToAnchor', + 'buffer', + 'closePopover', + 'disableFocusTrap', + 'display', + 'hasDragDrop', + 'isOpen', + 'offset', + 'onPositionChange', + 'ownFocus', + 'panelClassName', + 'panelPaddingSize', + 'popoverScreenReaderText', + 'repositionToCrossAxis', + ], + 'EuiPopover props' +); +disableStorybookControls(meta, ['input', 'inputRef']); + +export default meta; +type Story = StoryObj; + +export const Playground: Story = { + args: { + children: 'Popover content', + isOpen: true, + }, + render: (args) => , +}; +enableFunctionToggleControls(Playground, [ + 'closePopover', + 'onPanelResize', + 'onPositionChange', +]); + +const StatefulInputPopover = ({ + children, + input, + closePopover, + isOpen: _isOpen, + ...rest +}: EuiInputPopoverProps) => { + const [isOpen, setOpen] = useState(_isOpen); + + const handleOnClose = () => { + setOpen(false); + closePopover?.(); + }; + + return ( + setOpen(true)} + placeholder="Focus me to toggle an input popover" + aria-label="Popover attached to input element" + /> + } + {...rest} + > + {children} + + ); +}; + +export const AnchorPosition: Story = { + parameters: { + controls: { include: ['anchorPosition', 'panelMinWidth'] }, + }, + args: { + children: 'Popover content', + isOpen: true, + panelMinWidth: 500, + }, + render: (args) => ( +
+ +
+ ), +}; diff --git a/src/components/popover/popover.stories.tsx b/src/components/popover/popover.stories.tsx new file mode 100644 index 000000000000..22833f0ba73e --- /dev/null +++ b/src/components/popover/popover.stories.tsx @@ -0,0 +1,123 @@ +/* + * 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, { useState } from 'react'; +import { css } from '@emotion/react'; +import type { Meta, StoryObj } from '@storybook/react'; + +import { + disableStorybookControls, + enableFunctionToggleControls, +} from '../../../.storybook/utils'; +import { EuiButton } from '../button'; +import { EuiFlexGroup } from '../flex'; +import { EuiText } from '../text'; + +import { EuiPopoverTitle } from './popover_title'; +import { EuiPopoverFooter } from './popover_footer'; +import { EuiPopover, EuiPopoverProps } from './popover'; + +const meta: Meta = { + title: 'Layout/EuiPopover/EuiPopover', + component: EuiPopover, + parameters: { + layout: 'fullscreen', + }, + args: { + isOpen: false, + ownFocus: true, + panelPaddingSize: 'm', + buffer: 16, + hasArrow: true, + anchorPosition: 'downCenter', + display: 'inline-block', + repositionToCrossAxis: true, + repositionOnScroll: false, + // adding defaults for quicker/easier QA + arrowChildren: '', + attachToAnchor: false, + hasDragDrop: false, + panelClassName: '', + popoverScreenReaderText: '', + }, + argTypes: { + buffer: { control: 'number' }, // For ease of QA + }, +}; +disableStorybookControls(meta, ['panelRef', 'popoverRef', 'closePopover']); + +export default meta; +type Story = StoryObj; + +export const Playground: Story = { + args: { + children: 'This is a popover', + button: 'popover trigger', + isOpen: true, + }, + render: (args) => , +}; +enableFunctionToggleControls(Playground, ['onPositionChange']); + +const StatefulPopover = ({ + button, + closePopover, + isOpen: _isOpen, + ...rest +}: EuiPopoverProps) => { + const [isOpen, setOpen] = useState(_isOpen); + + const handleOnClose = () => { + setOpen(false); + closePopover?.(); + }; + + const trigger = ( + setOpen(!isOpen)}> + {button || 'trigger'} + + ); + + return ( + + + + ); +}; + +export const PanelPaddingSize: Story = { + parameters: { + controls: { include: ['panelPaddingSize'] }, + }, + args: { + children: ( + <> + Popover title + + Panel padding size will cascade down to its child{' '} + EuiPopoverTitle and EuiPopoverFooter + + Popover footer + + ), + button: 'popover trigger', + isOpen: true, + }, + render: (args) => , +}; diff --git a/src/components/popover/popover.tsx b/src/components/popover/popover.tsx index 1dc6e832264d..99dc29272540 100644 --- a/src/components/popover/popover.tsx +++ b/src/components/popover/popover.tsx @@ -191,7 +191,7 @@ export interface EuiPopoverProps extends PropsWithChildren, CommonProps { /** * Minimum distance between the popover and the bounding container; * Pass an array of 4 values to adjust each side differently: `[top, right, bottom, left]` - * Default is 16 + * @default 16 */ buffer?: number | [number, number, number, number]; /** @@ -455,6 +455,17 @@ export class EuiPopover extends Component { this.onOpenPopover(); } + // ensure recalculation of panel position on prop updates + if ( + this.props.isOpen && + (prevProps.anchorPosition !== this.props.anchorPosition || + prevProps.buffer !== this.props.buffer || + prevProps.offset !== this.props.offset || + prevProps.panelPaddingSize !== this.props.panelPaddingSize) + ) { + this.positionPopoverFluid(); + } + // update scroll listener if (prevProps.repositionOnScroll !== this.props.repositionOnScroll) { if (this.props.repositionOnScroll) { diff --git a/src/components/popover/popover_footer.stories.tsx b/src/components/popover/popover_footer.stories.tsx new file mode 100644 index 000000000000..0f7eba4ba9e4 --- /dev/null +++ b/src/components/popover/popover_footer.stories.tsx @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; + +import { PADDING_SIZES } from '../../global_styling'; +import { EuiButton } from '../button'; +import { EuiPopover } from './popover'; +import { EuiPopoverFooter, EuiPopoverFooterProps } from './popover_footer'; + +const meta: Meta = { + title: 'Layout/EuiPopover/EuiPopoverFooter', + component: EuiPopoverFooter, + decorators: [ + (Story) => ( + trigger}> + + + ), + ], + argTypes: { + paddingSize: { + control: 'select', + options: [undefined, ...PADDING_SIZES], + }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Playground: Story = { + args: { + children: 'Popover footer', + }, +}; diff --git a/src/components/popover/popover_title.stories.tsx b/src/components/popover/popover_title.stories.tsx new file mode 100644 index 000000000000..0885d7e32145 --- /dev/null +++ b/src/components/popover/popover_title.stories.tsx @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; + +import { PADDING_SIZES } from '../../global_styling'; +import { EuiButton } from '../button'; +import { EuiPopover } from './popover'; +import { EuiPopoverTitle, EuiPopoverTitleProps } from './popover_title'; + +const meta: Meta = { + title: 'Layout/EuiPopover/EuiPopoverTitle', + component: EuiPopoverTitle, + decorators: [ + (Story) => ( + trigger}> + + + ), + ], + argTypes: { + paddingSize: { + control: 'select', + options: [undefined, ...PADDING_SIZES], + }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Playground: Story = { + args: { + children: 'Popover title', + }, +}; diff --git a/src/components/popover/wrapping_popover.stories.tsx b/src/components/popover/wrapping_popover.stories.tsx new file mode 100644 index 000000000000..a827e29b2b80 --- /dev/null +++ b/src/components/popover/wrapping_popover.stories.tsx @@ -0,0 +1,123 @@ +/* + * 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, { useEffect, useState } from 'react'; +import { css } from '@emotion/react'; +import type { Meta, StoryObj } from '@storybook/react'; + +import { + enableFunctionToggleControls, + hideStorybookControls, + moveStorybookControlsToCategory, +} from '../../../.storybook/utils'; +import { EuiButton } from '../button'; +import { EuiFlexGroup } from '../flex'; +import { EuiPopoverProps } from './popover'; +import * as PopoverStories from './popover.stories'; +import { + EuiWrappingPopover, + EuiWrappingPopoverProps, +} from './wrapping_popover'; + +// NOTE: extended EuiPopoverProps are not resolved for some reason +// so we are currently manually adding them back +// TODO: remove this once types are properly resolved and added as control args +const popoverArgs = PopoverStories.default.args ?? {}; +for (const arg of Object.keys(popoverArgs)) { + if (arg === 'button') { + popoverArgs[arg] = undefined; + break; + } +} + +const meta: Meta = { + title: 'Layout/EuiWrappingPopover', + component: EuiWrappingPopover, + parameters: { + layout: 'fullscreen', + }, + args: { + ...(popoverArgs as Omit), + // adding additional args that are not specifically defined in EuiPopover + // NOTE: only adding a subset of extended props based on usefulness + // TODO: remove once props are properly resolved + offset: 16, + buffer: 16, + }, +}; +enableFunctionToggleControls(meta, ['closePopover', 'onPositionChange']); +moveStorybookControlsToCategory( + meta, + [ + ...Object.keys(popoverArgs).map( + (arg) => arg as keyof EuiWrappingPopoverProps + ), + 'offset', + 'buffer', + 'closePopover', + 'onPositionChange', + ], + 'EuiPopover props' +); + +export default meta; +type Story = StoryObj; + +export const Playground: Story = { + args: { + children: 'This is a popover', + }, + render: (args) => , +}; +// hiding isOpen as we need to rely on the connected state toggle +// to ensure the anchor is available before the popover is opened +hideStorybookControls(Playground, ['isOpen']); + +const StatefulPopover = ({ + button, + closePopover, + isOpen: _isOpen, + ...rest +}: EuiWrappingPopoverProps) => { + const [isOpen, setOpen] = useState(_isOpen); + + const handleOnClose = () => { + setOpen(false); + closePopover?.(); + }; + + // playground workaround: ensures the anchor element is available + // before adding the popover with initial isOpen state + useEffect(() => { + setTimeout(() => { + setOpen(true); + }, 100); + }, []); + + return ( + + setOpen(!isOpen)}> + popover trigger + + {isOpen && ( + + )} + + ); +}; diff --git a/src/components/portal/portal.stories.tsx b/src/components/portal/portal.stories.tsx new file mode 100644 index 000000000000..b97b6bd1c4ad --- /dev/null +++ b/src/components/portal/portal.stories.tsx @@ -0,0 +1,48 @@ +/* + * 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, { useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; + +import { EuiButton } from '../button'; +import { EuiSpacer } from '../spacer'; +import { EuiPortal, EuiPortalProps } from './portal'; + +const meta: Meta = { + title: 'Utilities/EuiPortal', + component: EuiPortal, + argTypes: { + children: { control: 'text' }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Playground: Story = { + args: { + children: 'This element is appended to the body in the DOM if you inspect', + }, + render: (args) => , +}; + +const StatefulPlayground = (args: EuiPortalProps) => { + const [isActive, setActive] = useState(true); + + return ( + <> + setActive(!isActive)}>Toggle portal + {isActive && ( + <> + + + + )} + + ); +}; diff --git a/src/components/progress/progress.stories.tsx b/src/components/progress/progress.stories.tsx new file mode 100644 index 000000000000..2a2d77fd03b5 --- /dev/null +++ b/src/components/progress/progress.stories.tsx @@ -0,0 +1,50 @@ +/* + * 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 { hideStorybookControls } from '../../../.storybook/utils'; +import { EuiProgress, COLORS } from './progress'; + +const meta: Meta = { + title: 'Display/EuiProgress', + component: EuiProgress, + argTypes: { + color: { control: 'select', options: [...COLORS] }, + // for quicker/easier QA + label: { control: 'text' }, + value: { control: 'number' }, + valueText: { control: 'boolean' }, + }, + args: { + color: 'success', + size: 'm', + position: 'static', + valueText: false, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Determinate: Story = { + args: { + label: '', + value: 70, + max: 100, + }, +}; + +export const Indeterminate: Story = {}; +hideStorybookControls(Indeterminate, [ + 'max', + 'value', + 'valueText', + 'label', + 'labelProps', +]);