feat: add InlineDeleteConfirm base component (#26762)

This commit is contained in:
lyzno1 2025-10-11 16:33:31 +08:00 committed by GitHub
parent 456dbfe7d7
commit bd5df5cf1c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 254 additions and 0 deletions

View File

@ -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')
})
})
})

View File

@ -0,0 +1,90 @@
'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 h-16 w-[120px] flex-col',
'rounded-xl border-0 border-t-[0.5px] border-components-panel-border',
'bg-background-overlay-backdrop backdrop-blur-[10px]',
'shadow-lg',
'p-0 pt-1',
className,
)}
>
<div className={cn(
'flex h-[60px] w-full 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]',
)}>
<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>
</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

View File

@ -18,6 +18,10 @@ const translation = {
cancel: 'Cancel',
clear: 'Clear',
save: 'Save',
yes: 'Yes',
no: 'No',
deleteConfirmTitle: 'Delete?',
confirmAction: 'Please confirm your action.',
saveAndEnable: 'Save & Enable',
edit: 'Edit',
add: 'Add',

View File

@ -67,6 +67,10 @@ const translation = {
selectAll: 'すべて選択',
deSelectAll: 'すべて選択解除',
config: 'コンフィグ',
yes: 'はい',
no: 'いいえ',
deleteConfirmTitle: '削除しますか?',
confirmAction: '操作を確認してください。',
},
errorMsg: {
fieldRequired: '{{field}}は必要です',

View File

@ -18,6 +18,10 @@ const translation = {
cancel: '取消',
clear: '清空',
save: '保存',
yes: '是',
no: '否',
deleteConfirmTitle: '删除?',
confirmAction: '请确认您的操作。',
saveAndEnable: '保存并启用',
edit: '编辑',
add: '添加',