dify
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
import React from 'react'
|
||||
import { cleanup, fireEvent, render } from '@testing-library/react'
|
||||
import InlineDeleteConfirm from './index'
|
||||
|
||||
// Mock react-i18next
|
||||
jest.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => {
|
||||
const translations: Record<string, string> = {
|
||||
'common.operation.deleteConfirmTitle': 'Delete?',
|
||||
'common.operation.yes': 'Yes',
|
||||
'common.operation.no': 'No',
|
||||
'common.operation.confirmAction': 'Please confirm your action.',
|
||||
}
|
||||
return translations[key] || key
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
describe('InlineDeleteConfirm', () => {
|
||||
describe('Rendering', () => {
|
||||
test('should render with default text', () => {
|
||||
const onConfirm = jest.fn()
|
||||
const onCancel = jest.fn()
|
||||
const { getByText } = render(
|
||||
<InlineDeleteConfirm onConfirm={onConfirm} onCancel={onCancel} />,
|
||||
)
|
||||
|
||||
expect(getByText('Delete?')).toBeInTheDocument()
|
||||
expect(getByText('No')).toBeInTheDocument()
|
||||
expect(getByText('Yes')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
test('should render with custom text', () => {
|
||||
const onConfirm = jest.fn()
|
||||
const onCancel = jest.fn()
|
||||
const { getByText } = render(
|
||||
<InlineDeleteConfirm
|
||||
title="Remove?"
|
||||
confirmText="Confirm"
|
||||
cancelText="Cancel"
|
||||
onConfirm={onConfirm}
|
||||
onCancel={onCancel}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(getByText('Remove?')).toBeInTheDocument()
|
||||
expect(getByText('Cancel')).toBeInTheDocument()
|
||||
expect(getByText('Confirm')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
test('should have proper ARIA attributes', () => {
|
||||
const onConfirm = jest.fn()
|
||||
const onCancel = jest.fn()
|
||||
const { container } = render(
|
||||
<InlineDeleteConfirm onConfirm={onConfirm} onCancel={onCancel} />,
|
||||
)
|
||||
|
||||
const wrapper = container.firstChild as HTMLElement
|
||||
expect(wrapper).toHaveAttribute('aria-labelledby', 'inline-delete-confirm-title')
|
||||
expect(wrapper).toHaveAttribute('aria-describedby', 'inline-delete-confirm-description')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Button interactions', () => {
|
||||
test('should call onCancel when cancel button is clicked', () => {
|
||||
const onConfirm = jest.fn()
|
||||
const onCancel = jest.fn()
|
||||
const { getByText } = render(
|
||||
<InlineDeleteConfirm onConfirm={onConfirm} onCancel={onCancel} />,
|
||||
)
|
||||
|
||||
fireEvent.click(getByText('No'))
|
||||
expect(onCancel).toHaveBeenCalledTimes(1)
|
||||
expect(onConfirm).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('should call onConfirm when confirm button is clicked', () => {
|
||||
const onConfirm = jest.fn()
|
||||
const onCancel = jest.fn()
|
||||
const { getByText } = render(
|
||||
<InlineDeleteConfirm onConfirm={onConfirm} onCancel={onCancel} />,
|
||||
)
|
||||
|
||||
fireEvent.click(getByText('Yes'))
|
||||
expect(onConfirm).toHaveBeenCalledTimes(1)
|
||||
expect(onCancel).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Variant prop', () => {
|
||||
test('should render with delete variant by default', () => {
|
||||
const onConfirm = jest.fn()
|
||||
const onCancel = jest.fn()
|
||||
const { getByText } = render(
|
||||
<InlineDeleteConfirm onConfirm={onConfirm} onCancel={onCancel} />,
|
||||
)
|
||||
|
||||
const confirmButton = getByText('Yes').closest('button')
|
||||
expect(confirmButton?.className).toContain('btn-destructive')
|
||||
})
|
||||
|
||||
test('should render without destructive class for warning variant', () => {
|
||||
const onConfirm = jest.fn()
|
||||
const onCancel = jest.fn()
|
||||
const { getByText } = render(
|
||||
<InlineDeleteConfirm
|
||||
variant="warning"
|
||||
onConfirm={onConfirm}
|
||||
onCancel={onCancel}
|
||||
/>,
|
||||
)
|
||||
|
||||
const confirmButton = getByText('Yes').closest('button')
|
||||
expect(confirmButton?.className).not.toContain('btn-destructive')
|
||||
})
|
||||
|
||||
test('should render without destructive class for info variant', () => {
|
||||
const onConfirm = jest.fn()
|
||||
const onCancel = jest.fn()
|
||||
const { getByText } = render(
|
||||
<InlineDeleteConfirm
|
||||
variant="info"
|
||||
onConfirm={onConfirm}
|
||||
onCancel={onCancel}
|
||||
/>,
|
||||
)
|
||||
|
||||
const confirmButton = getByText('Yes').closest('button')
|
||||
expect(confirmButton?.className).not.toContain('btn-destructive')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Custom className', () => {
|
||||
test('should apply custom className to wrapper', () => {
|
||||
const onConfirm = jest.fn()
|
||||
const onCancel = jest.fn()
|
||||
const { container } = render(
|
||||
<InlineDeleteConfirm
|
||||
className="custom-class"
|
||||
onConfirm={onConfirm}
|
||||
onCancel={onCancel}
|
||||
/>,
|
||||
)
|
||||
|
||||
const wrapper = container.firstChild as HTMLElement
|
||||
expect(wrapper.className).toContain('custom-class')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,87 @@
|
||||
import type { Meta, StoryObj } from '@storybook/nextjs'
|
||||
import { fn } from 'storybook/test'
|
||||
import { useState } from 'react'
|
||||
import InlineDeleteConfirm from '.'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/Feedback/InlineDeleteConfirm',
|
||||
component: InlineDeleteConfirm,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Compact confirmation prompt that appears inline, commonly used near delete buttons or destructive controls.',
|
||||
},
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
variant: {
|
||||
control: 'select',
|
||||
options: ['delete', 'warning', 'info'],
|
||||
},
|
||||
},
|
||||
args: {
|
||||
title: 'Delete this item?',
|
||||
confirmText: 'Delete',
|
||||
cancelText: 'Cancel',
|
||||
onConfirm: fn(),
|
||||
onCancel: fn(),
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
} satisfies Meta<typeof InlineDeleteConfirm>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
const InlineDeleteConfirmDemo = (args: Story['args']) => {
|
||||
const [visible, setVisible] = useState(true)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-start gap-3">
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-md border border-divider-subtle px-3 py-1.5 text-xs font-medium text-text-secondary hover:bg-state-base-hover"
|
||||
onClick={() => setVisible(true)}
|
||||
>
|
||||
Trigger inline confirm
|
||||
</button>
|
||||
{visible && (
|
||||
<InlineDeleteConfirm
|
||||
{...args}
|
||||
onConfirm={() => {
|
||||
console.log('✅ Confirm clicked')
|
||||
setVisible(false)
|
||||
}}
|
||||
onCancel={() => {
|
||||
console.log('❎ Cancel clicked')
|
||||
setVisible(false)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const Playground: Story = {
|
||||
render: args => <InlineDeleteConfirmDemo {...args} />,
|
||||
}
|
||||
|
||||
export const WarningVariant: Story = {
|
||||
render: args => <InlineDeleteConfirmDemo {...args} />,
|
||||
args: {
|
||||
variant: 'warning',
|
||||
title: 'Archive conversation?',
|
||||
confirmText: 'Archive',
|
||||
cancelText: 'Keep',
|
||||
},
|
||||
}
|
||||
|
||||
export const InfoVariant: Story = {
|
||||
render: args => <InlineDeleteConfirmDemo {...args} />,
|
||||
args: {
|
||||
variant: 'info',
|
||||
title: 'Remove collaborator?',
|
||||
confirmText: 'Remove',
|
||||
cancelText: 'Keep',
|
||||
},
|
||||
}
|
||||
83
dify/web/app/components/base/inline-delete-confirm/index.tsx
Normal file
83
dify/web/app/components/base/inline-delete-confirm/index.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
export type InlineDeleteConfirmProps = {
|
||||
title?: string
|
||||
confirmText?: string
|
||||
cancelText?: string
|
||||
onConfirm: () => void
|
||||
onCancel: () => void
|
||||
className?: string
|
||||
variant?: 'delete' | 'warning' | 'info'
|
||||
}
|
||||
|
||||
const InlineDeleteConfirm: FC<InlineDeleteConfirmProps> = ({
|
||||
title,
|
||||
confirmText,
|
||||
cancelText,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
className,
|
||||
variant = 'delete',
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const titleText = title || t('common.operation.deleteConfirmTitle', 'Delete?')
|
||||
const confirmTxt = confirmText || t('common.operation.yes', 'Yes')
|
||||
const cancelTxt = cancelText || t('common.operation.no', 'No')
|
||||
|
||||
return (
|
||||
<div
|
||||
aria-labelledby="inline-delete-confirm-title"
|
||||
aria-describedby="inline-delete-confirm-description"
|
||||
className={cn(
|
||||
'flex w-[120px] flex-col justify-center gap-1.5',
|
||||
'rounded-[10px] border-[0.5px] border-components-panel-border-subtle',
|
||||
'bg-components-panel-bg-blur px-2 pb-2 pt-1.5',
|
||||
'backdrop-blur-[10px]',
|
||||
'shadow-lg',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div
|
||||
id="inline-delete-confirm-title"
|
||||
className="system-xs-semibold text-text-primary"
|
||||
>
|
||||
{titleText}
|
||||
</div>
|
||||
|
||||
<div className="flex w-full items-center justify-center gap-1">
|
||||
<Button
|
||||
size="small"
|
||||
variant="secondary"
|
||||
onClick={onCancel}
|
||||
aria-label={cancelTxt}
|
||||
className="flex-1"
|
||||
>
|
||||
{cancelTxt}
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
variant="primary"
|
||||
destructive={variant === 'delete'}
|
||||
onClick={onConfirm}
|
||||
aria-label={confirmTxt}
|
||||
className="flex-1"
|
||||
>
|
||||
{confirmTxt}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<span id="inline-delete-confirm-description" className="sr-only">
|
||||
{t('common.operation.confirmAction', 'Please confirm your action.')}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
InlineDeleteConfirm.displayName = 'InlineDeleteConfirm'
|
||||
|
||||
export default InlineDeleteConfirm
|
||||
Reference in New Issue
Block a user