@dnax/ui 0.0.33 → 0.0.34
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/QBtnActions.vue +78 -41
- package/package.json +1 -1
- package/styles/main.css +13 -13
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
// piloté par données : plus besoin de compose un QMenu/QList séparé).
|
|
4
4
|
// <q-btn-actions label="Actions" :actions="acts" @select-action="onSelect" />
|
|
5
5
|
// Chaque action : { label, value?, icon?, color?, description?, onClick? }.
|
|
6
|
+
// Le panneau est téléporté dans <body> en position fixed : il survit aux
|
|
7
|
+
// contextes d'empilement/overflow (cellules sticky de table, scroll containers).
|
|
6
8
|
|
|
7
9
|
export interface BtnAction {
|
|
8
10
|
/** Libellé affiché dans le menu */
|
|
@@ -46,7 +48,7 @@ interface Props {
|
|
|
46
48
|
noCaret?: boolean
|
|
47
49
|
/** Icône de la flèche déroulante */
|
|
48
50
|
dropdownIcon?: string
|
|
49
|
-
/** Alignement du menu
|
|
51
|
+
/** Alignement du menu : bord gauche du bouton ("left") ou bord droit ("right") */
|
|
50
52
|
align?: "left" | "right"
|
|
51
53
|
/** Largeur minimale du menu (ex. "200px") */
|
|
52
54
|
menuWidth?: string
|
|
@@ -96,6 +98,9 @@ const emit = defineEmits<{
|
|
|
96
98
|
|
|
97
99
|
const open = ref(false)
|
|
98
100
|
const rootEl = ref<HTMLElement | null>(null)
|
|
101
|
+
const panelEl = ref<HTMLElement | null>(null)
|
|
102
|
+
/** Position fixed du panneau (téléporté dans <body>) */
|
|
103
|
+
const panelPos = ref({ top: 0, left: 0, transform: "" })
|
|
99
104
|
|
|
100
105
|
const canOpen = computed(() => !props.disable && !props.loading && props.actions.length > 0)
|
|
101
106
|
|
|
@@ -114,28 +119,60 @@ const effectiveRadius = useRadius("QBtnActions", () => props.radius)
|
|
|
114
119
|
const roundedStyle = computed(() => radiusStyle(effectiveRadius.value))
|
|
115
120
|
|
|
116
121
|
const panelStyle = computed<Record<string, string>>(() => ({
|
|
122
|
+
top: `${panelPos.value.top}px`,
|
|
123
|
+
left: `${panelPos.value.left}px`,
|
|
124
|
+
transform: panelPos.value.transform,
|
|
117
125
|
minWidth: props.menuWidth,
|
|
118
126
|
...(roundedStyle.value ?? {}),
|
|
119
127
|
}))
|
|
120
128
|
|
|
129
|
+
// ═════════ Positionnement (fixed, téléporté) ═════════
|
|
130
|
+
const placePanel = () => {
|
|
131
|
+
const el = rootEl.value
|
|
132
|
+
if (!el || typeof document === "undefined") return
|
|
133
|
+
const rect = el.getBoundingClientRect()
|
|
134
|
+
if (props.align === "left") {
|
|
135
|
+
panelPos.value = { top: rect.bottom + 4, left: rect.left, transform: "none" }
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
// bord droit du panneau aligné sur le bord droit du bouton
|
|
139
|
+
panelPos.value = { top: rect.bottom + 4, left: rect.right, transform: "translateX(-100%)" }
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const enableTracking = () => {
|
|
144
|
+
window.addEventListener("resize", placePanel)
|
|
145
|
+
window.addEventListener("scroll", placePanel, true)
|
|
146
|
+
}
|
|
147
|
+
const disableTracking = () => {
|
|
148
|
+
window.removeEventListener("resize", placePanel)
|
|
149
|
+
window.removeEventListener("scroll", placePanel, true)
|
|
150
|
+
}
|
|
151
|
+
|
|
121
152
|
// ═════════ Ouverture / fermeture ═════════
|
|
122
153
|
const toggle = () => (open.value ? close() : openMenu())
|
|
123
154
|
|
|
124
155
|
const openMenu = async () => {
|
|
125
156
|
if (!canOpen.value) return
|
|
157
|
+
placePanel()
|
|
126
158
|
open.value = true
|
|
159
|
+
enableTracking()
|
|
127
160
|
await nextTick()
|
|
128
|
-
;(
|
|
161
|
+
;(panelEl.value?.querySelector(".q-btn-actions__item:not(:disabled)") as HTMLElement | null)?.focus()
|
|
129
162
|
}
|
|
130
163
|
|
|
131
164
|
const close = () => {
|
|
132
165
|
if (!open.value) return
|
|
133
166
|
open.value = false
|
|
167
|
+
disableTracking()
|
|
134
168
|
}
|
|
135
169
|
|
|
136
|
-
// Clic extérieur → ferme
|
|
170
|
+
// Clic extérieur (bouton ET panneau téléporté) → ferme
|
|
137
171
|
const onDocMousedown = (e: MouseEvent) => {
|
|
138
|
-
if (
|
|
172
|
+
if (!open.value) return
|
|
173
|
+
const target = e.target as Node
|
|
174
|
+
if (rootEl.value?.contains(target) || panelEl.value?.contains(target)) return
|
|
175
|
+
close()
|
|
139
176
|
}
|
|
140
177
|
|
|
141
178
|
// Clavier : flèches/Home/End dans le menu, Échap ferme (focus revient au trigger)
|
|
@@ -149,7 +186,7 @@ const onKeydown = (e: KeyboardEvent) => {
|
|
|
149
186
|
}
|
|
150
187
|
if (!["ArrowDown", "ArrowUp", "Home", "End"].includes(e.key)) return
|
|
151
188
|
const items = Array.from(
|
|
152
|
-
|
|
189
|
+
panelEl.value?.querySelectorAll(".q-btn-actions__item:not(:disabled)") ?? [],
|
|
153
190
|
) as HTMLElement[]
|
|
154
191
|
if (!items.length) return
|
|
155
192
|
const current = items.indexOf(document.activeElement as HTMLElement)
|
|
@@ -181,16 +218,12 @@ onMounted(() => {
|
|
|
181
218
|
})
|
|
182
219
|
onBeforeUnmount(() => {
|
|
183
220
|
if (typeof document !== "undefined") document.removeEventListener("mousedown", onDocMousedown)
|
|
221
|
+
disableTracking()
|
|
184
222
|
})
|
|
185
223
|
</script>
|
|
186
224
|
|
|
187
225
|
<template>
|
|
188
|
-
<div
|
|
189
|
-
ref="rootEl"
|
|
190
|
-
class="q-btn-actions"
|
|
191
|
-
:class="[`q-btn-actions--align-${align}`, dark && 'dark']"
|
|
192
|
-
@keydown="onKeydown"
|
|
193
|
-
>
|
|
226
|
+
<div ref="rootEl" class="q-btn-actions" :class="dark && 'dark'" @keydown="onKeydown">
|
|
194
227
|
<QBtn
|
|
195
228
|
class="q-btn-actions__trigger"
|
|
196
229
|
:class="triggerClasses"
|
|
@@ -223,35 +256,39 @@ onBeforeUnmount(() => {
|
|
|
223
256
|
/>
|
|
224
257
|
</QBtn>
|
|
225
258
|
|
|
226
|
-
<
|
|
227
|
-
<
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
<
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
259
|
+
<Teleport to="body">
|
|
260
|
+
<Transition name="q-popup">
|
|
261
|
+
<div
|
|
262
|
+
v-if="open"
|
|
263
|
+
ref="panelEl"
|
|
264
|
+
class="q-btn-actions__panel"
|
|
265
|
+
role="menu"
|
|
266
|
+
:aria-label="label ?? 'Actions'"
|
|
267
|
+
:style="panelStyle"
|
|
268
|
+
@keydown="onKeydown"
|
|
269
|
+
>
|
|
270
|
+
<template v-for="(action, i) in actions" :key="i">
|
|
271
|
+
<div v-if="action.separator" class="q-btn-actions__separator" role="separator" />
|
|
272
|
+
<button
|
|
273
|
+
v-if="!isSeparatorOnly(action)"
|
|
274
|
+
type="button"
|
|
275
|
+
role="menuitem"
|
|
276
|
+
class="q-btn-actions__item"
|
|
277
|
+
:class="{ 'q-btn-actions__item--disabled': action.disable }"
|
|
278
|
+
:disabled="action.disable"
|
|
279
|
+
:style="itemStyle(action)"
|
|
280
|
+
@click="onSelect(action)"
|
|
281
|
+
>
|
|
282
|
+
<Icon :icon="action.icon" v-if="action.icon" class="q-btn-actions__item-icon" aria-hidden="true" />
|
|
283
|
+
<span class="q-btn-actions__item-text">
|
|
284
|
+
<span class="q-btn-actions__item-label">{{ action.label }}</span>
|
|
285
|
+
<span v-if="action.description" class="q-btn-actions__item-desc">{{ action.description }}</span>
|
|
286
|
+
</span>
|
|
287
|
+
<Icon :icon="action.iconRight" v-if="action.iconRight" class="q-btn-actions__item-icon" aria-hidden="true" />
|
|
288
|
+
</button>
|
|
289
|
+
</template>
|
|
290
|
+
</div>
|
|
291
|
+
</Transition>
|
|
292
|
+
</Teleport>
|
|
256
293
|
</div>
|
|
257
294
|
</template>
|
package/package.json
CHANGED
package/styles/main.css
CHANGED
|
@@ -2463,9 +2463,8 @@ html.dark body {
|
|
|
2463
2463
|
}
|
|
2464
2464
|
|
|
2465
2465
|
.q-btn-actions__panel {
|
|
2466
|
-
position:
|
|
2467
|
-
|
|
2468
|
-
z-index: 1200;
|
|
2466
|
+
position: fixed;
|
|
2467
|
+
z-index: 3000;
|
|
2469
2468
|
padding: 4px;
|
|
2470
2469
|
background-color: #fff;
|
|
2471
2470
|
border: 1px solid rgb(0 0 0 / 0.12);
|
|
@@ -2473,14 +2472,6 @@ html.dark body {
|
|
|
2473
2472
|
box-shadow: 0 4px 16px rgb(0 0 0 / 0.15);
|
|
2474
2473
|
}
|
|
2475
2474
|
|
|
2476
|
-
.q-btn-actions--align-left .q-btn-actions__panel {
|
|
2477
|
-
left: 0;
|
|
2478
|
-
}
|
|
2479
|
-
|
|
2480
|
-
.q-btn-actions--align-right .q-btn-actions__panel {
|
|
2481
|
-
right: 0;
|
|
2482
|
-
}
|
|
2483
|
-
|
|
2484
2475
|
.q-btn-actions__item {
|
|
2485
2476
|
display: flex;
|
|
2486
2477
|
align-items: center;
|
|
@@ -2541,11 +2532,20 @@ html.dark body {
|
|
|
2541
2532
|
}
|
|
2542
2533
|
|
|
2543
2534
|
.q-popup-enter-active {
|
|
2544
|
-
animation: q-popup-in 0.12s ease;
|
|
2535
|
+
animation: q-popup-fade-in 0.12s ease;
|
|
2545
2536
|
}
|
|
2546
2537
|
|
|
2547
2538
|
.q-popup-leave-active {
|
|
2548
|
-
animation: q-popup-in 0.1s ease reverse;
|
|
2539
|
+
animation: q-popup-fade-in 0.1s ease reverse;
|
|
2540
|
+
}
|
|
2541
|
+
|
|
2542
|
+
@keyframes q-popup-fade-in {
|
|
2543
|
+
from {
|
|
2544
|
+
opacity: 0;
|
|
2545
|
+
}
|
|
2546
|
+
to {
|
|
2547
|
+
opacity: 1;
|
|
2548
|
+
}
|
|
2549
2549
|
}
|
|
2550
2550
|
|
|
2551
2551
|
/* ===== QImg ===== */
|