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.
Files changed (146) hide show
  1. package/dist/api/Mutation.json +1 -1
  2. package/dist/api/QLinearProgress.json +1 -1
  3. package/dist/icon-set/bootstrap-icons.umd.prod.js +1 -1
  4. package/dist/icon-set/eva-icons.umd.prod.js +1 -1
  5. package/dist/icon-set/fontawesome-v5-pro.umd.prod.js +1 -1
  6. package/dist/icon-set/fontawesome-v5.umd.prod.js +1 -1
  7. package/dist/icon-set/fontawesome-v6-pro.umd.prod.js +1 -1
  8. package/dist/icon-set/fontawesome-v6.umd.prod.js +1 -1
  9. package/dist/icon-set/fontawesome-v7-pro.umd.prod.js +1 -1
  10. package/dist/icon-set/fontawesome-v7.umd.prod.js +1 -1
  11. package/dist/icon-set/ionicons-v4.umd.prod.js +1 -1
  12. package/dist/icon-set/line-awesome.umd.prod.js +1 -1
  13. package/dist/icon-set/material-icons-outlined.umd.prod.js +1 -1
  14. package/dist/icon-set/material-icons-round.umd.prod.js +1 -1
  15. package/dist/icon-set/material-icons-sharp.umd.prod.js +1 -1
  16. package/dist/icon-set/material-icons.umd.prod.js +1 -1
  17. package/dist/icon-set/material-symbols-outlined.umd.prod.js +1 -1
  18. package/dist/icon-set/material-symbols-rounded.umd.prod.js +1 -1
  19. package/dist/icon-set/material-symbols-sharp.umd.prod.js +1 -1
  20. package/dist/icon-set/mdi-v3.umd.prod.js +1 -1
  21. package/dist/icon-set/mdi-v4.umd.prod.js +1 -1
  22. package/dist/icon-set/mdi-v5.umd.prod.js +1 -1
  23. package/dist/icon-set/mdi-v6.umd.prod.js +1 -1
  24. package/dist/icon-set/mdi-v7.umd.prod.js +1 -1
  25. package/dist/icon-set/svg-bootstrap-icons.umd.prod.js +1 -1
  26. package/dist/icon-set/svg-eva-icons.umd.prod.js +1 -1
  27. package/dist/icon-set/svg-fontawesome-v5.umd.prod.js +1 -1
  28. package/dist/icon-set/svg-fontawesome-v6.umd.prod.js +1 -1
  29. package/dist/icon-set/svg-fontawesome-v7.umd.prod.js +1 -1
  30. package/dist/icon-set/svg-ionicons-v4.umd.prod.js +1 -1
  31. package/dist/icon-set/svg-ionicons-v5.umd.prod.js +1 -1
  32. package/dist/icon-set/svg-ionicons-v6.umd.prod.js +1 -1
  33. package/dist/icon-set/svg-ionicons-v7.umd.prod.js +1 -1
  34. package/dist/icon-set/svg-ionicons-v8.umd.prod.js +1 -1
  35. package/dist/icon-set/svg-line-awesome.umd.prod.js +1 -1
  36. package/dist/icon-set/svg-material-icons-outlined.umd.prod.js +1 -1
  37. package/dist/icon-set/svg-material-icons-round.umd.prod.js +1 -1
  38. package/dist/icon-set/svg-material-icons-sharp.umd.prod.js +1 -1
  39. package/dist/icon-set/svg-material-icons.umd.prod.js +1 -1
  40. package/dist/icon-set/svg-material-symbols-outlined.umd.prod.js +1 -1
  41. package/dist/icon-set/svg-material-symbols-rounded.umd.prod.js +1 -1
  42. package/dist/icon-set/svg-material-symbols-sharp.umd.prod.js +1 -1
  43. package/dist/icon-set/svg-mdi-v6.umd.prod.js +1 -1
  44. package/dist/icon-set/svg-mdi-v7.umd.prod.js +1 -1
  45. package/dist/icon-set/svg-themify.umd.prod.js +1 -1
  46. package/dist/icon-set/themify.umd.prod.js +1 -1
  47. package/dist/lang/ar-TN.umd.prod.js +1 -1
  48. package/dist/lang/ar.umd.prod.js +1 -1
  49. package/dist/lang/az-Latn.umd.prod.js +1 -1
  50. package/dist/lang/bg.umd.prod.js +1 -1
  51. package/dist/lang/bn.umd.prod.js +1 -1
  52. package/dist/lang/bs-BA.umd.prod.js +1 -1
  53. package/dist/lang/ca.umd.prod.js +1 -1
  54. package/dist/lang/cs.umd.prod.js +1 -1
  55. package/dist/lang/da.umd.prod.js +1 -1
  56. package/dist/lang/de-CH.umd.prod.js +1 -1
  57. package/dist/lang/de-DE.umd.prod.js +1 -1
  58. package/dist/lang/de.umd.prod.js +1 -1
  59. package/dist/lang/el.umd.prod.js +1 -1
  60. package/dist/lang/en-GB.umd.prod.js +1 -1
  61. package/dist/lang/en-US.umd.prod.js +1 -1
  62. package/dist/lang/eo.umd.prod.js +1 -1
  63. package/dist/lang/es.umd.prod.js +1 -1
  64. package/dist/lang/et.umd.prod.js +1 -1
  65. package/dist/lang/eu.umd.prod.js +1 -1
  66. package/dist/lang/fa-IR.umd.prod.js +1 -1
  67. package/dist/lang/fa.umd.prod.js +1 -1
  68. package/dist/lang/fi.umd.prod.js +1 -1
  69. package/dist/lang/fr.umd.prod.js +1 -1
  70. package/dist/lang/gn.umd.prod.js +1 -1
  71. package/dist/lang/he.umd.prod.js +1 -1
  72. package/dist/lang/hi.umd.prod.js +1 -1
  73. package/dist/lang/hr.umd.prod.js +1 -1
  74. package/dist/lang/hu.umd.prod.js +1 -1
  75. package/dist/lang/id.umd.prod.js +1 -1
  76. package/dist/lang/is.umd.prod.js +1 -1
  77. package/dist/lang/it.umd.prod.js +1 -1
  78. package/dist/lang/ja.umd.prod.js +1 -1
  79. package/dist/lang/kk.umd.prod.js +1 -1
  80. package/dist/lang/km.umd.prod.js +1 -1
  81. package/dist/lang/ko-KR.umd.prod.js +1 -1
  82. package/dist/lang/kur-CKB.umd.prod.js +1 -1
  83. package/dist/lang/lb.umd.prod.js +1 -1
  84. package/dist/lang/lt.umd.prod.js +1 -1
  85. package/dist/lang/lu.umd.prod.js +1 -1
  86. package/dist/lang/lv.umd.prod.js +1 -1
  87. package/dist/lang/mk.umd.prod.js +1 -1
  88. package/dist/lang/ml.umd.prod.js +1 -1
  89. package/dist/lang/mm.umd.prod.js +1 -1
  90. package/dist/lang/ms-MY.umd.prod.js +1 -1
  91. package/dist/lang/ms.umd.prod.js +1 -1
  92. package/dist/lang/my.umd.prod.js +1 -1
  93. package/dist/lang/nb-NO.umd.prod.js +1 -1
  94. package/dist/lang/nl.umd.prod.js +1 -1
  95. package/dist/lang/pl.umd.prod.js +1 -1
  96. package/dist/lang/pt-BR.umd.prod.js +1 -1
  97. package/dist/lang/pt.umd.prod.js +1 -1
  98. package/dist/lang/ro.umd.prod.js +1 -1
  99. package/dist/lang/ru.umd.prod.js +1 -1
  100. package/dist/lang/sk.umd.prod.js +1 -1
  101. package/dist/lang/sl.umd.prod.js +1 -1
  102. package/dist/lang/sm.umd.prod.js +1 -1
  103. package/dist/lang/sq.umd.prod.js +1 -1
  104. package/dist/lang/sr-CYR.umd.prod.js +1 -1
  105. package/dist/lang/sr.umd.prod.js +1 -1
  106. package/dist/lang/sv.umd.prod.js +1 -1
  107. package/dist/lang/ta.umd.prod.js +1 -1
  108. package/dist/lang/th.umd.prod.js +1 -1
  109. package/dist/lang/tl.umd.prod.js +1 -1
  110. package/dist/lang/tr.umd.prod.js +1 -1
  111. package/dist/lang/ug.umd.prod.js +1 -1
  112. package/dist/lang/uk.umd.prod.js +1 -1
  113. package/dist/lang/ur-PK.umd.prod.js +1 -1
  114. package/dist/lang/uz-Cyrl.umd.prod.js +1 -1
  115. package/dist/lang/uz-Latn.umd.prod.js +1 -1
  116. package/dist/lang/vi.umd.prod.js +1 -1
  117. package/dist/lang/zh-CN.umd.prod.js +1 -1
  118. package/dist/lang/zh-TW.umd.prod.js +1 -1
  119. package/dist/quasar.client.js +92 -30
  120. package/dist/quasar.css +16 -11
  121. package/dist/quasar.prod.css +1 -1
  122. package/dist/quasar.rtl.css +20 -20
  123. package/dist/quasar.rtl.prod.css +1 -1
  124. package/dist/quasar.sass +18 -12
  125. package/dist/quasar.server.prod.cjs +4 -4
  126. package/dist/quasar.server.prod.js +4 -4
  127. package/dist/quasar.umd.js +92 -29
  128. package/dist/quasar.umd.prod.js +31 -31
  129. package/dist/transforms/import-map.json +1 -1
  130. package/dist/types/composables.d.ts +5 -0
  131. package/dist/types/index.d.ts +1 -1
  132. package/dist/web-types/web-types.json +1 -1
  133. package/package.json +1 -1
  134. package/src/components/btn/QBtn.js +1 -1
  135. package/src/components/fab/QFab.sass +11 -9
  136. package/src/components/linear-progress/QLinearProgress.js +4 -11
  137. package/src/components/linear-progress/QLinearProgress.json +1 -1
  138. package/src/components/linear-progress/QLinearProgress.sass +7 -2
  139. package/src/composables/private.use-fullscreen/use-fullscreen.js +72 -17
  140. package/src/composables/private.use-fullscreen/use-fullscreen.test.js +158 -0
  141. package/src/composables/use-animation-frame/use-animation-frame.js +47 -0
  142. package/src/composables/use-animation-frame/use-animation-frame.test.js +122 -0
  143. package/src/composables.js +1 -1
  144. package/src/directives/mutation/Mutation.json +1 -1
  145. package/src/utils/private.click-outside/click-outside.js +27 -12
  146. package/src/utils/private.click-outside/click-outside.test.js +132 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quasar",
3
- "version": "2.22.0",
3
+ "version": "2.23.1",
4
4
  "description": "Build high-performance Vue.js user interfaces (SPA, PWA, SSR, SSG, Mobile and Desktop) in record time",
5
5
  "keywords": [
6
6
  "desktop",
@@ -290,7 +290,7 @@ export default createComponent({
290
290
  blurTarget !== document.activeElement
291
291
  ) {
292
292
  blurTarget.setAttribute('tabindex', -1)
293
- blurTarget.focus()
293
+ blurTarget.focus({ preventScroll: true })
294
294
  }
295
295
 
296
296
  if (touchTarget === rootRef.value) {
@@ -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, reverse, $q) {
21
+ function width(val) {
22
22
  return {
23
- transform: reverse
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, widthReverse.value, proxy.$q)
92
+ width(motion.value ? 1 : props.value)
100
93
  )
101
94
  const modelClass = computed(
102
95
  () =>
@@ -34,7 +34,7 @@
34
34
 
35
35
  "reverse": {
36
36
  "type": "Boolean",
37
- "desc": "Reverse direction of progress",
37
+ "desc": "Reverse direction of progress (has no effect for query behaviour)",
38
38
  "category": "behavior"
39
39
  },
40
40
 
@@ -16,8 +16,13 @@
16
16
 
17
17
  &--reverse
18
18
  .q-linear-progress
19
- &__model, &__track
20
- transform-origin: 0 100%
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
- fullscreenFillerNode,
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 exitFullscreen() {
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
- fullscreenFillerNode.replaceWith(proxy.$el)
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
- if (counter === 0) {
92
- document.body.classList.remove('q-body--fullscreen-mixin')
119
+ document.body.classList.remove('q-body--fullscreen-mixin')
93
120
 
94
- if (!isUnmounting && proxy.$el.scrollIntoView !== void 0) {
95
- setTimeout(() => {
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
- }, 0)
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
- onBeforeUnmount(() => {
111
- isUnmounting = true
112
- exitFullscreen()
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
+ })
@@ -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-intersection=\"myHandler\""],
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
- // check last portal vm if it's
26
- // a QDialog and not in seamless mode
27
- let portalIndex = portalProxyList.length - 1
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
- while (portalIndex >= 0) {
30
- const proxy = portalProxyList[portalIndex].$
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 (proxy.type.name === 'QTooltip') {
34
- portalIndex--
35
+ if (name === 'QTooltip') {
35
36
  continue
36
37
  }
37
38
 
38
- if (proxy.type.name !== 'QDialog') {
39
- break
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
- if (!proxy.props.seamless) return
49
+ continue
50
+ }
43
51
 
44
- portalIndex--
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)) &&