@dnax/ui 0.0.21 → 0.0.23
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/components/QAutocomplete.vue +30 -2
- package/components/QPage.vue +14 -1
- package/components/QRouteTab.vue +106 -0
- package/components/QVirtualScroll.vue +3 -3
- package/index.ts +1 -0
- package/package.json +1 -1
- package/styles/main.css +6 -0
|
@@ -162,8 +162,11 @@ const sheetTitle = computed(
|
|
|
162
162
|
|
|
163
163
|
// — Swipe down pour fermer (mode sheet, opt-in swipe-to-close) —
|
|
164
164
|
const dragging = ref(false)
|
|
165
|
+
const swipeClosing = ref(false)
|
|
165
166
|
let dragStartY = 0
|
|
166
167
|
let dragDy = 0
|
|
168
|
+
let swipeCloseTimer: number | undefined
|
|
169
|
+
|
|
167
170
|
const canSwipe = computed(() => props.swipeToClose && props.mode === "sheet")
|
|
168
171
|
|
|
169
172
|
const onDragStart = (e: PointerEvent) => {
|
|
@@ -183,16 +186,39 @@ const onDragMove = (e: PointerEvent) => {
|
|
|
183
186
|
const onDragEnd = () => {
|
|
184
187
|
if (!dragging.value) return
|
|
185
188
|
dragging.value = false
|
|
186
|
-
if (dragDy > 80)
|
|
187
|
-
|
|
189
|
+
if (dragDy > 80) {
|
|
190
|
+
// Au-delà du seuil : on CONTINUE le glissement jusqu'à sortie complète
|
|
191
|
+
// (translateY(100%)) puis on ferme à la fin de la transition — au lieu de
|
|
192
|
+
// laisser le panneau revenir en place avant le fade (saut visuel).
|
|
193
|
+
swipeClosing.value = true
|
|
194
|
+
if (sheetRef.value) sheetRef.value.style.transform = "translateY(100%)"
|
|
195
|
+
window.clearTimeout(swipeCloseTimer)
|
|
196
|
+
swipeCloseTimer = window.setTimeout(() => {
|
|
197
|
+
swipeClosing.value = false
|
|
198
|
+
closePopup()
|
|
199
|
+
}, 400) // fallback si la transition ne se déclenche pas (déjà à 100%, reduced-motion)
|
|
200
|
+
}
|
|
201
|
+
else if (sheetRef.value) {
|
|
202
|
+
// Sous le seuil : retour en douceur à la position initiale
|
|
203
|
+
sheetRef.value.style.transform = ""
|
|
204
|
+
}
|
|
188
205
|
dragDy = 0
|
|
189
206
|
}
|
|
190
207
|
|
|
208
|
+
// Ferme dès que la transition de glissement se termine (plus précis que le timeout)
|
|
209
|
+
const onSheetTransitionEnd = (e: TransitionEvent) => {
|
|
210
|
+
if (e.propertyName !== "transform" || !swipeClosing.value) return
|
|
211
|
+
window.clearTimeout(swipeCloseTimer)
|
|
212
|
+
swipeClosing.value = false
|
|
213
|
+
closePopup()
|
|
214
|
+
}
|
|
215
|
+
|
|
191
216
|
const sheetClasses = computed(() => [
|
|
192
217
|
`q-autocomplete__sheet--${props.mode}`,
|
|
193
218
|
modeOptions.value?.class,
|
|
194
219
|
dragging.value && "q-autocomplete__sheet--dragging",
|
|
195
220
|
canSwipe.value && "q-autocomplete__sheet--swipeable",
|
|
221
|
+
swipeClosing.value && "q-autocomplete__sheet--swipe-closing",
|
|
196
222
|
])
|
|
197
223
|
|
|
198
224
|
// — Valeurs des options —
|
|
@@ -371,6 +397,7 @@ onMounted(() => {
|
|
|
371
397
|
})
|
|
372
398
|
onBeforeUnmount(() => {
|
|
373
399
|
if (typeof document !== "undefined") document.removeEventListener("mousedown", onDocMousedown)
|
|
400
|
+
window.clearTimeout(swipeCloseTimer)
|
|
374
401
|
})
|
|
375
402
|
|
|
376
403
|
// Verrou du scroll de la page derrière le panneau (modal/sheet)
|
|
@@ -480,6 +507,7 @@ watch(open, (v) => {
|
|
|
480
507
|
role="dialog"
|
|
481
508
|
aria-modal="true"
|
|
482
509
|
:aria-label="sheetTitle"
|
|
510
|
+
@transitionend="onSheetTransitionEnd"
|
|
483
511
|
>
|
|
484
512
|
<div
|
|
485
513
|
class="q-autocomplete__sheet-header"
|
package/components/QPage.vue
CHANGED
|
@@ -31,6 +31,19 @@ interface Props {
|
|
|
31
31
|
|
|
32
32
|
defineProps<Props>()
|
|
33
33
|
|
|
34
|
+
// Slots typés (mode virtual) : même contrat que QVirtualScroll
|
|
35
|
+
// (item, index, ref pour la mesure ; before/after pour les zones sticky)
|
|
36
|
+
defineSlots<{
|
|
37
|
+
default?: (props: { item: any; index: number; ref: (el: unknown) => void }) => any
|
|
38
|
+
before?: () => any
|
|
39
|
+
after?: () => any
|
|
40
|
+
}>()
|
|
41
|
+
|
|
42
|
+
// L'inférence du scope ne traverse pas la branche v-if → cast explicite
|
|
43
|
+
// (le scope reçu EST le scope du QVirtualScroll)
|
|
44
|
+
type VirtualScope = { item: any; index: number; ref: (el: unknown) => void }
|
|
45
|
+
const forwardScope = (scope: unknown) => scope as VirtualScope
|
|
46
|
+
|
|
34
47
|
const rootEl = ref<HTMLElement | null>(null)
|
|
35
48
|
// ref fonctionnel : sur un élément → l'élément ; sur q-virtual-scroll (composant) → .$el
|
|
36
49
|
const setRoot = (el: unknown) => {
|
|
@@ -58,7 +71,7 @@ useFixedBarOffset(rootEl, "page")
|
|
|
58
71
|
v-bind="$attrs"
|
|
59
72
|
>
|
|
60
73
|
<template #default="scope">
|
|
61
|
-
<slot v-bind="scope" />
|
|
74
|
+
<slot v-bind="forwardScope(scope)" />
|
|
62
75
|
</template>
|
|
63
76
|
<template #before>
|
|
64
77
|
<slot name="before" />
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// QRouteTab — QTab lié à la route (pattern Quasar QRouteTab) :
|
|
3
|
+
// <q-route-tab to="/docs/components/tabs" label="Tabs" exact />
|
|
4
|
+
// Le tab devient actif automatiquement quand la route courante matche `to`
|
|
5
|
+
// (le v-model du QTabs suit), et cliquer navigue vers la destination.
|
|
6
|
+
import { computed, inject, watch } from "vue"
|
|
7
|
+
import type { RouteLocationRaw } from "vue-router"
|
|
8
|
+
import { useRoute, useRouter } from "vue-router"
|
|
9
|
+
import QTab from "./QTab.vue"
|
|
10
|
+
import { qTabsKey } from "./QTabs.vue"
|
|
11
|
+
|
|
12
|
+
let seq = 0
|
|
13
|
+
|
|
14
|
+
interface Props {
|
|
15
|
+
/** Destination de route (même API que RouterLink `to`) */
|
|
16
|
+
to: RouteLocationRaw
|
|
17
|
+
/** Match exact : path (et hash) identiques — sinon préfixe de chemin */
|
|
18
|
+
exact?: boolean
|
|
19
|
+
/** remplace l'entrée d'historique au clic (router.replace) */
|
|
20
|
+
replace?: boolean
|
|
21
|
+
// — API QTab (transmise) —
|
|
22
|
+
name?: string | number
|
|
23
|
+
icon?: string
|
|
24
|
+
label?: string | number
|
|
25
|
+
alert?: boolean | string
|
|
26
|
+
alertIcon?: string
|
|
27
|
+
count?: number
|
|
28
|
+
countColor?: string
|
|
29
|
+
countMax?: number
|
|
30
|
+
noCaps?: boolean
|
|
31
|
+
tabindex?: string | number
|
|
32
|
+
disable?: boolean
|
|
33
|
+
contentClass?: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
37
|
+
exact: false,
|
|
38
|
+
replace: false,
|
|
39
|
+
alert: false,
|
|
40
|
+
noCaps: false,
|
|
41
|
+
disable: false,
|
|
42
|
+
countMax: 99,
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
const tabs = inject(qTabsKey, null)
|
|
46
|
+
|
|
47
|
+
const resolvedName = props.name ?? `rt_${seq++}`
|
|
48
|
+
|
|
49
|
+
// Router optionnel : sans app router, QRouteTab se comporte comme un QTab
|
|
50
|
+
let router: ReturnType<typeof useRouter> | undefined
|
|
51
|
+
let route: ReturnType<typeof useRoute> | undefined
|
|
52
|
+
try {
|
|
53
|
+
router = useRouter()
|
|
54
|
+
route = useRoute()
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
router = undefined
|
|
58
|
+
route = undefined
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const matches = computed(() => {
|
|
62
|
+
if (!router || !route || !props.to) return false
|
|
63
|
+
const resolved = router.resolve(props.to)
|
|
64
|
+
if (props.exact) {
|
|
65
|
+
return resolved.path === route.path && resolved.hash === route.hash
|
|
66
|
+
}
|
|
67
|
+
// non-exact : la route courante commence par le path de `to` (préfixe de segment)
|
|
68
|
+
const prefix = resolved.path.endsWith("/") ? resolved.path : resolved.path + "/"
|
|
69
|
+
return route.path === resolved.path || route.path.startsWith(prefix)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
// La route change → le tab qui matche devient actif (sync du v-model QTabs)
|
|
73
|
+
watch(
|
|
74
|
+
() => route?.fullPath,
|
|
75
|
+
() => {
|
|
76
|
+
if (matches.value) tabs?.setActive(resolvedName)
|
|
77
|
+
},
|
|
78
|
+
{ immediate: true },
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
const onClick = () => {
|
|
82
|
+
if (props.disable || !router) return
|
|
83
|
+
if (props.replace) router.replace(props.to)
|
|
84
|
+
else router.push(props.to)
|
|
85
|
+
}
|
|
86
|
+
</script>
|
|
87
|
+
|
|
88
|
+
<template>
|
|
89
|
+
<q-tab
|
|
90
|
+
:name="resolvedName"
|
|
91
|
+
:label="label"
|
|
92
|
+
:icon="icon"
|
|
93
|
+
:alert="alert"
|
|
94
|
+
:alert-icon="alertIcon"
|
|
95
|
+
:count="count"
|
|
96
|
+
:count-color="countColor"
|
|
97
|
+
:count-max="countMax"
|
|
98
|
+
:no-caps="noCaps"
|
|
99
|
+
:tabindex="tabindex"
|
|
100
|
+
:disable="disable"
|
|
101
|
+
:content-class="contentClass"
|
|
102
|
+
@click="onClick"
|
|
103
|
+
>
|
|
104
|
+
<slot />
|
|
105
|
+
</q-tab>
|
|
106
|
+
</template>
|
|
@@ -35,7 +35,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
35
35
|
})
|
|
36
36
|
|
|
37
37
|
defineSlots<{
|
|
38
|
-
default?: (props: { item: any; index: number; ref: (el:
|
|
38
|
+
default?: (props: { item: any; index: number; ref: (el: unknown) => void }) => any
|
|
39
39
|
before?: () => any
|
|
40
40
|
after?: () => any
|
|
41
41
|
}>()
|
|
@@ -49,7 +49,7 @@ const scrollTop = ref(0)
|
|
|
49
49
|
// — Hauteurs mesurées par index (celles non mesurées → taille estimée) —
|
|
50
50
|
const itemSizes = new Map<number, number>()
|
|
51
51
|
const observers = new Map<number, ResizeObserver>()
|
|
52
|
-
const refFns = new Map<number, (el:
|
|
52
|
+
const refFns = new Map<number, (el: unknown) => void>()
|
|
53
53
|
|
|
54
54
|
// Préfixe cumulé des offsets (reconstruit quand les tailles changent, pas au scroll)
|
|
55
55
|
let prefix: number[] = [0]
|
|
@@ -103,7 +103,7 @@ const observeItem = (index: number, el: Element | null) => {
|
|
|
103
103
|
const itemRef = (index: number) => {
|
|
104
104
|
let fn = refFns.get(index)
|
|
105
105
|
if (!fn) {
|
|
106
|
-
fn = (el) => observeItem(index, el)
|
|
106
|
+
fn = (el: unknown) => observeItem(index, el as Element | null)
|
|
107
107
|
refFns.set(index, fn)
|
|
108
108
|
}
|
|
109
109
|
return fn
|
package/index.ts
CHANGED
|
@@ -91,6 +91,7 @@ export { default as QRadio } from "./components/QRadio.vue"
|
|
|
91
91
|
export { default as QRating } from "./components/QRating.vue"
|
|
92
92
|
export { default as QReorder } from "./components/QReorder.vue"
|
|
93
93
|
export { default as QRollingText } from "./components/QRollingText.vue"
|
|
94
|
+
export { default as QRouteTab } from "./components/QRouteTab.vue"
|
|
94
95
|
export { default as QRow } from "./components/QRow.vue"
|
|
95
96
|
export { default as QSafeArea } from "./components/QSafeArea.vue"
|
|
96
97
|
export { default as QScrollArea } from "./components/QScrollArea.vue"
|
package/package.json
CHANGED
package/styles/main.css
CHANGED
|
@@ -3474,6 +3474,12 @@ html.dark body {
|
|
|
3474
3474
|
transition: none;
|
|
3475
3475
|
}
|
|
3476
3476
|
|
|
3477
|
+
/* fermeture par swipe : le glissement (translateY 100%) gère la sortie —
|
|
3478
|
+
l'animation leave (pop-out 24px) serait un saut depuis la position glissée */
|
|
3479
|
+
.q-autocomplete-modal-leave-active .q-autocomplete__sheet--swipe-closing {
|
|
3480
|
+
animation: none;
|
|
3481
|
+
}
|
|
3482
|
+
|
|
3477
3483
|
/* le header est la zone qui attrape le swipe (cursor grab) — seulement si
|
|
3478
3484
|
swipe-to-close est actif (classe --swipeable) */
|
|
3479
3485
|
.q-autocomplete__sheet--swipeable .q-autocomplete__sheet-header {
|