Skip to content

Commit

Permalink
fix(BFormFile): add properties placement and browser as in BootstrapV…
Browse files Browse the repository at this point in the history
…ue (#1797)

feat(BFormFile): add properties placement and browser as in BootstrapVue

test(BFormFile): add unit tests
  • Loading branch information
anrolmar committed Apr 2, 2024
1 parent 6c69ff9 commit 2fe4e69
Show file tree
Hide file tree
Showing 5 changed files with 637 additions and 29 deletions.
57 changes: 54 additions & 3 deletions apps/docs/src/data/components/formFile.data.ts
Expand Up @@ -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',
Expand Down
93 changes: 70 additions & 23 deletions packages/bootstrap-vue-next/src/components/BFormFile/BFormFile.vue
@@ -1,36 +1,49 @@
<template>
<label v-if="hasLabelSlot || label" :for="computedId" class="form-label" :class="labelClass">
<DefineTemplate>
<label class="input-group-text" :for="computedId">
{{ browserText }}
</label>
</DefineTemplate>

<label v-if="hasLabelSlot || label" class="form-label" :class="labelClass" :for="computedId">
<slot name="label">
{{ label }}
</slot>
</label>
<input
:id="computedId"
v-bind="$attrs"
ref="input"
type="file"
class="form-control"
:class="computedClasses"
:form="form"
:name="name"
:multiple="props.multiple"
:disabled="props.disabled"
:capture="props.capture"
:accept="computedAccept || undefined"
:required="props.required || undefined"
:aria-required="props.required || undefined"
:directory="props.directory"
:webkitdirectory="props.directory"
@change="onChange"
@drop="onDrop"
/>

<div class="input-group form-input-file">
<ReusableTemplate v-if="placement === 'start'" />
<input
:id="computedId"
v-bind="$attrs"
ref="input"
type="file"
class="form-control"
:class="computedClasses"
:form="form"
:name="name"
:multiple="props.multiple"
:disabled="props.disabled"
:capture="props.capture"
:accept="computedAccept || undefined"
:required="props.required || undefined"
:aria-label="ariaLabel"
:aria-labelledby="ariaLabelledby"
:aria-required="props.required || undefined"
:directory="props.directory"
:webkitdirectory="props.directory"
@change="onChange"
@drop="onDrop"
/>
<ReusableTemplate v-if="placement === 'end'" />
</div>
</template>

<script setup lang="ts">
import {createReusableTemplate, useFocus, useVModel} from '@vueuse/core'
import {computed, ref, toRef, watch} from 'vue'
import {useFocus, useVModel} from '@vueuse/core'
import type {ClassValue, Size} from '../../types'
import {useId, useStateClass} from '../../composables'
import type {ClassValue, Size} from '../../types'
import {isEmptySlot} from '../../utils'
defineOptions({
Expand All @@ -44,8 +57,11 @@ const slots = defineSlots<{
const props = withDefaults(
defineProps<{
ariaLabel?: string
ariaLabelledby?: string
accept?: string | readonly string[]
autofocus?: boolean
browserText?: string
capture?: boolean | 'user' | 'environment'
directory?: boolean
disabled?: boolean
Expand All @@ -58,13 +74,17 @@ const props = withDefaults(
name?: string
noDrop?: boolean
noTraverse?: boolean
placement?: 'start' | 'end'
required?: boolean
size?: Size
state?: boolean | null
}>(),
{
ariaLabel: undefined,
ariaLabelledby: undefined,
accept: '',
autofocus: false,
browserText: 'Choose',
// eslint-disable-next-line vue/require-valid-default-prop
capture: false,
directory: false,
Expand All @@ -78,6 +98,7 @@ const props = withDefaults(
name: undefined,
noDrop: false,
noTraverse: false,
placement: 'start',
required: false,
size: undefined,
state: null,
Expand All @@ -100,6 +121,7 @@ const input = ref<HTMLInputElement | null>(null)
const {focused} = useFocus(input, {initialValue: props.autofocus})
const hasLabelSlot = toRef(() => !isEmptySlot(slots['label']))
const computedAccept = toRef(() =>
typeof props.accept === 'string' ? props.accept : props.accept.join(',')
)
Expand Down Expand Up @@ -146,4 +168,29 @@ defineExpose({
},
reset,
})
const [DefineTemplate, ReusableTemplate] = createReusableTemplate()
</script>

<style scoped>
.form-input-file {
input[type='file'] {
margin-left: -2px !important;
&::-webkit-file-upload-button {
display: none;
}
&::file-selector-button {
display: none;
}
}
&:hover {
label {
background-color: #dde0e3;
cursor: pointer;
}
}
}
</style>

0 comments on commit 2fe4e69

Please sign in to comment.