import type { Meta, StoryObj } from '@storybook/react' import { expect } from 'storybook/test' import { SvgAdd1 } from '@chainlink/blocks-icons' import { colors } from '../../theme/colors/colors' import { Button } from '../Button' import { DROPDOWN_SIZE_OPTIONS } from '../dropdownHeights' import { typographyVariants } from '../Typography' import { Select, SelectContent, SelectGroup, SelectIcon, SelectItem, SelectLabel, SelectSeparator, SelectTrigger, SelectValue, } from './Select' const meta: Meta = { component: Select, title: 'Form/Select/Primitive', subcomponents: { SelectTrigger, SelectValue, SelectIcon, SelectContent, SelectItem, SelectGroup, SelectLabel, SelectSeparator, }, argTypes: { size: { control: 'select', options: DROPDOWN_SIZE_OPTIONS, }, clearable: { control: 'boolean', table: { type: { summary: 'boolean' }, defaultValue: { summary: 'true' }, }, }, onValueChange: { control: false }, }, args: { size: 'default', clearable: true, }, } export default meta type Story = StoryObj export const Default: Story = { render: (_args) => ( ), } export const SmallSize: Story = { render: (_args) => ( ), } export const XsSize: Story = { render: (_args) => ( ), } export const OpenState: Story = { render: (_args, context) => { const openProp = context.viewMode === 'story' ? { defaultOpen: true } : {} return ( ) }, play: async ({ canvasElement }) => { // Find the select trigger const trigger = canvasElement.querySelector( '[data-slot="select-trigger"]', ) as HTMLElement await expect(trigger).toBeTruthy() // Find the caret SVG const caret = trigger.querySelector( 'svg[data-slot="select-caret"]', ) as SVGSVGElement await expect(caret).toBeTruthy() // Check the caret color - it should use input-muted which maps to text-input-muted-foreground // The SVG uses currentColor, so we check the computed color // input-muted-foreground maps to gray-400 which is #9fa7b2 const computedStyle = window.getComputedStyle(caret) const caretColor = computedStyle.color // Convert hex color to RGB values: #9fa7b2 -> (159, 167, 178) const hexColor = colors.gray[400].replace('#', '') const expectedR = parseInt(hexColor.substring(0, 2), 16) const expectedG = parseInt(hexColor.substring(2, 4), 16) const expectedB = parseInt(hexColor.substring(4, 6), 16) // Extract RGB values from computed color using a temporary canvas // This handles any color format the browser returns (rgb(), color(srgb ...), etc.) const canvas = document.createElement('canvas') const ctx = canvas.getContext('2d') if (!ctx) { throw new Error('Could not get canvas context') } ctx.fillStyle = caretColor ctx.fillRect(0, 0, 1, 1) const imageData = ctx.getImageData(0, 0, 1, 1) const [actualR, actualG, actualB] = imageData.data // Verify the caret color matches the expected input-muted-foreground color await expect(actualR).toBe(expectedR) await expect(actualG).toBe(expectedG) await expect(actualB).toBe(expectedB) // Check if the caret is rotated when select is open // The select is open in story mode (defaultOpen: true), so the caret should be rotated 180deg const transform = computedStyle.transform await expect(transform).toBeTruthy() // When rotated 180deg, the transform should not be 'none' // The transform will be a matrix or rotate value when rotated await expect(transform).not.toBe('none') // Verify the trigger is in open state const triggerState = trigger.getAttribute('data-state') await expect(triggerState).toBe('open') // Verify rotation is applied (transform should contain rotation) // A 180deg rotation will have a matrix like matrix(..., -1, ..., 1, ...) or rotate(180deg) await expect( transform.includes('rotate') || transform.includes('matrix'), ).toBe(true) }, } export const EmptyStates: Story = { render: (_args, context) => { const openProp = context.viewMode === 'story' ? { defaultOpen: true } : {} return (
) }, parameters: { docs: { description: { story: 'Use an empty state inside SelectContent when the option list can be empty after filtering or loading.', }, }, }, } export const ItemsWithIcons: Story = { render: (_args, context) => { const openProp = context.viewMode === 'story' ? { defaultOpen: true } : {} return ( ) }, } export const Clearable: Story = { render: (_args, context) => { const openProp = context.viewMode === 'story' ? { defaultOpen: true } : {} return ( ) }, } export const NotClearable: Story = { render: (_args, context) => { const openProp = context.viewMode === 'story' ? { defaultOpen: true } : {} return ( ) }, play: async ({ canvasElement }) => { const trigger = canvasElement.querySelector( '[data-slot="select-trigger"]', ) as HTMLElement await expect(trigger.querySelector('[data-slot="select-clear"]')).toBeNull() }, } export const ContentWiderThanTrigger: Story = { render: (_args, context) => { const openProp = context.viewMode === 'story' ? { defaultOpen: true } : {} return ( ) }, parameters: { docs: { description: { story: 'Use a wider content panel when option labels or addresses need more room than the trigger.', }, }, }, play: async ({ canvasElement }) => { const trigger = canvasElement.querySelector( '[data-slot="select-trigger"]', ) as HTMLElement await expect(trigger).toBeTruthy() const content = document.querySelector( '[data-slot="select-content"]', ) as HTMLElement await expect(content).toBeTruthy() const triggerWidth = trigger.getBoundingClientRect().width const contentWidth = content.getBoundingClientRect().width await expect(contentWidth).toBeGreaterThan(triggerWidth) }, } export const ButtonCustomTrigger: Story = { render: (_args, context) => { const openProp = context.viewMode === 'story' ? { defaultOpen: true } : {} return ( ) }, parameters: { docs: { description: { story: 'Use asChild on SelectTrigger when the trigger should look like another Blocks component.', }, }, }, } export const TextCustomTrigger: Story = { render: (_args, context) => { const openProp = context.viewMode === 'story' ? { defaultOpen: true } : {} return ( ) }, parameters: { docs: { description: { story: 'Use a custom trigger when the selected value must appear inside non-standard text or heading UI.', }, }, }, }