@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.
@@ -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 sous le bouton */
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
- ;(rootEl.value?.querySelector(".q-btn-actions__item:not(:disabled)") as HTMLElement | null)?.focus()
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 (rootEl.value && !rootEl.value.contains(e.target as Node)) close()
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
- rootEl.value?.querySelectorAll(".q-btn-actions__item:not(:disabled)") ?? [],
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
- <Transition name="q-popup">
227
- <div
228
- v-if="open"
229
- class="q-btn-actions__panel"
230
- role="menu"
231
- :aria-label="label ?? 'Actions'"
232
- :style="panelStyle"
233
- >
234
- <template v-for="(action, i) in actions" :key="i">
235
- <div v-if="action.separator" class="q-btn-actions__separator" role="separator" />
236
- <button
237
- v-if="!isSeparatorOnly(action)"
238
- type="button"
239
- role="menuitem"
240
- class="q-btn-actions__item"
241
- :class="{ 'q-btn-actions__item--disabled': action.disable }"
242
- :disabled="action.disable"
243
- :style="itemStyle(action)"
244
- @click="onSelect(action)"
245
- >
246
- <Icon :icon="action.icon" v-if="action.icon" class="q-btn-actions__item-icon" aria-hidden="true" />
247
- <span class="q-btn-actions__item-text">
248
- <span class="q-btn-actions__item-label">{{ action.label }}</span>
249
- <span v-if="action.description" class="q-btn-actions__item-desc">{{ action.description }}</span>
250
- </span>
251
- <Icon :icon="action.iconRight" v-if="action.iconRight" class="q-btn-actions__item-icon" aria-hidden="true" />
252
- </button>
253
- </template>
254
- </div>
255
- </Transition>
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dnax/ui",
3
- "version": "0.0.33",
3
+ "version": "0.0.34",
4
4
  "module": "index.ts",
5
5
  "description": "UI design System",
6
6
  "type": "module",
package/styles/main.css CHANGED
@@ -2463,9 +2463,8 @@ html.dark body {
2463
2463
  }
2464
2464
 
2465
2465
  .q-btn-actions__panel {
2466
- position: absolute;
2467
- top: calc(100% + 4px);
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 ===== */