Skip to content

feat(useFileDialog): add MaybRef to multiple, accept, capture, reset, and directory #4813

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Jul 28, 2025
Merged
151 changes: 143 additions & 8 deletions packages/core/useFileDialog/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it, vi } from 'vitest'
import { shallowRef } from 'vue'
import { nextTick, shallowRef } from 'vue'
import { useFileDialog } from './index'

class DataTransferMock {
Expand Down Expand Up @@ -59,17 +59,29 @@ describe('useFileDialog', () => {
expect(input.click).toBeCalled()
})

it('should work with input element passed as template ref', () => {
const inputEl = document.createElement('input')
inputEl.click = vi.fn()
it('should work with input element passed as template ref', async () => {
const inputEl1 = document.createElement('input')
inputEl1.click = vi.fn()
const input = shallowRef<HTMLInputElement>(inputEl1)
const { open } = useFileDialog({ input })

expect(inputEl1.click).toHaveBeenCalledTimes(0)
open()
expect(inputEl1.type).toBe('file')
expect(inputEl1.click).toHaveBeenCalledTimes(1)

const inputRef = shallowRef<HTMLInputElement>(inputEl)
const inputEl2 = document.createElement('input')
inputEl2.click = vi.fn()

const { open } = useFileDialog({ input: inputRef })
input.value = inputEl2
await nextTick()

expect(inputEl2.type).toBe('file')
expect(inputEl2.click).toHaveBeenCalledTimes(0)

open()
expect(inputEl.type).toBe('file')
expect(inputEl.click).toHaveBeenCalled()

expect(inputEl2.click).toHaveBeenCalledTimes(1)
})

it('should trigger onchange and update files when file is selected', async () => {
Expand All @@ -93,4 +105,127 @@ describe('useFileDialog', () => {
expect(files.value?.[0]).toEqual(file)
expect(changeHandler).toHaveBeenCalledWith(files.value)
})

it('should work with ref value for multiple option', async () => {
const input = document.createElement('input')
input.click = vi.fn()

const multipleRef = shallowRef(true)

const { open } = useFileDialog({
input,
multiple: multipleRef,
})

expect(input.multiple).toBe(true)

open()

expect(input.multiple).toBe(true)

multipleRef.value = false
await nextTick()

expect(input.multiple).toBe(false)

open()

expect(input.multiple).toBe(false)
})

it('should work with ref value for accept option', async () => {
const input = document.createElement('input')
input.click = vi.fn()

const acceptRef = shallowRef('image/*')

const { open } = useFileDialog({
input,
accept: acceptRef,
})

expect(input.accept).toBe('image/*')

open()

expect(input.accept).toBe('image/*')

acceptRef.value = 'video/*'
await nextTick()

expect(input.accept).toBe('video/*')

open()

expect(input.accept).toBe('video/*')
})

it('should work with ref value for directory option', async () => {
const input = document.createElement('input')
input.click = vi.fn()

const directoryRef = shallowRef(true)

const { open } = useFileDialog({
input,
directory: directoryRef,
})

expect(input.webkitdirectory).toBe(true)

open()

expect(input.webkitdirectory).toBe(true)

directoryRef.value = false
await nextTick()

expect(input.webkitdirectory).toBe(false)

open()

expect(input.webkitdirectory).toBe(false)
})

it('should work with ref value for reset option', () => {
const input = document.createElement('input')
input.click = vi.fn()

const resetRef = shallowRef(true)

const { open } = useFileDialog({
input,
reset: resetRef,
})
open()

expect(input.click).toHaveBeenCalled() // Assuming reset does not change input attributes
})

it('should work with ref value for capture option', async () => {
const input = document.createElement('input')
input.click = vi.fn()

const captureRef = shallowRef('user')

const { open } = useFileDialog({
input,
capture: captureRef,
})

expect(input.capture).toBe('user')

open()

expect(input.capture).toBe('user')

captureRef.value = 'environment'
await nextTick()

expect(input.capture).toBe('environment')

open()

expect(input.capture).toBe('environment')
})
})
80 changes: 47 additions & 33 deletions packages/core/useFileDialog/index.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,37 @@
import type { EventHookOn } from '@vueuse/shared'
import type { Ref } from 'vue'
import type { MaybeRef, Ref } from 'vue'
import type { ConfigurableDocument } from '../_configurable'
import type { MaybeElementRef } from '../unrefElement'
import { createEventHook, hasOwn } from '@vueuse/shared'
import { ref as deepRef, readonly } from 'vue'
import { computed, ref as deepRef, readonly, toValue, watchEffect } from 'vue'
import { defaultDocument } from '../_configurable'
import { unrefElement } from '../unrefElement'

export interface UseFileDialogOptions extends ConfigurableDocument {
/**
* @default true
*/
multiple?: boolean
multiple?: MaybeRef<boolean>
/**
* @default '*'
*/
accept?: string
accept?: MaybeRef<string>
/**
* Select the input source for the capture file.
* @see [HTMLInputElement Capture](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/capture)
*/
capture?: string
capture?: MaybeRef<string>
/**
* Reset when open file dialog.
* @default false
*/
reset?: boolean
reset?: MaybeRef<boolean>
/**
* Select directories instead of files.
* @see [HTMLInputElement webkitdirectory](https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/webkitdirectory)
* @default false
*/
directory?: boolean
directory?: MaybeRef<boolean>

/**
* Initial files to set.
Expand Down Expand Up @@ -90,49 +90,63 @@ export function useFileDialog(options: UseFileDialogOptions = {}): UseFileDialog
const files = deepRef<FileList | null>(prepareInitialFiles(options.initialFiles))
const { on: onChange, trigger: changeTrigger } = createEventHook()
const { on: onCancel, trigger: cancelTrigger } = createEventHook()
let input: HTMLInputElement | undefined
if (document) {
input = unrefElement(options.input) || document.createElement('input')
input.type = 'file'

input.onchange = (event: Event) => {
const result = event.target as HTMLInputElement
files.value = result.files
changeTrigger(files.value)
const inputRef = computed(() => {
const input = unrefElement(options.input) ?? (document ? document.createElement('input') : undefined)
if (input) {
input.type = 'file'

input.onchange = (event: Event) => {
const result = event.target as HTMLInputElement
files.value = result.files
changeTrigger(files.value)
}

input.oncancel = () => {
cancelTrigger()
}
}

input.oncancel = () => {
cancelTrigger()
}
}
return input
})

const reset = () => {
files.value = null
if (input && input.value) {
input.value = ''
if (inputRef.value && inputRef.value.value) {
inputRef.value.value = ''
changeTrigger(null)
}
}

const applyOptions = (options: UseFileDialogOptions) => {
const el = inputRef.value
if (!el)
return
el.multiple = toValue(options.multiple)!
el.accept = toValue(options.accept)!
// webkitdirectory key is not stabled, maybe replaced in the future.
el.webkitdirectory = toValue(options.directory)!
if (hasOwn(options, 'capture'))
el.capture = toValue(options.capture)!
}

const open = (localOptions?: Partial<UseFileDialogOptions>) => {
if (!input)
const el = inputRef.value
if (!el)
return
const _options = {
const mergedOptions = {
...DEFAULT_OPTIONS,
...options,
...localOptions,
}
input.multiple = _options.multiple!
input.accept = _options.accept!
// webkitdirectory key is not stabled, maybe replaced in the future.
input.webkitdirectory = _options.directory!
if (hasOwn(_options, 'capture'))
input.capture = _options.capture!
if (_options.reset)
applyOptions(mergedOptions)
if (toValue(mergedOptions.reset))
reset()
input.click()
el.click()
}

watchEffect(() => {
applyOptions(options)
})

return {
files: readonly(files),
open,
Expand Down
Loading
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy