Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
'use client'

import { useCallback, useMemo } from 'react'
import { Button } from '@sim/emcn'
import { Plus } from '@sim/emcn/icons'
import { AddRowButton } from '@sim/emcn'
import { useTableColumns } from '@/lib/table/hooks'
import type { FilterRule } from '@/lib/table/query-builder/constants'
import { useFilterBuilder } from '@/lib/table/query-builder/use-query-builder'
Expand Down Expand Up @@ -83,16 +82,7 @@ export function FilterBuilder({

if (rules.length === 0) {
if (isReadOnly) return null
return (
<Button
variant='ghost'
onClick={addRule}
className='h-7 w-full justify-start gap-1.5 border border-[var(--border-1)] border-dashed text-[var(--text-muted)] text-small'
>
<Plus className='size-[14px]' />
Add filter condition
</Button>
)
return <AddRowButton onClick={addRule}>Add filter condition</AddRowButton>
}

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
'use client'

import { useCallback, useMemo } from 'react'
import { Button, type ComboboxOption } from '@sim/emcn'
import { Plus } from '@sim/emcn/icons'
import { AddRowButton, type ComboboxOption } from '@sim/emcn'
import { generateId } from '@sim/utils/id'
import { useTableColumns } from '@/lib/table/hooks'
import { SORT_DIRECTION_OPTIONS, type SortRule } from '@/lib/table/query-builder/constants'
Expand Down Expand Up @@ -86,16 +85,7 @@ export function SortBuilder({

if (rules.length === 0) {
if (isReadOnly) return null
return (
<Button
variant='ghost'
onClick={addRule}
className='h-7 w-full justify-start gap-1.5 border border-[var(--border-1)] border-dashed text-[var(--text-muted)] text-small'
>
<Plus className='size-[14px]' />
Add sort
</Button>
)
return <AddRowButton onClick={addRule}>Add sort</AddRowButton>
}

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/** @vitest-environment jsdom */
import { act, createRef } from 'react'
import { AddRowButton } from '@sim/emcn'
import { createRoot, type Root } from 'react-dom/client'
import { afterEach, describe, expect, it, vi } from 'vitest'

let root: Root | null = null
let container: HTMLDivElement | null = null

afterEach(() => {
if (root) act(() => root?.unmount())
container?.remove()
root = null
container = null
})

describe('AddRowButton', () => {
it('forwards its ref and action, honors disabled, and preserves native form submission', () => {
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
container = document.createElement('div')
document.body.appendChild(container)
root = createRoot(container)
const ref = createRef<HTMLButtonElement>()
const onClick = vi.fn()
const onSubmit = vi.fn((event) => event.preventDefault())
const render = (disabled: boolean) => (
<form onSubmit={onSubmit}>
<AddRowButton ref={ref} onClick={onClick} disabled={disabled}>
Add filter condition
</AddRowButton>
</form>
)
act(() => root?.render(render(false)))
const button = ref.current!
expect(button).toBe(container.querySelector('button'))
expect(button.textContent).toBe('Add filter condition')
expect(button.querySelector('svg')?.getAttribute('aria-hidden')).toBe('true')
act(() => button.click())
expect(onClick).toHaveBeenCalledOnce()
expect(onSubmit).toHaveBeenCalledOnce()

act(() => root?.render(render(true)))
expect(button.disabled).toBe(true)
act(() => button.click())
expect(onClick).toHaveBeenCalledOnce()
})
})
32 changes: 32 additions & 0 deletions packages/emcn/src/components/add-row-button/add-row-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { type ComponentPropsWithoutRef, forwardRef } from 'react'
import { Plus } from '../../icons/plus'
import { cn } from '../../lib/cn'
import { Button } from '../button/button'

export type AddRowButtonProps = ComponentPropsWithoutRef<'button'>

/**
* Full-width dashed action for adding the first row to an empty editor list.
* Keeps the existing 28px filter/sort treatment; list state stays with the caller.
* Forwards native button props and refs, preserving Button's native form behavior.
*
* @example <AddRowButton onClick={addRule}>Add filter condition</AddRowButton>
*/
export const AddRowButton = forwardRef<HTMLButtonElement, AddRowButtonProps>(
({ children, className, ...props }, ref) => (
<Button
{...props}
ref={ref}
variant='ghost'
className={cn(
'h-7 w-full justify-start gap-1.5 border border-[var(--border-1)] border-dashed text-[var(--text-muted)] text-small',
className
)}
>
<Plus className='size-[14px]' />
{children}
</Button>
)
)

AddRowButton.displayName = 'AddRowButton'
1 change: 1 addition & 0 deletions packages/emcn/src/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { AddRowButton, type AddRowButtonProps } from './add-row-button/add-row-button'
export { Avatar, AvatarFallback, AvatarImage } from './avatar/avatar'
export { Badge, type BadgeProps } from './badge/badge'
export { Banner } from './banner/banner'
Expand Down
Loading