import type { Meta, StoryObj } from '@storybook/react' import type React from 'react' import { expect, userEvent, waitFor } from 'storybook/test' import { SvgCog } from '@chainlink/blocks-icons' import { cn } from '../../utils' import { viewports } from '../../utils/viewports' import { Button, ButtonIcon } from '../Button' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from '../DropdownMenu' import { LegacyCombobox, LegacyComboboxContent, LegacyComboboxTrigger, LegacyComboboxValue, OptionRow, } from '../LegacyCombobox' import { MultiSelect } from '../MultiSelect/MultiSelect' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '../Select' import { SidePanel, SidePanelTrigger, SidePanelScrollArea, SidePanelContent, SidePanelTitle, SidePanelFooter, SidePanelHeader, SidePanelCloseMobile, } from './SidePanel' export default { title: 'Blocks/SidePanel', component: SidePanel, parameters: { chromatic: { viewports: viewports.all }, }, } satisfies Meta type Story = StoryObj const PlaceholderContent: React.FC = () => (
{new Array(55).fill(0).map((_workflowId, i) => (
))}
) export const Default: Story = { render: (_args, context) => ( Workflow Arigato Sama ), } export const WithFooter: Story = { render: (_args, context) => ( Workflow Arigato Sama Footer actions ), } export const WideVariant: Story = { render: (_args, context) => ( Wide Workflow Arigato Sama Footer actions ), } export const WithAdditionalRightSideButton: Story = { render: (_args, context) => ( Title on Left ), } export const WithStandAloneMobileCloseButton: Story = { render: (_args, context) => ( ), } const formComponentsOptions = [ { label: 'Option 1', value: 'option1' }, { label: 'Option 2', value: 'option2' }, { label: 'Option 3', value: 'option3' }, ] const comboboxOptions = [ { value: 'arbitrum', label: 'Arbitrum', src: 'https://d2f70xi62kby8n.cloudfront.net/bridge/icons/networks/arbitrum.svg?auto=compress%2Cformat', }, { value: 'avalanche', label: 'Avalanche', src: 'https://d2f70xi62kby8n.cloudfront.net/bridge/icons/networks/avalanche.svg?auto=compress%2Cformat', }, { value: 'base', label: 'Base', src: 'https://d2f70xi62kby8n.cloudfront.net/bridge/icons/networks/base.svg?auto=compress%2Cformat', }, { value: 'bsc', label: 'BNB Chain', src: 'https://d2f70xi62kby8n.cloudfront.net/bridge/icons/networks/bsc.svg?auto=compress%2Cformat', }, { value: 'celo', label: 'Celo', src: 'https://d2f70xi62kby8n.cloudfront.net/bridge/icons/networks/celo.svg?auto=compress%2Cformat', }, { value: 'ethereum', label: 'Ethereum', src: 'https://d2f70xi62kby8n.cloudfront.net/bridge/icons/networks/ethereum.svg?auto=compress%2Cformat', }, { value: 'fantom', label: 'Fantom', src: 'https://d2f70xi62kby8n.cloudfront.net/bridge/icons/networks/fantom.svg?auto=compress%2Cformat', }, { value: 'gnosis', label: 'Gnosis', src: 'https://d2f70xi62kby8n.cloudfront.net/bridge/icons/networks/gnosis.svg?auto=compress%2Cformat', }, { value: 'kroma', label: 'Kroma', src: 'https://d2f70xi62kby8n.cloudfront.net/bridge/icons/networks/kroma.svg?auto=compress%2Cformat', }, { value: 'linea', label: 'Linea', src: 'https://d2f70xi62kby8n.cloudfront.net/bridge/icons/networks/linea.svg?auto=compress%2Cformat', }, { value: 'mantle', label: 'Mantle', src: 'https://d2f70xi62kby8n.cloudfront.net/bridge/icons/networks/mantle.svg?auto=compress%2Cformat', }, { value: 'metis', label: 'Metis', src: 'https://d2f70xi62kby8n.cloudfront.net/bridge/icons/networks/metis.svg?auto=compress%2Cformat', }, { value: 'mode', label: 'Mode', src: 'https://d2f70xi62kby8n.cloudfront.net/bridge/icons/networks/mode.svg?auto=compress%2Cformat', }, { value: 'moonbeam', label: 'Moonbeam', src: 'https://d2f70xi62kby8n.cloudfront.net/bridge/icons/networks/moonbeam.svg?auto=compress%2Cformat', }, { value: 'moonriver', label: 'Moonriver', src: 'https://d2f70xi62kby8n.cloudfront.net/bridge/icons/networks/moonriver.svg?auto=compress%2Cformat', }, { value: 'optimism', label: 'Optimism', src: 'https://d2f70xi62kby8n.cloudfront.net/bridge/icons/networks/optimism.svg?auto=compress%2Cformat', }, { value: 'polygon', label: 'Polygon', src: 'https://d2f70xi62kby8n.cloudfront.net/bridge/icons/networks/polygon.svg?auto=compress%2Cformat', }, { value: 'polygon-zkevm', label: 'Polygon zkEVM', src: 'https://d2f70xi62kby8n.cloudfront.net/bridge/icons/networks/polygon-zkevm.svg?auto=compress%2Cformat', }, { value: 'scroll', label: 'Scroll', src: 'https://d2f70xi62kby8n.cloudfront.net/bridge/icons/networks/scroll.svg?auto=compress%2Cformat', }, { value: 'wemix', label: 'Wemix', src: 'https://d2f70xi62kby8n.cloudfront.net/bridge/icons/networks/wemix.svg?auto=compress%2Cformat', }, { value: 'zksync', label: 'zkSync Era', src: 'https://d2f70xi62kby8n.cloudfront.net/bridge/icons/networks/zksync.svg?auto=compress%2Cformat', }, ] const multiSelectOptions = [ { label: 'React', value: 'react' }, { label: 'Vue', value: 'vue' }, { label: 'Angular', value: 'angular' }, { label: 'Svelte', value: 'svelte' }, ] export const WithFormComponents: Story = { parameters: { // Radix popovers can throw intermittent errors during close animations test: { dangerouslyIgnoreUnhandledErrors: true }, }, render: (_args, context) => { return ( Form Components Demo
{}} />
{comboboxOptions.map((option) => ( {option.label} {option.label} ))}
My Account Profile Billing Settings Logout
) }, play: async ({ canvasElement: _canvasElement }) => { // Wait for SidePanel to be open await waitFor( async () => { const sidePanel = document.querySelector('[role="dialog"]') await expect(sidePanel).toBeInTheDocument() }, { timeout: 3000 }, ) // Find and click the MultiSelect trigger button to open it const multiSelectTrigger = await waitFor( async () => { const triggers = Array.from( document.querySelectorAll('[role="combobox"]'), ) as HTMLElement[] // Find the MultiSelect trigger by checking if it has the placeholder text const trigger = triggers.find((t) => t.getAttribute('aria-label')?.includes('Select frameworks'), ) if (!trigger) { throw new Error('MultiSelect trigger not found') } return trigger }, { timeout: 3000 }, ) // Click to open the MultiSelect await userEvent.click(multiSelectTrigger) // Wait for MultiSelect popover content to be visible const multiSelectContent = await waitFor( async () => { const content = document.querySelector('[role="listbox"]') if (!content) { throw new Error('MultiSelect content not found') } return content as HTMLElement }, { timeout: 3000 }, ) // Find and click on 'React' option by text content const reactOption = await waitFor( async () => { const options = Array.from( multiSelectContent.querySelectorAll('[role="option"]'), ) as HTMLElement[] const option = options.find((opt) => opt.textContent?.includes('React')) if (!option) { throw new Error('React option not found') } return option }, { timeout: 2000 }, ) await userEvent.click(reactOption) // Wait a bit for the selection to be processed await waitFor( async () => { // Verify React is selected by checking if it has the selected state const isSelected = reactOption.getAttribute('aria-selected') === 'true' await expect(isSelected).toBe(true) }, { timeout: 1000 }, ) // Find and click on 'Vue' option by text content const vueOption = await waitFor( async () => { const options = Array.from( multiSelectContent.querySelectorAll('[role="option"]'), ) as HTMLElement[] const option = options.find((opt) => opt.textContent?.includes('Vue')) if (!option) { throw new Error('Vue option not found') } return option }, { timeout: 2000 }, ) await userEvent.click(vueOption) // Wait a bit for the selection to be processed await waitFor( async () => { // Verify Vue is selected const isSelected = vueOption.getAttribute('aria-selected') === 'true' await expect(isSelected).toBe(true) }, { timeout: 1000 }, ) // Find the search input const searchInput = await waitFor( async () => { const input = multiSelectContent.querySelector( 'input[aria-label="Search through available options"]', ) as HTMLInputElement if (!input) { throw new Error('Search input not found') } return input }, { timeout: 2000 }, ) // Click on the search input await userEvent.click(searchInput) // Type 'Svelte' in the search input await userEvent.type(searchInput, 'Svelte', { delay: 50 }) // Wait for the search results to update await waitFor( async () => { // Verify 'Svelte' option is visible in the filtered results const options = Array.from( multiSelectContent.querySelectorAll('[role="option"]'), ) as HTMLElement[] const svelteOption = options.find((opt) => opt.textContent?.includes('Svelte'), ) await expect(svelteOption).toBeInTheDocument() // Verify the option text contains 'Svelte' if (svelteOption) { await expect(svelteOption.textContent).toContain('Svelte') } }, { timeout: 2000 }, ) // Click outside the MultiSelect to close it // Find the SidePanel title to click outside const sidePanelTitle = await waitFor( async () => { const title = document.querySelector('[data-slot="sheet-title"]') if (!title) { throw new Error('SidePanel title not found') } return title as HTMLElement }, { timeout: 2000 }, ) await userEvent.click(sidePanelTitle) // Wait for the MultiSelect to close await waitFor( async () => { const content = document.querySelector('[role="listbox"]') // The MultiSelect should be closed, so content should not be visible await expect(content).not.toBeInTheDocument() }, { timeout: 2000 }, ) // Test LegacyCombobox // Find and click the LegacyCombobox trigger to open it const comboboxTrigger = await waitFor( async () => { const trigger = document.querySelector( '[data-slot="combobox-trigger"]', ) as HTMLElement if (!trigger) { throw new Error('LegacyCombobox trigger not found') } return trigger }, { timeout: 3000 }, ) // Click to open the LegacyCombobox await userEvent.click(comboboxTrigger) // Wait for the search input to be visible (indicates LegacyCombobox is open) const comboboxSearchInput = await waitFor( async () => { // Wait a bit for the popover to open await new Promise((resolve) => setTimeout(resolve, 100)) const input = document.querySelector( 'input[placeholder="Search..."]', ) as HTMLInputElement if (!input) { throw new Error('LegacyCombobox search input not found') } return input }, { timeout: 3000 }, ) // Click on the search input to focus it await userEvent.click(comboboxSearchInput) // Type 'Arb' in the search input await userEvent.type(comboboxSearchInput, 'Arb', { delay: 50 }) // Wait for the search results to update and find the first option const firstOption = await waitFor( async () => { // Wait a bit for the search to filter await new Promise((resolve) => setTimeout(resolve, 100)) // Find options using cmdk-item attribute const options = Array.from( document.querySelectorAll('[cmdk-item]'), ) as HTMLElement[] if (options.length === 0) { throw new Error('No LegacyCombobox options found after search') } // Get the first option (should be Arbitrum after filtering) const option = options[0] await expect(option).toBeInTheDocument() return option }, { timeout: 3000 }, ) // Click on the first option await userEvent.click(firstOption) // Wait for the combobox to close after selecting the option await waitFor( async () => { const content = document.querySelector('[data-radix-popover-content]') // The combobox should be closed, so content should not be visible await expect(content).not.toBeInTheDocument() }, { timeout: 2000 }, ) }, }