diff --git a/package.json b/package.json
index bca0c95c349..42daa72069a 100755
--- a/package.json
+++ b/package.json
@@ -71,7 +71,6 @@
"vue-analytics": "^5.16.1",
"vue-loader": "^16.1.2",
"vue-meta": "^2.4.0",
- "vue-router": "^3.0.1",
"webpack": "^5.37.0",
"webpack-cli": "^4.7.0",
"webpack-dev-server": "4.0.0-beta.3",
diff --git a/packages/vuetify/build/webpack.dev.config.js b/packages/vuetify/build/webpack.dev.config.js
index 596648c27f7..3d16ad606d7 100644
--- a/packages/vuetify/build/webpack.dev.config.js
+++ b/packages/vuetify/build/webpack.dev.config.js
@@ -16,7 +16,12 @@ module.exports = merge(baseWebpackConfig, {
library: 'Vuetify'
},
resolve: {
- alias: { vuetify$: resolve('../src/entry-bundler.ts') }
+ alias: {
+ vuetify$: resolve('../src/entry-bundler.ts'),
+ 'vuetify/src': resolve('../src/'),
+ vue$: require.resolve('vue/dist/vue.esm-bundler.js')
+ },
+ symlinks: false,
},
module: {
rules: [
diff --git a/packages/vuetify/dev/index.js b/packages/vuetify/dev/index.js
index bf58ca0cfcf..8b585322992 100644
--- a/packages/vuetify/dev/index.js
+++ b/packages/vuetify/dev/index.js
@@ -1,4 +1,6 @@
import { createApp } from 'vue'
+import { createRouter, createWebHashHistory } from 'vue-router'
+
import App from './App'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
@@ -10,8 +12,33 @@ import '@mdi/font/css/materialdesignicons.css'
library.add(fas)
+const component1 = {
+ template: `
Page 1
`,
+}
+const component2 = {
+ template: `Page 2
`,
+}
+
+const router = createRouter({
+ history: createWebHashHistory(),
+ routes: [
+ {
+ path: '/page1',
+ name: 'Page 1',
+ component: component1,
+ },
+ {
+ path: '/page2',
+ name: 'Page 2',
+ component: component2,
+ },
+ { path: '/:pathMatch(.*)*', redirect: '/page1' },
+ ],
+})
+
const app = createApp(App)
+app.use(router)
app.use(vuetify)
app.component('FontAwesomeIcon', FontAwesomeIcon)
diff --git a/packages/vuetify/package.json b/packages/vuetify/package.json
index f81a6ed80ae..75effc3951f 100755
--- a/packages/vuetify/package.json
+++ b/packages/vuetify/package.json
@@ -102,6 +102,7 @@
"style-loader": "^2.0.0",
"url-loader": "^4.1.1",
"vue-meta": "^2.4.0",
+ "vue-router": "^4.0.10",
"vuetify-loader": "^1.7.2"
},
"peerDependencies": {
diff --git a/packages/vuetify/src/components/VBtn/VBtn.sass b/packages/vuetify/src/components/VBtn/VBtn.sass
index 8c7492078c8..6509955ed1a 100644
--- a/packages/vuetify/src/components/VBtn/VBtn.sass
+++ b/packages/vuetify/src/components/VBtn/VBtn.sass
@@ -26,7 +26,7 @@
+button-sizes()
+button-density('height', $button-density)
- +states('.v-btn__overlay')
+ @include states('.v-btn__overlay', false)
&--icon
border-radius: $button-icon-border-radius
@@ -53,8 +53,7 @@
opacity: .62
&:focus,
- &:hover,
- &.v-btn--active
+ &:hover
opacity: 1
.v-btn__overlay
@@ -142,8 +141,10 @@
transform: $button-bottom-navigation-shift-transform
transition: inherit
- &--is-active
+ &--active
.v-bottom-navigation &
+ @include active-states('.v-btn__overlay')
+
filter: $button-bottom-navigation-active-filter
opacity: $button-bottom-navigation-active-opacity
diff --git a/packages/vuetify/src/components/VBtn/VBtn.tsx b/packages/vuetify/src/components/VBtn/VBtn.tsx
index 76050a7eb9f..ae3770d4445 100644
--- a/packages/vuetify/src/components/VBtn/VBtn.tsx
+++ b/packages/vuetify/src/components/VBtn/VBtn.tsx
@@ -5,12 +5,14 @@ import './VBtn.sass'
import { VIcon } from '@/components'
// Composables
-import { makeDensityProps, useDensity } from '@/composables/density'
import { makeBorderProps, useBorder } from '@/composables/border'
-import { makeRoundedProps, useRounded } from '@/composables/rounded'
+import { makeDensityProps, useDensity } from '@/composables/density'
import { makeDimensionProps, useDimension } from '@/composables/dimensions'
import { makeElevationProps, useElevation } from '@/composables/elevation'
import { makePositionProps, usePosition } from '@/composables/position'
+import { makeRoundedProps, useRounded } from '@/composables/rounded'
+import { makeRouterProps, useLink } from '@/composables/router'
+import { makeSizeProps, useSize } from '@/composables/size'
import { makeTagProps } from '@/composables/tag'
import { makeThemeProps, useTheme } from '@/composables/theme'
import { useColor } from '@/composables/color'
@@ -22,8 +24,6 @@ import { Ripple } from '@/directives/ripple'
import { computed, defineComponent } from 'vue'
import { makeProps } from '@/util'
-import { makeSizeProps, useSize } from '@/composables/size'
-
export default defineComponent({
name: 'VBtn',
@@ -48,12 +48,13 @@ export default defineComponent({
...makeDimensionProps(),
...makeElevationProps(),
...makePositionProps(),
+ ...makeRouterProps(),
...makeSizeProps(),
...makeTagProps({ tag: 'button' }),
...makeThemeProps(),
}),
- setup (props, { slots }) {
+ setup (props, { attrs, slots }) {
const { themeClasses } = useTheme(props)
const { borderClasses } = useBorder(props, 'v-btn')
const { roundedClasses } = useRounded(props, 'v-btn')
@@ -62,6 +63,7 @@ export default defineComponent({
const { elevationClasses } = useElevation(props)
const { positionClasses, positionStyles } = usePosition(props, 'v-btn')
const { sizeClasses } = useSize(props, 'v-btn')
+ const link = useLink(props, attrs)
const isContained = computed(() => {
return !(props.text || props.plain || props.outlined || props.border !== false)
@@ -75,70 +77,77 @@ export default defineComponent({
[isContained.value ? 'background' : 'text']: props.color,
})))
- return () => (
-
-
-
- { !props.icon && props.prependIcon && (
-
- )}
-
- { typeof props.icon === 'boolean'
- ? slots.default?.()
- : (
+ return () => {
+ const Tag = (link.isLink.value) ? 'a' : props.tag
+
+ return (
+
+
+
+ { !props.icon && props.prependIcon && (
+
+ ) }
+
+ { typeof props.icon === 'boolean'
+ ? slots.default?.()
+ : (
+
+ )
+ }
+
+ { !props.icon && props.appendIcon && (
- )
- }
-
- { !props.icon && props.appendIcon && (
-
- )}
-
- )
+ ) }
+
+ )
+ }
},
})
diff --git a/packages/vuetify/src/components/VCard/VCard.sass b/packages/vuetify/src/components/VCard/VCard.sass
index 6eec4c49d73..c26d5494022 100644
--- a/packages/vuetify/src/components/VCard/VCard.sass
+++ b/packages/vuetify/src/components/VCard/VCard.sass
@@ -6,6 +6,7 @@
color: $card-color
position: relative
padding: $card-padding
+ text-decoration: none
@include border($card-border...)
@include elevation($card-elevation)
diff --git a/packages/vuetify/src/components/VCard/VCard.tsx b/packages/vuetify/src/components/VCard/VCard.tsx
index 5e1a44552b3..2f8fe7384e6 100644
--- a/packages/vuetify/src/components/VCard/VCard.tsx
+++ b/packages/vuetify/src/components/VCard/VCard.tsx
@@ -23,9 +23,10 @@ import { makeDimensionProps, useDimension } from '@/composables/dimensions'
import { makeElevationProps, useElevation } from '@/composables/elevation'
import { makePositionProps, usePosition } from '@/composables/position'
import { makeRoundedProps, useRounded } from '@/composables/rounded'
+import { makeRouterProps, useLink } from '@/composables/router'
import { makeTagProps } from '@/composables/tag'
-import { useBackgroundColor } from '@/composables/color'
import { makeThemeProps, useTheme } from '@/composables/theme'
+import { useBackgroundColor } from '@/composables/color'
// Directives
import { Ripple } from '@/directives/ripple'
@@ -54,6 +55,7 @@ export default defineComponent({
subtitle: String,
text: String,
title: String,
+
...makeThemeProps(),
...makeBorderProps(),
...makeDensityProps(),
@@ -62,9 +64,10 @@ export default defineComponent({
...makePositionProps(),
...makeRoundedProps(),
...makeTagProps(),
+ ...makeRouterProps(),
}),
- setup (props, { slots }) {
+ setup (props, { attrs, slots }) {
const { themeClasses } = useTheme(props)
const { backgroundColorClasses, backgroundColorStyles } = useBackgroundColor(toRef(props, 'color'))
const { borderClasses } = useBorder(props, 'v-card')
@@ -73,27 +76,29 @@ export default defineComponent({
const { positionClasses, positionStyles } = usePosition(props, 'v-card')
const { roundedClasses } = useRounded(props, 'v-card')
const { densityClasses } = useDensity(props, 'v-card')
+ const link = useLink(props, attrs)
return () => {
+ const Tag = (link.isLink.value) ? 'a' : props.tag
const hasTitle = !!(slots.title || props.title)
const hasSubtitle = !!(slots.subtitle || props.subtitle)
- const hasHeaderText = !!(hasTitle || hasSubtitle)
+ const hasHeaderText = hasTitle || hasSubtitle
const hasAppend = !!(slots.append || props.appendAvatar || props.appendIcon)
const hasPrepend = !!(slots.prepend || props.prependAvatar || props.prependIcon)
const hasImage = !!(slots.image || props.image)
const hasHeader = hasHeaderText || hasPrepend || hasAppend
const hasText = !!(slots.text || props.text)
- const hasOverlay = props.link && !props.disabled
+ const isClickable = !props.disabled && (link.isClickable.value || props.link)
return (
-
- { hasOverlay && () }
+ { isClickable && () }
{ hasImage && (
@@ -190,7 +197,7 @@ export default defineComponent({
{ slots.actions && (
) }
-
+
)
}
},
diff --git a/packages/vuetify/src/components/VList/VListItem.sass b/packages/vuetify/src/components/VList/VListItem.sass
index da155a35134..96c59b05989 100644
--- a/packages/vuetify/src/components/VList/VListItem.sass
+++ b/packages/vuetify/src/components/VList/VListItem.sass
@@ -6,6 +6,7 @@
padding: $list-item-padding
position: relative
outline: none
+ text-decoration: none
transition: $list-item-transition
@include states('.v-list-item__overlay')
diff --git a/packages/vuetify/src/components/VList/VListItem.tsx b/packages/vuetify/src/components/VList/VListItem.tsx
index 92b11f64bd9..0e8edc7b124 100644
--- a/packages/vuetify/src/components/VList/VListItem.tsx
+++ b/packages/vuetify/src/components/VList/VListItem.tsx
@@ -16,9 +16,10 @@ import { makeDensityProps, useDensity } from '@/composables/density'
import { makeDimensionProps, useDimension } from '@/composables/dimensions'
import { makeElevationProps, useElevation } from '@/composables/elevation'
import { makeRoundedProps, useRounded } from '@/composables/rounded'
+import { makeRouterProps, useLink } from '@/composables/router'
import { makeTagProps } from '@/composables/tag'
-import { useColor } from '@/composables/color'
import { makeThemeProps, useTheme } from '@/composables/theme'
+import { useColor } from '@/composables/color'
// Directives
import { Ripple } from '@/directives/ripple'
@@ -46,20 +47,26 @@ export default defineComponent({
subtitle: String,
contained: String,
title: String,
+
...makeBorderProps(),
...makeDensityProps(),
...makeDimensionProps(),
...makeElevationProps(),
...makeRoundedProps(),
+ ...makeRouterProps(),
...makeTagProps(),
...makeThemeProps(),
}),
setup (props, { attrs, slots }) {
+ const link = useLink(props, attrs)
+ const isActive = computed(() => {
+ return props.active || link.isExactActive?.value
+ })
const { themeClasses } = useTheme(props)
const { colorClasses, colorStyles } = useColor(computed(() => {
const key = props.contained && props.active ? 'background' : 'text'
- const color = (props.active && props.activeColor) || props.color
+ const color = (isActive.value && props.activeColor) || props.color
return { [`${key}`]: color }
}))
@@ -70,24 +77,24 @@ export default defineComponent({
const { roundedClasses } = useRounded(props, 'v-list-item')
return () => {
+ const Tag = (link.isLink.value) ? 'a' : props.tag
const hasTitle = (slots.title || props.title)
const hasSubtitle = (slots.subtitle || props.subtitle)
const hasHeader = !!(hasTitle || hasSubtitle)
const hasAppend = (slots.append || props.appendAvatar || props.appendIcon)
const hasPrepend = (slots.prepend || props.prependAvatar || props.prependIcon)
- const isLink = !!(props.link || attrs.onClick || attrs.onClickOnce)
- const isClickable = isLink && !props.disabled
+ const isClickable = !props.disabled && (link.isClickable.value || props.link)
return (
-
- { (isClickable || props.active) && () }
+ { (isClickable || isActive.value) && () }
{ hasPrepend && (
slots.prepend
@@ -156,7 +165,7 @@ export default defineComponent({
)
) }
-
+
)
}
},
diff --git a/packages/vuetify/src/components/VOverlay/VOverlay.tsx b/packages/vuetify/src/components/VOverlay/VOverlay.tsx
index 591f2a8afc9..46fd4cd6671 100644
--- a/packages/vuetify/src/components/VOverlay/VOverlay.tsx
+++ b/packages/vuetify/src/components/VOverlay/VOverlay.tsx
@@ -1,16 +1,18 @@
// Styles
import './VOverlay.sass'
-// Directives
-import { ClickOutside } from '@/directives/click-outside'
-
// Composables
-import { useBackgroundColor } from '@/composables/color'
-import { makeTransitionProps, MaybeTransition } from '@/composables/transition'
import { makeThemeProps, useTheme } from '@/composables/theme'
+import { makeTransitionProps, MaybeTransition } from '@/composables/transition'
+import { useBackButton } from '@/composables/router'
+import { useBackgroundColor } from '@/composables/color'
import { useProxiedModel } from '@/composables/proxiedModel'
+import { useRtl } from '@/composables/rtl'
import { useTeleport } from '@/composables/teleport'
+// Directives
+import { ClickOutside } from '@/directives/click-outside'
+
// Utilities
import { convertToUnit, getScrollParent, getScrollParents, standardEasing, useRender } from '@/util'
import { makeProps } from '@/util/makeProps'
@@ -29,7 +31,6 @@ import {
// Types
import type { BackgroundColorData } from '@/composables/color'
import type { Prop, PropType, Ref } from 'vue'
-import { useRtl } from '@/composables/rtl'
function useBooted (isActive: Ref, eager: Ref) {
const isBooted = ref(eager.value)
@@ -186,6 +187,7 @@ export default defineComponent({
setup (props, { slots, attrs, emit }) {
const isActive = useProxiedModel(props, 'modelValue')
+
const { teleportTarget } = useTeleport(toRef(props, 'attach'))
const { themeClasses } = useTheme(props)
const { rtlClasses } = useRtl()
@@ -219,8 +221,14 @@ export default defineComponent({
}
}
- const content = ref()
+ useBackButton(next => {
+ next(!isActive.value)
+ if (!props.persistent) isActive.value = false
+ else animateClick()
+ })
+
+ const content = ref()
watch(isActive, val => {
nextTick(() => {
if (val) {
diff --git a/packages/vuetify/src/composables/router.tsx b/packages/vuetify/src/composables/router.tsx
new file mode 100644
index 00000000000..9e981ef28c4
--- /dev/null
+++ b/packages/vuetify/src/composables/router.tsx
@@ -0,0 +1,98 @@
+// Utilities
+import { propsFactory } from '@/util'
+import {
+ computed,
+ getCurrentInstance,
+ onBeforeUnmount,
+ onMounted,
+ resolveDynamicComponent,
+ toRef,
+} from 'vue'
+
+// Types
+import type { ComputedRef, PropType, Ref, SetupContext } from 'vue'
+import type {
+ RouterLink as _RouterLink,
+ useLink as _useLink,
+ NavigationGuardNext,
+ RouteLocationNormalizedLoaded,
+ RouteLocationRaw,
+ Router,
+ UseLinkOptions,
+} from 'vue-router'
+
+export function useRoute (): Ref {
+ const vm = getCurrentInstance()
+
+ return computed(() => vm?.proxy?.$route)
+}
+
+export function useRouter (): Router | undefined {
+ return getCurrentInstance()?.proxy?.$router
+}
+
+interface LinkProps extends Partial {
+ href?: string
+}
+
+interface UseLink extends Omit>, 'href'> {
+ isLink: ComputedRef
+ isClickable: ComputedRef
+ href: Ref
+}
+
+export function useLink (props: LinkProps, attrs: SetupContext['attrs']): UseLink {
+ const RouterLink = resolveDynamicComponent('RouterLink') as typeof _RouterLink | string
+
+ const isLink = computed(() => !!(props.href || props.to))
+ const isClickable = computed(() => {
+ return isLink?.value || !!(attrs.onClick || attrs.onClickOnce)
+ })
+
+ if (typeof RouterLink === 'string') {
+ return {
+ isLink,
+ isClickable,
+ href: toRef(props, 'href'),
+ }
+ }
+
+ const link = props.to ? RouterLink.useLink(props as UseLinkOptions) : undefined
+
+ return {
+ ...link,
+ isLink,
+ isClickable,
+ href: computed(() => props.to ? link?.route.value.href : props.href),
+ }
+}
+
+export const makeRouterProps = propsFactory({
+ href: String,
+ replace: Boolean,
+ to: [String, Object] as PropType,
+}, 'router')
+
+export function useBackButton (cb: (next: NavigationGuardNext) => void) {
+ const router = useRouter()
+ let popped = false
+ let removeGuard: (() => void) | undefined
+
+ onMounted(() => {
+ window.addEventListener('popstate', onPopstate)
+ removeGuard = router?.beforeEach((to, from, next) => {
+ setTimeout(() => popped ? cb(next) : next())
+ })
+ })
+ onBeforeUnmount(() => {
+ window.removeEventListener('popstate', onPopstate)
+ removeGuard?.()
+ })
+
+ function onPopstate (e: PopStateEvent) {
+ if (e.state.replaced) return
+
+ popped = true
+ setTimeout(() => (popped = false))
+ }
+}
diff --git a/packages/vuetify/src/styles/tools/_states.sass b/packages/vuetify/src/styles/tools/_states.sass
index f3bbfa27f78..c0d5a4f808d 100644
--- a/packages/vuetify/src/styles/tools/_states.sass
+++ b/packages/vuetify/src/styles/tools/_states.sass
@@ -1,4 +1,4 @@
-@mixin states ($selector: '&::before')
+@mixin states ($selector: '&::before', $active: true)
&:hover
#{$selector}
opacity: calc(#{map-get($states, 'hover')} * var(--v-theme-overlay-multiplier))
@@ -7,8 +7,9 @@
#{$selector}
opacity: calc(#{map-get($states, 'focus')} * var(--v-theme-overlay-multiplier))
- &--active
- +active-states($selector)
+ @if ($active)
+ &--active
+ @include active-states($selector)
@mixin active-states ($selector: '::before')
&:hover#{$selector},
diff --git a/packages/vuetify/tsconfig.json b/packages/vuetify/tsconfig.json
index 6b50a821617..b57d1d1e2c7 100644
--- a/packages/vuetify/tsconfig.json
+++ b/packages/vuetify/tsconfig.json
@@ -13,7 +13,8 @@
],
"types": [
"jest",
- "node"
+ "node",
+ "vue-router"
]
}
}
diff --git a/yarn.lock b/yarn.lock
index 5d5dbe1182a..786848ff2fc 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4772,6 +4772,11 @@
optionalDependencies:
prettier "^1.18.2"
+"@vue/devtools-api@^6.0.0-beta.14":
+ version "6.0.0-beta.14"
+ resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.0.0-beta.14.tgz#6ed2d6f8d66a9256c9ad04bfff08309ba87b9723"
+ integrity sha512-44fPrrN1cqcs6bFkT0C+yxTM6PZXLbR+ESh1U1j8UD22yO04gXvxH62HApMjLbS3WqJO/iCNC+CYT+evPQh2EQ==
+
"@vue/reactivity@3.1.1":
version "3.1.1"
resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.1.1.tgz#9c02fd146a6c3b03e7d658b7cf76f4b69b0f98c8"
@@ -18351,16 +18356,18 @@ vue-prism-component@^1.2.0:
resolved "https://registry.yarnpkg.com/vue-prism-component/-/vue-prism-component-1.2.0.tgz#406252e16979def13b5d28827d95b2b6dc647825"
integrity sha512-0N9CNuQu+36CJpdsZHrhdq7d18oBvjVMjawyKdIr8xuzFWLfdxECZQYbFaYoopPBg3SvkEEMtkhYqdgTQl5Y+A==
-vue-router@^3.0.1:
- version "3.4.7"
- resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.4.7.tgz#bf189bafd16f4e4ef783c4a6250a3090f2c1fa1b"
- integrity sha512-CbHXue5BLrDivOk5O4eZ0WT4Yj8XwdXa4kCnsEIOzYUPF/07ZukayA2jGxDCJxLc9SgVQX9QX0OuGOwGlVB4Qg==
-
vue-router@^3.1.6:
version "3.4.3"
resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.4.3.tgz#fa93768616ee338aa174f160ac965167fa572ffa"
integrity sha512-BADg1mjGWX18Dpmy6bOGzGNnk7B/ZA0RxuA6qedY/YJwirMfKXIDzcccmHbQI0A6k5PzMdMloc0ElHfyOoX35A==
+vue-router@^4.0.10:
+ version "4.0.10"
+ resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-4.0.10.tgz#ec8fda032949b2a31d3273170f8f376e86eb52ac"
+ integrity sha512-YbPf6QnZpyyWfnk7CUt2Bme+vo7TLfg1nGZNkvYqKYh4vLaFw6Gn8bPGdmt5m4qrGnKoXLqc4htAsd3dIukICA==
+ dependencies:
+ "@vue/devtools-api" "^6.0.0-beta.14"
+
vue-server-renderer@^2.6.12:
version "2.6.12"
resolved "https://registry.yarnpkg.com/vue-server-renderer/-/vue-server-renderer-2.6.12.tgz#a8cb9c49439ef205293cb41c35d0d2b0541653a5"