Skip to content

Commit a44bdc2

Browse files
hunterwilhelmantfu
andauthored
feat(useFileDialog): add MaybRef to multiple, accept, capture, reset, and directory (#4813)
Co-authored-by: Anthony Fu <anthonyfu117@hotmail.com> Co-authored-by: Anthony Fu <github@antfu.me>
1 parent 3a2df2e commit a44bdc2

File tree

2 files changed

+190
-41
lines changed

2 files changed

+190
-41
lines changed

packages/core/useFileDialog/index.test.ts

Lines changed: 143 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, expect, it, vi } from 'vitest'
2-
import { shallowRef } from 'vue'
2+
import { nextTick, shallowRef } from 'vue'
33
import { useFileDialog } from './index'
44

55
class DataTransferMock {
@@ -59,17 +59,29 @@ describe('useFileDialog', () => {
5959
expect(input.click).toBeCalled()
6060
})
6161

62-
it('should work with input element passed as template ref', () => {
63-
const inputEl = document.createElement('input')
64-
inputEl.click = vi.fn()
62+
it('should work with input element passed as template ref', async () => {
63+
const inputEl1 = document.createElement('input')
64+
inputEl1.click = vi.fn()
65+
const input = shallowRef<HTMLInputElement>(inputEl1)
66+
const { open } = useFileDialog({ input })
67+
68+
expect(inputEl1.click).toHaveBeenCalledTimes(0)
69+
open()
70+
expect(inputEl1.type).toBe('file')
71+
expect(inputEl1.click).toHaveBeenCalledTimes(1)
6572

66-
const inputRef = shallowRef<HTMLInputElement>(inputEl)
73+
const inputEl2 = document.createElement('input')
74+
inputEl2.click = vi.fn()
6775

68-
const { open } = useFileDialog({ input: inputRef })
76+
input.value = inputEl2
77+
await nextTick()
78+
79+
expect(inputEl2.type).toBe('file')
80+
expect(inputEl2.click).toHaveBeenCalledTimes(0)
6981

7082
open()
71-
expect(inputEl.type).toBe('file')
72-
expect(inputEl.click).toHaveBeenCalled()
83+
84+
expect(inputEl2.click).toHaveBeenCalledTimes(1)
7385
})
7486

7587
it('should trigger onchange and update files when file is selected', async () => {
@@ -93,4 +105,127 @@ describe('useFileDialog', () => {
93105
expect(files.value?.[0]).toEqual(file)
94106
expect(changeHandler).toHaveBeenCalledWith(files.value)
95107
})
108+
109+
it('should work with ref value for multiple option', async () => {
110+
const input = document.createElement('input')
111+
input.click = vi.fn()
112+
113+
const multipleRef = shallowRef(true)
114+
115+
const { open } = useFileDialog({
116+
input,
117+
multiple: multipleRef,
118+
})
119+
120+
expect(input.multiple).toBe(true)
121+
122+
open()
123+
124+
expect(input.multiple).toBe(true)
125+
126+
multipleRef.value = false
127+
await nextTick()
128+
129+
expect(input.multiple).toBe(false)
130+
131+
open()
132+
133+
expect(input.multiple).toBe(false)
134+
})
135+
136+
it('should work with ref value for accept option', async () => {
137+
const input = document.createElement('input')
138+
input.click = vi.fn()
139+
140+
const acceptRef = shallowRef('image/*')
141+
142+
const { open } = useFileDialog({
143+
input,
144+
accept: acceptRef,
145+
})
146+
147+
expect(input.accept).toBe('image/*')
148+
149+
open()
150+
151+
expect(input.accept).toBe('image/*')
152+
153+
acceptRef.value = 'video/*'
154+
await nextTick()
155+
156+
expect(input.accept).toBe('video/*')
157+
158+
open()
159+
160+
expect(input.accept).toBe('video/*')
161+
})
162+
163+
it('should work with ref value for directory option', async () => {
164+
const input = document.createElement('input')
165+
input.click = vi.fn()
166+
167+
const directoryRef = shallowRef(true)
168+
169+
const { open } = useFileDialog({
170+
input,
171+
directory: directoryRef,
172+
})
173+
174+
expect(input.webkitdirectory).toBe(true)
175+
176+
open()
177+
178+
expect(input.webkitdirectory).toBe(true)
179+
180+
directoryRef.value = false
181+
await nextTick()
182+
183+
expect(input.webkitdirectory).toBe(false)
184+
185+
open()
186+
187+
expect(input.webkitdirectory).toBe(false)
188+
})
189+
190+
it('should work with ref value for reset option', () => {
191+
const input = document.createElement('input')
192+
input.click = vi.fn()
193+
194+
const resetRef = shallowRef(true)
195+
196+
const { open } = useFileDialog({
197+
input,
198+
reset: resetRef,
199+
})
200+
open()
201+
202+
expect(input.click).toHaveBeenCalled() // Assuming reset does not change input attributes
203+
})
204+
205+
it('should work with ref value for capture option', async () => {
206+
const input = document.createElement('input')
207+
input.click = vi.fn()
208+
209+
const captureRef = shallowRef('user')
210+
211+
const { open } = useFileDialog({
212+
input,
213+
capture: captureRef,
214+
})
215+
216+
expect(input.capture).toBe('user')
217+
218+
open()
219+
220+
expect(input.capture).toBe('user')
221+
222+
captureRef.value = 'environment'
223+
await nextTick()
224+
225+
expect(input.capture).toBe('environment')
226+
227+
open()
228+
229+
expect(input.capture).toBe('environment')
230+
})
96231
})

packages/core/useFileDialog/index.ts

Lines changed: 47 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,37 @@
11
import type { EventHookOn } from '@vueuse/shared'
2-
import type { Ref } from 'vue'
2+
import type { MaybeRef, Ref } from 'vue'
33
import type { ConfigurableDocument } from '../_configurable'
44
import type { MaybeElementRef } from '../unrefElement'
55
import { createEventHook, hasOwn } from '@vueuse/shared'
6-
import { ref as deepRef, readonly } from 'vue'
6+
import { computed, ref as deepRef, readonly, toValue, watchEffect } from 'vue'
77
import { defaultDocument } from '../_configurable'
88
import { unrefElement } from '../unrefElement'
99

1010
export interface UseFileDialogOptions extends ConfigurableDocument {
1111
/**
1212
* @default true
1313
*/
14-
multiple?: boolean
14+
multiple?: MaybeRef<boolean>
1515
/**
1616
* @default '*'
1717
*/
18-
accept?: string
18+
accept?: MaybeRef<string>
1919
/**
2020
* Select the input source for the capture file.
2121
* @see [HTMLInputElement Capture](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/capture)
2222
*/
23-
capture?: string
23+
capture?: MaybeRef<string>
2424
/**
2525
* Reset when open file dialog.
2626
* @default false
2727
*/
28-
reset?: boolean
28+
reset?: MaybeRef<boolean>
2929
/**
3030
* Select directories instead of files.
3131
* @see [HTMLInputElement webkitdirectory](https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/webkitdirectory)
3232
* @default false
3333
*/
34-
directory?: boolean
34+
directory?: MaybeRef<boolean>
3535

3636
/**
3737
* Initial files to set.
@@ -90,49 +90,63 @@ export function useFileDialog(options: UseFileDialogOptions = {}): UseFileDialog
9090
const files = deepRef<FileList | null>(prepareInitialFiles(options.initialFiles))
9191
const { on: onChange, trigger: changeTrigger } = createEventHook()
9292
const { on: onCancel, trigger: cancelTrigger } = createEventHook()
93-
let input: HTMLInputElement | undefined
94-
if (document) {
95-
input = unrefElement(options.input) || document.createElement('input')
96-
input.type = 'file'
97-
98-
input.onchange = (event: Event) => {
99-
const result = event.target as HTMLInputElement
100-
files.value = result.files
101-
changeTrigger(files.value)
93+
const inputRef = computed(() => {
94+
const input = unrefElement(options.input) ?? (document ? document.createElement('input') : undefined)
95+
if (input) {
96+
input.type = 'file'
97+
98+
input.onchange = (event: Event) => {
99+
const result = event.target as HTMLInputElement
100+
files.value = result.files
101+
changeTrigger(files.value)
102+
}
103+
104+
input.oncancel = () => {
105+
cancelTrigger()
106+
}
102107
}
103-
104-
input.oncancel = () => {
105-
cancelTrigger()
106-
}
107-
}
108+
return input
109+
})
108110

109111
const reset = () => {
110112
files.value = null
111-
if (input && input.value) {
112-
input.value = ''
113+
if (inputRef.value && inputRef.value.value) {
114+
inputRef.value.value = ''
113115
changeTrigger(null)
114116
}
115117
}
116118

119+
const applyOptions = (options: UseFileDialogOptions) => {
120+
const el = inputRef.value
121+
if (!el)
122+
return
123+
el.multiple = toValue(options.multiple)!
124+
el.accept = toValue(options.accept)!
125+
// webkitdirectory key is not stabled, maybe replaced in the future.
126+
el.webkitdirectory = toValue(options.directory)!
127+
if (hasOwn(options, 'capture'))
128+
el.capture = toValue(options.capture)!
129+
}
130+
117131
const open = (localOptions?: Partial<UseFileDialogOptions>) => {
118-
if (!input)
132+
const el = inputRef.value
133+
if (!el)
119134
return
120-
const _options = {
135+
const mergedOptions = {
121136
...DEFAULT_OPTIONS,
122137
...options,
123138
...localOptions,
124139
}
125-
input.multiple = _options.multiple!
126-
input.accept = _options.accept!
127-
// webkitdirectory key is not stabled, maybe replaced in the future.
128-
input.webkitdirectory = _options.directory!
129-
if (hasOwn(_options, 'capture'))
130-
input.capture = _options.capture!
131-
if (_options.reset)
140+
applyOptions(mergedOptions)
141+
if (toValue(mergedOptions.reset))
132142
reset()
133-
input.click()
143+
el.click()
134144
}
135145

146+
watchEffect(() => {
147+
applyOptions(options)
148+
})
149+
136150
return {
137151
files: readonly(files),
138152
open,

0 commit comments

Comments
 (0)