diff --git a/.claude/rules/emcn-components.md b/.claude/rules/emcn-components.md index af477a7d886..0342cadb8cb 100644 --- a/.claude/rules/emcn-components.md +++ b/.claude/rules/emcn-components.md @@ -24,10 +24,10 @@ The menu surface intentionally diverges from the pill: `dropdown-menu.tsx` items - **`Chip` / `ChipLink`** — the pill button (` + Configure PII Types + setOpen(false)}>Select PII Types to Detect e.stopPropagation()}> diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-setup-wizard/slack-setup-wizard.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-setup-wizard/slack-setup-wizard.tsx index 4ffb44d9739..13dd81199df 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-setup-wizard/slack-setup-wizard.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-setup-wizard/slack-setup-wizard.tsx @@ -1,7 +1,16 @@ 'use client' import { type ReactNode, useCallback, useMemo, useState } from 'react' -import { Checkbox, ChipInput, cn, Label, SecretInput, Tooltip, Wizard } from '@sim/emcn' +import { + Checkbox, + ChipInput, + ChipModalField, + cn, + Label, + SecretInput, + Tooltip, + Wizard, +} from '@sim/emcn' import { Check, ChevronRight, CircleInfo, Clipboard } from '@sim/emcn/icons' import { useShallow } from 'zustand/react/shallow' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value' @@ -213,23 +222,20 @@ function StepConfigure({ }: StepConfigureProps) { return (
-
- - onAppNameChange(e.target.value)} - disabled={disabled} - placeholder={DEFAULT_APP_NAME} - className='h-9' - /> -
-
+ + {(aria) => ( + onAppNameChange(e.target.value)} + disabled={disabled} + placeholder={DEFAULT_APP_NAME} + className='h-9' + {...aria} + /> + )} + +
{GROUP_ORDER.map((group) => { const items = SLACK_CAPABILITIES.filter((c) => c.group === group) if (items.length === 0) return null diff --git a/packages/emcn/src/components/chip-input/chip-input.test.tsx b/packages/emcn/src/components/chip-input/chip-input.test.tsx index acbc4226246..542ce50f165 100644 --- a/packages/emcn/src/components/chip-input/chip-input.test.tsx +++ b/packages/emcn/src/components/chip-input/chip-input.test.tsx @@ -30,6 +30,19 @@ afterEach(() => { }) describe('ChipInput', () => { + it.each([undefined, 'lg'] as const)( + 'emits a single height for size %s without forwarding it to the native field', + (size) => { + const input = mount() + const heights = input.parentElement?.className + .split(' ') + .filter((token) => token.startsWith('h-')) + expect(heights).toEqual([size === 'lg' ? 'h-9' : 'h-[30px]']) + expect(input.hasAttribute('size')).toBe(false) + expect(input.disabled).toBe(true) + } + ) + it('keeps the focused input mounted when custom leading content changes', () => { const input = mount() const render = (color: string) => ( @@ -76,12 +89,13 @@ describe('chip form controls', () => { error aria-invalid aria-describedby='error' - className='h-[34px]' + size='lg' />

Enter a work email

{ expect(input.labels?.[0].textContent).toBe('Work email') expect(input.getAttribute('aria-describedby')).toBe('error') expect(input.getAttribute('aria-invalid')).toBe('true') - expect(input.parentElement?.className).toContain('h-[34px]') + expect(input.parentElement?.className).toContain('h-9') expect(input.parentElement?.className).toContain('border-[var(--text-error)]') expect(textareaRef.current?.rows).toBe(3) + expect(textareaRef.current?.className).toContain('font-mono') expect(textareaRef.current?.className).toContain('min-h-[80px]') act(() => container?.querySelector('button')?.click()) expect(submit).not.toHaveBeenCalled() diff --git a/packages/emcn/src/components/chip-input/chip-input.tsx b/packages/emcn/src/components/chip-input/chip-input.tsx index 444cbb1704a..3c5d7d1f161 100644 --- a/packages/emcn/src/components/chip-input/chip-input.tsx +++ b/packages/emcn/src/components/chip-input/chip-input.tsx @@ -28,11 +28,19 @@ */ import * as React from 'react' import { cn } from '../../lib/cn' -import { chipFieldSurfaceClass, chipFieldTextClass, chipGeometryClass } from '../chip/chip-chrome' +import { + chipContentGeometryClass, + chipFieldSurfaceClass, + chipFieldTextClass, + chipRadiusClass, + chipSizeClasses, +} from '../chip/chip-chrome' type ChipInputIcon = React.ComponentType<{ className?: string }> export interface ChipInputProps extends Omit, 'size'> { + /** Control height: 30px by default, or the larger 36px auth spacing scale. */ + size?: keyof typeof chipSizeClasses /** Leading icon component (e.g. `Search` from `@sim/emcn/icons`). Rendered at 14px in `--text-icon`, with the chip's 1.5 gap. */ icon?: ChipInputIcon /** Custom leading content, such as a color swatch. Takes precedence over `icon`. */ @@ -62,6 +70,7 @@ export const ChipInput = React.forwardRef( error, disabled, type = 'text', + size = 'md', ...props }, ref @@ -69,7 +78,9 @@ export const ChipInput = React.forwardRef(
{ }) }) +describe('ChipModalField composition', () => { + it.each(['vertical', 'horizontal'] as const)( + 'retains labels and feedback in %s fields', + (orientation) => { + mount( + Help} + orientation={orientation} + value='Review' + onChange={() => {}} + required + error='Enter a valid name' + hint='Hidden while invalid' + /> + ) + const input = container!.querySelector('input')! + expect(input.labels?.[0].textContent).toBe('Name*') + expect(input.getAttribute('aria-required')).toBe('true') + expect(input.getAttribute('aria-invalid')).toBe('true') + const error = document.getElementById(input.getAttribute('aria-describedby')!)! + expect(error.textContent).toBe('Enter a valid name') + expect(error.getAttribute('role')).toBe('alert') + expect(container!.textContent).not.toContain('Hidden while invalid') + expect(input.labels?.[0].contains(buttonByText('Help'))).toBe(false) + act(() => input.focus()) + expect(document.activeElement).toBe(input) + } + ) + + it('forwards monospace and height settings to a labeled textarea', () => { + mount( + {}} + mono + minHeight={120} + rows={4} + hint='Enter JSON' + /> + ) + const textarea = container!.querySelector('textarea')! + expect(textarea.labels?.[0].textContent).toBe('JSON') + expect(textarea.className).toContain('font-mono') + expect(textarea.style.minHeight).toBe('120px') + expect(textarea.rows).toBe(4) + expect(document.getElementById(textarea.getAttribute('aria-describedby')!)?.textContent).toBe( + 'Enter JSON' + ) + }) +}) + describe('ChipModalField file actions', () => { it('names each upload action with its field title', () => { mount( @@ -501,7 +556,13 @@ describe('ChipModal default actions', () => { {}} srTitle='Visible action'> {}}>Visible action - {}} /> + {}} + />