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
18 changes: 12 additions & 6 deletions docs/components/upload.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,17 @@
}
`" />

## 键盘操作

触发区关联原生文件输入,支持通过 `Tab` 聚焦,并使用浏览器默认的文件选择快捷键打开文件选择器。

## API

| 属性 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| accept | 接受的文件类型 | `string` | - |
| multiple | 是否多选 | `boolean` | `false` |
| fileList | 文件列表 | `UploadFileItem[]` | - |
| onChange | 文件变化回调 | `(files) => void` | - |
| 属性 | 说明 | 类型 | 默认值 |
| ----------- | -------------- | ------------------ | ------------------------------ |
| accept | 接受的文件类型 | `string` | - |
| multiple | 是否多选 | `boolean` | `false` |
| disabled | 禁用上传 | `boolean` | `false` |
| fileList | 文件列表 | `UploadFileItem[]` | - |
| triggerText | 触发区内容 | `ReactNode` | `Click or drag file to upload` |
| onChange | 文件变化回调 | `(files) => void` | - |
65 changes: 65 additions & 0 deletions packages/ui/src/components/form/controls/Upload/Upload.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { describe, expect, it, vi } from 'vitest'

import { Upload } from './Upload'

describe('Upload', () => {
it('exposes a focusable file input through the visible trigger text', async () => {
const user = userEvent.setup()

render(<Upload triggerText="Select files" />)

const input = screen.getByLabelText('Select files')
expect(input).toHaveAttribute('type', 'file')

await user.tab()
expect(input).toHaveFocus()
})

it('calls onChange with selected file metadata', async () => {
const user = userEvent.setup()
const onChange = vi.fn()
const file = new File(['hello'], 'report.txt', {
lastModified: 123,
type: 'text/plain',
})

render(<Upload triggerText="Upload report" onChange={onChange} />)

await user.upload(screen.getByLabelText('Upload report'), file)

expect(onChange).toHaveBeenCalledWith([
{
uid: 'report.txt-123',
name: 'report.txt',
size: 5,
},
])
})

it('passes accept, multiple, and disabled attributes to the input', () => {
render(<Upload triggerText="Upload images" accept="image/png" multiple disabled />)

const input = screen.getByLabelText('Upload images')

expect(input).toHaveAttribute('accept', 'image/png')
expect(input).toHaveAttribute('multiple')
expect(input).toBeDisabled()
})

it('renders the controlled file list with list semantics', () => {
render(
<Upload
fileList={[
{ uid: '1', name: 'first.pdf' },
{ uid: '2', name: 'second.pdf' },
]}
/>,
)

expect(screen.getByRole('list', { name: 'Uploaded files' })).toBeInTheDocument()
expect(screen.getByText('first.pdf')).toBeInTheDocument()
expect(screen.getByText('second.pdf')).toBeInTheDocument()
})
})
17 changes: 13 additions & 4 deletions packages/ui/src/components/form/controls/Upload/Upload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,19 +40,28 @@ export const Upload = forwardRef<HTMLDivElement, UploadProps>(function Upload(

return (
<div ref={ref} className={cn('space-y-2', className)} {...props}>
<label className={cn('flex cursor-pointer items-center justify-center rounded-lg border border-dashed border-slate-300 px-4 py-6 text-sm text-slate-600 hover:bg-slate-50 dark:border-slate-700 dark:text-slate-300 dark:hover:bg-slate-900', disabled && 'cursor-not-allowed opacity-60')}>
<label className="block">
<input
type="file"
className="hidden"
className="peer sr-only"
accept={accept}
multiple={multiple}
disabled={disabled}
onChange={handleChange}
/>
{triggerText}
<span
className={cn(
'flex cursor-pointer items-center justify-center rounded-lg border border-dashed border-slate-300 px-4 py-6 text-sm text-slate-600 hover:bg-slate-50 peer-focus-visible:outline peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-brand-500 peer-disabled:cursor-not-allowed peer-disabled:opacity-60 dark:border-slate-700 dark:text-slate-300 dark:hover:bg-slate-900',
)}
>
{triggerText}
</span>
</label>
{fileList.length > 0 ? (
<ul className="space-y-1 text-sm text-slate-600 dark:text-slate-300">
<ul
aria-label="Uploaded files"
className="space-y-1 text-sm text-slate-600 dark:text-slate-300"
>
{fileList.map((file) => (
<li key={file.uid} className="rounded bg-slate-100 px-2 py-1 dark:bg-slate-800">
{file.name}
Expand Down
Loading