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"