diff --git a/apps/docs/src/data/components/formFile.data.ts b/apps/docs/src/data/components/formFile.data.ts
index b8a63703d..c388c8a73 100644
--- a/apps/docs/src/data/components/formFile.data.ts
+++ b/apps/docs/src/data/components/formFile.data.ts
@@ -5,107 +5,158 @@ export default {
{
component: 'BFormFile',
props: [
+ {
+ prop: 'ariaLabel',
+ type: 'string',
+ default: undefined,
+ description: 'Sets the value of `aria-label` attribute on the rendered element',
+ },
+ {
+ prop: 'ariaLabelledBy',
+ type: 'string',
+ default: undefined,
+ description:
+ 'The ID of the element that provides a label for this component. Used as the value for the `aria-labelledby` attribute',
+ },
{
prop: 'accept',
type: 'string | string[]',
default: '',
+ description: "Value to set on the file input's `accept` attribute",
},
{
prop: 'autofocus',
type: 'boolean',
default: false,
+ description:
+ 'When set to `true`, attempts to auto-focus the control when it is mounted, or re-activated when in a keep-alive. Does not set the `autofocus` attribute on the control',
},
{
prop: 'capture',
type: "'boolean' | 'user' | 'environment'",
default: false,
+ description:
+ 'When set, will instruction the browser to use the devices camera (if supported)',
},
{
prop: 'directory',
type: 'boolean',
default: false,
+ description: 'Enable `directory` mode (on browsers that support it)',
},
{
prop: 'disabled',
type: 'boolean',
default: false,
+ description:
+ "When set to `true`, disables the component's functionality and places it in a disabled state",
},
{
prop: 'form',
type: 'string',
default: undefined,
+ description:
+ 'ID of the form that the form control belongs to. Sets the `form` attribute on the control',
},
{
prop: 'id',
type: 'string',
default: undefined,
+ description:
+ 'Used to set the `id` attribute on the rendered content, and used as the base to generate any additional element IDs as needed',
},
{
prop: 'multiple',
type: 'boolean',
default: false,
+ description:
+ 'When set, will allow multiple files to be selected. `v-model` will be an array',
},
{
prop: 'name',
type: 'string',
default: undefined,
+ description: 'Sets the value of the `name` attribute on the form control',
},
{
prop: 'noDrop',
type: 'boolean',
default: false,
+ description: 'Disable drag and drop mode',
},
{
prop: 'noTraverse',
type: 'boolean',
default: false,
+ description: 'Wether to returns files as a flat array when in `directory` mode',
},
{
prop: 'required',
type: 'boolean',
default: false,
+ description: 'Adds the `required` attribute to the form control',
},
{
prop: 'size',
type: 'Size',
default: undefined,
+ description: "Set the size of the component's appearance. 'sm', 'md' (default), or 'lg'",
},
{
prop: 'state',
type: 'boolean | null',
default: undefined,
+ description:
+ 'Controls the validation state appearance of the component. `true` for valid, `false` for invalid, or `null` for no validation state',
},
{
prop: 'modelValue',
type: 'File[] | File | null',
default: undefined,
+ description:
+ 'The current value of the file input. Will be a single `File` object or an array of `File` objects (if `multiple` or `directory` is set). Can be set to `null`, or an empty array to reset the file input',
},
{
prop: 'label',
type: 'string',
default: '',
+ description: 'Sets the label for the form group which the file input is rendered',
},
{
prop: 'labelClass',
type: 'ClassValue',
default: undefined,
+ description: 'Sets the styling for the label',
+ },
+ {
+ prop: 'placement',
+ type: `'start' | 'end'`,
+ default: `'start'`,
+ description: 'Sets the placement for the file button',
+ },
+ {
+ prop: 'browser',
+ type: 'String',
+ default: 'Browse',
+ description: 'Text content for the file browse button',
},
],
emits: [
{
event: 'update:modelValue',
- description: '',
+ description: 'Updates the `v-model` value (see docs for more details)',
args: [
{
arg: 'value',
type: 'File | File[] | null',
- description: '',
+ description:
+ 'Will be a single File object in single mode or an array of File objects in multiple mode',
},
],
},
{
event: 'change',
- description: '',
+ description: 'Original change event of the input',
args: [
{
arg: 'value',
diff --git a/packages/bootstrap-vue-next/src/components/BFormFile/BFormFile.vue b/packages/bootstrap-vue-next/src/components/BFormFile/BFormFile.vue
index abaf9057a..1f4ae083a 100644
--- a/packages/bootstrap-vue-next/src/components/BFormFile/BFormFile.vue
+++ b/packages/bootstrap-vue-next/src/components/BFormFile/BFormFile.vue
@@ -1,36 +1,49 @@
-
+
+
diff --git a/packages/bootstrap-vue-next/src/components/BFormFile/form-file.spec.ts b/packages/bootstrap-vue-next/src/components/BFormFile/form-file.spec.ts
new file mode 100644
index 000000000..6aa4f7b4b
--- /dev/null
+++ b/packages/bootstrap-vue-next/src/components/BFormFile/form-file.spec.ts
@@ -0,0 +1,507 @@
+import { enableAutoUnmount, mount } from '@vue/test-utils'
+import { afterEach, describe, expect, it } from 'vitest'
+import BFormFile from './BFormFile.vue'
+
+describe('form-file', () => {
+ enableAutoUnmount(afterEach)
+
+ it('tag is default div', () => {
+ const wrapper = mount(BFormFile)
+ expect(wrapper.element.tagName).toBe('DIV')
+ })
+
+ it('has class form-control-{type} when prop size', () => {
+ const wrapper = mount(BFormFile, {
+ props: {size: 'lg'},
+ })
+
+ const $input = wrapper.find('input')
+ expect($input.classes()).toContain('form-control-lg')
+ })
+
+ it('does not have class form-control-{type} when prop size undefined', () => {
+ const wrapper = mount(BFormFile, {
+ props: {size: undefined},
+ })
+
+ const $input = wrapper.find('input')
+ expect($input.classes()).not.toContain('form-control-lg')
+ })
+
+ it('wrapper has class form-input-file', () => {
+ const wrapper = mount(BFormFile)
+ expect(wrapper.find('div').classes()).toContain('form-input-file')
+ })
+
+ describe('input attributes', () => {
+ it('has input element', () => {
+ const wrapper = mount(BFormFile)
+ const $input = wrapper.find('input')
+ expect($input.exists()).toBe(true)
+ })
+
+ it('input element has attr id', () => {
+ const wrapper = mount(BFormFile)
+ const $input = wrapper.get('input')
+ expect($input.attributes('id')).toBeDefined()
+ })
+
+ it('input element attr id contains content from prop id', () => {
+ const wrapper = mount(BFormFile, {
+ props: {id: 'foobar'},
+ })
+ const $input = wrapper.get('input')
+ expect($input.attributes('id')).toBe('foobar')
+ })
+
+ it('input element has attr type to be file', () => {
+ const wrapper = mount(BFormFile)
+ const $input = wrapper.get('input')
+ expect($input.attributes('type')).toBe('file')
+ })
+
+ it('input element has attr disabled when prop disabled', () => {
+ const wrapper = mount(BFormFile, {
+ props: {disabled: true},
+ })
+ const $input = wrapper.get('input')
+ expect($input.attributes('disabled')).toBe('')
+ })
+
+ it('input element does not have attr disabled when prop disabled is false', () => {
+ const wrapper = mount(BFormFile, {
+ props: {disabled: false},
+ })
+ const $input = wrapper.get('input')
+ expect($input.attributes('disabled')).toBeUndefined()
+ })
+
+ it('input element does not have attr disabled when prop disabled is undefined', () => {
+ const wrapper = mount(BFormFile, {
+ props: {disabled: undefined},
+ })
+ const $input = wrapper.get('input')
+ expect($input.attributes('disabled')).toBeUndefined()
+ })
+
+ it('input element has attr required when prop name and prop required', () => {
+ const wrapper = mount(BFormFile, {
+ props: {required: true, name: 'foo'},
+ })
+ const $input = wrapper.get('input')
+ expect($input.attributes('required')).toBe('')
+ })
+
+ it('input element does not have attr required when prop name is empty string and prop required', () => {
+ const wrapper = mount(BFormFile)
+ const $input = wrapper.get('input')
+ expect($input.attributes('required')).toBeUndefined()
+ })
+
+ it('input element has attr name to be prop name', () => {
+ const wrapper = mount(BFormFile, {
+ props: {name: 'foobar'},
+ })
+ const $input = wrapper.get('input')
+ expect($input.attributes('name')).toBe('foobar')
+ })
+
+ it('input element has attr name is undefined when prop name undefined', () => {
+ const wrapper = mount(BFormFile, {
+ props: {name: undefined},
+ })
+ const $input = wrapper.get('input')
+ expect($input.attributes('name')).toBeUndefined()
+ })
+
+ it('input element has attr form to be prop form', () => {
+ const wrapper = mount(BFormFile, {
+ props: {form: 'foobar'},
+ })
+ const $input = wrapper.get('input')
+ expect($input.attributes('form')).toBe('foobar')
+ })
+
+ it('input element has attr form is undefined when prop form undefined', () => {
+ const wrapper = mount(BFormFile, {
+ props: {form: undefined},
+ })
+ const $input = wrapper.get('input')
+ expect($input.attributes('form')).toBeUndefined()
+ })
+
+ it('input element has attr aria-label to be prop ariaLabel', () => {
+ const wrapper = mount(BFormFile, {
+ props: {ariaLabel: 'foobar'},
+ })
+ const $input = wrapper.get('input')
+ expect($input.attributes('aria-label')).toBe('foobar')
+ })
+
+ it('input element has attr aria-label is undefined when prop ariaLabel undefined', () => {
+ const wrapper = mount(BFormFile, {
+ props: {ariaLabel: undefined},
+ })
+ const $input = wrapper.get('input')
+ expect($input.attributes('aria-label')).toBeUndefined()
+ })
+
+ it('input element has attr aria-labelledby to be prop ariaLabelledby', () => {
+ const wrapper = mount(BFormFile, {
+ props: {ariaLabelledby: 'foobar'},
+ })
+ const $input = wrapper.get('input')
+ expect($input.attributes('aria-labelledby')).toBe('foobar')
+ })
+
+ it('input element has attr aria-labelledby is undefined when prop ariaLabelledby undefined', () => {
+ const wrapper = mount(BFormFile, {
+ props: {ariaLabelledby: undefined},
+ })
+ const $input = wrapper.get('input')
+ expect($input.attributes('aria-labelledby')).toBeUndefined()
+ })
+
+ it('input element has attr aria-labelledby is undefined when prop ariaLabelledby undefined', () => {
+ const wrapper = mount(BFormFile, {
+ props: {ariaLabelledby: undefined},
+ })
+ const $input = wrapper.get('input')
+ expect($input.attributes('aria-labelledby')).toBeUndefined()
+ })
+
+ it('input element has attr value to be true when value is undefined', () => {
+ const wrapper = mount(BFormFile, {
+ props: {value: undefined},
+ })
+ const $input = wrapper.get('input')
+ expect($input.attributes('value')).toBeUndefined()
+ })
+
+ it('input element aria-required when prop name and prop required true', () => {
+ const wrapper = mount(BFormFile, {
+ props: {name: 'foo', required: true},
+ })
+ const $input = wrapper.get('input')
+ expect($input.attributes('aria-required')).toBe('true')
+ })
+
+ it('input element does not have aria-required when prop name and prop required false', () => {
+ const wrapper = mount(BFormFile, {
+ props: {name: 'foo', required: false},
+ })
+ const $input = wrapper.get('input')
+ expect($input.attributes('aria-required')).toBeUndefined()
+ })
+
+ it('input element has set attr multiple to true when prop multiple is true', () => {
+ const wrapper = mount(BFormFile, {
+ props: {multiple: 'true'},
+ })
+ const $input = wrapper.get('input')
+ expect($input.attributes('multiple')).toBeDefined()
+ })
+
+ it('input element has set attr multiple to false when prop multiple is false', () => {
+ const wrapper = mount(BFormFile, {
+ props: {multiple: false},
+ })
+ const $input = wrapper.get('input')
+ expect($input.attributes('multiple')).toBeUndefined()
+ })
+
+ it('input element has set attr multiple to false when prop multiple is undefined', () => {
+ const wrapper = mount(BFormFile, {
+ props: {multiple: undefined},
+ })
+ const $input = wrapper.get('input')
+ expect($input.attributes('multiple')).toBeUndefined()
+ })
+
+ it('input element has set attr capture to true when prop capture is true', () => {
+ const wrapper = mount(BFormFile, {
+ props: {capture: true},
+ })
+ const $input = wrapper.get('input')
+ expect($input.attributes('capture')).toBe('true')
+ })
+
+ it('input element has set attr capture to false when prop capture is false', () => {
+ const wrapper = mount(BFormFile, {
+ props: {capture: false},
+ })
+ const $input = wrapper.get('input')
+ expect($input.attributes('capture')).toBe('false')
+ })
+
+ it('input element has set attr capture to false when prop disabled is undefined', () => {
+ const wrapper = mount(BFormFile, {
+ props: {capture: undefined},
+ })
+ const $input = wrapper.get('input')
+ expect($input.attributes('capture')).toBe('false')
+ })
+
+ it('input element has set attr accept to empty when prop accept is true', () => {
+ const wrapper = mount(BFormFile, {
+ props: {accept: ''},
+ })
+ const $input = wrapper.get('input')
+ expect($input.attributes('accept')).toBeUndefined()
+ })
+
+ it('input element has set attr accept to "foo" when prop accept is false', () => {
+ const wrapper = mount(BFormFile, {
+ props: {accept: 'foo'},
+ })
+ const $input = wrapper.get('input')
+ expect($input.attributes('accept')).toBe('foo')
+ })
+
+ it('input element does not have attr accept when prop accept is undefined', () => {
+ const wrapper = mount(BFormFile, {
+ props: {accept: undefined},
+ })
+ const $input = wrapper.get('input')
+ expect($input.attributes('accept')).toBeUndefined()
+ })
+
+ it('input element has set attr directory to true when prop directory is true', () => {
+ const wrapper = mount(BFormFile, {
+ props: {directory: true},
+ })
+ const $input = wrapper.get('input')
+ expect($input.attributes('directory')).toBe('true')
+ })
+
+ it('input element has set attr directory to false when prop directory is false', () => {
+ const wrapper = mount(BFormFile, {
+ props: {directory: false},
+ })
+ const $input = wrapper.get('input')
+ expect($input.attributes('directory')).toBe('false')
+ })
+
+ it('input element has set attr directory to false when prop directory is undefined', () => {
+ const wrapper = mount(BFormFile, {
+ props: {directory: undefined},
+ })
+ const $input = wrapper.get('input')
+ expect($input.attributes('directory')).toBe('false')
+ })
+
+ it('default has custom attributes transferred input element', async () => {
+ const wrapper = mount(BFormFile, {
+ propsData: {
+ id: 'foo',
+ foo: 'bar',
+ },
+ })
+
+ const $input = wrapper.find('input')
+ expect($input.attributes('foo')).toBeDefined()
+ expect($input.attributes('foo')).toEqual('bar')
+ })
+ })
+
+ describe('label attributes', () => {
+ it('has label by default', () => {
+ const wrapper = mount(BFormFile)
+ const $label = wrapper.find('label')
+ expect($label.exists()).toBe(true)
+ })
+
+ it('has label when has label slot defined', () => {
+ const wrapper = mount(BFormFile, {
+ props: {labelClass: 'labelClass'},
+ slots: {label: 'foo'},
+ })
+ const $label = wrapper.get('label')
+ expect($label).not.toBeUndefined()
+ expect($label.attributes('class')?.includes('labelClass'))
+ })
+ it('has label has attr for to be defined by default', () => {
+ const wrapper = mount(BFormFile, {
+ props: {id: 'fooFile'},
+ slots: {default: 'foo'},
+ })
+
+ const $input = wrapper.get('input')
+ const $label = wrapper.get('label')
+
+ const $labelFor = $label.attributes('for')
+ const $inputId = $input.attributes('id')
+
+ expect($labelFor).toBe($inputId)
+ })
+ })
+
+ describe('file button', () => {
+ it('the button is placed on the start when prop placement is start', () => {
+ const wrapper = mount(BFormFile, {
+ props: {id: 'fooFile', placement: 'start'},
+ })
+
+ const $label = wrapper.get('label')
+ expect($label.classes()).toContain('input-group-text')
+
+ const childElements = wrapper.get('div').findAll('*')
+
+ expect(childElements[0].exists())
+ expect(childElements[0].element.tagName).toBe('LABEL')
+
+ expect(childElements[1].exists())
+ expect(childElements[1].element.tagName).toBe('INPUT')
+ })
+ it('the button is placed on the end when prop placement is end', () => {
+ const wrapper = mount(BFormFile, {
+ props: {id: 'fooFile', placement: 'end'},
+ })
+
+ const $label = wrapper.get('label')
+ expect($label.classes()).not.toContain('form-label')
+ expect($label.classes()).toContain('input-group-text')
+
+ const childElements = wrapper.get('div').findAll('*')
+
+ expect(childElements[0].exists())
+ expect(childElements[0].element.tagName).toBe('INPUT')
+
+ expect(childElements[1].exists())
+ expect(childElements[1].element.tagName).toBe('LABEL')
+ })
+ it('the button is placed on the start when prop placement is undefined', () => {
+ const wrapper = mount(BFormFile, {
+ props: {id: 'fooFile'},
+ })
+
+ const $label = wrapper.get('label')
+ expect($label.classes()).not.toContain('form-label')
+ expect($label.classes()).toContain('input-group-text')
+
+ const childElements = wrapper.get('div').findAll('*')
+
+ expect(childElements[0].exists())
+ expect(childElements[0].element.tagName).toBe('LABEL')
+
+ expect(childElements[1].exists())
+ expect(childElements[1].element.tagName).toBe('INPUT')
+ })
+
+ it('the button has value text when prop browser text is defined', () => {
+ const wrapper = mount(BFormFile, {
+ props: {browserText: 'Browse'},
+ })
+
+ const $label = wrapper.get('label')
+ expect($label.classes()).not.toContain('form-label')
+ expect($label.classes()).toContain('input-group-text')
+ expect($label.text()).toBe('Browse')
+ })
+ it('the button has `Choose` when prop browser text is undefined', () => {
+ const wrapper = mount(BFormFile)
+
+ const $label = wrapper.get('label')
+ expect($label.classes()).not.toContain('form-label')
+ expect($label.classes()).toContain('input-group-text')
+ expect($label.text()).toBe('Choose')
+ })
+ })
+
+ describe('model behavior', () => {
+ it('emits input even when file changed', async () => {
+ const file = new File(['foo'], 'foo.txt', {
+ type: 'text/plain',
+ lastModified: Date.now(),
+ })
+
+ const wrapper = mount(BFormFile, {props: {id: 'foo', modelValue: file}})
+
+ const $input = wrapper.get('input')
+ await $input.trigger('change')
+
+ expect(wrapper.emitted('change')).toBeDefined()
+ expect(wrapper.emitted('change')).toHaveLength(1)
+ expect(wrapper.emitted('change')?.[0][0]).toBeInstanceOf(Event)
+ })
+
+ it('emits update:modelValue===file event when file changed', async () => {
+ const file = new File(['foo'], 'foo.txt', {
+ type: 'text/plain',
+ lastModified: Date.now(),
+ })
+
+ const dataTransfer = new ClipboardEvent('').clipboardData ?? new DataTransfer()
+ dataTransfer.items.add(file)
+
+ const wrapper = mount(BFormFile, {props: {id: 'foo', modelValue: undefined}})
+
+ const $input = wrapper.get('input')
+ $input.element.files = dataTransfer.files
+ await $input.trigger('change')
+
+ expect(wrapper.emitted('update:modelValue')).toBeDefined()
+ expect(wrapper.emitted('update:modelValue')).toHaveLength(1)
+ expect(wrapper.emitted('update:modelValue')?.[0][0]).toStrictEqual(file)
+ })
+
+ it('emits update:modelValue===file event when file changed in multiple mode', async () => {
+ const file1 = new File(['foo'], 'foo.txt', {
+ type: 'text/plain',
+ lastModified: Date.now(),
+ })
+
+ const file2 = new File(['foo2'], 'foo2.txt', {
+ type: 'text/plain',
+ lastModified: Date.now(),
+ })
+
+ const dataTransfer = new ClipboardEvent('').clipboardData ?? new DataTransfer()
+ dataTransfer.items.add(file1)
+ dataTransfer.items.add(file2)
+
+ const wrapper = mount(BFormFile, {props: {id: 'foo', modelValue: undefined, multiple: true}})
+
+ const $input = wrapper.get('input')
+ $input.element.files = dataTransfer.files
+ await $input.trigger('change')
+
+ expect(wrapper.emitted('update:modelValue')).toBeDefined()
+ expect(wrapper.emitted('update:modelValue')).toHaveLength(1)
+ expect((wrapper.emitted('update:modelValue')?.[0][0] as File[])[0]).toStrictEqual(file1)
+ expect((wrapper.emitted('update:modelValue')?.[0][0] as File[])[1]).toStrictEqual(file2)
+ })
+
+ it('emits update:modelValue===file event when file changed in directory mode', async () => {
+ const file1 = new File(['foo'], 'foo.txt', {
+ type: 'text/plain',
+ lastModified: Date.now(),
+ })
+
+ const file2 = new File(['foo2'], 'foo2.txt', {
+ type: 'text/plain',
+ lastModified: Date.now(),
+ })
+
+ const dataTransfer = new ClipboardEvent('').clipboardData ?? new DataTransfer()
+ dataTransfer.items.add(file1)
+ dataTransfer.items.add(file2)
+
+ const wrapper = mount(BFormFile, {
+ props: {id: 'foo', modelValue: undefined, multiple: true, directory: true},
+ })
+
+ const $input = wrapper.get('input')
+ $input.element.files = dataTransfer.files
+ await $input.trigger('change')
+
+ expect(wrapper.emitted('update:modelValue')).toBeDefined()
+ expect(wrapper.emitted('update:modelValue')).toHaveLength(1)
+ expect((wrapper.emitted('update:modelValue')?.[0][0] as File[])[0]).toStrictEqual(file1)
+ expect((wrapper.emitted('update:modelValue')?.[0][0] as File[])[1]).toStrictEqual(file2)
+ })
+
+ it('reset() method works in single mode', async () => {})
+ it('reset() method works in multiple mode', async () => {})
+ })
+})
diff --git a/packages/bootstrap-vue-next/src/components/BFormGroup/BFormGroup.vue b/packages/bootstrap-vue-next/src/components/BFormGroup/BFormGroup.vue
index b1983ccac..b99f7134a 100644
--- a/packages/bootstrap-vue-next/src/components/BFormGroup/BFormGroup.vue
+++ b/packages/bootstrap-vue-next/src/components/BFormGroup/BFormGroup.vue
@@ -162,7 +162,10 @@ export default defineComponent({
const stateClass = useStateClass(() => props.state)
- const computedAriaInvalid = useAriaInvalid(() => props.ariaInvalid, () => props.state)
+ const computedAriaInvalid = useAriaInvalid(
+ () => props.ariaInvalid,
+ () => props.state
+ )
watch(
() => ariaDescribedby,
diff --git a/packages/bootstrap-vue-next/src/composables/useModalManager.ts b/packages/bootstrap-vue-next/src/composables/useModalManager.ts
index 1bf8bf327..4f4aa664d 100644
--- a/packages/bootstrap-vue-next/src/composables/useModalManager.ts
+++ b/packages/bootstrap-vue-next/src/composables/useModalManager.ts
@@ -82,8 +82,8 @@ export default (modalOpen: Readonly[>) => {
)
return {
- activePosition: computed(() =>
- stack?.value.findIndex((el) => el.exposed?.id === currentModal.exposed?.id)
+ activePosition: computed(
+ () => stack?.value.findIndex((el) => el.exposed?.id === currentModal.exposed?.id)
),
activeModalCount: countStack,
}
]