diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 2189442b9af..453215c8dd7 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -12,7 +12,7 @@ env:
jobs:
pre_job:
- runs-on: ubuntu-latest
+ runs-on: ubuntu-24.04
outputs:
should_skip: ${{ steps.skip_check.outputs.should_skip }}
steps:
@@ -28,7 +28,7 @@ jobs:
name: Build vuetify
needs: pre_job
if: needs.pre_job.outputs.should_skip != 'true'
- runs-on: ubuntu-latest
+ runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: vuetifyjs/setup-action@master
@@ -44,7 +44,7 @@ jobs:
name: Lint
needs: [pre_job, build-vuetify]
if: needs.pre_job.outputs.should_skip != 'true'
- runs-on: ubuntu-latest
+ runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
@@ -64,7 +64,7 @@ jobs:
name: Test (Unit)
needs: pre_job
if: needs.pre_job.outputs.should_skip != 'true'
- runs-on: ubuntu-latest
+ runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: vuetifyjs/setup-action@master
@@ -75,7 +75,7 @@ jobs:
name: Test (e2e)
needs: pre_job
if: needs.pre_job.outputs.should_skip != 'true'
- runs-on: ubuntu-latest
+ runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: vuetifyjs/setup-action@master
@@ -84,7 +84,7 @@ jobs:
deploy:
needs: [lint, test-unit, test-e2e, build-vuetify]
- runs-on: ubuntu-latest
+ runs-on: ubuntu-24.04
if: github.event_name == 'push' && startswith(github.ref, 'refs/tags/v') && github.repository_owner == 'vuetifyjs'
steps:
- uses: actions/checkout@v4
@@ -113,7 +113,7 @@ jobs:
name: Build docs
needs: [pre_job, build-vuetify]
if: needs.pre_job.outputs.should_skip != 'true' && github.event_name == 'push' && github.repository_owner == 'vuetifyjs' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/next')
- runs-on: ubuntu-latest
+ runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
@@ -144,7 +144,7 @@ jobs:
publish-docs:
needs: [lint, test-unit, build-docs]
- runs-on: ubuntu-latest
+ runs-on: ubuntu-24.04
environment: Production
if: github.event_name == 'push' && github.repository_owner == 'vuetifyjs' && github.ref == 'refs/heads/master'
steps:
diff --git a/.github/workflows/close-issue.yml b/.github/workflows/close-issue.yml
index 1122540141d..539ed67ea26 100644
--- a/.github/workflows/close-issue.yml
+++ b/.github/workflows/close-issue.yml
@@ -12,7 +12,7 @@ env:
jobs:
close:
- runs-on: ubuntu-latest
+ runs-on: ubuntu-24.04
if: github.repository_owner == 'vuetifyjs'
steps:
- uses: vuetifyjs/close-action@master
diff --git a/.github/workflows/crowdin-uploads.yml b/.github/workflows/crowdin-uploads.yml
index 97fb9f013ea..88ecc4cfa15 100644
--- a/.github/workflows/crowdin-uploads.yml
+++ b/.github/workflows/crowdin-uploads.yml
@@ -21,7 +21,7 @@ env:
jobs:
upload-to-crowdin:
- runs-on: ubuntu-latest
+ runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v4
diff --git a/.github/workflows/nightly-pr.yml b/.github/workflows/nightly-pr.yml
index b55f1652a72..20f52ef9876 100644
--- a/.github/workflows/nightly-pr.yml
+++ b/.github/workflows/nightly-pr.yml
@@ -9,7 +9,7 @@ on:
jobs:
deploy:
- runs-on: ubuntu-latest
+ runs-on: ubuntu-24.04
if: ${{ github.repository_owner == 'vuetifyjs' }}
steps:
- uses: actions/checkout@v4
diff --git a/.github/workflows/nightly-schedule.yml b/.github/workflows/nightly-schedule.yml
index 4bebb563b90..3ff8bfd46d9 100644
--- a/.github/workflows/nightly-schedule.yml
+++ b/.github/workflows/nightly-schedule.yml
@@ -6,7 +6,7 @@ on:
jobs:
deploy:
- runs-on: ubuntu-latest
+ runs-on: ubuntu-24.04
if: ${{ github.repository_owner == 'vuetifyjs' }}
strategy:
max-parallel: 1
@@ -49,7 +49,7 @@ jobs:
percy:
name: Visual regression tests
- runs-on: ubuntu-latest
+ runs-on: ubuntu-24.04
if: ${{ github.repository_owner == 'vuetifyjs' }}
steps:
- uses: actions/checkout@v4
@@ -66,16 +66,10 @@ jobs:
fi
- uses: vuetifyjs/setup-action@master
- run: echo "COMMIT=$(git rev-parse HEAD)" >> $GITHUB_ENV
- - run: pnpm cy:run
+ - run: pnpm test:percy
working-directory: ./packages/vuetify
env:
PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }}
PERCY_BRANCH: master
PERCY_TARGET_BRANCH: master
PERCY_COMMIT: ${{ env.COMMIT }}
- - uses: actions/upload-artifact@v3
- if: failure()
- with:
- name: cypress-screenshots
- path: ./packages/vuetify/cypress/screenshots/
- if-no-files-found: ignore
diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml
index 2b7abfdfc6d..e192907708c 100644
--- a/.github/workflows/stale.yml
+++ b/.github/workflows/stale.yml
@@ -6,7 +6,7 @@ on:
jobs:
stale:
- runs-on: ubuntu-latest
+ runs-on: ubuntu-24.04
steps:
- uses: actions/stale@v9
with:
diff --git a/.github/workflows/triage.yml b/.github/workflows/triage.yml
index 3f25be8c898..96e89a8e845 100644
--- a/.github/workflows/triage.yml
+++ b/.github/workflows/triage.yml
@@ -5,7 +5,7 @@ on:
jobs:
triage:
- runs-on: ubuntu-latest
+ runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: vuetifyjs/triage-action@master
diff --git a/README.md b/README.md
index 6197af29abf..232962ba6cd 100644
--- a/README.md
+++ b/README.md
@@ -61,8 +61,8 @@ Funds donated through GitHub Sponsors and Patreon go directly to support John an
-
-
+
+
@@ -75,8 +75,18 @@ Funds donated through GitHub Sponsors and Patreon go directly to support John an
-
- Your Logo Here
+
+
+
+
+
+
+
+
+
+
+
+
@@ -89,59 +99,63 @@ Funds donated through GitHub Sponsors and Patreon go directly to support John an
-
-
+
+
-
-
-
+
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
+
+
+
+
+
-
-
+
+
-
diff --git a/lerna.json b/lerna.json
index cc0a72a1952..a56d17b2aa6 100644
--- a/lerna.json
+++ b/lerna.json
@@ -13,5 +13,5 @@
}
},
"npmClient": "pnpm",
- "version": "3.7.4"
+ "version": "3.7.6"
}
\ No newline at end of file
diff --git a/packages/api-generator/package.json b/packages/api-generator/package.json
index d901ba6797e..435323a47c4 100755
--- a/packages/api-generator/package.json
+++ b/packages/api-generator/package.json
@@ -1,6 +1,6 @@
{
"name": "@vuetify/api-generator",
- "version": "3.7.4",
+ "version": "3.7.6",
"private": true,
"description": "",
"scripts": {
diff --git a/packages/api-generator/src/locale/en/Select.json b/packages/api-generator/src/locale/en/Select.json
index e2af9a03120..d625eecaa29 100644
--- a/packages/api-generator/src/locale/en/Select.json
+++ b/packages/api-generator/src/locale/en/Select.json
@@ -1,6 +1,6 @@
{
"props": {
- "closeText": "Text set to to the inputs `aria-label` and `title` when input menu is closed.",
+ "closeText": "Text set to the inputs `aria-label` and `title` when input menu is closed.",
"chips": "Changes display of selections to chips.",
"closableChips": "Enables the [closable](/api/v-chip/#props-closable) prop on all [v-chip](/components/chips/) components.",
"hideSelected": "Do not display in the select menu items that are already selected.",
@@ -9,6 +9,6 @@
"menuProps": "Pass props through to the `v-menu` component. Accepts an object with anything from [v-menu](/api/v-menu/#props) props, camelCase keys are recommended.",
"multiple": "Changes select to multiple. Accepts array for value.",
"openOnClear": "Open's the menu whenever the clear icon is clicked.",
- "openText": "Text set to to the inputs **aria-label** and **title** when input menu is open."
+ "openText": "Text set to the inputs **aria-label** and **title** when input menu is open."
}
}
diff --git a/packages/docs/package.json b/packages/docs/package.json
index 6ea0dba64be..e9057270748 100644
--- a/packages/docs/package.json
+++ b/packages/docs/package.json
@@ -3,7 +3,7 @@
"description": "A Vue.js project",
"private": true,
"author": "John Leider ",
- "version": "3.7.4",
+ "version": "3.7.6",
"repository": {
"type": "git",
"url": "git+https://github.com/vuetifyjs/vuetify.git",
diff --git a/packages/docs/src/components/about/TeamMember.vue b/packages/docs/src/components/about/TeamMember.vue
index cd48347ecd0..2884e44a483 100644
--- a/packages/docs/src/components/about/TeamMember.vue
+++ b/packages/docs/src/components/about/TeamMember.vue
@@ -174,7 +174,7 @@
color: '#212121',
href: `https://x.com/${props.member.twitter}`,
icon: '$x',
- tooltip: 'X',
+ tooltip: 'Xitter',
})
}
diff --git a/packages/docs/src/data/nav.json b/packages/docs/src/data/nav.json
index 9d0a526bde2..d0f83422675 100644
--- a/packages/docs/src/data/nav.json
+++ b/packages/docs/src/data/nav.json
@@ -237,6 +237,10 @@
"title": "date-inputs",
"subfolder": "components"
},
+ {
+ "title": "file-upload",
+ "subfolder": "components"
+ },
{
"title": "number-inputs",
"subfolder": "components"
diff --git a/packages/docs/src/data/team.json b/packages/docs/src/data/team.json
index dc76b77bfce..294163b331d 100644
--- a/packages/docs/src/data/team.json
+++ b/packages/docs/src/data/team.json
@@ -53,7 +53,6 @@
"location": "Melbourne, Australia",
"name": "Kael Watts-Deuchar",
"team": "core",
- "twitter": "kaelwd",
"joined": "Oct 2017"
},
"nekosaur": {
diff --git a/packages/docs/src/examples/border/colors.vue b/packages/docs/src/examples/border/colors.vue
index 3d22020222b..9e239d1cc95 100644
--- a/packages/docs/src/examples/border/colors.vue
+++ b/packages/docs/src/examples/border/colors.vue
@@ -11,7 +11,7 @@
-
"sucess sm"
+
"success sm"
diff --git a/packages/docs/src/examples/v-combobox/misc-advanced.vue b/packages/docs/src/examples/v-combobox/misc-advanced.vue
index 0b945b77a25..21f75a7781e 100644
--- a/packages/docs/src/examples/v-combobox/misc-advanced.vue
+++ b/packages/docs/src/examples/v-combobox/misc-advanced.vue
@@ -2,112 +2,161 @@
-
-
- Create
+
+
+
+
+
+ Create
{{ search }}
-
-
-
-
- {{ item.text }}
-
-
- $delete
-
-
-
-
-
-
- {{ item.text }}
-
-
-
-
- {{ editing !== item ? 'mdi-pencil' : 'mdi-check' }}
-
-
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/docs/src/examples/v-file-upload/prop-disabled.vue b/packages/docs/src/examples/v-file-upload/prop-disabled.vue
new file mode 100644
index 00000000000..af636f7f41d
--- /dev/null
+++ b/packages/docs/src/examples/v-file-upload/prop-disabled.vue
@@ -0,0 +1,3 @@
+
+
+
diff --git a/packages/docs/src/examples/v-file-upload/prop-scrim.vue b/packages/docs/src/examples/v-file-upload/prop-scrim.vue
new file mode 100644
index 00000000000..56d88c513ad
--- /dev/null
+++ b/packages/docs/src/examples/v-file-upload/prop-scrim.vue
@@ -0,0 +1,3 @@
+
+
+
diff --git a/packages/docs/src/examples/v-file-upload/slot-item.vue b/packages/docs/src/examples/v-file-upload/slot-item.vue
new file mode 100644
index 00000000000..65f4772a483
--- /dev/null
+++ b/packages/docs/src/examples/v-file-upload/slot-item.vue
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/docs/src/examples/v-file-upload/usage.vue b/packages/docs/src/examples/v-file-upload/usage.vue
new file mode 100644
index 00000000000..417a9201e72
--- /dev/null
+++ b/packages/docs/src/examples/v-file-upload/usage.vue
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/docs/src/examples/v-progress-linear/usage.vue b/packages/docs/src/examples/v-progress-linear/usage.vue
index 9b374fcc9ea..f0f3a6305e7 100644
--- a/packages/docs/src/examples/v-progress-linear/usage.vue
+++ b/packages/docs/src/examples/v-progress-linear/usage.vue
@@ -12,7 +12,7 @@
diff --git a/packages/docs/src/i18n/messages/en.json b/packages/docs/src/i18n/messages/en.json
index 9edd159e23d..045129a726d 100644
--- a/packages/docs/src/i18n/messages/en.json
+++ b/packages/docs/src/i18n/messages/en.json
@@ -359,5 +359,5 @@
"view-source": "View source",
"viewport": "Viewport",
"vuetify": "Vuetify",
- "x": "X"
+ "x": "Xitter"
}
diff --git a/packages/docs/src/pages/en/components/banners.md b/packages/docs/src/pages/en/components/banners.md
index 083307c7822..778773b25e3 100644
--- a/packages/docs/src/pages/en/components/banners.md
+++ b/packages/docs/src/pages/en/components/banners.md
@@ -85,12 +85,12 @@ Banners may have one or two text buttons that don't stand out that much.
#### Icon
-The icon slot allows you to to explicitly control the content and functionality within it.
+The icon slot allows you to explicitly control the content and functionality within it.
#### Prepend
-The prepend slot allows you to to explicitly control the content and functionality within it. Icons also help to emphasize a banner's message.
+The prepend slot allows you to explicitly control the content and functionality within it. Icons also help to emphasize a banner's message.
diff --git a/packages/docs/src/pages/en/components/combobox.md b/packages/docs/src/pages/en/components/combobox.md
index 2e60804297a..8795c35c656 100644
--- a/packages/docs/src/pages/en/components/combobox.md
+++ b/packages/docs/src/pages/en/components/combobox.md
@@ -71,3 +71,11 @@ Previously known as **tags** - user is allowed to enter more than one value.
In this example we utilize a custom **no-data** slot to provide context to the user when searching / creating items.
+
+### Misc
+
+#### Advanced custom options
+
+The `v-combobox` improves upon the added functionality from `v-select` and `v-autocomplete`. This provides you with an expansive interface to create truly customized implementations. This example takes advantage of some more advanced features such as a custom **filter** algorithm, inline list editing and dynamic input items.
+
+
diff --git a/packages/docs/src/pages/en/components/file-upload.md b/packages/docs/src/pages/en/components/file-upload.md
new file mode 100644
index 00000000000..dc5f24402eb
--- /dev/null
+++ b/packages/docs/src/pages/en/components/file-upload.md
@@ -0,0 +1,99 @@
+---
+emphasized: true
+meta:
+ title: File upload
+ description: The file upload component is a drag and drop area for uploading files.
+ keywords: file uploading, file upload, file drag and drop, file drop area, file dropzone, file upload component
+related:
+ - /components/buttons/
+ - /components/file-inputs/
+ - /components/sheets/
+features:
+ report: true
+ label: 'C: VFileUpload'
+ github: '/labs/VFileUpload/'
+---
+
+# File upload
+
+
+
+::: warning
+
+This feature requires [v3.7.6](/getting-started/release-notes/?version=v3.7.6)
+
+:::
+
+## Installation
+
+Labs components require a manual import and installation of the component.
+
+```js { resource="src/plugins/vuetify.js" }
+import { VFileUpload } from 'vuetify/labs/VFileUpload'
+
+export default createVuetify({
+ components: {
+ VFileUpload,
+ },
+})
+```
+
+## Usage
+
+The `v-file-upload` component is a drag and drop area for uploading files. It can be customized with slots and has support for density and multiple styles.
+
+
+
+
+
+## API
+
+| Component | Description |
+| - | - |
+| [v-file-upload](/api/v-file-upload/) | Primary Component |
+| [v-file-upload-item](/api/v-file-upload-item/) | Item Component |
+| [v-file-input](/api/v-file-input/) | File input component |
+
+
+
+## Guide
+
+The v-file-upload component is a more visual counterpart to the [v-file-input](/components/file-inputs/) component. It provides a drag and drop area for files, and can be customized with slots.
+
+### Props
+
+Utilize various properties to customize the look and feel of the `v-file-upload` component.
+
+#### Density
+
+The **density** prop is used to control the vertical space the upload takes up.
+
+
+
+#### Content
+
+Use the **browse-text**, **divider-text**, **icon**, **title**, or **subtitle** props to customize the text displayed in the component.
+
+
+
+#### Disabled
+
+The **disabled** property reduces the opacity of the component and prevents interaction.
+
+
+
+#### Scrim
+
+The **scrim** property allows you to set a colored scrim when hovering over the component with files.
+
+
+
+### Slots
+
+The `v-file-upload` component has several slots that can be used to customize the component.
+
+#### Item
+
+The **item** slot is used to customize the appearance of the file item.
+
+
diff --git a/packages/vuetify/package.json b/packages/vuetify/package.json
index 4a18c195cf6..2992728a82a 100755
--- a/packages/vuetify/package.json
+++ b/packages/vuetify/package.json
@@ -1,7 +1,7 @@
{
"name": "vuetify",
"description": "Vue Material Component Framework",
- "version": "3.7.4",
+ "version": "3.7.6",
"author": {
"name": "John Leider",
"email": "john@vuetifyjs.com"
diff --git a/packages/vuetify/src/components/VAppBar/__tests__/VAppBar.spec.browser.tsx b/packages/vuetify/src/components/VAppBar/__tests__/VAppBar.spec.browser.tsx
new file mode 100644
index 00000000000..ad0b8616db1
--- /dev/null
+++ b/packages/vuetify/src/components/VAppBar/__tests__/VAppBar.spec.browser.tsx
@@ -0,0 +1,98 @@
+// Components
+import { VAppBar } from '..'
+import { VLayout } from '@/components/VLayout'
+import { VMain } from '@/components/VMain'
+
+// Utilities
+import { render, screen, scroll } from '@test'
+import { ref } from 'vue'
+
+describe('VAppBar', () => {
+ it('allows custom height', async () => {
+ const height = ref(64)
+ render(() => (
+
+
+
+ ))
+
+ expect.element(screen.getByCSS('.v-app-bar')).toHaveStyle({ height: '64px' })
+
+ height.value = 128
+ expect.element(screen.getByCSS('.v-app-bar')).toHaveStyle({ height: '128px' })
+ })
+
+ it('supports density', async () => {
+ const density = ref('default')
+ render(() => (
+
+
+
+ ))
+
+ expect.element(screen.getByCSS('.v-app-bar')).toHaveStyle({ height: '64px' })
+
+ density.value = 'prominent'
+ expect.element(screen.getByCSS('.v-app-bar')).toHaveStyle({ height: '128px' })
+
+ density.value = 'comfortable'
+ expect.element(screen.getByCSS('.v-app-bar')).toHaveStyle({ height: '56px' })
+
+ density.value = 'compact'
+ expect.element(screen.getByCSS('.v-app-bar')).toHaveStyle({ height: '48px' })
+ })
+
+ describe('scroll behavior', () => {
+ it('hides on scroll', async () => {
+ const scrollBehavior = ref()
+ render(() => (
+
+
+
+
+ ))
+
+ expect.element(screen.getByCSS('.v-app-bar')).toBeVisible()
+
+ await scroll({ top: 500 })
+ await scroll({ top: 250 })
+ expect.element(screen.getByCSS('.v-app-bar')).toBeVisible()
+
+ await scroll({ top: 0 })
+ expect.element(screen.getByCSS('.v-app-bar')).toBeVisible()
+ })
+
+ it('should hide correctly when scroll to the bottom', async () => {
+ render(() => (
+
+
+
+ { Array.from({ length: 7 }, () => (
+ box
+ ))}
+
+
+ ))
+
+ expect.element(screen.getByCSS('.v-app-bar')).toBeVisible()
+
+ await scroll({ top: 1000 })
+ expect.element(screen.getByCSS('.v-app-bar')).not.toBeVisible()
+ })
+
+ it('collapses', async () => {
+ render(() => (
+
+
+
+
+ ))
+
+ expect.element(screen.getByCSS('.v-app-bar')).toBeVisible()
+
+ await scroll({ top: 500 })
+ await scroll({ top: 0 })
+ expect.element(screen.getByCSS('.v-app-bar')).not.toHaveClass('v-toolbar--collapse')
+ })
+ })
+})
diff --git a/packages/vuetify/src/components/VAppBar/__tests__/VAppBar.spec.cy.tsx b/packages/vuetify/src/components/VAppBar/__tests__/VAppBar.spec.cy.tsx
deleted file mode 100644
index a9efd0b63d7..00000000000
--- a/packages/vuetify/src/components/VAppBar/__tests__/VAppBar.spec.cy.tsx
+++ /dev/null
@@ -1,190 +0,0 @@
-///
-
-// Components
-import { VAppBar } from '..'
-import { VLayout } from '@/components/VLayout'
-import { VMain } from '@/components/VMain'
-
-// Utilities
-import { ref } from 'vue'
-
-// Constants
-const SCROLL_OPTIONS = { ensureScrollable: true, duration: 50 }
-
-describe('VAppBar', () => {
- it('allows custom height', () => {
- cy
- .mount(({ height }: any) => (
-
-
-
- ))
- .get('.v-app-bar').should('have.css', 'height', '64px')
- .setProps({ height: 128 })
- .get('.v-app-bar').should('have.css', 'height', '128px')
- })
-
- it('supports density', () => {
- cy
- .mount(({ density = 'default' }: any) => (
-
-
-
- ))
- .get('.v-app-bar').should('have.css', 'height', '64px')
- .setProps({ density: 'prominent' })
- .get('.v-app-bar').should('have.css', 'height', '128px')
- .setProps({ density: 'comfortable' })
- .get('.v-app-bar').should('have.css', 'height', '56px')
- .setProps({ density: 'compact' })
- .get('.v-app-bar').should('have.css', 'height', '48px')
- })
-
- it('is hidden on mount', () => {
- const model = ref(false)
-
- cy
- .mount(() => (
-
-
-
- ))
- .get('.v-app-bar')
- .should('not.be.visible')
- .then(() => (model.value = true))
- .get('.v-app-bar')
- .should('be.visible')
- })
-
- describe('scroll behavior', () => {
- it('hides', () => {
- cy.mount(({ scrollBehavior }: any) => (
-
-
-
-
-
- ))
- .setProps({ scrollBehavior: 'hide' })
- .get('.v-app-bar').should('be.visible')
- .window().scrollTo(0, 500, SCROLL_OPTIONS)
- .get('.v-app-bar').should('not.be.visible')
- .window().scrollTo(0, 250, SCROLL_OPTIONS)
- .get('.v-app-bar').should('be.visible')
- .window().scrollTo(0, 0, SCROLL_OPTIONS)
- .get('.v-app-bar').should('be.visible')
-
- .setProps({ scrollBehavior: 'hide inverted' })
- .get('.v-app-bar').should('not.be.visible')
- .window().scrollTo(0, 500, SCROLL_OPTIONS)
- .get('.v-app-bar').should('be.visible')
- .window().scrollTo(0, 250, SCROLL_OPTIONS)
- .get('.v-app-bar').should('not.be.visible')
- .window().scrollTo(0, 0, SCROLL_OPTIONS)
- .get('.v-app-bar').should('not.be.visible')
- })
-
- it('should hide correctly when scroll to the bottom', () => {
- cy.mount(({ scrollBehavior }: any) => (
-
-
-
-
- {
- Array.from({ length: 7 }, () => (
-
- box
-
- ))
- }
-
-
- ))
- .setProps({ scrollBehavior: 'hide' })
- .get('.v-app-bar').should('be.visible')
- .window().scrollTo('bottom')
- .get('.v-app-bar').should('not.be.visible')
- })
-
- it('collapses', () => {
- cy.mount(({ scrollBehavior }: any) => (
-
-
-
-
-
- ))
- .setProps({ scrollBehavior: 'collapse' })
- .get('.v-app-bar').should('be.visible')
- .get('.v-app-bar').should('have.not.class', 'v-toolbar--collapse')
- .window().scrollTo(0, 500, SCROLL_OPTIONS)
- .get('.v-app-bar').should('have.class', 'v-toolbar--collapse')
- .window().scrollTo(0, 0, SCROLL_OPTIONS)
-
- .setProps({ scrollBehavior: 'collapse inverted' })
- .get('.v-app-bar').should('be.visible')
- .get('.v-app-bar').should('have.class', 'v-toolbar--collapse')
- .window().scrollTo(0, 500, SCROLL_OPTIONS)
- .get('.v-app-bar').should('not.have.class', 'v-toolbar--collapse')
- .window().scrollTo(0, 0, SCROLL_OPTIONS)
- })
-
- it('elevates', () => {
- cy.mount(({ scrollBehavior }: any) => (
-
-
-
-
-
- ))
- .setProps({ scrollBehavior: 'elevate' })
- .get('.v-app-bar').should('have.class', 'v-toolbar--flat')
- .window().scrollTo(0, 500, SCROLL_OPTIONS)
- .get('.v-app-bar').should('not.have.class', 'v-toolbar--flat')
- .window().scrollTo(0, 0, SCROLL_OPTIONS)
-
- .setProps({ scrollBehavior: 'elevate inverted' })
- .get('.v-app-bar').should('not.have.class', 'v-toolbar--flat')
- .window().scrollTo(0, 500, SCROLL_OPTIONS)
- .get('.v-app-bar').should('have.class', 'v-toolbar--flat')
- .window().scrollTo(0, 0, SCROLL_OPTIONS)
- })
-
- it('fades image', () => {
- cy.mount(({ scrollBehavior, image }: any) => (
-
-
-
-
-
- ))
- .setProps({
- image: 'https://picsum.photos/1920/1080?random',
- scrollBehavior: 'fade-image',
- })
- .get('.v-toolbar__image').should('have.css', 'opacity', '1')
- .window().scrollTo(0, 150, SCROLL_OPTIONS)
- .get('.v-toolbar__image').should('have.css', 'opacity', '0.5')
- .window().scrollTo(0, 300, SCROLL_OPTIONS)
- .get('.v-toolbar__image').should('have.css', 'opacity', '0')
- .window().scrollTo(0, 60, SCROLL_OPTIONS)
- .get('.v-toolbar__image').should('have.css', 'opacity', '0.8')
- .window().scrollTo(0, 0, SCROLL_OPTIONS)
- .get('.v-toolbar__image').should('have.css', 'opacity', '1')
-
- .setProps({ scrollBehavior: 'fade-image inverted' })
- .get('.v-toolbar__image').should('have.css', 'opacity', '0')
- .window().scrollTo(0, 150, SCROLL_OPTIONS)
- .get('.v-toolbar__image').should('have.css', 'opacity', '0.5')
- .window().scrollTo(0, 300, SCROLL_OPTIONS)
- .get('.v-toolbar__image').should('have.css', 'opacity', '1')
- .window().scrollTo(0, 60, SCROLL_OPTIONS)
- .get('.v-toolbar__image').should('have.css', 'opacity', '0.2')
- .window().scrollTo(0, 0, SCROLL_OPTIONS)
- .get('.v-toolbar__image').should('have.css', 'opacity', '0')
- })
- })
-})
diff --git a/packages/vuetify/src/components/VAutocomplete/VAutocomplete.tsx b/packages/vuetify/src/components/VAutocomplete/VAutocomplete.tsx
index d9118990211..ea4ad190782 100644
--- a/packages/vuetify/src/components/VAutocomplete/VAutocomplete.tsx
+++ b/packages/vuetify/src/components/VAutocomplete/VAutocomplete.tsx
@@ -166,7 +166,7 @@ export const VAutocomplete = genericComponent isPristine.value ? '' : search.value)
const displayItems = computed(() => {
@@ -192,7 +192,7 @@ export const VAutocomplete = genericComponent (
(props.hideNoData && !displayItems.value.length) ||
- props.readonly || form?.isReadonly.value
+ form.isReadonly.value || form.isDisabled.value
))
const listRef = ref()
@@ -219,20 +219,17 @@ export const VAutocomplete = genericComponent -1 ||
- ['Enter', 'ArrowDown', 'ArrowUp'].includes(e.key)
- ) {
+ if (['Enter', 'ArrowDown', 'ArrowUp'].includes(e.key)) {
e.preventDefault()
}
@@ -265,6 +262,7 @@ export const VAutocomplete = genericComponent
+
))}
{ ({ item, index, itemRef }) => {
const itemProps = mergeProps(item.props, {
ref: itemRef,
- key: index,
+ key: item.value,
active: (highlightFirst.value && index === 0) ? true : undefined,
onClick: () => select(item, null),
})
diff --git a/packages/vuetify/src/components/VBadge/VBadge.sass b/packages/vuetify/src/components/VBadge/VBadge.sass
index 0c815887d28..2d83462679c 100644
--- a/packages/vuetify/src/components/VBadge/VBadge.sass
+++ b/packages/vuetify/src/components/VBadge/VBadge.sass
@@ -10,6 +10,7 @@
align-items: center
display: inline-flex
border-radius: $badge-border-radius
+ font-family: $badge-font-family
font-size: $badge-font-size
font-weight: $badge-font-weight
height: $badge-height
@@ -25,6 +26,9 @@
@include tools.theme($badge-theme...)
+ &:has(.v-icon)
+ padding: $badge-icon-padding
+
.v-badge--bordered &
&::after
border-radius: inherit
diff --git a/packages/vuetify/src/components/VBottomNavigation/__tests__/VBottomNavigation.spec.browser.tsx b/packages/vuetify/src/components/VBottomNavigation/__tests__/VBottomNavigation.spec.browser.tsx
new file mode 100644
index 00000000000..184c969cbeb
--- /dev/null
+++ b/packages/vuetify/src/components/VBottomNavigation/__tests__/VBottomNavigation.spec.browser.tsx
@@ -0,0 +1,50 @@
+// Components
+import { VBottomNavigation } from '../VBottomNavigation'
+import { VLayout } from '@/components/VLayout'
+
+// Utilities
+import { render, screen } from '@test'
+import { ref } from 'vue'
+
+describe('VBottomNavigation', () => {
+ it('allows custom height', async () => {
+ const height = ref(200)
+ render(() => (
+
+
+
+ ))
+ const navigation = screen.getByCSS('.v-bottom-navigation')
+ expect.element(navigation).toHaveStyle({ height: '200px' })
+ height.value = 150
+ expect.element(navigation).toHaveStyle({ height: '150px' })
+ })
+
+ it('supports density', async () => {
+ const density = ref('default')
+ render(() => (
+
+
+
+ ))
+ const navigation = screen.getByCSS('.v-bottom-navigation')
+ expect.element(navigation).toHaveStyle({ height: '56px' })
+ density.value = 'comfortable'
+ expect.element(navigation).toHaveStyle({ height: '48px' })
+ density.value = 'comfortable'
+ expect.element(navigation).toHaveStyle({ height: '40px' })
+ })
+
+ it('is not visible when inactive', async () => {
+ const active = ref(true)
+ render(() => (
+
+
+
+ ))
+ const navigation = screen.getByCSS('.v-bottom-navigation')
+ expect.element(navigation).toHaveClass('v-bottom-navigation--active')
+ active.value = false
+ expect.element(navigation).not.toHaveClass('v-bottom-navigation--active')
+ })
+})
diff --git a/packages/vuetify/src/components/VBottomNavigation/__tests__/VBottomNavigation.spec.cy.tsx b/packages/vuetify/src/components/VBottomNavigation/__tests__/VBottomNavigation.spec.cy.tsx
deleted file mode 100644
index dde80eb165a..00000000000
--- a/packages/vuetify/src/components/VBottomNavigation/__tests__/VBottomNavigation.spec.cy.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-///
-
-// Components
-import { VBottomNavigation } from '..'
-import { VLayout } from '@/components/VLayout'
-
-describe('VBottomNavigation', () => {
- it('should allow custom height', () => {
- cy.mount(() => (
-
-
-
- ))
-
- cy.get('.v-bottom-navigation').should('have.css', 'height', '200px')
- })
-
- it('should support density', () => {
- cy.mount(({ density }: any) => (
-
-
-
- ))
-
- cy.get('.v-bottom-navigation').should('have.css', 'height', '56px')
- .setProps({ density: 'comfortable' })
- .get('.v-bottom-navigation').should('have.css', 'height', '48px')
- .setProps({ density: 'compact' })
- .get('.v-bottom-navigation').should('have.css', 'height', '40px')
- })
-
- it('should not be visible if active is false', () => {
- cy.mount(() => (
-
-
-
- ))
-
- cy.get('.v-bottom-navigation').should('not.be.visible')
- })
-})
diff --git a/packages/vuetify/src/components/VBottomSheet/__tests__/VBottomSheet.spec.browser.tsx b/packages/vuetify/src/components/VBottomSheet/__tests__/VBottomSheet.spec.browser.tsx
new file mode 100644
index 00000000000..5f618e4095e
--- /dev/null
+++ b/packages/vuetify/src/components/VBottomSheet/__tests__/VBottomSheet.spec.browser.tsx
@@ -0,0 +1,48 @@
+// Components
+import { VBottomSheet } from '..'
+
+// Utilities
+import { render, screen } from '@test'
+import { ref } from 'vue'
+
+describe('VBottomSheet', () => {
+ it('renders properly with default props', async () => {
+ render(() => (
+
+ Content inside bottom sheet
+
+ ))
+
+ const bottomSheet = screen.getByCSS('.v-bottom-sheet')
+ expect.element(bottomSheet).toBeVisible()
+ expect.element(bottomSheet).not.toHaveClass('v-bottom-sheet--inset')
+ expect.element(bottomSheet).toHaveTextContent('Content inside bottom sheet')
+ })
+
+ it('applies inset class when inset prop is true', async () => {
+ const inset = ref(false)
+
+ render(() => (
+
+ Content inside bottom sheet
+
+ ))
+
+ const bottomSheet = screen.getByCSS('.v-bottom-sheet')
+ expect.element(bottomSheet).not.toHaveClass('v-bottom-sheet--inset')
+
+ inset.value = true
+ expect.element(bottomSheet).toHaveClass('v-bottom-sheet--inset')
+ })
+
+ it('applies custom styles and classes', async () => {
+ render(() => (
+
+ Custom styles
+
+ ))
+ const bottomSheet = screen.getByCSS('.v-bottom-sheet')
+ expect.element(bottomSheet).toHaveClass('custom-class')
+ expect.element(bottomSheet).toHaveStyle({ color: 'rgb(255, 0, 0)' })
+ })
+})
diff --git a/packages/vuetify/src/components/VBottomSheet/__tests__/VBottomSheet.spec.cy.tsx b/packages/vuetify/src/components/VBottomSheet/__tests__/VBottomSheet.spec.cy.tsx
deleted file mode 100644
index 51f41a0a6d9..00000000000
--- a/packages/vuetify/src/components/VBottomSheet/__tests__/VBottomSheet.spec.cy.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-///
-
-// Components
-import { VBottomSheet } from '..'
-import { VSheet } from '@/components/VSheet'
-
-// Tests
-describe('VBottomSheet', () => {
- it('reduces maximum width with the inset prop', () => {
- cy.mount(({ inset }: any) => (
-
-
- Lorem ipsum dolor sit amet consectetur adipisicing elit. Sed hic, iusto tenetur rerum eum libero numquam reprehenderit
-
-
- ))
- .get('.v-bottom-sheet')
- .should('have.not.class', 'v-bottom-sheet--inset')
- .setProps({ inset: true })
- .get('.v-bottom-sheet')
- .should('have.class', 'v-bottom-sheet--inset')
- .should('have.css', 'max-width', '70%')
- })
-})
diff --git a/packages/vuetify/src/components/VBreadcrumbs/__tests__/VBreadcrumbs.spec.browser.tsx b/packages/vuetify/src/components/VBreadcrumbs/__tests__/VBreadcrumbs.spec.browser.tsx
new file mode 100644
index 00000000000..6cbdf6147dc
--- /dev/null
+++ b/packages/vuetify/src/components/VBreadcrumbs/__tests__/VBreadcrumbs.spec.browser.tsx
@@ -0,0 +1,113 @@
+// Components
+import { VBreadcrumbs } from '../VBreadcrumbs'
+import { VBreadcrumbsDivider } from '../VBreadcrumbsDivider'
+import { VBreadcrumbsItem } from '../VBreadcrumbsItem'
+
+// Utilities
+import { render, screen } from '@test'
+
+describe('VBreadcrumbs', () => {
+ it('should use item slot', () => {
+ render(() => (
+
+ {{
+ title: ({ item }: any) => `${item.title}!`,
+ }}
+
+ ))
+
+ const items = screen.getAllByText(/hello!|world!/i)
+ expect(items).toHaveLength(2)
+ expect.element(items[0]).toHaveTextContent('hello!')
+ })
+
+ it('should use divider slot', () => {
+ render(() => (
+
+ {{
+ divider: () => '-',
+ }}
+
+ ))
+
+ expect(screen.getByText('-')).toBeVisible()
+ })
+
+ it('should use bg-color', () => {
+ render(() => (
+
+ ))
+ expect(screen.getByCSS('.v-breadcrumbs')).toHaveClass('bg-primary')
+ })
+
+ it('should use color', () => {
+ render(() => (
+
+ ))
+
+ const items = screen.getAllByCSS('.v-breadcrumbs-item')
+ items.forEach(item => expect(item).toHaveClass('text-primary'))
+ })
+
+ it('should render link if href is set', () => {
+ render(() => (
+
+ ))
+
+ const links = screen.getAllByCSS('a.v-breadcrumbs-item--link')
+ links.forEach(link => expect(link).toHaveAttribute('href'))
+ })
+
+ it('should apply active color', async () => {
+ render(() => (
+
+
+
+
+ ))
+ // Initial check for the active color class
+ expect(screen.getByCSS('.v-breadcrumbs-item.text-primary')).toBeInTheDocument()
+
+ const items = screen.getAllByCSS('.v-breadcrumbs-item')
+ expect.element(items[0]).toHaveClass('text-primary')
+ })
+ it('should disable last item by default if using items prop', () => {
+ render(() => (
+
+ ))
+
+ const lastItem = screen.getByCSS('.v-breadcrumbs-item:last-child')
+ expect.element(lastItem).toHaveClass('v-breadcrumbs-item--disabled')
+ })
+
+ it('should override last item disabled by default', () => {
+ render(() => (
+
+ ))
+
+ const lastItem = screen.getByCSS('.v-breadcrumbs-item:last-child')
+ expect.element(lastItem).not.toHaveClass('v-breadcrumbs-item--disabled')
+ })
+
+ it('should provide default divider', () => {
+ render(() => (
+
+
+
+
+
+
+
+ ))
+
+ expect(screen.getByText('/')).toBeInTheDocument()
+ expect(screen.getByText('-')).toBeInTheDocument()
+ })
+})
diff --git a/packages/vuetify/src/components/VBreadcrumbs/__tests__/VBreadcrumbs.spec.cy.tsx b/packages/vuetify/src/components/VBreadcrumbs/__tests__/VBreadcrumbs.spec.cy.tsx
deleted file mode 100644
index 0f2f228f1af..00000000000
--- a/packages/vuetify/src/components/VBreadcrumbs/__tests__/VBreadcrumbs.spec.cy.tsx
+++ /dev/null
@@ -1,193 +0,0 @@
-///
-
-// Components
-import { VBreadcrumbs } from '..'
-import { Application } from '../../../../cypress/templates'
-import { VBreadcrumbsDivider } from '../VBreadcrumbsDivider'
-import { VBreadcrumbsItem } from '../VBreadcrumbsItem'
-
-// Utilities
-import { createRouter, createWebHistory } from 'vue-router'
-
-describe('VBreadcrumbs', () => {
- it('should use item slot', () => {
- cy.mount(() => (
-
-
- {{
- title: ({ item }: any) => `${item.title}!`,
- }}
-
-
- ))
-
- cy.get('.v-breadcrumbs-item').should('have.length', 2).eq(0).should('have.text', 'hello!')
- })
-
- it('should use divider slot', () => {
- cy.mount(() => (
-
-
- {{
- divider: () => '-',
- }}
-
-
- ))
-
- cy.get('.v-breadcrumbs-divider').should('have.length', 1).eq(0).should('have.text', '-')
- })
-
- it('should render icon', () => {
- cy.mount(() => (
-
-
-
- ))
-
- cy.get('.v-icon').should('exist').should('have.class', 'mdi-home').should('have.length', 1)
- })
-
- it('should use bg-color', () => {
- cy.mount(() => (
-
-
-
- ))
-
- cy.get('.v-breadcrumbs').should('have.class', 'bg-primary')
- })
-
- it('should use color', () => {
- cy.mount(() => (
-
-
-
- ))
-
- cy.get('.v-breadcrumbs-item').should('have.class', 'text-primary')
- })
-
- it('should render link if href is set', () => {
- cy.mount(() => (
-
-
-
- ))
-
- cy.get('a.v-breadcrumbs-item--link').should('exist').should('have.attr', 'href')
- })
-
- it('should use router if to is set', () => {
- const router = createRouter({
- history: createWebHistory(),
- routes: [
- {
- path: '/',
- component: { template: 'Home' },
- },
- {
- path: '/about',
- component: { template: 'About' },
- },
- ],
- })
-
- cy.mount(() => (
-
-
-
- ), {
- global: {
- plugins: [router],
- },
- })
-
- cy.get('.v-breadcrumbs').should('exist')
-
- cy.get('.v-breadcrumbs-item').should('exist').eq(0).click()
- cy.then(() => {
- expect(router.currentRoute.value.path).to.equal('/about')
- })
-
- // Return back to root to not break succeeding tests that don't have /about path.
- cy.get('.v-breadcrumbs').then(() => {
- router.push('/')
- })
- })
-
- it('should apply active color', () => {
- const router = createRouter({
- history: createWebHistory(),
- routes: [
- {
- path: '/',
- component: { template: 'Home' },
- },
- {
- path: '/world',
- component: { template: 'World' },
- },
- ],
- })
-
- cy.mount(() => (
-
-
-
- /
-
-
-
- ), {
- global: {
- plugins: [router],
- },
- })
-
- cy.get('.v-breadcrumbs-item').eq(0).should('have.class', 'text-primary')
-
- cy.get('.v-breadcrumbs').then(() => {
- router.push('/world')
- })
-
- cy.get('.v-breadcrumbs-item').eq(1).should('have.class', 'text-primary')
- })
-
- it('should disabled last item by default if using items prop', () => {
- cy.mount(() => (
-
-
-
- ))
-
- cy.get('.v-breadcrumbs-item').last().should('have.class', 'v-breadcrumbs-item--disabled')
- })
-
- it('should be possible to override last item disabled by default', () => {
- cy.mount(() => (
-
-
-
- ))
-
- cy.get('.v-breadcrumbs-item').last().should('not.have.class', 'v-breadcrumbs-item--disabled')
- })
-
- it('should provide default divider', () => {
- cy.mount(() => (
-
-
-
-
-
-
-
-
-
- ))
-
- cy.get('.v-breadcrumbs-divider').first().should('have.text', '/')
- cy.get('.v-breadcrumbs-divider').last().should('have.text', '-')
- })
-})
diff --git a/packages/vuetify/src/components/VBtnGroup/__tests__/VBtnGroup.spec.cy.tsx b/packages/vuetify/src/components/VBtnGroup/__tests__/VBtnGroup.spec.cy.tsx
deleted file mode 100644
index ca129c7683e..00000000000
--- a/packages/vuetify/src/components/VBtnGroup/__tests__/VBtnGroup.spec.cy.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-///
-
-// Components
-import { VBtnGroup } from '..'
-import { VBtn } from '@/components/VBtn'
-
-const colors = ['success', 'info', 'warning', 'error', 'invalid'] as const
-const densities = ['default', 'comfortable', 'compact'] as const
-const variants = ['elevated', 'flat', 'tonal', 'outlined', 'text', 'plain'] as const
-
-// TODO: screenshot tests
-describe('VBtnGroup', () => {
- describe('color', () => {
- it('should render set length', () => {
- cy.mount(() => (
- <>
- { colors.map(color => (
-
- { color } Button 1
- Button 2
- Button 3
-
- ))}
- >
- ))
- .get('.v-btn-group')
- .should('have.length', colors.length)
- })
- })
-
- describe('density', () => {
- it('supports density props', () => {
- cy.mount(() => (
- <>
- { densities.map(density => (
-
- { density } Button 1
- Button 2
- Button 3
-
- ))}
- >
- ))
- .get('.v-btn-group')
- .should('have.length', densities.length)
- })
- })
-
- describe('variant', () => {
- it('supports variant props', () => {
- cy.mount(() => (
- <>
- { variants.map(variant => (
-
- { variant } Button 1
- Button 2
- Button 3
-
- ))}
- >
- ))
- .get('.v-btn-group')
- .should('have.length', variants.length)
- })
- })
-})
diff --git a/packages/vuetify/src/components/VChip/VChip.tsx b/packages/vuetify/src/components/VChip/VChip.tsx
index 8c38e4e7bf6..0aac78b625b 100644
--- a/packages/vuetify/src/components/VChip/VChip.tsx
+++ b/packages/vuetify/src/components/VChip/VChip.tsx
@@ -68,7 +68,7 @@ export const makeVChipProps = propsFactory({
draggable: Boolean,
filter: Boolean,
filterIcon: {
- type: String,
+ type: IconValue,
default: '$complete',
},
label: Boolean,
@@ -186,6 +186,7 @@ export const VChip = genericComponent()({
'v-chip--link': isClickable.value,
'v-chip--filter': hasFilter,
'v-chip--pill': props.pill,
+ [`${props.activeClass}`]: props.activeClass && link.isActive?.value,
},
themeClasses.value,
borderClasses.value,
diff --git a/packages/vuetify/src/components/VChip/__tests__/VChip.spec.browser.tsx b/packages/vuetify/src/components/VChip/__tests__/VChip.spec.browser.tsx
index cfea3967483..bc822445272 100644
--- a/packages/vuetify/src/components/VChip/__tests__/VChip.spec.browser.tsx
+++ b/packages/vuetify/src/components/VChip/__tests__/VChip.spec.browser.tsx
@@ -1,4 +1,4 @@
-import { VChip } from '../'
+import { VChip } from '../VChip'
// Utilities
import { render, screen, userEvent } from '@test'
diff --git a/packages/vuetify/src/components/VColorPicker/VColorPickerPreview.tsx b/packages/vuetify/src/components/VColorPicker/VColorPickerPreview.tsx
index 7dc55ae0391..36d0e39592b 100644
--- a/packages/vuetify/src/components/VColorPicker/VColorPickerPreview.tsx
+++ b/packages/vuetify/src/components/VColorPicker/VColorPickerPreview.tsx
@@ -13,16 +13,17 @@ import { onUnmounted } from 'vue'
import { nullColor } from './util'
import {
defineComponent,
- HexToHSV,
HSVtoCSS,
+ parseColor,
propsFactory,
+ RGBtoHSV,
SUPPORTS_EYE_DROPPER,
useRender,
} from '@/util'
// Types
import type { PropType } from 'vue'
-import type { Hex, HSV } from '@/util'
+import type { HSV } from '@/util'
export const makeVColorPickerPreviewProps = propsFactory({
color: {
@@ -54,7 +55,7 @@ export const VColorPickerPreview = defineComponent({
const eyeDropper = new window.EyeDropper()
try {
const result = await eyeDropper.open({ signal: abortController.signal })
- const colorHexValue = HexToHSV(result.sRGBHex as Hex)
+ const colorHexValue = RGBtoHSV(parseColor(result.sRGBHex))
emit('update:color', { ...(props.color ?? nullColor), ...colorHexValue })
} catch (e) {}
}
diff --git a/packages/vuetify/src/components/VCombobox/VCombobox.tsx b/packages/vuetify/src/components/VCombobox/VCombobox.tsx
index 262ab674406..54014e7bc42 100644
--- a/packages/vuetify/src/components/VCombobox/VCombobox.tsx
+++ b/packages/vuetify/src/components/VCombobox/VCombobox.tsx
@@ -165,7 +165,7 @@ export const VCombobox = genericComponent !!(props.chips || slots.chip))
const hasSelectionSlot = computed(() => hasChips.value || !!slots.selection)
@@ -243,7 +243,7 @@ export const VCombobox = genericComponent (
(props.hideNoData && !displayItems.value.length) ||
- props.readonly || form?.isReadonly.value
+ form.isReadonly.value || form.isDisabled.value
))
const listRef = ref()
@@ -270,21 +270,18 @@ export const VCombobox = genericComponent -1 ||
- ['Enter', 'ArrowDown', 'ArrowUp'].includes(e.key)
- ) {
+ if (['Enter', 'ArrowDown', 'ArrowUp'].includes(e.key)) {
e.preventDefault()
}
@@ -326,6 +323,7 @@ export const VCombobox = genericComponent
+
))}
{ ({ item, index, itemRef }) => {
const itemProps = mergeProps(item.props, {
ref: itemRef,
- key: index,
+ key: item.value,
active: (highlightFirst.value && index === 0) ? true : undefined,
onClick: () => select(item, null),
})
diff --git a/packages/vuetify/src/components/VDataIterator/__tests__/VDataIterator.spec.browser.tsx b/packages/vuetify/src/components/VDataIterator/__tests__/VDataIterator.spec.browser.tsx
new file mode 100644
index 00000000000..829b3fcbad6
--- /dev/null
+++ b/packages/vuetify/src/components/VDataIterator/__tests__/VDataIterator.spec.browser.tsx
@@ -0,0 +1,42 @@
+// Utilities
+import { render, screen } from '@test'
+import { VDataIterator } from '../VDataIterator'
+
+const DESSERT_ITEMS = [
+ { name: 'Frozen Yogurt', calories: 159 },
+ { name: 'Ice cream sandwich', calories: 237 },
+ { name: 'Eclair', calories: 262 },
+ { name: 'Cupcake', calories: 305 },
+ { name: 'Gingerbread', calories: 356 },
+ { name: 'Jelly bean', calories: 375 },
+ { name: 'Lollipop', calories: 392 },
+ { name: 'Honeycomb', calories: 408 },
+ { name: 'Donut', calories: 452 },
+ { name: 'KitKat', calories: 518 },
+]
+
+describe('VDataIterator', () => {
+ it('should render items in the default slot', async () => {
+ render(() => (
+
+ { ({ groupedItems }) => {
+ return groupedItems.map(item => {
+ const dataItem = item as {
+ raw: { name: string, calories: number }
+ }
+ return (
+
+ { dataItem.raw.name } - { dataItem.raw.calories } calories
+
+ )
+ })
+ }}
+
+ ))
+
+ const listItems = screen.getAllByRole('listitem')
+ expect(listItems).toHaveLength(5)
+ expect.element(listItems[0]).toContain('Frozen Yogurt - 159 calories')
+ expect.element(listItems[4]).toContain('Gingerbread - 356 calories')
+ })
+})
diff --git a/packages/vuetify/src/components/VDataIterator/__tests__/VDataIterator.spec.cy.tsx b/packages/vuetify/src/components/VDataIterator/__tests__/VDataIterator.spec.cy.tsx
deleted file mode 100644
index c4815cf80ba..00000000000
--- a/packages/vuetify/src/components/VDataIterator/__tests__/VDataIterator.spec.cy.tsx
+++ /dev/null
@@ -1,105 +0,0 @@
-///
-
-import { Application } from '../../../../cypress/templates'
-import { VDataIterator } from '..'
-
-const DESSERT_ITEMS = [
- {
- name: 'Frozen Yogurt',
- calories: 159,
- fat: 6.0,
- carbs: 24,
- protein: 4.0,
- iron: '1%',
- },
- {
- name: 'Ice cream sandwich',
- calories: 237,
- fat: 9.0,
- carbs: 37,
- protein: 4.3,
- iron: '1%',
- },
- {
- name: 'Eclair',
- calories: 262,
- fat: 16.0,
- carbs: 23,
- protein: 6.0,
- iron: '7%',
- },
- {
- name: 'Cupcake',
- calories: 305,
- fat: 3.7,
- carbs: 67,
- protein: 4.3,
- iron: '8%',
- },
- {
- name: 'Gingerbread',
- calories: 356,
- fat: 16.0,
- carbs: 49,
- protein: 3.9,
- iron: '16%',
- },
- {
- name: 'Jelly bean',
- calories: 375,
- fat: 0.0,
- carbs: 94,
- protein: 0.0,
- iron: '0%',
- },
- {
- name: 'Lollipop',
- calories: 392,
- fat: 0.2,
- carbs: 98,
- protein: 0,
- iron: '2%',
- },
- {
- name: 'Honeycomb',
- calories: 408,
- fat: 3.2,
- carbs: 87,
- protein: 6.5,
- iron: '45%',
- },
- {
- name: 'Donut',
- calories: 452,
- fat: 25.0,
- carbs: 51,
- protein: 4.9,
- iron: '22%',
- },
- {
- name: 'KitKat',
- calories: 518,
- fat: 26.0,
- carbs: 65,
- protein: 7,
- iron: '6%',
- },
-]
-
-describe('VDataIterator', () => {
- it('should render default slot', () => {
- cy.mount(() => (
-
-
- {{
- default: ({ items }) => items.map(item => (
- { item.raw.name }
- )),
- }}
-
-
- ))
-
- cy.get('.dessert-item').should('have.length', 5)
- })
-})
diff --git a/packages/vuetify/src/components/VDataTable/__tests__/VDataTableVirtual.spec.browser.tsx b/packages/vuetify/src/components/VDataTable/__tests__/VDataTableVirtual.spec.browser.tsx
new file mode 100644
index 00000000000..9d0c0e78509
--- /dev/null
+++ b/packages/vuetify/src/components/VDataTable/__tests__/VDataTableVirtual.spec.browser.tsx
@@ -0,0 +1,43 @@
+// Utilities
+import { render, screen } from '@test'
+import { VDataTableVirtual } from '..'
+
+const DESSERT_HEADERS = [
+ { title: 'Dessert (100g serving)', key: 'name' },
+ { title: 'Calories', key: 'calories' },
+ { title: 'Fat (g)', key: 'fat' },
+ { title: 'Carbs (g)', key: 'carbs' },
+ { title: 'Protein (g)', key: 'protein' },
+ { title: 'Iron (%)', key: 'iron' },
+]
+
+const DESSERT_ITEMS = [
+ { name: 'Frozen Yogurt', calories: 159, fat: 6.0, carbs: 24, protein: 4.0, iron: '1%' },
+ { name: 'Ice cream sandwich', calories: 237, fat: 9.0, carbs: 37, protein: 4.3, iron: '1%' },
+ { name: 'Eclair', calories: 262, fat: 16.0, carbs: 23, protein: 6.0, iron: '7%' },
+ { name: 'Cupcake', calories: 305, fat: 3.7, carbs: 67, protein: 4.3, iron: '8%' },
+ { name: 'Gingerbread', calories: 356, fat: 16.0, carbs: 49, protein: 3.9, iron: '16%' },
+ { name: 'Jelly bean', calories: 375, fat: 0.0, carbs: 94, protein: 0.0, iron: '0%' },
+ { name: 'Lollipop', calories: 392, fat: 0.2, carbs: 98, protein: 0, iron: '2%' },
+ { name: 'Honeycomb', calories: 408, fat: 3.2, carbs: 87, protein: 6.5, iron: '45%' },
+ { name: 'Donut', calories: 452, fat: 25.0, carbs: 51, protein: 4.9, iron: '22%' },
+ { name: 'KitKat', calories: 518, fat: 26.0, carbs: 65, protein: 7, iron: '6%' },
+]
+
+describe('VDataTableVirtual', () => {
+ it('should render only visible items', async () => {
+ const items = [...new Array(10)].reduce(curr => {
+ curr.push(...DESSERT_ITEMS)
+ return curr
+ }, [])
+
+ render(() => (
+
+ ))
+
+ const rows = screen.getAllByRole('row')
+ expect(rows.length).toBeLessThan(items.length)
+ expect.element(rows[0]).not.toHaveStyle({ height: '0px' })
+ expect.element(rows[rows.length - 1]).toHaveStyle({ height: '0px' })
+ })
+})
diff --git a/packages/vuetify/src/components/VDataTable/__tests__/VDataTableVirtual.spec.cy.tsx b/packages/vuetify/src/components/VDataTable/__tests__/VDataTableVirtual.spec.cy.tsx
deleted file mode 100644
index cd62cae473f..00000000000
--- a/packages/vuetify/src/components/VDataTable/__tests__/VDataTableVirtual.spec.cy.tsx
+++ /dev/null
@@ -1,121 +0,0 @@
-///
-
-import { Application } from '../../../../cypress/templates'
-import { VDataTableVirtual } from '..'
-
-const DESSERT_HEADERS = [
- { title: 'Dessert (100g serving)', key: 'name' },
- { title: 'Calories', key: 'calories' },
- { title: 'Fat (g)', key: 'fat' },
- { title: 'Carbs (g)', key: 'carbs' },
- { title: 'Protein (g)', key: 'protein' },
- { title: 'Iron (%)', key: 'iron' },
-]
-
-const DESSERT_ITEMS = [
- {
- name: 'Frozen Yogurt',
- calories: 159,
- fat: 6.0,
- carbs: 24,
- protein: 4.0,
- iron: '1%',
- },
- {
- name: 'Ice cream sandwich',
- calories: 237,
- fat: 9.0,
- carbs: 37,
- protein: 4.3,
- iron: '1%',
- },
- {
- name: 'Eclair',
- calories: 262,
- fat: 16.0,
- carbs: 23,
- protein: 6.0,
- iron: '7%',
- },
- {
- name: 'Cupcake',
- calories: 305,
- fat: 3.7,
- carbs: 67,
- protein: 4.3,
- iron: '8%',
- },
- {
- name: 'Gingerbread',
- calories: 356,
- fat: 16.0,
- carbs: 49,
- protein: 3.9,
- iron: '16%',
- },
- {
- name: 'Jelly bean',
- calories: 375,
- fat: 0.0,
- carbs: 94,
- protein: 0.0,
- iron: '0%',
- },
- {
- name: 'Lollipop',
- calories: 392,
- fat: 0.2,
- carbs: 98,
- protein: 0,
- iron: '2%',
- },
- {
- name: 'Honeycomb',
- calories: 408,
- fat: 3.2,
- carbs: 87,
- protein: 6.5,
- iron: '45%',
- },
- {
- name: 'Donut',
- calories: 452,
- fat: 25.0,
- carbs: 51,
- protein: 4.9,
- iron: '22%',
- },
- {
- name: 'KitKat',
- calories: 518,
- fat: 26.0,
- carbs: 65,
- protein: 7,
- iron: '6%',
- },
-]
-
-describe('VDataTable', () => {
- it('should render only visible items', () => {
- const items = [...new Array(10)].reduce(curr => {
- curr.push(...DESSERT_ITEMS)
- return curr
- }, [])
- cy.mount(() => (
-
-
-
- ))
-
- cy.get('tbody tr')
- .should('have.length.lt', items.length)
- .get('tbody tr')
- .last()
- .should('have.css', 'height')
- .and('not.equal', '0px')
- .get('tbody tr')
- .first()
- .should('have.css', 'height')
- .and('equal', '0px')
- })
-})
diff --git a/packages/vuetify/src/components/VDatePicker/VDatePickerHeader.tsx b/packages/vuetify/src/components/VDatePicker/VDatePickerHeader.tsx
index 15e13d99bee..2149d8dd912 100644
--- a/packages/vuetify/src/components/VDatePicker/VDatePickerHeader.tsx
+++ b/packages/vuetify/src/components/VDatePicker/VDatePickerHeader.tsx
@@ -7,6 +7,7 @@ import { VDefaultsProvider } from '@/components/VDefaultsProvider'
// Composables
import { useBackgroundColor } from '@/composables/color'
+import { IconValue } from '@/composables/icons'
import { MaybeTransition } from '@/composables/transition'
// Utilities
@@ -20,7 +21,7 @@ export type VDatePickerHeaderSlots = {
}
export const makeVDatePickerHeaderProps = propsFactory({
- appendIcon: String,
+ appendIcon: IconValue,
color: String,
header: String,
transition: String,
diff --git a/packages/vuetify/src/components/VDialog/VDialog.sass b/packages/vuetify/src/components/VDialog/VDialog.sass
index e45e92a08df..3061d59b696 100644
--- a/packages/vuetify/src/components/VDialog/VDialog.sass
+++ b/packages/vuetify/src/components/VDialog/VDialog.sass
@@ -71,6 +71,12 @@
border-radius: 0
.v-dialog--scrollable > .v-overlay__content
+ > form
+ &,
+ > .v-card
+ max-height: 100%
+ max-width: 100%
+
&,
> form
&,
@@ -78,8 +84,6 @@
display: flex
flex: 1 1 100%
flex-direction: column
- max-height: 100%
- max-width: 100%
> .v-card > .v-card-text
backface-visibility: hidden
diff --git a/packages/vuetify/src/components/VField/VField.tsx b/packages/vuetify/src/components/VField/VField.tsx
index b3d658c127a..b12b064782e 100644
--- a/packages/vuetify/src/components/VField/VField.tsx
+++ b/packages/vuetify/src/components/VField/VField.tsx
@@ -223,7 +223,7 @@ export const VField = genericComponent(
useRender(() => {
const isOutlined = props.variant === 'outlined'
const hasPrepend = !!(slots['prepend-inner'] || props.prependInnerIcon)
- const hasClear = !!(props.clearable || slots.clear)
+ const hasClear = !!(props.clearable || slots.clear) && !props.disabled
const hasAppend = !!(slots['append-inner'] || props.appendInnerIcon || hasClear)
const label = () => (
slots.label
@@ -303,9 +303,11 @@ export const VField = genericComponent(
)}
-
- { label() }
-
+ { hasLabel.value && (
+
+ { label() }
+
+ )}
{ slots.default?.({
...slotProps.value,
diff --git a/packages/vuetify/src/components/VIcon/__tests__/VIcon.spec.browser.tsx b/packages/vuetify/src/components/VIcon/__tests__/VIcon.spec.browser.tsx
new file mode 100644
index 00000000000..74c828e877b
--- /dev/null
+++ b/packages/vuetify/src/components/VIcon/__tests__/VIcon.spec.browser.tsx
@@ -0,0 +1,59 @@
+import { VIcon } from '../VIcon'
+
+// Utilities
+import { render, screen } from '@test'
+
+describe('VIcon', () => {
+ describe('icon prop', () => {
+ it('should render icon from default set', () => {
+ render(() => )
+
+ const icon = screen.getByText('', { selector: '.mdi-home' })
+ expect(icon).toHaveClass('mdi-home')
+ expect(icon).toHaveClass('mdi')
+ })
+ })
+
+ describe('default slot', () => {
+ it('should render icon from default set', () => {
+ render(() => mdi-home )
+
+ const icon = screen.getByText('', { selector: '.mdi-home' })
+ expect(icon).toHaveClass('mdi-home')
+ expect(icon).toHaveClass('mdi')
+ })
+
+ it('should render default slot if no icon value is found', () => {
+ const Foo = () => (
+
+
+
+ )
+
+ render(() => (
+
+
+
+ ))
+
+ const svg = screen.getByText('', { selector: '.foo' })
+ expect(svg).toBeInTheDocument()
+ })
+ })
+
+ it('should render svg icon', () => {
+ render(() => )
+
+ const svg = screen.getByText('', { selector: 'svg' })
+ expect(svg).toBeInTheDocument()
+ const path = svg.querySelector('path')
+ expect(path).toHaveAttribute('d', 'M7,10L12,15L17,10H7Z')
+ })
+
+ it('should render class icon', () => {
+ render(() => )
+
+ const icon = screen.getByText('', { selector: '.foo' })
+ expect(icon).toHaveClass('foo')
+ })
+})
diff --git a/packages/vuetify/src/components/VIcon/__tests__/VIcon.spec.cy.tsx b/packages/vuetify/src/components/VIcon/__tests__/VIcon.spec.cy.tsx
deleted file mode 100644
index c1b34f9a4a7..00000000000
--- a/packages/vuetify/src/components/VIcon/__tests__/VIcon.spec.cy.tsx
+++ /dev/null
@@ -1,126 +0,0 @@
-///
-
-// Components
-import { VClassIcon } from '..'
-import { VIcon } from '../VIcon'
-
-// Icons
-import { aliases } from '@/iconsets/mdi'
-
-// Utilities
-import { defineComponent } from 'vue'
-
-describe('VIcon', () => {
- describe('icon prop', () => {
- it('should render icon from default set', () => {
- cy.mount(() => (
-
- ))
-
- cy.get('.v-icon').should('have.class', 'mdi')
- cy.get('.v-icon').should('have.class', 'mdi-home')
- })
-
- it('should render aliased icon', () => {
- cy.mount(() => (
-
- ), null, { icons: { aliases } })
-
- cy.get('.v-icon').should('have.class', 'mdi')
- cy.get('.v-icon').should('have.class', 'mdi-close')
- })
-
- it('should render icon from alternative set', () => {
- cy.mount(() => (
-
- ), null, {
- icons: {
- defaultSet: 'mdi',
- sets: {
- foo: {
- component: props => ,
- },
- },
- },
- })
-
- cy.get('.v-icon').should('have.class', 'bar')
- })
- })
-
- describe('default slot', () => {
- it('should render icon from default set', () => {
- cy.mount(() => (
- mdi-home
- ))
-
- cy.get('.v-icon').should('have.class', 'mdi')
- cy.get('.v-icon').should('have.class', 'mdi-home')
- })
-
- it('should render aliased icon', () => {
- cy.mount(() => (
- $close
- ), null, { icons: { aliases } })
-
- cy.get('.v-icon').should('have.class', 'mdi')
- cy.get('.v-icon').should('have.class', 'mdi-close')
- })
-
- it('should render icon from alternative set', () => {
- cy.mount(() => (
-
- foo:bar
-
- ), null, {
- icons: {
- defaultSet: 'mdi',
- sets: {
- foo: {
- component: props => ,
- },
- },
- },
- })
-
- cy.get('.v-icon').should('have.class', 'bar')
- })
-
- it('should render default slot if no icon value found', () => {
- const Foo = defineComponent({
- setup () {
- return () => (
-
-
-
- )
- },
- })
-
- cy.mount(() => (
-
- bar
-
- ))
-
- cy.get('.v-icon > svg.foo').should('exist')
- })
- })
-
- it('should render svg icon', () => {
- cy.mount(() => (
-
- ))
-
- cy.get('.v-icon svg').should('exist')
- cy.get('.v-icon path').should('have.attr', 'd', 'M7,10L12,15L17,10H7Z')
- })
-
- it('should render class icon', () => {
- cy.mount(() => (
-
- ))
-
- cy.get('.v-icon').should('have.class', 'foo')
- })
-})
diff --git a/packages/vuetify/src/components/VInput/VInput.sass b/packages/vuetify/src/components/VInput/VInput.sass
index 11ded05238b..449581bd1e8 100644
--- a/packages/vuetify/src/components/VInput/VInput.sass
+++ b/packages/vuetify/src/components/VInput/VInput.sass
@@ -33,7 +33,7 @@
.v-input--horizontal
grid-template-areas: "prepend control append" "a messages b"
grid-template-columns: max-content minmax(0, 1fr) max-content
- grid-template-rows: auto auto
+ grid-template-rows: 1fr auto
.v-input__prepend
margin-inline-end: $input-affix-margin-inside
diff --git a/packages/vuetify/src/components/VList/VList.tsx b/packages/vuetify/src/components/VList/VList.tsx
index cbdc04b2cce..c5cc5d8b490 100644
--- a/packages/vuetify/src/components/VList/VList.tsx
+++ b/packages/vuetify/src/components/VList/VList.tsx
@@ -13,6 +13,7 @@ import { provideDefaults } from '@/composables/defaults'
import { makeDensityProps, useDensity } from '@/composables/density'
import { makeDimensionProps, useDimension } from '@/composables/dimensions'
import { makeElevationProps, useElevation } from '@/composables/elevation'
+import { IconValue } from '@/composables/icons'
import { makeItemsProps } from '@/composables/list-items'
import { makeNestedProps, useNested } from '@/composables/nested/nested'
import { makeRoundedProps, useRounded } from '@/composables/rounded'
@@ -86,8 +87,8 @@ export const makeVListProps = propsFactory({
activeClass: String,
bgColor: String,
disabled: Boolean,
- expandIcon: String,
- collapseIcon: String,
+ expandIcon: IconValue,
+ collapseIcon: IconValue,
lines: {
type: [Boolean, String] as PropType<'one' | 'two' | 'three' | false>,
default: 'one',
diff --git a/packages/vuetify/src/components/VList/VListItem.tsx b/packages/vuetify/src/components/VList/VListItem.tsx
index dcd422e06ba..cf68ff0665b 100644
--- a/packages/vuetify/src/components/VList/VListItem.tsx
+++ b/packages/vuetify/src/components/VList/VListItem.tsx
@@ -27,7 +27,7 @@ import { genOverlays, makeVariantProps, useVariant } from '@/composables/variant
import { Ripple } from '@/directives/ripple'
// Utilities
-import { computed, watch } from 'vue'
+import { computed, onBeforeMount, watch } from 'vue'
import { deprecate, EventProp, genericComponent, propsFactory, useRender } from '@/util'
// Types
@@ -135,10 +135,11 @@ export const VListItem = genericComponent()({
(props.active || link.isActive?.value || (root.activatable.value ? isActivated.value : isSelected.value))
)
const isLink = computed(() => props.link !== false && link.isLink.value)
+ const isSelectable = computed(() => (!!list && (root.selectable.value || root.activatable.value || props.value != null)))
const isClickable = computed(() =>
!props.disabled &&
props.link !== false &&
- (props.link || link.isClickable.value || (!!list && (root.selectable.value || root.activatable.value || props.value != null)))
+ (props.link || link.isClickable.value || isSelectable.value)
)
const roundedProps = computed(() => props.rounded || props.nav)
@@ -148,15 +149,21 @@ export const VListItem = genericComponent()({
variant: props.variant,
}))
+ // useNestedItem doesn't call register until beforeMount,
+ // so this can't be an immediate watcher as we don't know parent yet
watch(() => link.isActive?.value, val => {
- if (val && parent.value != null) {
+ if (!val) return
+ handleActiveLink()
+ })
+ onBeforeMount(() => {
+ if (link.isActive?.value) handleActiveLink()
+ })
+ function handleActiveLink () {
+ if (parent.value != null) {
root.open(parent.value, true)
}
-
- if (val) {
- openOnSelect(val)
- }
- }, { immediate: true })
+ openOnSelect(true)
+ }
const { themeClasses } = provideTheme(props)
const { borderClasses } = useBorder(props)
@@ -244,7 +251,13 @@ export const VListItem = genericComponent()({
props.style,
]}
tabindex={ isClickable.value ? (list ? -2 : 0) : undefined }
- aria-selected={ root.activatable.value ? isActivated.value : isSelected.value }
+ aria-selected={
+ isSelectable.value ? (
+ root.activatable.value ? isActivated.value
+ : root.selectable.value ? isSelected.value
+ : isActive.value
+ ) : undefined
+ }
onClick={ onClick }
onKeydown={ isClickable.value && !isLink.value && onKeyDown }
v-ripple={ isClickable.value && props.ripple }
diff --git a/packages/vuetify/src/components/VProgressLinear/VProgressLinear.tsx b/packages/vuetify/src/components/VProgressLinear/VProgressLinear.tsx
index 62a43b9a7ce..a00f892dd03 100644
--- a/packages/vuetify/src/components/VProgressLinear/VProgressLinear.tsx
+++ b/packages/vuetify/src/components/VProgressLinear/VProgressLinear.tsx
@@ -141,7 +141,7 @@ export const VProgressLinear = genericComponent()({
aria-hidden={ props.active ? 'false' : 'true' }
aria-valuemin="0"
aria-valuemax={ props.max }
- aria-valuenow={ props.indeterminate ? undefined : normalizedValue.value }
+ aria-valuenow={ props.indeterminate ? undefined : Math.min(parseFloat(progress.value), max.value) }
onClick={ props.clickable && handleClick }
>
{ props.stream && (
diff --git a/packages/vuetify/src/components/VRangeSlider/VRangeSlider.tsx b/packages/vuetify/src/components/VRangeSlider/VRangeSlider.tsx
index a3e4e0a195d..f9fc67613fd 100644
--- a/packages/vuetify/src/components/VRangeSlider/VRangeSlider.tsx
+++ b/packages/vuetify/src/components/VRangeSlider/VRangeSlider.tsx
@@ -212,6 +212,7 @@ export const VRangeSlider = genericComponent()({
// and they are both at minimum value
// but only if focused from outside.
if (
+ max.value !== min.value &&
model.value[0] === model.value[1] &&
model.value[1] === min.value &&
e.relatedTarget !== stopThumbRef.value?.$el
@@ -247,6 +248,7 @@ export const VRangeSlider = genericComponent()({
// and they are both at maximum value
// but only if focused from outside.
if (
+ max.value !== min.value &&
model.value[0] === model.value[1] &&
model.value[0] === max.value &&
e.relatedTarget !== startThumbRef.value?.$el
diff --git a/packages/vuetify/src/components/VSelect/VSelect.tsx b/packages/vuetify/src/components/VSelect/VSelect.tsx
index 36483e21859..ad18b3bbf1f 100644
--- a/packages/vuetify/src/components/VSelect/VSelect.tsx
+++ b/packages/vuetify/src/components/VSelect/VSelect.tsx
@@ -167,7 +167,7 @@ export const VSelect = genericComponent model.value.map(selection => selection.value))
const isFocused = shallowRef(false)
const label = computed(() => menu.value ? props.closeText : props.openText)
@@ -184,7 +184,7 @@ export const VSelect = genericComponent (
(props.hideNoData && !displayItems.value.length) ||
- props.readonly || form?.isReadonly.value
+ form.isReadonly.value || form.isDisabled.value
))
const computedMenuProps = computed(() => {
@@ -215,7 +215,7 @@ export const VSelect = genericComponent
+
))}
{ ({ item, index, itemRef }) => {
const itemProps = mergeProps(item.props, {
ref: itemRef,
- key: index,
+ key: item.value,
onClick: () => select(item, null),
})
diff --git a/packages/vuetify/src/components/VSelectionControl/VSelectionControl.sass b/packages/vuetify/src/components/VSelectionControl/VSelectionControl.sass
index 20806de83d8..688cc0749a3 100644
--- a/packages/vuetify/src/components/VSelectionControl/VSelectionControl.sass
+++ b/packages/vuetify/src/components/VSelectionControl/VSelectionControl.sass
@@ -18,16 +18,12 @@
white-space: normal
word-break: break-word
height: 100%
+ opacity: 1
&--disabled
opacity: var(--v-disabled-opacity)
pointer-events: none
- &--error,
- &--disabled
- .v-label
- opacity: 1
-
&--error:not(.v-selection-control--disabled)
.v-label
color: rgb(var(--v-theme-error))
diff --git a/packages/vuetify/src/components/VSlideGroup/VSlideGroup.tsx b/packages/vuetify/src/components/VSlideGroup/VSlideGroup.tsx
index 950df1a444a..4663b1d95c0 100644
--- a/packages/vuetify/src/components/VSlideGroup/VSlideGroup.tsx
+++ b/packages/vuetify/src/components/VSlideGroup/VSlideGroup.tsx
@@ -279,26 +279,39 @@ export const VSlideGroup = genericComponent(
}
}
+ function getSiblingElement (el: HTMLElement | null, location: 'next' | 'prev') {
+ if (!el) return undefined
+ let sibling: HTMLElement | null = el
+ do {
+ sibling = sibling?.[location === 'next' ? 'nextElementSibling' : 'previousElementSibling'] as HTMLElement | null
+ } while (sibling?.hasAttribute('disabled'))
+ return sibling
+ }
+
function focus (location?: 'next' | 'prev' | 'first' | 'last') {
if (!contentRef.el) return
- let el: HTMLElement | undefined
+ let el: HTMLElement | null | undefined
if (!location) {
const focusable = focusableChildren(contentRef.el)
el = focusable[0]
} else if (location === 'next') {
- el = contentRef.el.querySelector(':focus')?.nextElementSibling as HTMLElement | undefined
+ el = getSiblingElement(contentRef.el.querySelector(':focus'), location)
if (!el) return focus('first')
} else if (location === 'prev') {
- el = contentRef.el.querySelector(':focus')?.previousElementSibling as HTMLElement | undefined
+ el = getSiblingElement(contentRef.el.querySelector(':focus'), location)
if (!el) return focus('last')
} else if (location === 'first') {
el = (contentRef.el.firstElementChild as HTMLElement)
+
+ if (el?.hasAttribute('disabled')) el = getSiblingElement(el, 'next')
} else if (location === 'last') {
el = (contentRef.el.lastElementChild as HTMLElement)
+
+ if (el?.hasAttribute('disabled')) el = getSiblingElement(el, 'prev')
}
if (el) {
diff --git a/packages/vuetify/src/components/VSlideGroup/__tests__/VSlideGroup.spec.cy.tsx b/packages/vuetify/src/components/VSlideGroup/__tests__/VSlideGroup.spec.cy.tsx
index 62bfd1a3794..934eb5774ff 100644
--- a/packages/vuetify/src/components/VSlideGroup/__tests__/VSlideGroup.spec.cy.tsx
+++ b/packages/vuetify/src/components/VSlideGroup/__tests__/VSlideGroup.spec.cy.tsx
@@ -8,6 +8,7 @@ import { VCard } from '@/components/VCard'
// Utilities
import { createRange } from '@/util'
+import { VBtn } from '../../VBtn'
describe('VSlideGroup', () => {
it('should support default scoped slot with selection', () => {
@@ -252,4 +253,24 @@ describe('VSlideGroup', () => {
cy.get('.item-7').should('exist').should('be.visible')
})
+
+ it('Skip disabled elements when moving focus', () => {
+ cy.mount(() => (
+
+
+ { createRange(5).map(i => (
+
+ { i }
+
+ ))}
+
+
+ ))
+
+ cy.get('.btn0').focus().type('{rightArrow}{rightArrow}')
+ cy.focused().should('have.class', 'btn4')
+
+ cy.focused().type('{leftArrow}')
+ cy.focused().should('have.class', 'btn1')
+ })
})
diff --git a/packages/vuetify/src/components/VSparkline/VBarline.tsx b/packages/vuetify/src/components/VSparkline/VBarline.tsx
index 7d765deb3b7..fcb2a039ae4 100644
--- a/packages/vuetify/src/components/VSparkline/VBarline.tsx
+++ b/packages/vuetify/src/components/VSparkline/VBarline.tsx
@@ -123,6 +123,7 @@ export const VBarline = genericComponent()({
const bars = computed(() => genBars(items.value, boundary.value))
const offsetX = computed(() => (Math.abs(bars.value[0].x - bars.value[1].x) - lineWidth.value) / 2)
+ const smooth = computed(() => typeof props.smooth === 'boolean' ? (props.smooth ? 2 : 0) : Number(props.smooth))
useRender(() => {
const gradientData = !props.gradient.slice().length ? [''] : props.gradient.slice().reverse()
@@ -155,8 +156,8 @@ export const VBarline = genericComponent()({
y={ item.y }
width={ lineWidth.value }
height={ item.height }
- rx={ typeof props.smooth === 'number' ? props.smooth : props.smooth ? 2 : 0 }
- ry={ typeof props.smooth === 'number' ? props.smooth : props.smooth ? 2 : 0 }
+ rx={ smooth.value }
+ ry={ smooth.value }
>
{ props.autoDraw && (
<>
diff --git a/packages/vuetify/src/components/VSparkline/VTrendline.tsx b/packages/vuetify/src/components/VSparkline/VTrendline.tsx
index 4cee7027c04..e3768fdc0ad 100644
--- a/packages/vuetify/src/components/VSparkline/VTrendline.tsx
+++ b/packages/vuetify/src/components/VSparkline/VTrendline.tsx
@@ -149,9 +149,11 @@ export const VTrendline = genericComponent()({
}, { immediate: true })
function genPath (fill: boolean) {
+ const smoothValue = typeof props.smooth === 'boolean' ? (props.smooth ? 8 : 0) : Number(props.smooth)
+
return _genPath(
genPoints(items.value, boundary.value),
- props.smooth ? 8 : Number(props.smooth),
+ smoothValue,
fill,
parseInt(props.height, 10)
)
diff --git a/packages/vuetify/src/components/VSparkline/util/line.ts b/packages/vuetify/src/components/VSparkline/util/line.ts
index 04c6e264bbc..61a3df59b5f 100644
--- a/packages/vuetify/src/components/VSparkline/util/line.ts
+++ b/packages/vuetify/src/components/VSparkline/util/line.ts
@@ -4,7 +4,7 @@ import { propsFactory } from '@/util'
// Types
import type { PropType } from 'vue'
-export type SparklineItem = number | { value: number }
+export type SparklineItem = string | number | { value: number }
export const makeLineProps = propsFactory({
autoDraw: Boolean,
@@ -55,7 +55,7 @@ export const makeLineProps = propsFactory({
default: 8,
},
showLabels: Boolean,
- smooth: Boolean,
+ smooth: [Boolean, String, Number],
width: {
type: [Number, String],
default: 300,
diff --git a/packages/vuetify/src/components/VStepper/VStepper.tsx b/packages/vuetify/src/components/VStepper/VStepper.tsx
index bdd67183429..d8cdbc408fe 100644
--- a/packages/vuetify/src/components/VStepper/VStepper.tsx
+++ b/packages/vuetify/src/components/VStepper/VStepper.tsx
@@ -15,6 +15,7 @@ import { makeVSheetProps, VSheet } from '@/components/VSheet/VSheet'
import { provideDefaults } from '@/composables/defaults'
import { makeDisplayProps, useDisplay } from '@/composables/display'
import { makeGroupProps, useGroup } from '@/composables/group'
+import { IconValue } from '@/composables/icons'
// Utilities
import { computed, toRefs } from 'vue'
@@ -48,10 +49,10 @@ export type VStepperSlots = {
export const makeStepperProps = propsFactory({
altLabels: Boolean,
bgColor: String,
- completeIcon: String,
- editIcon: String,
+ completeIcon: IconValue,
+ editIcon: IconValue,
editable: Boolean,
- errorIcon: String,
+ errorIcon: IconValue,
hideActions: Boolean,
items: {
type: Array as PropType,
diff --git a/packages/vuetify/src/components/VStepper/VStepperItem.tsx b/packages/vuetify/src/components/VStepper/VStepperItem.tsx
index 7873532a86c..686993a1885 100644
--- a/packages/vuetify/src/components/VStepper/VStepperItem.tsx
+++ b/packages/vuetify/src/components/VStepper/VStepperItem.tsx
@@ -7,6 +7,7 @@ import { VIcon } from '@/components/VIcon/VIcon'
// Composables
import { makeGroupItemProps, useGroupItem } from '@/composables/group'
+import { IconValue } from '@/composables/icons'
import { genOverlays } from '@/composables/variant'
// Directives
@@ -47,20 +48,20 @@ export const makeStepperItemProps = propsFactory({
subtitle: String,
complete: Boolean,
completeIcon: {
- type: String,
+ type: IconValue,
default: '$complete',
},
editable: Boolean,
editIcon: {
- type: String,
+ type: IconValue,
default: '$edit',
},
error: Boolean,
errorIcon: {
- type: String,
+ type: IconValue,
default: '$error',
},
- icon: String,
+ icon: IconValue,
ripple: {
type: [Boolean, Object] as PropType,
default: true,
@@ -141,6 +142,7 @@ export const VStepperItem = genericComponent()({
group?.selectedClass.value,
]}
disabled={ !props.editable }
+ type="button"
v-ripple={[
props.ripple && props.editable,
null,
diff --git a/packages/vuetify/src/components/VTextarea/VTextarea.sass b/packages/vuetify/src/components/VTextarea/VTextarea.sass
index 4f86f5742c9..b9bce78586b 100644
--- a/packages/vuetify/src/components/VTextarea/VTextarea.sass
+++ b/packages/vuetify/src/components/VTextarea/VTextarea.sass
@@ -43,6 +43,7 @@
opacity: 0
flex: 1
min-width: 0
+ height: 100%
transition: .15s opacity settings.$standard-easing
&:focus,
diff --git a/packages/vuetify/src/components/VVirtualScroll/VVirtualScroll.tsx b/packages/vuetify/src/components/VVirtualScroll/VVirtualScroll.tsx
index 74e143ca53f..67ce551f4f6 100644
--- a/packages/vuetify/src/components/VVirtualScroll/VVirtualScroll.tsx
+++ b/packages/vuetify/src/components/VVirtualScroll/VVirtualScroll.tsx
@@ -96,7 +96,7 @@ export const VVirtualScroll = genericComponent {
const children = computedItems.value.map(item => (
handleItemResize(item.index, height) }
>
diff --git a/packages/vuetify/src/composables/filter.ts b/packages/vuetify/src/composables/filter.ts
index 43d83e90237..d4abbedbea0 100644
--- a/packages/vuetify/src/composables/filter.ts
+++ b/packages/vuetify/src/composables/filter.ts
@@ -2,7 +2,7 @@
/* eslint-disable no-labels */
// Utilities
-import { computed, ref, unref, watchEffect } from 'vue'
+import { computed, shallowRef, unref, watchEffect } from 'vue'
import { getPropertyFromItem, propsFactory, wrapInArray } from '@/util'
// Types
@@ -139,8 +139,8 @@ export function useFilter (
customKeyFilter?: MaybeRef
}
) {
- const filteredItems: Ref = ref([])
- const filteredMatches: Ref>> = ref(new Map())
+ const filteredItems: Ref = shallowRef([])
+ const filteredMatches: Ref>> = shallowRef(new Map())
const transformedItems = computed(() => (
options?.transform
? unref(items).map(item => ([item, options.transform!(item)] as const))
diff --git a/packages/vuetify/src/composables/form.ts b/packages/vuetify/src/composables/form.ts
index 803c6d43bee..b87f5204b17 100644
--- a/packages/vuetify/src/composables/form.ts
+++ b/packages/vuetify/src/composables/form.ts
@@ -192,6 +192,11 @@ export function createForm (props: FormProps) {
}
}
-export function useForm () {
- return inject(FormKey, null)
+export function useForm (props?: { readonly: boolean | null, disabled: boolean | null }) {
+ const form = inject(FormKey, null)
+ return {
+ ...form,
+ isReadonly: computed(() => !!(props?.readonly ?? form?.isReadonly.value)),
+ isDisabled: computed(() => !!(props?.disabled ?? form?.isDisabled.value)),
+ }
}
diff --git a/packages/vuetify/src/composables/nested/nested.ts b/packages/vuetify/src/composables/nested/nested.ts
index 38e2f1300e0..a97188b1c0f 100644
--- a/packages/vuetify/src/composables/nested/nested.ts
+++ b/packages/vuetify/src/composables/nested/nested.ts
@@ -2,7 +2,17 @@
import { useProxiedModel } from '@/composables/proxiedModel'
// Utilities
-import { computed, inject, onBeforeUnmount, provide, ref, shallowRef, toRaw, toRef } from 'vue'
+import {
+ computed,
+ inject,
+ onBeforeMount,
+ onBeforeUnmount,
+ provide,
+ ref,
+ shallowRef,
+ toRaw,
+ toRef,
+} from 'vue'
import {
independentActiveStrategy,
independentSingleActiveStrategy,
@@ -334,7 +344,9 @@ export const useNestedItem = (id: Ref, isGroup: boolean) => {
isGroupActivator: parent.isGroupActivator,
}
- !parent.isGroupActivator && parent.root.register(computedId.value, parent.id.value, isGroup)
+ onBeforeMount(() => {
+ !parent.isGroupActivator && parent.root.register(computedId.value, parent.id.value, isGroup)
+ })
onBeforeUnmount(() => {
!parent.isGroupActivator && parent.root.unregister(computedId.value)
diff --git a/packages/vuetify/src/composables/validation.ts b/packages/vuetify/src/composables/validation.ts
index 5567e71f351..01a1868e4fa 100644
--- a/packages/vuetify/src/composables/validation.ts
+++ b/packages/vuetify/src/composables/validation.ts
@@ -83,22 +83,20 @@ export function useValidation (
) {
const model = useProxiedModel(props, 'modelValue')
const validationModel = computed(() => props.validationValue === undefined ? model.value : props.validationValue)
- const form = useForm()
+ const form = useForm(props)
const internalErrorMessages = ref([])
const isPristine = shallowRef(true)
const isDirty = computed(() => !!(
wrapInArray(model.value === '' ? null : model.value).length ||
wrapInArray(validationModel.value === '' ? null : validationModel.value).length
))
- const isDisabled = computed(() => !!(props.disabled ?? form?.isDisabled.value))
- const isReadonly = computed(() => !!(props.readonly ?? form?.isReadonly.value))
const errorMessages = computed(() => {
return props.errorMessages?.length
? wrapInArray(props.errorMessages).concat(internalErrorMessages.value).slice(0, Math.max(0, +props.maxErrors))
: internalErrorMessages.value
})
const validateOn = computed(() => {
- let value = (props.validateOn ?? form?.validateOn.value) || 'input'
+ let value = (props.validateOn ?? form.validateOn?.value) || 'input'
if (value === 'lazy') value = 'input lazy'
if (value === 'eager') value = 'input eager'
const set = new Set(value?.split(' ') ?? [])
@@ -125,8 +123,8 @@ export function useValidation (
return {
[`${name}--error`]: isValid.value === false,
[`${name}--dirty`]: isDirty.value,
- [`${name}--disabled`]: isDisabled.value,
- [`${name}--readonly`]: isReadonly.value,
+ [`${name}--disabled`]: form.isDisabled.value,
+ [`${name}--readonly`]: form.isReadonly.value,
}
})
@@ -134,7 +132,7 @@ export function useValidation (
const uid = computed(() => props.name ?? unref(id))
onBeforeMount(() => {
- form?.register({
+ form.register?.({
id: uid.value,
vm,
validate,
@@ -144,14 +142,14 @@ export function useValidation (
})
onBeforeUnmount(() => {
- form?.unregister(uid.value)
+ form.unregister?.(uid.value)
})
onMounted(async () => {
if (!validateOn.value.lazy) {
await validate(!validateOn.value.eager)
}
- form?.update(uid.value, isValid.value, errorMessages.value)
+ form.update?.(uid.value, isValid.value, errorMessages.value)
})
useToggleScope(() => validateOn.value.input || (validateOn.value.invalidInput && isValid.value === false), () => {
@@ -175,7 +173,7 @@ export function useValidation (
})
watch([isValid, errorMessages], () => {
- form?.update(uid.value, isValid.value, errorMessages.value)
+ form.update?.(uid.value, isValid.value, errorMessages.value)
})
async function reset () {
@@ -228,8 +226,8 @@ export function useValidation (
return {
errorMessages,
isDirty,
- isDisabled,
- isReadonly,
+ isDisabled: form.isDisabled,
+ isReadonly: form.isReadonly,
isPristine,
isValid,
isValidating,
diff --git a/packages/vuetify/src/composables/virtual.ts b/packages/vuetify/src/composables/virtual.ts
index d92b5a01678..2a43b0de30b 100644
--- a/packages/vuetify/src/composables/virtual.ts
+++ b/packages/vuetify/src/composables/virtual.ts
@@ -4,7 +4,7 @@ import { useResizeObserver } from '@/composables/resizeObserver'
// Utilities
import { computed, nextTick, onScopeDispose, ref, shallowRef, watch, watchEffect } from 'vue'
-import { clamp, debounce, IN_BROWSER, propsFactory } from '@/util'
+import { clamp, debounce, IN_BROWSER, isObject, propsFactory } from '@/util'
// Types
import type { Ref } from 'vue'
@@ -149,6 +149,7 @@ export function useVirtual (props: VirtualProps, items: Ref) {
}
})
+ let scrollTimeout = -1
function handleScroll () {
if (!containerRef.value || !markerRef.value) return
@@ -169,6 +170,9 @@ export function useVirtual (props: VirtualProps, items: Ref) {
lastScrollTop = scrollTop
lastScrollTime = scrollTime
+ window.clearTimeout(scrollTimeout)
+ scrollTimeout = window.setTimeout(handleScrollend, 500)
+
calculateVisibleItems()
}
function handleScrollend () {
@@ -177,6 +181,7 @@ export function useVirtual (props: VirtualProps, items: Ref) {
scrollVelocity = 0
lastScrollTime = 0
+ window.clearTimeout(scrollTimeout)
calculateVisibleItems()
}
@@ -233,6 +238,7 @@ export function useVirtual (props: VirtualProps, items: Ref) {
return items.value.slice(first.value, last.value).map((item, index) => ({
raw: item,
index: index + first.value,
+ key: (isObject(item) && 'value' in item) ? item.value : index + first.value,
}))
})
diff --git a/packages/vuetify/src/iconsets/mdi.ts b/packages/vuetify/src/iconsets/mdi.ts
index d1a9e788062..059e3cf9f67 100644
--- a/packages/vuetify/src/iconsets/mdi.ts
+++ b/packages/vuetify/src/iconsets/mdi.ts
@@ -47,6 +47,7 @@ const aliases: IconAliases = {
treeviewCollapse: 'mdi-menu-down',
treeviewExpand: 'mdi-menu-right',
eyeDropper: 'mdi-eyedropper',
+ upload: 'mdi-cloud-upload',
}
const mdi: IconSet = {
diff --git a/packages/vuetify/src/labs/VDateInput/VDateInput.tsx b/packages/vuetify/src/labs/VDateInput/VDateInput.tsx
index 54202f7cddc..9e595e55e9d 100644
--- a/packages/vuetify/src/labs/VDateInput/VDateInput.tsx
+++ b/packages/vuetify/src/labs/VDateInput/VDateInput.tsx
@@ -15,13 +15,27 @@ import { computed, shallowRef } from 'vue'
import { genericComponent, omit, propsFactory, useRender, wrapInArray } from '@/util'
// Types
-export interface VDateInputSlots {
+import type { PropType } from 'vue'
+import type { StrategyProps } from '@/components/VOverlay/locationStrategies'
+
+// Types
+export type VDateInputActionsSlot = {
+ save: () => void
+ cancel: () => void
+ isPristine: boolean
+}
+
+export type VDateInputSlots = {
+ actions: VDateInputActionsSlot
default: never
}
export const makeVDateInputProps = propsFactory({
hideActions: Boolean,
-
+ location: {
+ type: String as PropType,
+ default: 'bottom start',
+ },
...makeFocusProps(),
...makeVConfirmEditProps(),
...makeVTextFieldProps({
@@ -31,10 +45,10 @@ export const makeVDateInputProps = propsFactory({
...omit(makeVDatePickerProps({
weeksInMonth: 'dynamic' as const,
hideHeader: true,
- }), ['active']),
+ }), ['active', 'location']),
}, 'VDateInput')
-export const VDateInput = genericComponent()({
+export const VDateInput = genericComponent()({
name: 'VDateInput',
props: makeVDateInputProps(),
@@ -100,7 +114,7 @@ export const VDateInput = genericComponent()({
useRender(() => {
const confirmEditProps = VConfirmEdit.filterProps(props)
- const datePickerProps = VDatePicker.filterProps(omit(props, ['active']))
+ const datePickerProps = VDatePicker.filterProps(omit(props, ['active', 'location']))
const textFieldProps = VTextField.filterProps(props)
return (
@@ -120,6 +134,7 @@ export const VDateInput = genericComponent()({
v-model={ menu.value }
activator="parent"
min-width="0"
+ location={ props.location }
closeOnContentClick={ false }
openOnClick={ false }
>
@@ -127,9 +142,10 @@ export const VDateInput = genericComponent()({
{ ...confirmEditProps }
v-model={ model.value }
onSave={ onSave }
+ onCancel={ () => menu.value = false }
>
{{
- default: ({ actions, model: proxyModel }) => {
+ default: ({ actions, model: proxyModel, save, cancel, isPristine }) => {
return (
e.preventDefault() }
>
{{
- actions: !props.hideActions ? actions : undefined,
+ actions: !props.hideActions ? () => slots.actions?.({ save, cancel, isPristine }) ?? actions() : undefined,
}}
)
diff --git a/packages/vuetify/src/labs/VFileUpload/VFileUpload.sass b/packages/vuetify/src/labs/VFileUpload/VFileUpload.sass
new file mode 100644
index 00000000000..eafca31e8e9
--- /dev/null
+++ b/packages/vuetify/src/labs/VFileUpload/VFileUpload.sass
@@ -0,0 +1,75 @@
+@use '../../styles/tools'
+@use '../../styles/settings'
+@use './variables' as *
+
+@include tools.layer('components')
+ .v-file-upload
+ padding: $file-upload-padding
+ flex-direction: column
+ justify-content: center
+ align-items: center
+ position: relative
+
+ &.v-sheet
+ display: flex
+ border-radius: 4px
+ border-style: dashed
+ border-width: 2px
+
+ &.v-file-upload--density-compact
+ padding: 32px 0
+ flex-direction: row
+ gap: 1rem
+
+ .v-overlay__scrim
+ pointer-events: none
+
+ &--disabled
+ pointer-events: none
+ opacity: var(--v-disabled-opacity)
+
+ &--dragging
+ > *
+ pointer-events: none
+
+ &--clickable
+ cursor: pointer
+
+ input[type="file"]
+ left: 0
+ opacity: 0
+ position: absolute
+ cursor: pointer
+ top: 0
+ z-index: -1
+
+ .v-file-upload-title
+ font-size: $file-upload-title-font-size
+ font-weight: 600
+
+ .v-file-upload-icon
+ opacity: var(--v-medium-emphasis-opacity)
+ font-size: $file-upload-icon-font-size
+ margin-bottom: $file-upload-icon-margin-bottom
+
+ .v-file-upload--density-comfortable &
+ font-size: $file-upload-icon-font-size - .5rem
+ margin-bottom: $file-upload-icon-margin-bottom - .5rem
+
+ .v-file-upload--density-compact &
+ font-size: $file-upload-icon-font-size - 1rem
+ margin-bottom: $file-upload-icon-margin-bottom - 1rem
+
+ .v-file-upload-divider
+ align-items: center
+ display: flex
+ margin: $file-upload-divider-margin
+ justify-content: center
+ width: 100%
+
+ .v-file-upload-items
+ margin: $file-upload-items-margin
+
+ .v-file-upload-item
+ &:not(:first-child)
+ margin-top: 8px
diff --git a/packages/vuetify/src/labs/VFileUpload/VFileUpload.tsx b/packages/vuetify/src/labs/VFileUpload/VFileUpload.tsx
new file mode 100644
index 00000000000..a48a79b21cc
--- /dev/null
+++ b/packages/vuetify/src/labs/VFileUpload/VFileUpload.tsx
@@ -0,0 +1,340 @@
+// Styles
+import './VFileUpload.sass'
+
+// Components
+import { VFileUploadItem } from './VFileUploadItem'
+import { VBtn } from '@/components/VBtn/VBtn'
+import { VDefaultsProvider } from '@/components/VDefaultsProvider/VDefaultsProvider'
+import { makeVDividerProps, VDivider } from '@/components/VDivider/VDivider'
+import { VIcon } from '@/components/VIcon/VIcon'
+import { VOverlay } from '@/components/VOverlay/VOverlay'
+import { makeVSheetProps, VSheet } from '@/components/VSheet/VSheet'
+
+// Composables
+import { makeDelayProps } from '@/composables/delay'
+import { makeDensityProps, useDensity } from '@/composables/density'
+import { IconValue } from '@/composables/icons'
+import { useLocale } from '@/composables/locale'
+import { useProxiedModel } from '@/composables/proxiedModel'
+
+// Utilities
+import { onMounted, onUnmounted, ref, shallowRef } from 'vue'
+import { filterInputAttrs, genericComponent, only, propsFactory, useRender, wrapInArray } from '@/util'
+
+// Types
+import type { PropType, VNode } from 'vue'
+
+export type VFileUploadSlots = {
+ browse: {
+ props: { onClick: (e: MouseEvent) => void }
+ }
+ default: never
+ icon: never
+ input: {
+ inputNode: VNode
+ }
+ item: {
+ file: File
+ props: { 'onClick:remove': () => void }
+ }
+ title: never
+ divider: never
+}
+
+export const makeVFileUploadProps = propsFactory({
+ browseText: {
+ type: String,
+ default: '$vuetify.fileUpload.browse',
+ },
+ dividerText: {
+ type: String,
+ default: '$vuetify.fileUpload.divider',
+ },
+ title: {
+ type: String,
+ default: '$vuetify.fileUpload.title',
+ },
+ subtitle: String,
+ icon: {
+ type: IconValue,
+ default: '$upload',
+ },
+ modelValue: {
+ type: [Array, Object] as PropType,
+ default: null,
+ validator: (val: any) => {
+ return wrapInArray(val).every(v => v != null && typeof v === 'object')
+ },
+ },
+ clearable: Boolean,
+ disabled: Boolean,
+ hideBrowse: Boolean,
+ multiple: Boolean,
+ scrim: {
+ type: [Boolean, String],
+ default: true,
+ },
+ showSize: Boolean,
+ name: String,
+
+ ...makeDelayProps(),
+ ...makeDensityProps(),
+ ...only(makeVDividerProps({
+ length: 150,
+ }), ['length', 'thickness', 'opacity']),
+ ...makeVSheetProps(),
+}, 'VFileUpload')
+
+export const VFileUpload = genericComponent()({
+ name: 'VFileUpload',
+
+ inheritAttrs: false,
+
+ props: makeVFileUploadProps(),
+
+ emits: {
+ 'update:modelValue': (files: File[]) => true,
+ },
+
+ setup (props, { attrs, slots }) {
+ const { t } = useLocale()
+ const { densityClasses } = useDensity(props)
+ const model = useProxiedModel(
+ props,
+ 'modelValue',
+ props.modelValue,
+ val => wrapInArray(val),
+ val => (props.multiple || Array.isArray(props.modelValue)) ? val : val[0],
+ )
+
+ const dragOver = shallowRef(false)
+ const vSheetRef = ref | null>(null)
+ const inputRef = ref(null)
+
+ onMounted(() => {
+ vSheetRef.value?.$el.addEventListener('dragover', onDragOver)
+ vSheetRef.value?.$el.addEventListener('drop', onDrop)
+ })
+
+ onUnmounted(() => {
+ vSheetRef.value?.$el.removeEventListener('dragover', onDragOver)
+ vSheetRef.value?.$el.removeEventListener('drop', onDrop)
+ })
+
+ function onDragOver (e: DragEvent) {
+ e.preventDefault()
+ e.stopImmediatePropagation()
+ dragOver.value = true
+ }
+
+ function onDragLeave (e: DragEvent) {
+ e.preventDefault()
+ dragOver.value = false
+ }
+
+ function onDrop (e: DragEvent) {
+ e.preventDefault()
+ e.stopImmediatePropagation()
+ dragOver.value = false
+
+ const files = Array.from(e.dataTransfer?.files ?? [])
+
+ if (!files.length) return
+
+ if (!props.multiple) {
+ model.value = [files[0]]
+
+ return
+ }
+
+ const array = model.value.slice()
+
+ for (const file of files) {
+ if (!array.some(f => f.name === file.name)) {
+ array.push(file)
+ }
+ }
+
+ model.value = array
+ }
+
+ function onClick () {
+ inputRef.value?.click()
+ }
+
+ function onClickRemove (index: number) {
+ model.value = model.value.filter((_, i) => i !== index)
+
+ if (model.value.length > 0 || !inputRef.value) return
+
+ inputRef.value.value = ''
+ }
+
+ useRender(() => {
+ const hasTitle = !!(slots.title || props.title)
+ const hasIcon = !!(slots.icon || props.icon)
+ const hasBrowse = !!(!props.hideBrowse && (slots.browse || props.density === 'default'))
+ const cardProps = VSheet.filterProps(props)
+ const dividerProps = VDivider.filterProps(props)
+ const [rootAttrs, inputAttrs] = filterInputAttrs(attrs)
+
+ const inputNode = (
+ {
+ if (!e.target) return
+
+ const target = e.target as HTMLInputElement
+ model.value = [...target.files ?? []]
+ }}
+ { ...inputAttrs }
+ />
+ )
+
+ return (
+ <>
+
+ { hasIcon && (
+
+ { !slots.icon ? (
+
+ ) : (
+
+ { slots.icon() }
+
+ )}
+
+ )}
+
+ { hasTitle && (
+
+ { slots.title?.() ?? t(props.title) }
+
+ )}
+
+ { props.density === 'default' && (
+ <>
+
+ { slots.divider?.() ?? (
+
+ { t(props.dividerText) }
+
+ )}
+
+
+ { hasBrowse && (
+ <>
+ { !slots.browse ? (
+
+ ) : (
+
+ { slots.browse({ props: { onClick } }) }
+
+ )}
+ >
+ )}
+
+ { props.subtitle && (
+
+ { props.subtitle }
+
+ )}
+ >
+ )}
+
+
+
+ { slots.input?.({ inputNode }) ?? inputNode }
+
+
+ { model.value.length > 0 && (
+
+ { model.value.map((file, i) => {
+ const slotProps = {
+ file,
+ props: {
+ 'onClick:remove': () => onClickRemove(i),
+ },
+ }
+
+ return (
+
+ { slots.item?.(slotProps) ?? (
+ onClickRemove(i) }
+ v-slots={ slots }
+ />
+ )}
+
+ )
+ })}
+
+ )}
+ >
+ )
+ })
+ },
+})
+
+export type VFileUpload = InstanceType
diff --git a/packages/vuetify/src/labs/VFileUpload/VFileUploadItem.tsx b/packages/vuetify/src/labs/VFileUpload/VFileUploadItem.tsx
new file mode 100644
index 00000000000..24e5ea5ca30
--- /dev/null
+++ b/packages/vuetify/src/labs/VFileUpload/VFileUploadItem.tsx
@@ -0,0 +1,140 @@
+// Components
+import { VAvatar } from '@/components/VAvatar/VAvatar'
+import { VBtn } from '@/components/VBtn/VBtn'
+import { VDefaultsProvider } from '@/components/VDefaultsProvider/VDefaultsProvider'
+import { makeVListItemProps, VListItem } from '@/components/VList/VListItem'
+
+// Utilities
+import { computed, ref, watchEffect } from 'vue'
+import { genericComponent, humanReadableFileSize, propsFactory, useRender } from '@/util'
+
+// Types
+import type { PropType } from 'vue'
+import type { VListItemSlots } from '@/components/VList/VListItem'
+
+export type VFileUploadItemSlots = {
+ clear: {
+ props: { onClick: () => void }
+ }
+} & VListItemSlots
+
+export const makeVFileUploadItemProps = propsFactory({
+ clearable: Boolean,
+ file: {
+ type: Object as PropType,
+ default: null,
+ },
+ fileIcon: {
+ type: String,
+ // TODO: setup up a proper aliased icon
+ default: 'mdi-file-document',
+ },
+ showSize: Boolean,
+
+ ...makeVListItemProps({
+ border: true,
+ rounded: true,
+ lines: 'two' as const,
+ }),
+}, 'VFileUploadItem')
+
+export const VFileUploadItem = genericComponent()({
+ name: 'VFileUploadItem',
+
+ props: makeVFileUploadItemProps(),
+
+ emits: {
+ 'click:remove': () => true,
+ click: (e: MouseEvent | KeyboardEvent) => true,
+ },
+
+ setup (props, { emit, slots }) {
+ const preview = ref()
+ const base = computed(() => typeof props.showSize !== 'boolean' ? props.showSize : undefined)
+
+ function onClickRemove () {
+ emit('click:remove')
+ }
+
+ watchEffect(() => {
+ preview.value = props.file?.type.startsWith('image') ? URL.createObjectURL(props.file) : undefined
+ })
+
+ useRender(() => {
+ const listItemProps = VListItem.filterProps(props)
+
+ return (
+
+ {{
+ ...slots,
+ prepend: slotProps => (
+ <>
+ { !slots.prepend ? (
+
+ ) : (
+
+ { slots.prepend?.(slotProps) ?? (
+
+ )}
+
+ )}
+ >
+ ),
+ append: slotProps => (
+ <>
+ { props.clearable && (
+ <>
+ { !slots.clear ? (
+
+ ) : (
+
+ { slots.clear?.({
+ ...slotProps,
+ props: { onClick: onClickRemove },
+ }) ?? ( )}
+
+ )}
+ >
+ )}
+
+ { slots.append?.(slotProps) }
+ >
+ ),
+ }}
+
+ )
+ })
+ },
+})
+
+export type VFileUploadItem = InstanceType
diff --git a/packages/vuetify/src/labs/VFileUpload/_variables.scss b/packages/vuetify/src/labs/VFileUpload/_variables.scss
new file mode 100644
index 00000000000..38c3ba95aad
--- /dev/null
+++ b/packages/vuetify/src/labs/VFileUpload/_variables.scss
@@ -0,0 +1,12 @@
+@use '../../styles/tools';
+@use '../../styles/settings';
+
+$file-upload-title-font-size: 1.5rem !default;
+$file-upload-padding: 64px 0 !default;
+$file-upload-border-radius: 4px !default;
+$file-upload-border-width: 2px !default;
+$file-upload-title-font-weight: 600 !default;
+$file-upload-icon-font-size: 3rem !default;
+$file-upload-icon-margin-bottom: 1rem !default;
+$file-upload-divider-margin: 32px 0 !default;
+$file-upload-items-margin: 16px 0 !default;
diff --git a/packages/vuetify/src/labs/VFileUpload/index.ts b/packages/vuetify/src/labs/VFileUpload/index.ts
new file mode 100644
index 00000000000..3f85597a4d3
--- /dev/null
+++ b/packages/vuetify/src/labs/VFileUpload/index.ts
@@ -0,0 +1,2 @@
+export { VFileUpload } from './VFileUpload'
+export { VFileUploadItem } from './VFileUploadItem'
diff --git a/packages/vuetify/src/labs/VNumberInput/VNumberInput.tsx b/packages/vuetify/src/labs/VNumberInput/VNumberInput.tsx
index bbf59a65c74..f3ee59cbe3b 100644
--- a/packages/vuetify/src/labs/VNumberInput/VNumberInput.tsx
+++ b/packages/vuetify/src/labs/VNumberInput/VNumberInput.tsx
@@ -74,14 +74,17 @@ export const VNumberInput = genericComponent()({
const model = computed({
get: () => _model.value,
- set (val) {
- if (val === null) {
+ // model.value could be empty string from VTextField
+ // but _model.value should be eventually kept in type Number | null
+ set (val: Number | null | string) {
+ if (val === null || val === '') {
_model.value = null
return
}
- if (!isNaN(+val) && +val <= props.max && +val >= props.min) {
- _model.value = +val
+ const value = Number(val)
+ if (!isNaN(value) && value <= props.max && value >= props.min) {
+ _model.value = value
}
},
})
@@ -91,9 +94,9 @@ export const VNumberInput = genericComponent()({
const stepDecimals = computed(() => getDecimals(props.step))
const modelDecimals = computed(() => typeof model.value === 'number' ? getDecimals(model.value) : 0)
- const form = useForm()
+ const form = useForm(props)
const controlsDisabled = computed(() => (
- props.disabled || props.readonly || form?.isReadonly.value
+ form.isDisabled.value || form.isReadonly.value
))
const canIncrease = computed(() => {
@@ -119,7 +122,7 @@ export const VNumberInput = genericComponent()({
const decrementSlotProps = computed(() => ({ click: onClickDown }))
onMounted(() => {
- if (!props.readonly && !props.disabled) {
+ if (!controlsDisabled.value) {
clampModel()
}
})
@@ -294,9 +297,9 @@ export const VNumberInput = genericComponent()({
{ incrementControlNode() }
- ) : (!props.reverse
- ? <>{ dividerNode() }{ controlNode() }>
- : undefined)
+ ) : (props.reverse
+ ? undefined
+ : <>{ dividerNode() }{ controlNode() }>)
const hasAppendInner = slots['append-inner'] || appendInnerControl
diff --git a/packages/vuetify/src/labs/VTreeview/VTreeviewItem.tsx b/packages/vuetify/src/labs/VTreeview/VTreeviewItem.tsx
index c3a33eee6ba..b6968afa059 100644
--- a/packages/vuetify/src/labs/VTreeview/VTreeviewItem.tsx
+++ b/packages/vuetify/src/labs/VTreeview/VTreeviewItem.tsx
@@ -47,13 +47,8 @@ export const VTreeviewItem = genericComponent()({
(props.link || link.isClickable.value || (props.value != null && !!vListItemRef.value?.list) || isActivatableGroupActivator.value)
)
- function activateItem (e: MouseEvent | KeyboardEvent) {
- if (
- !isClickable.value ||
- (!isActivatableGroupActivator.value && vListItemRef.value?.isGroupActivator)
- ) return
-
- if (vListItemRef.value?.root.activatable.value) {
+ function activateGroupActivator (e: MouseEvent | KeyboardEvent) {
+ if (isClickable.value && isActivatableGroupActivator.value) {
vListItemRef.value?.activate(!vListItemRef.value?.isActivated, e)
}
}
@@ -78,7 +73,7 @@ export const VTreeviewItem = genericComponent()({
props.class,
]}
ripple={ false }
- onClick={ props.onClick ?? activateItem }
+ onClick={ props.onClick ?? activateGroupActivator }
>
{{
...slots,
diff --git a/packages/vuetify/src/labs/VTreeview/__tests__/VTreeview.spec.browser.tsx b/packages/vuetify/src/labs/VTreeview/__tests__/VTreeview.spec.browser.tsx
index 0e9ef6b7ec1..ec74dfca194 100644
--- a/packages/vuetify/src/labs/VTreeview/__tests__/VTreeview.spec.browser.tsx
+++ b/packages/vuetify/src/labs/VTreeview/__tests__/VTreeview.spec.browser.tsx
@@ -156,6 +156,27 @@ describe.each([
await userEvent.click(screen.getByText(/Nekosaur/))
expect(activated.value).toStrictEqual([203])
})
+
+ // https://github.com/vuetifyjs/vuetify/issues/20665
+ it('should emit only once', async () => {
+ const onActivated = vi.fn()
+ render(() => (
+
+ ))
+
+ await userEvent.click(screen.getByText(/John/))
+ expect(onActivated).toHaveBeenCalledOnce()
+
+ await userEvent.click(screen.getByText(/Human Resources/))
+ expect(onActivated).toHaveBeenCalledTimes(2)
+ })
})
describe('select', () => {
diff --git a/packages/vuetify/src/labs/components.ts b/packages/vuetify/src/labs/components.ts
index 6b753ff8790..341d9536360 100644
--- a/packages/vuetify/src/labs/components.ts
+++ b/packages/vuetify/src/labs/components.ts
@@ -1,5 +1,6 @@
export * from './VCalendar'
export * from './VDateInput'
+export * from './VFileUpload'
export * from './VNumberInput'
export * from './VPicker'
export * from './VStepperVertical'
diff --git a/packages/vuetify/src/locale/af.ts b/packages/vuetify/src/locale/af.ts
index 34fc7bd4c76..d7fd337c043 100644
--- a/packages/vuetify/src/locale/af.ts
+++ b/packages/vuetify/src/locale/af.ts
@@ -69,6 +69,11 @@ export default {
counter: '{0} files',
counterSize: '{0} files ({1} in total)',
},
+ fileUpload: {
+ title: 'Drag and drop files here',
+ divider: 'or',
+ browse: 'Browse Files',
+ },
timePicker: {
am: 'AM',
pm: 'PM',
diff --git a/packages/vuetify/src/locale/ar.ts b/packages/vuetify/src/locale/ar.ts
index b5ec2b2ea6f..e28cb07f5de 100644
--- a/packages/vuetify/src/locale/ar.ts
+++ b/packages/vuetify/src/locale/ar.ts
@@ -69,6 +69,11 @@ export default {
counter: '{0} ملفات',
counterSize: '{0} ملفات ({1} في المجموع)',
},
+ fileUpload: {
+ title: 'Drag and drop files here',
+ divider: 'or',
+ browse: 'Browse Files',
+ },
timePicker: {
am: 'صباحاً',
pm: 'مساءً',
diff --git a/packages/vuetify/src/locale/az.ts b/packages/vuetify/src/locale/az.ts
index b452d1ccd95..3349317351f 100644
--- a/packages/vuetify/src/locale/az.ts
+++ b/packages/vuetify/src/locale/az.ts
@@ -1,11 +1,11 @@
export default {
- badge: 'nişan',
+ badge: 'Nişan',
open: 'Open',
close: 'Bağla',
- dismiss: 'Dismiss',
+ dismiss: 'Rədd et',
confirmEdit: {
ok: 'OK',
- cancel: 'Cancel',
+ cancel: 'İmtina',
},
dataIterator: {
noResultsText: 'Uyğun məlumat tapılmadı',
@@ -36,18 +36,18 @@ export default {
divider: 'to',
},
datePicker: {
- itemsSelected: '{0} selected',
+ itemsSelected: '{0} seçilib',
range: {
- title: 'Select dates',
- header: 'Enter dates',
+ title: 'Tarixləri seçin',
+ header: 'Tarixləri daxil edin',
},
- title: 'Select date',
- header: 'Enter date',
+ title: 'Tarixi seçin',
+ header: 'Tarixi daxil edin',
input: {
- placeholder: 'Enter date',
+ placeholder: 'Tarixi daxil edin',
},
},
- noDataText: 'Bu görüntüdə məlumat yoxdur.',
+ noDataText: 'Heç bir məlumat yoxdur.',
carousel: {
prev: 'Əvvəlki görüntü',
next: 'Növbəti görüntü',
@@ -57,46 +57,51 @@ export default {
},
calendar: {
moreEvents: '{0} ədad daha',
- today: 'Today',
+ today: 'Bu gün',
},
input: {
- clear: 'Clear {0}',
- prependAction: '{0} prepended action',
- appendAction: '{0} appended action',
- otp: 'Please enter OTP character {0}',
+ clear: 'Təmizlə {0}',
+ prependAction: '{0} qabaqcıl əməliyyat',
+ appendAction: '{0} sonrakı əməliyyat',
+ otp: 'OTP daxil et {0}',
},
fileInput: {
counter: '{0} fayl',
counterSize: '{0} fayl (cəmi {1})',
},
+ fileUpload: {
+ title: 'Faylları buraya sürüşdürün',
+ divider: 'və ya',
+ browse: 'Faylları baxın',
+ },
timePicker: {
am: 'AM',
pm: 'PM',
- title: 'Select Time',
+ title: 'Vaxtı seçin',
},
pagination: {
ariaLabel: {
root: 'Səhifələmə Naviqasiyası',
next: 'Növbəti səhifə',
- previous: 'Əvəvlki səhifə',
+ previous: 'Əvvəlki səhifə',
page: 'Səhifəyə get {0}',
currentPage: 'Cari səhifə, Səhifə {0}',
- first: 'First page',
- last: 'Last page',
+ first: 'İlk səhifə',
+ last: 'Son səhifə',
},
},
stepper: {
- next: 'Next',
- prev: 'Previous',
+ next: 'Növbəti',
+ prev: 'Əvvəlki',
},
rating: {
ariaLabel: {
- item: 'Rating {0} of {1}',
+ item: 'Reytinq {0}/{1}',
},
},
- loading: 'Loading...',
+ loading: 'Yüklənir...',
infiniteScroll: {
- loadMore: 'Load more',
- empty: 'No more',
+ loadMore: 'Daha çox yüklə',
+ empty: 'Daha yoxdur',
},
}
diff --git a/packages/vuetify/src/locale/bg.ts b/packages/vuetify/src/locale/bg.ts
index f795df46016..f2a7c345147 100644
--- a/packages/vuetify/src/locale/bg.ts
+++ b/packages/vuetify/src/locale/bg.ts
@@ -69,6 +69,11 @@ export default {
counter: '{0} файла',
counterSize: '{0} файла ({1} общо)',
},
+ fileUpload: {
+ title: 'Drag and drop files here',
+ divider: 'or',
+ browse: 'Browse Files',
+ },
timePicker: {
am: 'пр. обяд',
pm: 'сл. обяд',
diff --git a/packages/vuetify/src/locale/ca.ts b/packages/vuetify/src/locale/ca.ts
index 09b06b0b9e9..93c064b55a4 100644
--- a/packages/vuetify/src/locale/ca.ts
+++ b/packages/vuetify/src/locale/ca.ts
@@ -69,6 +69,11 @@ export default {
counter: '{0} fitxers',
counterSize: '{0} fitxers ({1} en total)',
},
+ fileUpload: {
+ title: 'Drag and drop files here',
+ divider: 'or',
+ browse: 'Browse Files',
+ },
timePicker: {
am: 'AM',
pm: 'PM',
diff --git a/packages/vuetify/src/locale/ckb.ts b/packages/vuetify/src/locale/ckb.ts
index 64ffde6ea1a..ceef0ea6149 100644
--- a/packages/vuetify/src/locale/ckb.ts
+++ b/packages/vuetify/src/locale/ckb.ts
@@ -69,6 +69,11 @@ export default {
counter: '{0} فایل',
counterSize: '{0} فایل ({1} لە کۆی گشتی)',
},
+ fileUpload: {
+ title: 'Drag and drop files here',
+ divider: 'or',
+ browse: 'Browse Files',
+ },
timePicker: {
am: 'پێش نیوەڕۆژ',
pm: 'دوای نیوەڕۆژ',
diff --git a/packages/vuetify/src/locale/cs.ts b/packages/vuetify/src/locale/cs.ts
index 85a8b6b613a..261fc5a4099 100644
--- a/packages/vuetify/src/locale/cs.ts
+++ b/packages/vuetify/src/locale/cs.ts
@@ -69,6 +69,11 @@ export default {
counter: '{0} souborů',
counterSize: '{0} souborů ({1} celkem)',
},
+ fileUpload: {
+ title: 'Drag and drop files here',
+ divider: 'or',
+ browse: 'Browse Files',
+ },
timePicker: {
am: 'AM',
pm: 'PM',
diff --git a/packages/vuetify/src/locale/da.ts b/packages/vuetify/src/locale/da.ts
index 8a80af26500..4668d8e9fde 100644
--- a/packages/vuetify/src/locale/da.ts
+++ b/packages/vuetify/src/locale/da.ts
@@ -69,6 +69,11 @@ export default {
counter: '{0} filer',
counterSize: '{0} filer ({1} total)',
},
+ fileUpload: {
+ title: 'Drag and drop files here',
+ divider: 'or',
+ browse: 'Browse Files',
+ },
timePicker: {
am: 'AM',
pm: 'PM',
diff --git a/packages/vuetify/src/locale/de.ts b/packages/vuetify/src/locale/de.ts
index 9b350fc1222..253f9c3b7b4 100644
--- a/packages/vuetify/src/locale/de.ts
+++ b/packages/vuetify/src/locale/de.ts
@@ -69,6 +69,11 @@ export default {
counter: '{0} Dateien',
counterSize: '{0} Dateien ({1} gesamt)',
},
+ fileUpload: {
+ title: 'Drag and drop files here',
+ divider: 'or',
+ browse: 'Browse Files',
+ },
timePicker: {
am: 'AM',
pm: 'PM',
diff --git a/packages/vuetify/src/locale/el.ts b/packages/vuetify/src/locale/el.ts
index be1701683f9..fba9a4b9fb1 100755
--- a/packages/vuetify/src/locale/el.ts
+++ b/packages/vuetify/src/locale/el.ts
@@ -69,6 +69,11 @@ export default {
counter: '{0} files',
counterSize: '{0} files ({1} in total)',
},
+ fileUpload: {
+ title: 'Drag and drop files here',
+ divider: 'or',
+ browse: 'Browse Files',
+ },
timePicker: {
am: 'AM',
pm: 'PM',
diff --git a/packages/vuetify/src/locale/en.ts b/packages/vuetify/src/locale/en.ts
index cf94f5e9786..0c59dac42d9 100644
--- a/packages/vuetify/src/locale/en.ts
+++ b/packages/vuetify/src/locale/en.ts
@@ -69,6 +69,11 @@ export default {
counter: '{0} files',
counterSize: '{0} files ({1} in total)',
},
+ fileUpload: {
+ title: 'Drag and drop files here',
+ divider: 'or',
+ browse: 'Browse Files',
+ },
timePicker: {
am: 'AM',
pm: 'PM',
diff --git a/packages/vuetify/src/locale/es.ts b/packages/vuetify/src/locale/es.ts
index 849ddececaa..86142336aed 100644
--- a/packages/vuetify/src/locale/es.ts
+++ b/packages/vuetify/src/locale/es.ts
@@ -69,6 +69,11 @@ export default {
counter: '{0} archivos',
counterSize: '{0} archivos ({1} en total)',
},
+ fileUpload: {
+ title: 'Drag and drop files here',
+ divider: 'or',
+ browse: 'Browse Files',
+ },
timePicker: {
am: 'AM',
pm: 'PM',
diff --git a/packages/vuetify/src/locale/et.ts b/packages/vuetify/src/locale/et.ts
index b420079173d..1ff3d0c9875 100644
--- a/packages/vuetify/src/locale/et.ts
+++ b/packages/vuetify/src/locale/et.ts
@@ -69,6 +69,11 @@ export default {
counter: '{0} faili',
counterSize: '{0} faili (kokku {1})',
},
+ fileUpload: {
+ title: 'Drag and drop files here',
+ divider: 'or',
+ browse: 'Browse Files',
+ },
timePicker: {
am: 'AM',
pm: 'PM',
diff --git a/packages/vuetify/src/locale/fa.ts b/packages/vuetify/src/locale/fa.ts
index d80255e4351..559789c561e 100644
--- a/packages/vuetify/src/locale/fa.ts
+++ b/packages/vuetify/src/locale/fa.ts
@@ -69,6 +69,11 @@ export default {
counter: '{0} پرونده',
counterSize: '{0} پرونده ({1} در کل)',
},
+ fileUpload: {
+ title: 'Drag and drop files here',
+ divider: 'or',
+ browse: 'Browse Files',
+ },
timePicker: {
am: 'قبل از ظهر',
pm: 'بعد از ظهر',
diff --git a/packages/vuetify/src/locale/fi.ts b/packages/vuetify/src/locale/fi.ts
index d03c0703928..5d9b54d6581 100644
--- a/packages/vuetify/src/locale/fi.ts
+++ b/packages/vuetify/src/locale/fi.ts
@@ -69,6 +69,11 @@ export default {
counter: '{0} tiedostoa',
counterSize: '{0} tiedostoa ({1} yhteensä)',
},
+ fileUpload: {
+ title: 'Drag and drop files here',
+ divider: 'or',
+ browse: 'Browse Files',
+ },
timePicker: {
am: 'ap.',
pm: 'ip.',
diff --git a/packages/vuetify/src/locale/fr.ts b/packages/vuetify/src/locale/fr.ts
index 717e2902829..371798e0434 100644
--- a/packages/vuetify/src/locale/fr.ts
+++ b/packages/vuetify/src/locale/fr.ts
@@ -57,7 +57,7 @@ export default {
},
calendar: {
moreEvents: '{0} de plus',
- today: 'Today',
+ today: 'Aujourd\'hui',
},
input: {
clear: 'Vider {0}',
@@ -69,6 +69,11 @@ export default {
counter: '{0} fichier(s)',
counterSize: '{0} fichier(s) ({1} au total)',
},
+ fileUpload: {
+ title: 'Drag and drop files here',
+ divider: 'or',
+ browse: 'Browse Files',
+ },
timePicker: {
am: 'AM',
pm: 'PM',
diff --git a/packages/vuetify/src/locale/he.ts b/packages/vuetify/src/locale/he.ts
index ebc04e428e6..463ac6ddb7c 100644
--- a/packages/vuetify/src/locale/he.ts
+++ b/packages/vuetify/src/locale/he.ts
@@ -69,6 +69,11 @@ export default {
counter: '{0} קבצים',
counterSize: '{0} קבצים ({1} בסך הכל)',
},
+ fileUpload: {
+ title: 'Drag and drop files here',
+ divider: 'or',
+ browse: 'Browse Files',
+ },
timePicker: {
am: 'AM',
pm: 'PM',
diff --git a/packages/vuetify/src/locale/hr.ts b/packages/vuetify/src/locale/hr.ts
index d24be21324b..d064708a31c 100644
--- a/packages/vuetify/src/locale/hr.ts
+++ b/packages/vuetify/src/locale/hr.ts
@@ -69,6 +69,11 @@ export default {
counter: 'Odabranih datoteka: {0}',
counterSize: 'Odabranih datoteka: {0} ({1} ukupno)',
},
+ fileUpload: {
+ title: 'Drag and drop files here',
+ divider: 'or',
+ browse: 'Browse Files',
+ },
timePicker: {
am: 'AM',
pm: 'PM',
diff --git a/packages/vuetify/src/locale/hu.ts b/packages/vuetify/src/locale/hu.ts
index d23db5b5e15..ce7a5fac4c4 100644
--- a/packages/vuetify/src/locale/hu.ts
+++ b/packages/vuetify/src/locale/hu.ts
@@ -69,6 +69,11 @@ export default {
counter: '{0} fájl',
counterSize: '{0} fájl ({1} összesen)',
},
+ fileUpload: {
+ title: 'Drag and drop files here',
+ divider: 'or',
+ browse: 'Browse Files',
+ },
timePicker: {
am: 'de',
pm: 'du',
diff --git a/packages/vuetify/src/locale/id.ts b/packages/vuetify/src/locale/id.ts
index ad689c5f478..9ddda9a693d 100644
--- a/packages/vuetify/src/locale/id.ts
+++ b/packages/vuetify/src/locale/id.ts
@@ -69,6 +69,11 @@ export default {
counter: '{0} berkas',
counterSize: '{0} berkas (dari total {1})',
},
+ fileUpload: {
+ title: 'Drag and drop files here',
+ divider: 'or',
+ browse: 'Browse Files',
+ },
timePicker: {
am: 'AM',
pm: 'PM',
diff --git a/packages/vuetify/src/locale/it.ts b/packages/vuetify/src/locale/it.ts
index cc2d11e9f53..206aeeaad90 100644
--- a/packages/vuetify/src/locale/it.ts
+++ b/packages/vuetify/src/locale/it.ts
@@ -69,6 +69,11 @@ export default {
counter: '{0} file',
counterSize: '{0} file ({1} in totale)',
},
+ fileUpload: {
+ title: 'Drag and drop files here',
+ divider: 'or',
+ browse: 'Browse Files',
+ },
timePicker: {
am: 'AM',
pm: 'PM',
diff --git a/packages/vuetify/src/locale/ja.ts b/packages/vuetify/src/locale/ja.ts
index 1156492f552..034842f41c7 100644
--- a/packages/vuetify/src/locale/ja.ts
+++ b/packages/vuetify/src/locale/ja.ts
@@ -69,6 +69,11 @@ export default {
counter: '{0} ファイル',
counterSize: '{0} ファイル (合計 {1})',
},
+ fileUpload: {
+ title: 'Drag and drop files here',
+ divider: 'or',
+ browse: 'Browse Files',
+ },
timePicker: {
am: 'AM',
pm: 'PM',
diff --git a/packages/vuetify/src/locale/km.ts b/packages/vuetify/src/locale/km.ts
index 10c1cd735ac..5de87fc9454 100644
--- a/packages/vuetify/src/locale/km.ts
+++ b/packages/vuetify/src/locale/km.ts
@@ -69,6 +69,11 @@ export default {
counter: '{0} ឯកសារ',
counterSize: '{0} ឯកសារ ({1} សរុប)',
},
+ fileUpload: {
+ title: 'Drag and drop files here',
+ divider: 'or',
+ browse: 'Browse Files',
+ },
timePicker: {
am: 'ព្រឹក',
pm: 'ល្ងាច',
diff --git a/packages/vuetify/src/locale/ko.ts b/packages/vuetify/src/locale/ko.ts
index 1a0782eec8a..72b07e732bc 100644
--- a/packages/vuetify/src/locale/ko.ts
+++ b/packages/vuetify/src/locale/ko.ts
@@ -69,6 +69,11 @@ export default {
counter: '{0} files',
counterSize: '{0} files ({1} in total)',
},
+ fileUpload: {
+ title: 'Drag and drop files here',
+ divider: 'or',
+ browse: 'Browse Files',
+ },
timePicker: {
am: '오전',
pm: '오후',
diff --git a/packages/vuetify/src/locale/lt.ts b/packages/vuetify/src/locale/lt.ts
index 307340e31a3..2b2f492c4d2 100644
--- a/packages/vuetify/src/locale/lt.ts
+++ b/packages/vuetify/src/locale/lt.ts
@@ -69,6 +69,11 @@ export default {
counter: '{0} failų',
counterSize: '{0} failų ({1} iš viso)',
},
+ fileUpload: {
+ title: 'Drag and drop files here',
+ divider: 'or',
+ browse: 'Browse Files',
+ },
timePicker: {
am: 'AM',
pm: 'PM',
diff --git a/packages/vuetify/src/locale/lv.ts b/packages/vuetify/src/locale/lv.ts
index eb493589646..bb4b563e6be 100644
--- a/packages/vuetify/src/locale/lv.ts
+++ b/packages/vuetify/src/locale/lv.ts
@@ -69,6 +69,11 @@ export default {
counter: '{0} files',
counterSize: '{0} files ({1} in total)',
},
+ fileUpload: {
+ title: 'Drag and drop files here',
+ divider: 'or',
+ browse: 'Browse Files',
+ },
timePicker: {
am: 'AM',
pm: 'PM',
diff --git a/packages/vuetify/src/locale/nl.ts b/packages/vuetify/src/locale/nl.ts
index b8c6032fe32..becd4073dd7 100644
--- a/packages/vuetify/src/locale/nl.ts
+++ b/packages/vuetify/src/locale/nl.ts
@@ -2,7 +2,7 @@ export default {
badge: 'Insigne',
open: 'Openen',
close: 'Sluiten',
- dismiss: 'Dismiss',
+ dismiss: 'Sluiten',
confirmEdit: {
ok: 'OK',
cancel: 'Annuleren',
@@ -69,10 +69,15 @@ export default {
counter: '{0} bestanden',
counterSize: '{0} bestanden ({1} in totaal)',
},
+ fileUpload: {
+ title: 'Sleep en zet bestanden hier neer',
+ divider: 'of',
+ browse: 'Blader door bestanden',
+ },
timePicker: {
am: 'AM',
pm: 'PM',
- title: 'Select Time',
+ title: 'Selecteer tijd',
},
pagination: {
ariaLabel: {
diff --git a/packages/vuetify/src/locale/no.ts b/packages/vuetify/src/locale/no.ts
index c9fc72ddfc6..0796d33e009 100644
--- a/packages/vuetify/src/locale/no.ts
+++ b/packages/vuetify/src/locale/no.ts
@@ -69,6 +69,11 @@ export default {
counter: '{0} filer',
counterSize: '{0} filer ({1} totalt)',
},
+ fileUpload: {
+ title: 'Drag and drop files here',
+ divider: 'or',
+ browse: 'Browse Files',
+ },
timePicker: {
am: 'AM',
pm: 'PM',
diff --git a/packages/vuetify/src/locale/pl.ts b/packages/vuetify/src/locale/pl.ts
index 0e45ad375df..ccc577f6ed0 100644
--- a/packages/vuetify/src/locale/pl.ts
+++ b/packages/vuetify/src/locale/pl.ts
@@ -69,6 +69,11 @@ export default {
counter: 'Liczba plików: {0}',
counterSize: 'Liczba plików: {0} (łącznie {1})',
},
+ fileUpload: {
+ title: 'Drag and drop files here',
+ divider: 'or',
+ browse: 'Browse Files',
+ },
timePicker: {
am: 'AM',
pm: 'PM',
diff --git a/packages/vuetify/src/locale/pt.ts b/packages/vuetify/src/locale/pt.ts
index d6094cb7edd..65a63d04bbe 100644
--- a/packages/vuetify/src/locale/pt.ts
+++ b/packages/vuetify/src/locale/pt.ts
@@ -69,6 +69,11 @@ export default {
counter: '{0} arquivo(s)',
counterSize: '{0} arquivo(s) ({1} no total)',
},
+ fileUpload: {
+ title: 'Drag and drop files here',
+ divider: 'or',
+ browse: 'Browse Files',
+ },
timePicker: {
am: 'AM',
pm: 'PM',
diff --git a/packages/vuetify/src/locale/ro.ts b/packages/vuetify/src/locale/ro.ts
index 615b2861637..8f898a72fb9 100644
--- a/packages/vuetify/src/locale/ro.ts
+++ b/packages/vuetify/src/locale/ro.ts
@@ -69,6 +69,11 @@ export default {
counter: '{0} fișiere',
counterSize: '{0} fișiere ({1} în total)',
},
+ fileUpload: {
+ title: 'Drag and drop files here',
+ divider: 'or',
+ browse: 'Browse Files',
+ },
timePicker: {
am: 'AM',
pm: 'PM',
diff --git a/packages/vuetify/src/locale/ru.ts b/packages/vuetify/src/locale/ru.ts
index 3c6eb2cde0e..3082bab7450 100644
--- a/packages/vuetify/src/locale/ru.ts
+++ b/packages/vuetify/src/locale/ru.ts
@@ -69,6 +69,11 @@ export default {
counter: 'Файлов: {0}',
counterSize: 'Файлов: {0} (всего {1})',
},
+ fileUpload: {
+ title: 'Drag and drop files here',
+ divider: 'or',
+ browse: 'Browse Files',
+ },
timePicker: {
am: 'AM',
pm: 'PM',
diff --git a/packages/vuetify/src/locale/sk.ts b/packages/vuetify/src/locale/sk.ts
index 9b1ffe2481e..f2c8e51b043 100644
--- a/packages/vuetify/src/locale/sk.ts
+++ b/packages/vuetify/src/locale/sk.ts
@@ -69,6 +69,11 @@ export default {
counter: '{0} súborov',
counterSize: '{0} súborov ({1} celkom)',
},
+ fileUpload: {
+ title: 'Drag and drop files here',
+ divider: 'or',
+ browse: 'Browse Files',
+ },
timePicker: {
am: 'AM',
pm: 'PM',
diff --git a/packages/vuetify/src/locale/sl.ts b/packages/vuetify/src/locale/sl.ts
index c36e668c611..2d39a6c857f 100644
--- a/packages/vuetify/src/locale/sl.ts
+++ b/packages/vuetify/src/locale/sl.ts
@@ -69,6 +69,11 @@ export default {
counter: '{0} datotek',
counterSize: '{0} datotek (skupno {1})',
},
+ fileUpload: {
+ title: 'Drag and drop files here',
+ divider: 'or',
+ browse: 'Browse Files',
+ },
timePicker: {
am: 'AM',
pm: 'PM',
diff --git a/packages/vuetify/src/locale/sr-Cyrl.ts b/packages/vuetify/src/locale/sr-Cyrl.ts
index 6e9c1f9cc16..3230a8f1e4b 100644
--- a/packages/vuetify/src/locale/sr-Cyrl.ts
+++ b/packages/vuetify/src/locale/sr-Cyrl.ts
@@ -69,6 +69,11 @@ export default {
counter: '{0} фајлова',
counterSize: '{0} фајлова ({1} укупно)',
},
+ fileUpload: {
+ title: 'Drag and drop files here',
+ divider: 'or',
+ browse: 'Browse Files',
+ },
timePicker: {
am: 'AM',
pm: 'PM',
diff --git a/packages/vuetify/src/locale/sr-Latn.ts b/packages/vuetify/src/locale/sr-Latn.ts
index 485d81bd2d0..7b7c1e92857 100644
--- a/packages/vuetify/src/locale/sr-Latn.ts
+++ b/packages/vuetify/src/locale/sr-Latn.ts
@@ -69,6 +69,11 @@ export default {
counter: '{0} fajlova',
counterSize: '{0} fajlova ({1} ukupno)',
},
+ fileUpload: {
+ title: 'Drag and drop files here',
+ divider: 'or',
+ browse: 'Browse Files',
+ },
timePicker: {
am: 'AM',
pm: 'PM',
diff --git a/packages/vuetify/src/locale/sv.ts b/packages/vuetify/src/locale/sv.ts
index 2a0b576b7aa..63508c57130 100644
--- a/packages/vuetify/src/locale/sv.ts
+++ b/packages/vuetify/src/locale/sv.ts
@@ -69,6 +69,11 @@ export default {
counter: '{0} filer',
counterSize: '{0} filer ({1})',
},
+ fileUpload: {
+ title: 'Drag and drop files here',
+ divider: 'or',
+ browse: 'Browse Files',
+ },
timePicker: {
am: 'AM',
pm: 'PM',
diff --git a/packages/vuetify/src/locale/th.ts b/packages/vuetify/src/locale/th.ts
index 672643cd6c0..2d2dce683eb 100644
--- a/packages/vuetify/src/locale/th.ts
+++ b/packages/vuetify/src/locale/th.ts
@@ -69,6 +69,11 @@ export default {
counter: '{0} ไฟล์',
counterSize: '{0} ไฟล์ (รวม {1})',
},
+ fileUpload: {
+ title: 'Drag and drop files here',
+ divider: 'or',
+ browse: 'Browse Files',
+ },
timePicker: {
am: 'AM',
pm: 'PM',
diff --git a/packages/vuetify/src/locale/tr.ts b/packages/vuetify/src/locale/tr.ts
index 2463d23ba1e..0bd7eaaddae 100644
--- a/packages/vuetify/src/locale/tr.ts
+++ b/packages/vuetify/src/locale/tr.ts
@@ -69,6 +69,11 @@ export default {
counter: '{0} dosya',
counterSize: '{0} dosya (toplamda {1})',
},
+ fileUpload: {
+ title: 'Drag and drop files here',
+ divider: 'or',
+ browse: 'Browse Files',
+ },
timePicker: {
am: 'AM',
pm: 'PM',
diff --git a/packages/vuetify/src/locale/uk.ts b/packages/vuetify/src/locale/uk.ts
index 4d1d28540ac..e1e6fc13dca 100644
--- a/packages/vuetify/src/locale/uk.ts
+++ b/packages/vuetify/src/locale/uk.ts
@@ -69,6 +69,11 @@ export default {
counter: '{0} файлів',
counterSize: '{0} файлів ({1} загалом)',
},
+ fileUpload: {
+ title: 'Drag and drop files here',
+ divider: 'or',
+ browse: 'Browse Files',
+ },
timePicker: {
am: 'AM',
pm: 'PM',
diff --git a/packages/vuetify/src/locale/vi.ts b/packages/vuetify/src/locale/vi.ts
index 67915a2dc1e..1d6c493ad68 100644
--- a/packages/vuetify/src/locale/vi.ts
+++ b/packages/vuetify/src/locale/vi.ts
@@ -69,6 +69,11 @@ export default {
counter: '{0} tệp',
counterSize: '{0} tệp (tổng cộng {1})',
},
+ fileUpload: {
+ title: 'Drag and drop files here',
+ divider: 'or',
+ browse: 'Browse Files',
+ },
timePicker: {
am: 'SA',
pm: 'CH',
diff --git a/packages/vuetify/src/locale/zh-Hans.ts b/packages/vuetify/src/locale/zh-Hans.ts
index dec1c61f811..eed400c16c3 100644
--- a/packages/vuetify/src/locale/zh-Hans.ts
+++ b/packages/vuetify/src/locale/zh-Hans.ts
@@ -69,6 +69,11 @@ export default {
counter: '{0} 个文件',
counterSize: '{0} 个文件(共 {1})',
},
+ fileUpload: {
+ title: 'Drag and drop files here',
+ divider: 'or',
+ browse: 'Browse Files',
+ },
timePicker: {
am: 'AM',
pm: 'PM',
diff --git a/packages/vuetify/src/locale/zh-Hant.ts b/packages/vuetify/src/locale/zh-Hant.ts
index dbae45b6526..7bebfcd5b49 100644
--- a/packages/vuetify/src/locale/zh-Hant.ts
+++ b/packages/vuetify/src/locale/zh-Hant.ts
@@ -69,6 +69,11 @@ export default {
counter: '{0} 個檔案',
counterSize: '{0} 個檔案(共 {1})',
},
+ fileUpload: {
+ title: 'Drag and drop files here',
+ divider: 'or',
+ browse: 'Browse Files',
+ },
timePicker: {
am: 'AM',
pm: 'PM',