quasar 2.22.0 → 2.23.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/Mutation.json +1 -1
- package/dist/api/QLinearProgress.json +1 -1
- package/dist/icon-set/bootstrap-icons.umd.prod.js +1 -1
- package/dist/icon-set/eva-icons.umd.prod.js +1 -1
- package/dist/icon-set/fontawesome-v5-pro.umd.prod.js +1 -1
- package/dist/icon-set/fontawesome-v5.umd.prod.js +1 -1
- package/dist/icon-set/fontawesome-v6-pro.umd.prod.js +1 -1
- package/dist/icon-set/fontawesome-v6.umd.prod.js +1 -1
- package/dist/icon-set/fontawesome-v7-pro.umd.prod.js +1 -1
- package/dist/icon-set/fontawesome-v7.umd.prod.js +1 -1
- package/dist/icon-set/ionicons-v4.umd.prod.js +1 -1
- package/dist/icon-set/line-awesome.umd.prod.js +1 -1
- package/dist/icon-set/material-icons-outlined.umd.prod.js +1 -1
- package/dist/icon-set/material-icons-round.umd.prod.js +1 -1
- package/dist/icon-set/material-icons-sharp.umd.prod.js +1 -1
- package/dist/icon-set/material-icons.umd.prod.js +1 -1
- package/dist/icon-set/material-symbols-outlined.umd.prod.js +1 -1
- package/dist/icon-set/material-symbols-rounded.umd.prod.js +1 -1
- package/dist/icon-set/material-symbols-sharp.umd.prod.js +1 -1
- package/dist/icon-set/mdi-v3.umd.prod.js +1 -1
- package/dist/icon-set/mdi-v4.umd.prod.js +1 -1
- package/dist/icon-set/mdi-v5.umd.prod.js +1 -1
- package/dist/icon-set/mdi-v6.umd.prod.js +1 -1
- package/dist/icon-set/mdi-v7.umd.prod.js +1 -1
- package/dist/icon-set/svg-bootstrap-icons.umd.prod.js +1 -1
- package/dist/icon-set/svg-eva-icons.umd.prod.js +1 -1
- package/dist/icon-set/svg-fontawesome-v5.umd.prod.js +1 -1
- package/dist/icon-set/svg-fontawesome-v6.umd.prod.js +1 -1
- package/dist/icon-set/svg-fontawesome-v7.umd.prod.js +1 -1
- package/dist/icon-set/svg-ionicons-v4.umd.prod.js +1 -1
- package/dist/icon-set/svg-ionicons-v5.umd.prod.js +1 -1
- package/dist/icon-set/svg-ionicons-v6.umd.prod.js +1 -1
- package/dist/icon-set/svg-ionicons-v7.umd.prod.js +1 -1
- package/dist/icon-set/svg-ionicons-v8.umd.prod.js +1 -1
- package/dist/icon-set/svg-line-awesome.umd.prod.js +1 -1
- package/dist/icon-set/svg-material-icons-outlined.umd.prod.js +1 -1
- package/dist/icon-set/svg-material-icons-round.umd.prod.js +1 -1
- package/dist/icon-set/svg-material-icons-sharp.umd.prod.js +1 -1
- package/dist/icon-set/svg-material-icons.umd.prod.js +1 -1
- package/dist/icon-set/svg-material-symbols-outlined.umd.prod.js +1 -1
- package/dist/icon-set/svg-material-symbols-rounded.umd.prod.js +1 -1
- package/dist/icon-set/svg-material-symbols-sharp.umd.prod.js +1 -1
- package/dist/icon-set/svg-mdi-v6.umd.prod.js +1 -1
- package/dist/icon-set/svg-mdi-v7.umd.prod.js +1 -1
- package/dist/icon-set/svg-themify.umd.prod.js +1 -1
- package/dist/icon-set/themify.umd.prod.js +1 -1
- package/dist/lang/ar-TN.umd.prod.js +1 -1
- package/dist/lang/ar.umd.prod.js +1 -1
- package/dist/lang/az-Latn.umd.prod.js +1 -1
- package/dist/lang/bg.umd.prod.js +1 -1
- package/dist/lang/bn.umd.prod.js +1 -1
- package/dist/lang/bs-BA.umd.prod.js +1 -1
- package/dist/lang/ca.umd.prod.js +1 -1
- package/dist/lang/cs.umd.prod.js +1 -1
- package/dist/lang/da.umd.prod.js +1 -1
- package/dist/lang/de-CH.umd.prod.js +1 -1
- package/dist/lang/de-DE.umd.prod.js +1 -1
- package/dist/lang/de.umd.prod.js +1 -1
- package/dist/lang/el.umd.prod.js +1 -1
- package/dist/lang/en-GB.umd.prod.js +1 -1
- package/dist/lang/en-US.umd.prod.js +1 -1
- package/dist/lang/eo.umd.prod.js +1 -1
- package/dist/lang/es.umd.prod.js +1 -1
- package/dist/lang/et.umd.prod.js +1 -1
- package/dist/lang/eu.umd.prod.js +1 -1
- package/dist/lang/fa-IR.umd.prod.js +1 -1
- package/dist/lang/fa.umd.prod.js +1 -1
- package/dist/lang/fi.umd.prod.js +1 -1
- package/dist/lang/fr.umd.prod.js +1 -1
- package/dist/lang/gn.umd.prod.js +1 -1
- package/dist/lang/he.umd.prod.js +1 -1
- package/dist/lang/hi.umd.prod.js +1 -1
- package/dist/lang/hr.umd.prod.js +1 -1
- package/dist/lang/hu.umd.prod.js +1 -1
- package/dist/lang/id.umd.prod.js +1 -1
- package/dist/lang/is.umd.prod.js +1 -1
- package/dist/lang/it.umd.prod.js +1 -1
- package/dist/lang/ja.umd.prod.js +1 -1
- package/dist/lang/kk.umd.prod.js +1 -1
- package/dist/lang/km.umd.prod.js +1 -1
- package/dist/lang/ko-KR.umd.prod.js +1 -1
- package/dist/lang/kur-CKB.umd.prod.js +1 -1
- package/dist/lang/lb.umd.prod.js +1 -1
- package/dist/lang/lt.umd.prod.js +1 -1
- package/dist/lang/lu.umd.prod.js +1 -1
- package/dist/lang/lv.umd.prod.js +1 -1
- package/dist/lang/mk.umd.prod.js +1 -1
- package/dist/lang/ml.umd.prod.js +1 -1
- package/dist/lang/mm.umd.prod.js +1 -1
- package/dist/lang/ms-MY.umd.prod.js +1 -1
- package/dist/lang/ms.umd.prod.js +1 -1
- package/dist/lang/my.umd.prod.js +1 -1
- package/dist/lang/nb-NO.umd.prod.js +1 -1
- package/dist/lang/nl.umd.prod.js +1 -1
- package/dist/lang/pl.umd.prod.js +1 -1
- package/dist/lang/pt-BR.umd.prod.js +1 -1
- package/dist/lang/pt.umd.prod.js +1 -1
- package/dist/lang/ro.umd.prod.js +1 -1
- package/dist/lang/ru.umd.prod.js +1 -1
- package/dist/lang/sk.umd.prod.js +1 -1
- package/dist/lang/sl.umd.prod.js +1 -1
- package/dist/lang/sm.umd.prod.js +1 -1
- package/dist/lang/sq.umd.prod.js +1 -1
- package/dist/lang/sr-CYR.umd.prod.js +1 -1
- package/dist/lang/sr.umd.prod.js +1 -1
- package/dist/lang/sv.umd.prod.js +1 -1
- package/dist/lang/ta.umd.prod.js +1 -1
- package/dist/lang/th.umd.prod.js +1 -1
- package/dist/lang/tl.umd.prod.js +1 -1
- package/dist/lang/tr.umd.prod.js +1 -1
- package/dist/lang/ug.umd.prod.js +1 -1
- package/dist/lang/uk.umd.prod.js +1 -1
- package/dist/lang/ur-PK.umd.prod.js +1 -1
- package/dist/lang/uz-Cyrl.umd.prod.js +1 -1
- package/dist/lang/uz-Latn.umd.prod.js +1 -1
- package/dist/lang/vi.umd.prod.js +1 -1
- package/dist/lang/zh-CN.umd.prod.js +1 -1
- package/dist/lang/zh-TW.umd.prod.js +1 -1
- package/dist/quasar.client.js +92 -30
- package/dist/quasar.css +16 -11
- package/dist/quasar.prod.css +1 -1
- package/dist/quasar.rtl.css +20 -20
- package/dist/quasar.rtl.prod.css +1 -1
- package/dist/quasar.sass +18 -12
- package/dist/quasar.server.prod.cjs +4 -4
- package/dist/quasar.server.prod.js +4 -4
- package/dist/quasar.umd.js +92 -29
- package/dist/quasar.umd.prod.js +31 -31
- package/dist/transforms/import-map.json +1 -1
- package/dist/types/composables.d.ts +5 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/web-types/web-types.json +1 -1
- package/package.json +1 -1
- package/src/components/btn/QBtn.js +1 -1
- package/src/components/fab/QFab.sass +11 -9
- package/src/components/linear-progress/QLinearProgress.js +4 -11
- package/src/components/linear-progress/QLinearProgress.json +1 -1
- package/src/components/linear-progress/QLinearProgress.sass +7 -2
- package/src/composables/private.use-fullscreen/use-fullscreen.js +72 -17
- package/src/composables/private.use-fullscreen/use-fullscreen.test.js +158 -0
- package/src/composables/use-animation-frame/use-animation-frame.js +47 -0
- package/src/composables/use-animation-frame/use-animation-frame.test.js +122 -0
- package/src/composables.js +1 -1
- package/src/directives/mutation/Mutation.json +1 -1
- package/src/utils/private.click-outside/click-outside.js +27 -12
- package/src/utils/private.click-outside/click-outside.test.js +132 -0
package/package.json
CHANGED
|
@@ -100,20 +100,22 @@
|
|
|
100
100
|
.q-btn
|
|
101
101
|
margin: 5px
|
|
102
102
|
|
|
103
|
+
// physical direction, not text-based: don't let rtlcss mirror it
|
|
103
104
|
&--right
|
|
104
|
-
transform-origin: 0 50%
|
|
105
|
-
transform: scale(.4) translateX(-62px)
|
|
105
|
+
transform-origin: 0 50% #{"/* rtl:ignore */"}
|
|
106
|
+
transform: scale(.4) translateX(-62px) #{"/* rtl:ignore */"}
|
|
106
107
|
height: 56px
|
|
107
|
-
left: 100%
|
|
108
|
-
margin-left: 9px
|
|
108
|
+
left: 100% #{"/* rtl:ignore */"}
|
|
109
|
+
margin-left: 9px #{"/* rtl:ignore */"}
|
|
110
|
+
flex-direction: row #{"/* rtl:row-reverse */"}
|
|
109
111
|
|
|
110
112
|
&--left
|
|
111
|
-
transform-origin: 100% 50%
|
|
112
|
-
transform: scale(.4) translateX(62px)
|
|
113
|
+
transform-origin: 100% 50% #{"/* rtl:ignore */"}
|
|
114
|
+
transform: scale(.4) translateX(62px) #{"/* rtl:ignore */"}
|
|
113
115
|
height: 56px
|
|
114
|
-
right: 100%
|
|
115
|
-
margin-right: 9px
|
|
116
|
-
flex-direction: row-reverse
|
|
116
|
+
right: 100% #{"/* rtl:ignore */"}
|
|
117
|
+
margin-right: 9px #{"/* rtl:ignore */"}
|
|
118
|
+
flex-direction: row-reverse #{"/* rtl:row */"}
|
|
117
119
|
|
|
118
120
|
&--up
|
|
119
121
|
transform-origin: 50% 100%
|
|
@@ -18,11 +18,9 @@ const defaultSizes = {
|
|
|
18
18
|
xl: 14
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
function width(val
|
|
21
|
+
function width(val) {
|
|
22
22
|
return {
|
|
23
|
-
transform:
|
|
24
|
-
? `translateX(${$q.lang.rtl ? '-' : ''}100%) scale3d(${-val},1,1)`
|
|
25
|
-
: `scale3d(${val},1,1)`
|
|
23
|
+
transform: `scale3d(${val},1,1)`
|
|
26
24
|
}
|
|
27
25
|
}
|
|
28
26
|
|
|
@@ -62,7 +60,6 @@ export default createComponent({
|
|
|
62
60
|
const sizeStyle = useSize(props, defaultSizes)
|
|
63
61
|
|
|
64
62
|
const motion = computed(() => props.indeterminate || props.query)
|
|
65
|
-
const widthReverse = computed(() => props.reverse !== props.query)
|
|
66
63
|
const style = computed(() => ({
|
|
67
64
|
...(sizeStyle.value !== null ? sizeStyle.value : {}),
|
|
68
65
|
'--q-linear-progress-speed': `${props.animationSpeed}ms`
|
|
@@ -77,11 +74,7 @@ export default createComponent({
|
|
|
77
74
|
)
|
|
78
75
|
|
|
79
76
|
const trackStyle = computed(() =>
|
|
80
|
-
width(
|
|
81
|
-
props.buffer !== void 0 ? props.buffer : 1,
|
|
82
|
-
widthReverse.value,
|
|
83
|
-
proxy.$q
|
|
84
|
-
)
|
|
77
|
+
width(props.buffer !== void 0 ? props.buffer : 1)
|
|
85
78
|
)
|
|
86
79
|
const transitionSuffix = computed(
|
|
87
80
|
() => `with${props.instantFeedback ? 'out' : ''}-transition`
|
|
@@ -96,7 +89,7 @@ export default createComponent({
|
|
|
96
89
|
)
|
|
97
90
|
|
|
98
91
|
const modelStyle = computed(() =>
|
|
99
|
-
width(motion.value ? 1 : props.value
|
|
92
|
+
width(motion.value ? 1 : props.value)
|
|
100
93
|
)
|
|
101
94
|
const modelClass = computed(
|
|
102
95
|
() =>
|
|
@@ -16,8 +16,13 @@
|
|
|
16
16
|
|
|
17
17
|
&--reverse
|
|
18
18
|
.q-linear-progress
|
|
19
|
-
&__model, &__track
|
|
20
|
-
transform-origin:
|
|
19
|
+
&__model--determinate, &__track
|
|
20
|
+
transform-origin: 100% 0
|
|
21
|
+
|
|
22
|
+
// motion modes span the full width, so anchoring cannot reverse them: mirror them in place
|
|
23
|
+
&__model--indeterminate, &__model--query
|
|
24
|
+
transform-origin: 50% 0
|
|
25
|
+
scale: -1 1
|
|
21
26
|
|
|
22
27
|
&__model
|
|
23
28
|
|
|
@@ -1,16 +1,25 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getCurrentInstance,
|
|
3
|
+
onActivated,
|
|
3
4
|
onBeforeMount,
|
|
4
5
|
onBeforeUnmount,
|
|
6
|
+
onDeactivated,
|
|
5
7
|
onMounted,
|
|
6
8
|
ref,
|
|
7
9
|
watch
|
|
8
10
|
} from 'vue'
|
|
9
11
|
|
|
10
12
|
import History from '../../plugins/private.history/History.js'
|
|
11
|
-
import { vmHasRouter } from '../../utils/private.vm/vm.js'
|
|
13
|
+
import { vmHasRouter, vmIsDestroyed } from '../../utils/private.vm/vm.js'
|
|
14
|
+
import useTick from '../use-tick/use-tick.js'
|
|
15
|
+
import useAnimationFrame from '../use-animation-frame/use-animation-frame.js'
|
|
16
|
+
import {
|
|
17
|
+
getHorizontalScrollPosition,
|
|
18
|
+
getVerticalScrollPosition
|
|
19
|
+
} from '../../utils/scroll/scroll.js'
|
|
12
20
|
|
|
13
21
|
let counter = 0
|
|
22
|
+
let restoreState = null
|
|
14
23
|
|
|
15
24
|
export const useFullscreenProps = {
|
|
16
25
|
fullscreen: Boolean,
|
|
@@ -23,10 +32,11 @@ export default function useFullscreen() {
|
|
|
23
32
|
const vm = getCurrentInstance()
|
|
24
33
|
const { props, emit, proxy } = vm
|
|
25
34
|
|
|
26
|
-
let historyEntry,
|
|
27
|
-
|
|
28
|
-
isUnmounting = false
|
|
35
|
+
let historyEntry, fullscreenFillerNode
|
|
36
|
+
|
|
29
37
|
const inFullscreen = ref(false)
|
|
38
|
+
const { registerTick, removeTick } = useTick()
|
|
39
|
+
const { registerAnimationFrame, removeAnimationFrame } = useAnimationFrame()
|
|
30
40
|
|
|
31
41
|
if (vmHasRouter(vm)) {
|
|
32
42
|
watch(
|
|
@@ -58,7 +68,19 @@ export default function useFullscreen() {
|
|
|
58
68
|
}
|
|
59
69
|
|
|
60
70
|
function setFullscreen() {
|
|
61
|
-
if (inFullscreen.value) return
|
|
71
|
+
if (vmIsDestroyed(vm) || inFullscreen.value) return
|
|
72
|
+
|
|
73
|
+
removeAnimationFrame()
|
|
74
|
+
removeTick()
|
|
75
|
+
|
|
76
|
+
if (counter === 0) {
|
|
77
|
+
restoreState = {
|
|
78
|
+
left: getHorizontalScrollPosition(window),
|
|
79
|
+
top: getVerticalScrollPosition(window),
|
|
80
|
+
width: window.innerWidth,
|
|
81
|
+
height: window.innerHeight
|
|
82
|
+
}
|
|
83
|
+
}
|
|
62
84
|
|
|
63
85
|
inFullscreen.value = true
|
|
64
86
|
proxy.$el.replaceWith(fullscreenFillerNode)
|
|
@@ -75,7 +97,7 @@ export default function useFullscreen() {
|
|
|
75
97
|
History.add(historyEntry)
|
|
76
98
|
}
|
|
77
99
|
|
|
78
|
-
function
|
|
100
|
+
function exitFullscreenImpl(shouldRestoreElement) {
|
|
79
101
|
if (!inFullscreen.value) return
|
|
80
102
|
|
|
81
103
|
if (historyEntry !== void 0) {
|
|
@@ -83,20 +105,48 @@ export default function useFullscreen() {
|
|
|
83
105
|
historyEntry = void 0
|
|
84
106
|
}
|
|
85
107
|
|
|
86
|
-
|
|
108
|
+
if (shouldRestoreElement === true) {
|
|
109
|
+
fullscreenFillerNode.replaceWith(proxy.$el)
|
|
110
|
+
} else {
|
|
111
|
+
fullscreenFillerNode.remove()
|
|
112
|
+
}
|
|
113
|
+
|
|
87
114
|
inFullscreen.value = false
|
|
88
115
|
|
|
89
116
|
counter = Math.max(0, counter - 1)
|
|
117
|
+
if (counter !== 0) return
|
|
90
118
|
|
|
91
|
-
|
|
92
|
-
document.body.classList.remove('q-body--fullscreen-mixin')
|
|
119
|
+
document.body.classList.remove('q-body--fullscreen-mixin')
|
|
93
120
|
|
|
94
|
-
|
|
95
|
-
|
|
121
|
+
if (restoreState === null) return
|
|
122
|
+
|
|
123
|
+
const { left, top, width, height } = restoreState
|
|
124
|
+
restoreState = null
|
|
125
|
+
|
|
126
|
+
if (vmIsDestroyed(vm)) return
|
|
127
|
+
|
|
128
|
+
registerTick(() => {
|
|
129
|
+
if (counter !== 0) return
|
|
130
|
+
registerAnimationFrame(() => {
|
|
131
|
+
if (counter !== 0) return
|
|
132
|
+
|
|
133
|
+
if (window.innerWidth !== width || window.innerHeight !== height) {
|
|
134
|
+
/**
|
|
135
|
+
* If user has resized the window while in fullscreen mode,
|
|
136
|
+
* we cannot restore the scroll position because it will be wrong.
|
|
137
|
+
* So we just scroll the element into view instead,
|
|
138
|
+
* which is the best we can do.
|
|
139
|
+
*/
|
|
96
140
|
proxy.$el.scrollIntoView()
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
|
|
141
|
+
} else {
|
|
142
|
+
window.scrollTo(left, top)
|
|
143
|
+
}
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function exitFullscreen() {
|
|
149
|
+
exitFullscreenImpl(true)
|
|
100
150
|
}
|
|
101
151
|
|
|
102
152
|
onBeforeMount(() => {
|
|
@@ -107,10 +157,15 @@ export default function useFullscreen() {
|
|
|
107
157
|
if (props.fullscreen) setFullscreen()
|
|
108
158
|
})
|
|
109
159
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
160
|
+
onDeactivated(() => {
|
|
161
|
+
// A direct child of KeepAlive has already been moved to its storage
|
|
162
|
+
// container at this point. Its filler must not move it back into the DOM.
|
|
163
|
+
exitFullscreenImpl(proxy.$el.isConnected)
|
|
164
|
+
})
|
|
165
|
+
onActivated(() => {
|
|
166
|
+
if (props.fullscreen) setFullscreen()
|
|
113
167
|
})
|
|
168
|
+
onBeforeUnmount(exitFullscreen)
|
|
114
169
|
|
|
115
170
|
// expose public methods
|
|
116
171
|
Object.assign(proxy, {
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from 'vitest'
|
|
2
|
+
import { flushPromises, mount } from '@vue/test-utils'
|
|
3
|
+
import { KeepAlive, defineComponent, h, ref } from 'vue'
|
|
4
|
+
|
|
5
|
+
import useFullscreen, {
|
|
6
|
+
useFullscreenEmits,
|
|
7
|
+
useFullscreenProps
|
|
8
|
+
} from './use-fullscreen.js'
|
|
9
|
+
|
|
10
|
+
let wrapper, mountTarget
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
wrapper?.unmount()
|
|
14
|
+
mountTarget?.remove()
|
|
15
|
+
wrapper = mountTarget = void 0
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const FullscreenComponent = defineComponent({
|
|
19
|
+
name: 'FullscreenComponent',
|
|
20
|
+
props: useFullscreenProps,
|
|
21
|
+
emits: useFullscreenEmits,
|
|
22
|
+
|
|
23
|
+
setup() {
|
|
24
|
+
const { inFullscreen } = useFullscreen()
|
|
25
|
+
|
|
26
|
+
return () =>
|
|
27
|
+
h(
|
|
28
|
+
'section',
|
|
29
|
+
{
|
|
30
|
+
'data-test': 'fullscreen-component',
|
|
31
|
+
'data-fullscreen': inFullscreen.value
|
|
32
|
+
},
|
|
33
|
+
'Fullscreen component'
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
const OtherComponent = defineComponent({
|
|
39
|
+
name: 'OtherComponent',
|
|
40
|
+
setup: () => () => h('section', { 'data-test': 'other-component' })
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
describe('[useFullscreen API]', () => {
|
|
44
|
+
describe('[Variables]', () => {
|
|
45
|
+
describe('[(variable)useFullscreenProps]', () => {
|
|
46
|
+
test('is defined correctly', () => {
|
|
47
|
+
expect(useFullscreenProps).toBeTypeOf('object')
|
|
48
|
+
expect(Object.keys(useFullscreenProps)).not.toHaveLength(0)
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
describe('[(variable)useFullscreenEmits]', () => {
|
|
53
|
+
test('is defined correctly', () => {
|
|
54
|
+
expect(Array.isArray(useFullscreenEmits)).toBe(true)
|
|
55
|
+
expect(useFullscreenEmits).not.toHaveLength(0)
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
describe('[Functions]', () => {
|
|
61
|
+
describe('[(function)default]', () => {
|
|
62
|
+
test('does not move a deactivated root back into the live DOM', async () => {
|
|
63
|
+
const showFullscreen = ref(true)
|
|
64
|
+
|
|
65
|
+
mountTarget = document.createElement('div')
|
|
66
|
+
document.body.append(mountTarget)
|
|
67
|
+
|
|
68
|
+
wrapper = mount(
|
|
69
|
+
defineComponent({
|
|
70
|
+
setup: () => () =>
|
|
71
|
+
h(KeepAlive, null, () =>
|
|
72
|
+
showFullscreen.value === true
|
|
73
|
+
? h(FullscreenComponent, { fullscreen: true })
|
|
74
|
+
: h(OtherComponent)
|
|
75
|
+
)
|
|
76
|
+
}),
|
|
77
|
+
{ attachTo: mountTarget }
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
await flushPromises()
|
|
81
|
+
|
|
82
|
+
const getFullscreenElement = () =>
|
|
83
|
+
document.body.querySelector('[data-test="fullscreen-component"]')
|
|
84
|
+
|
|
85
|
+
expect(getFullscreenElement()).not.toBeNull()
|
|
86
|
+
expect(getFullscreenElement().dataset.fullscreen).toBe('true')
|
|
87
|
+
expect(
|
|
88
|
+
document.body.classList.contains('q-body--fullscreen-mixin')
|
|
89
|
+
).toBe(true)
|
|
90
|
+
|
|
91
|
+
showFullscreen.value = false
|
|
92
|
+
await flushPromises()
|
|
93
|
+
|
|
94
|
+
expect(getFullscreenElement()).toBeNull()
|
|
95
|
+
expect(wrapper.find('[data-test="other-component"]').exists()).toBe(
|
|
96
|
+
true
|
|
97
|
+
)
|
|
98
|
+
expect(
|
|
99
|
+
document.body.classList.contains('q-body--fullscreen-mixin')
|
|
100
|
+
).toBe(false)
|
|
101
|
+
|
|
102
|
+
showFullscreen.value = true
|
|
103
|
+
await flushPromises()
|
|
104
|
+
|
|
105
|
+
expect(getFullscreenElement()).not.toBeNull()
|
|
106
|
+
expect(getFullscreenElement().dataset.fullscreen).toBe('true')
|
|
107
|
+
expect(
|
|
108
|
+
document.body.classList.contains('q-body--fullscreen-mixin')
|
|
109
|
+
).toBe(true)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
test('restores a nested fullscreen component to cached content', async () => {
|
|
113
|
+
const showFullscreen = ref(true)
|
|
114
|
+
const CachedComponent = defineComponent({
|
|
115
|
+
name: 'CachedComponent',
|
|
116
|
+
setup: () => () =>
|
|
117
|
+
h('main', null, [h(FullscreenComponent, { fullscreen: true })])
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
mountTarget = document.createElement('div')
|
|
121
|
+
document.body.append(mountTarget)
|
|
122
|
+
|
|
123
|
+
wrapper = mount(
|
|
124
|
+
defineComponent({
|
|
125
|
+
setup: () => () =>
|
|
126
|
+
h(KeepAlive, null, () =>
|
|
127
|
+
showFullscreen.value === true
|
|
128
|
+
? h(CachedComponent)
|
|
129
|
+
: h(OtherComponent)
|
|
130
|
+
)
|
|
131
|
+
}),
|
|
132
|
+
{ attachTo: mountTarget }
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
await flushPromises()
|
|
136
|
+
|
|
137
|
+
const getFullscreenElement = () =>
|
|
138
|
+
document.body.querySelector('[data-test="fullscreen-component"]')
|
|
139
|
+
|
|
140
|
+
expect(getFullscreenElement()).not.toBeNull()
|
|
141
|
+
|
|
142
|
+
showFullscreen.value = false
|
|
143
|
+
await flushPromises()
|
|
144
|
+
|
|
145
|
+
expect(getFullscreenElement()).toBeNull()
|
|
146
|
+
expect(wrapper.find('[data-test="other-component"]').exists()).toBe(
|
|
147
|
+
true
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
showFullscreen.value = true
|
|
151
|
+
await flushPromises()
|
|
152
|
+
|
|
153
|
+
expect(getFullscreenElement()).not.toBeNull()
|
|
154
|
+
expect(getFullscreenElement().dataset.fullscreen).toBe('true')
|
|
155
|
+
})
|
|
156
|
+
})
|
|
157
|
+
})
|
|
158
|
+
})
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { getCurrentInstance, onBeforeUnmount, onDeactivated } from 'vue'
|
|
2
|
+
|
|
3
|
+
import { vmIsDestroyed } from '../../utils/private.vm/vm.js'
|
|
4
|
+
import { noop } from '../../utils/event/event.js'
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
* Usage:
|
|
8
|
+
* registerAnimationFrame(fn)
|
|
9
|
+
* removeAnimationFrame()
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export default function useAnimationFrame() {
|
|
13
|
+
if (__QUASAR_SSR_SERVER__) {
|
|
14
|
+
return {
|
|
15
|
+
removeAnimationFrame: noop,
|
|
16
|
+
registerAnimationFrame: noop
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let frameId = null
|
|
21
|
+
const vm = getCurrentInstance()
|
|
22
|
+
|
|
23
|
+
function removeAnimationFrame() {
|
|
24
|
+
if (frameId !== null) {
|
|
25
|
+
cancelAnimationFrame(frameId)
|
|
26
|
+
frameId = null
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
onDeactivated(removeAnimationFrame)
|
|
31
|
+
onBeforeUnmount(removeAnimationFrame)
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
removeAnimationFrame,
|
|
35
|
+
|
|
36
|
+
registerAnimationFrame(fn) {
|
|
37
|
+
removeAnimationFrame()
|
|
38
|
+
|
|
39
|
+
if (!vmIsDestroyed(vm)) {
|
|
40
|
+
frameId = requestAnimationFrame(() => {
|
|
41
|
+
frameId = null
|
|
42
|
+
fn()
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test, vi } from 'vitest'
|
|
2
|
+
import { mount } from '@vue/test-utils'
|
|
3
|
+
import { defineComponent } from 'vue'
|
|
4
|
+
|
|
5
|
+
import useAnimationFrame from './use-animation-frame.js'
|
|
6
|
+
|
|
7
|
+
let wrapper
|
|
8
|
+
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
if (wrapper !== null) {
|
|
11
|
+
wrapper.unmount()
|
|
12
|
+
wrapper = null
|
|
13
|
+
}
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
function nextAnimationFrame() {
|
|
17
|
+
const { promise, resolve } = Promise.withResolvers()
|
|
18
|
+
requestAnimationFrame(resolve)
|
|
19
|
+
return promise
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe('[useAnimationFrame API]', () => {
|
|
23
|
+
describe('[Functions]', () => {
|
|
24
|
+
describe('[(function)default]', () => {
|
|
25
|
+
test('correctly registers an animation frame', async () => {
|
|
26
|
+
const fn = vi.fn()
|
|
27
|
+
|
|
28
|
+
wrapper = mount(
|
|
29
|
+
defineComponent({
|
|
30
|
+
template: '<div />',
|
|
31
|
+
setup() {
|
|
32
|
+
const { registerAnimationFrame } = useAnimationFrame()
|
|
33
|
+
|
|
34
|
+
registerAnimationFrame(fn)
|
|
35
|
+
return {}
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
expect(fn).not.toHaveBeenCalled()
|
|
41
|
+
await nextAnimationFrame()
|
|
42
|
+
expect(fn).toHaveBeenCalledTimes(1)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('removeAnimationFrame works correctly', async () => {
|
|
46
|
+
const fn = vi.fn()
|
|
47
|
+
|
|
48
|
+
wrapper = mount(
|
|
49
|
+
defineComponent({
|
|
50
|
+
template: '<div />',
|
|
51
|
+
setup() {
|
|
52
|
+
const { registerAnimationFrame, removeAnimationFrame } =
|
|
53
|
+
useAnimationFrame()
|
|
54
|
+
|
|
55
|
+
registerAnimationFrame(fn)
|
|
56
|
+
return { registerAnimationFrame, removeAnimationFrame }
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
expect(fn).not.toHaveBeenCalled()
|
|
62
|
+
wrapper.vm.removeAnimationFrame()
|
|
63
|
+
|
|
64
|
+
await nextAnimationFrame()
|
|
65
|
+
expect(fn).not.toHaveBeenCalled()
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('unmount stops animation frame', async () => {
|
|
69
|
+
const fn = vi.fn()
|
|
70
|
+
|
|
71
|
+
wrapper = mount(
|
|
72
|
+
defineComponent({
|
|
73
|
+
template: '<div />',
|
|
74
|
+
setup() {
|
|
75
|
+
const { registerAnimationFrame } = useAnimationFrame()
|
|
76
|
+
|
|
77
|
+
registerAnimationFrame(fn)
|
|
78
|
+
return {}
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
expect(fn).not.toHaveBeenCalled()
|
|
84
|
+
|
|
85
|
+
wrapper.unmount()
|
|
86
|
+
wrapper = null
|
|
87
|
+
|
|
88
|
+
await nextAnimationFrame()
|
|
89
|
+
|
|
90
|
+
expect(fn).not.toHaveBeenCalled()
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test('can override animation frame', async () => {
|
|
94
|
+
const fn1 = vi.fn()
|
|
95
|
+
const fn2 = vi.fn()
|
|
96
|
+
|
|
97
|
+
wrapper = mount(
|
|
98
|
+
defineComponent({
|
|
99
|
+
template: '<div />',
|
|
100
|
+
setup() {
|
|
101
|
+
const { registerAnimationFrame } = useAnimationFrame()
|
|
102
|
+
return { registerAnimationFrame }
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
expect(fn1).not.toHaveBeenCalled()
|
|
108
|
+
wrapper.vm.registerAnimationFrame(fn1)
|
|
109
|
+
expect(fn1).not.toHaveBeenCalled()
|
|
110
|
+
|
|
111
|
+
wrapper.vm.registerAnimationFrame(fn2)
|
|
112
|
+
expect(fn1).not.toHaveBeenCalled()
|
|
113
|
+
expect(fn2).not.toHaveBeenCalled()
|
|
114
|
+
|
|
115
|
+
await nextAnimationFrame()
|
|
116
|
+
|
|
117
|
+
expect(fn1).not.toHaveBeenCalled()
|
|
118
|
+
expect(fn2).toHaveBeenCalledTimes(1)
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
})
|
package/src/composables.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
export { default as useAnimationFrame } from './composables/use-animation-frame/use-animation-frame.js'
|
|
1
2
|
export { default as useDialogPluginComponent } from './composables/use-dialog-plugin-component/use-dialog-plugin-component.js'
|
|
2
3
|
export { default as useFormChild } from './composables/use-form/use-form-child.js'
|
|
3
4
|
export { default as useMeta } from './composables/use-meta/use-meta.js'
|
|
4
5
|
export { default as useQuasar } from './composables/use-quasar/use-quasar.js'
|
|
5
|
-
|
|
6
6
|
export { default as useHydration } from './composables/use-hydration/use-hydration.js'
|
|
7
7
|
export { default as useId } from './composables/use-id/use-id.js'
|
|
8
8
|
export { default as useInterval } from './composables/use-interval/use-interval.js'
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"value": {
|
|
7
7
|
"type": ["Function"],
|
|
8
8
|
"desc": "Function to call when mutation occurs; It is HIGHLY recommended to reference it from your vue component scope, otherwise the directive update cycle will continuously recreate the observer which hits performance hard",
|
|
9
|
-
"examples": ["# v-
|
|
9
|
+
"examples": ["# v-mutation=\"myHandler\""],
|
|
10
10
|
|
|
11
11
|
"returns": {
|
|
12
12
|
"type": "Boolean",
|
|
@@ -22,31 +22,46 @@ function globalHandler(evt) {
|
|
|
22
22
|
return
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
let
|
|
25
|
+
// a modal (non-seamless) QDialog shields popups opened before it,
|
|
26
|
+
// so only popups opened after it can be closed by this click (#18091)
|
|
27
|
+
let closableContentEls = null
|
|
28
|
+
let sawClosablePortal = false
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
const proxy = portalProxyList[
|
|
30
|
+
for (let i = portalProxyList.length - 1; i >= 0; i--) {
|
|
31
|
+
const proxy = portalProxyList[i].$
|
|
32
|
+
const name = proxy.type.name
|
|
31
33
|
|
|
32
34
|
// skip QTooltip portals
|
|
33
|
-
if (
|
|
34
|
-
portalIndex--
|
|
35
|
+
if (name === 'QTooltip') {
|
|
35
36
|
continue
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
if (
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
if (name === 'QDialog') {
|
|
40
|
+
if (proxy.props.seamless !== true) {
|
|
41
|
+
if (sawClosablePortal === false) return
|
|
42
|
+
|
|
43
|
+
closableContentEls = new Set(
|
|
44
|
+
portalProxyList.slice(i + 1).map(vm => vm.contentEl)
|
|
45
|
+
)
|
|
46
|
+
break
|
|
47
|
+
}
|
|
41
48
|
|
|
42
|
-
|
|
49
|
+
continue
|
|
50
|
+
}
|
|
43
51
|
|
|
44
|
-
|
|
52
|
+
sawClosablePortal = true
|
|
45
53
|
}
|
|
46
54
|
|
|
47
55
|
for (let i = registeredList.length - 1; i >= 0; i--) {
|
|
48
56
|
const state = registeredList[i]
|
|
49
57
|
|
|
58
|
+
if (
|
|
59
|
+
closableContentEls !== null &&
|
|
60
|
+
!closableContentEls.has(state.innerRef.value)
|
|
61
|
+
) {
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
|
|
50
65
|
if (
|
|
51
66
|
(state.anchorEl.value === null ||
|
|
52
67
|
!state.anchorEl.value.contains(target)) &&
|