import React, { useRef, useState } from 'react'; import { faker } from '@faker-js/faker'; import { storybookExcludedControlParams, StoryMetaType, } from '@lg-tools/storybook-utils'; import { StoryFn, StoryObj } from '@storybook/react'; import { Button } from '@leafygreen-ui/button'; import { css, cx } from '@leafygreen-ui/emotion'; import { useEventListener } from '@leafygreen-ui/hooks'; import { palette } from '@leafygreen-ui/palette'; import { color, spacing } from '@leafygreen-ui/tokens'; import { Body, InlineCode } from '@leafygreen-ui/typography'; import { getPopoverRenderModeProps } from './utils/getPopoverRenderModeProps'; import { Align, DismissMode, Justify, Popover, PopoverProps, RenderMode, ToggleEvent, } from './Popover'; const SEED = 0; faker.seed(SEED); const popoverStyles = css` border: 1px solid ${palette.gray.light1}; text-align: center; padding: ${spacing[300]}px; overflow: hidden; // Reset these properties since they'll be inherited // from the container element when not using a portal. font-size: initial; color: initial; background-color: initial; `; const containerStyles = css` position: relative; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; `; const scrollableOuterStyles = css` width: 500px; height: 90vh; background-color: ${palette.gray.light2}; overflow: scroll; position: relative; `; const scrollableInnerStyles = css` position: relative; height: 160vh; width: 80vw; display: flex; align-items: center; justify-content: center; `; const defaultExcludedControls = [ ...storybookExcludedControlParams, 'active', 'children', 'portalClassName', 'refButtonPosition', 'refEl', ]; const meta: StoryMetaType = { title: 'Composition/Overlays/Popover', component: Popover, parameters: { default: 'LiveExample', controls: { exclude: defaultExcludedControls, }, generate: { storyNames: [ 'Top', 'Right', 'Bottom', 'Left', 'CenterHorizontal', 'CenterVertical', ], combineArgs: { justify: Object.values(Justify), }, args: { active: true, children:
Popover content
, }, decorator: Instance => { return (
); }, }, }, args: { adjustOnMutation: false, align: Align.Top, buttonText: 'Button Text', dismissMode: DismissMode.Auto, justify: Justify.Start, renderMode: RenderMode.TopLayer, spacing: 4, }, argTypes: { align: { options: Object.values(Align), control: { type: 'radio' }, }, buttonText: { type: 'string', description: 'Storybook only prop. Used to change the reference button text', }, dismissMode: { options: Object.values(DismissMode), control: { type: 'radio' }, }, justify: { options: Object.values(Justify), control: { type: 'radio' }, }, renderMode: { options: Object.values(RenderMode), control: { type: 'radio' }, }, }, }; export default meta; type PopoverStoryProps = PopoverProps & { buttonText: string; }; export const LiveExample: StoryFn = ({ buttonText, ...props }: PopoverStoryProps) => { const { portalClassName, portalContainer, portalRef, scrollContainer, dismissMode, renderMode = RenderMode.TopLayer, onToggle, ...rest } = props; const buttonRef = useRef(null); const [active, setActive] = useState(false); const handleClick = (e: React.MouseEvent) => { // eslint-disable-next-line no-console console.log('handleClick', e); setActive(active => !active); }; const handleToggle = (e: ToggleEvent) => { // eslint-disable-next-line no-console console.log('handleToggle', e); onToggle?.(e); const newActive = e.newState === 'open'; setActive(newActive); }; const popoverProps = { active, refEl: buttonRef, ...getPopoverRenderModeProps({ dismissMode, onToggle: handleToggle, portalClassName, portalContainer, portalRef, renderMode, scrollContainer, }), ...rest, }; return (
Popover content
); }; LiveExample.parameters = { chromatic: { disableSnapshot: true, }, }; const PortalPopoverInScrollableContainer = ({ buttonText, ...props }: PopoverStoryProps) => { const { dismissMode, onToggle, renderMode, ...rest } = props; const [active, setActive] = useState(false); const portalRef = useRef(null); const scrollContainer = useRef(null); return (
); }; export const RenderModePortalInScrollableContainer = { render: PortalPopoverInScrollableContainer, parameters: { chromatic: { disableSnapshot: true, }, controls: { exclude: [...defaultExcludedControls, 'dismissMode', 'renderMode'], }, }, argTypes: { renderMode: { control: 'none' }, portalClassName: { control: 'none' }, refEl: { control: 'none' }, className: { control: 'none' }, active: { control: 'none' }, }, }; const InlinePopover = ({ buttonText, ...props }: PopoverStoryProps) => { const { dismissMode, onToggle, renderMode, portalClassName, portalContainer, portalRef, scrollContainer, ...rest } = props; const buttonRef = useRef(null); const [active, setActive] = useState(false); return (
Popover content
); }; export const RenderModeInline = { render: InlinePopover, parameters: { chromatic: { disableSnapshot: true, }, controls: { exclude: [...defaultExcludedControls, 'dismissMode', 'renderMode'], }, }, argTypes: { renderMode: { control: 'none' }, portalClassName: { control: 'none' }, refEl: { control: 'none' }, className: { control: 'none' }, active: { control: 'none' }, }, }; const generatedStoryExcludedControlParams = [ ...storybookExcludedControlParams, 'active', 'adjustOnMutation', 'align', 'buttonText', 'children', 'dismissMode', 'justify', 'portalClassName', 'refButtonPosition', 'refEl', 'renderMode', 'spacing', 'usePortal', ]; export const Top = { render: () => <>, args: { align: Align.Top, }, parameters: { controls: { exclude: generatedStoryExcludedControlParams, }, }, }; export const Bottom = { render: () => <>, args: { align: Align.Bottom, }, parameters: { controls: { exclude: generatedStoryExcludedControlParams, }, }, }; export const Left = { render: () => <>, args: { align: Align.Left, }, parameters: { controls: { exclude: generatedStoryExcludedControlParams, }, }, }; export const Right = { render: () => <>, args: { align: Align.Right, }, parameters: { controls: { exclude: generatedStoryExcludedControlParams, }, }, }; export const CenterHorizontal = { render: () => <>, args: { align: Align.CenterHorizontal, }, parameters: { controls: { exclude: generatedStoryExcludedControlParams, }, }, }; export const CenterVertical: StoryObj = { render: () => <>, args: { align: Align.CenterVertical, }, parameters: { controls: { exclude: generatedStoryExcludedControlParams, }, }, }; const paragraphs: Array = faker.lorem.paragraphs(12).split('\n'); export const MaxHeight: StoryObj = { render: () => { // eslint-disable-next-line react-hooks/rules-of-hooks const trigger1Ref = useRef(null); // eslint-disable-next-line react-hooks/rules-of-hooks const trigger2Ref = useRef(null); const MAX_HEIGHT = 512; const MAX_WIDTH = 350; return (
maxHeight: undefined (default) maxHeight: {MAX_HEIGHT}px
By default, a popover takes up the maximum amount of vertical space {paragraphs.map((p, i) => ( {p} ))} The height can be further restricted with the{' '} maxHeight prop {paragraphs.map((p, i) => ( {p} ))}
); }, }; export const MaxWidth: StoryObj = { render: () => { // eslint-disable-next-line react-hooks/rules-of-hooks const trigger1Ref = useRef(null); // eslint-disable-next-line react-hooks/rules-of-hooks const trigger2Ref = useRef(null); const MAX_HEIGHT = 128; const MAX_WIDTH = 350; return (
maxWidth: undefined (default) By default, a popover takes up the maximum amount of horizontal space {paragraphs.map((p, i) => ( {p} ))}
maxWidth: {MAX_WIDTH}px The width can be further restricted with the{' '} maxWidth prop {paragraphs.map((p, i) => ( {p} ))}
); }, }; export const MovingPopover: StoryObj = { render: () => { // eslint-disable-next-line react-hooks/rules-of-hooks const triggerRef = useRef(null); // eslint-disable-next-line react-hooks/rules-of-hooks const [position, setPosition] = useState({ top: 0, left: 0 }); // eslint-disable-next-line react-hooks/rules-of-hooks useEventListener( 'click', event => { const { clientX, clientY } = event; const position = { top: clientY, left: clientX }; // @ts-ignore setPosition(position); }, { dependencies: [setPosition], }, ); return ( <>
Popover with refEl position{' '} {JSON.stringify(position, null, 2)} ); }, play: async () => { // We'll simulate a click at coordinates {left: 200, top: 180} await new Promise(r => setTimeout(r, 200)); // Let the story render const clickX = 200; const clickY = 180; // Simulate click event on the document at (clickX, clickY) // Use the 'pointerEvents' API if available, otherwise dispatch manually const event = new MouseEvent('click', { clientX: clickX, clientY: clickY, bubbles: true, cancelable: true, view: window, }); document.dispatchEvent(event); }, };