fix payment qr fallback and admin guidance

This commit is contained in:
IanShaw027
2026-04-22 07:33:14 -07:00
parent 5551349349
commit f35e967516
20 changed files with 845 additions and 43 deletions

View File

@@ -1,23 +1,69 @@
<script setup lang="ts">
import { ref, useTemplateRef, nextTick } from 'vue'
import { onBeforeUnmount, onMounted, ref, useTemplateRef, nextTick } from 'vue'
defineProps<{
const props = withDefaults(defineProps<{
content?: string
}>()
trigger?: 'hover' | 'click'
widthClass?: string
}>(), {
trigger: 'hover',
widthClass: 'w-64',
})
const show = ref(false)
const triggerRef = useTemplateRef<HTMLElement>('trigger')
const tooltipRef = useTemplateRef<HTMLElement>('tooltip')
const tooltipStyle = ref({ top: '0px', left: '0px' })
function onEnter() {
function openTooltip() {
show.value = true
nextTick(updatePosition)
}
function onLeave() {
function closeTooltip() {
show.value = false
}
function onEnter() {
if (props.trigger !== 'hover') return
openTooltip()
}
function onLeave() {
if (props.trigger !== 'hover') return
closeTooltip()
}
function onClick(event: MouseEvent) {
if (props.trigger !== 'click') return
event.stopPropagation()
if (show.value) {
closeTooltip()
return
}
openTooltip()
}
function onDocumentClick(event: MouseEvent) {
if (props.trigger !== 'click' || !show.value) return
const target = event.target as Node | null
if (!target) return
if (triggerRef.value?.contains(target) || tooltipRef.value?.contains(target)) return
closeTooltip()
}
function onDocumentKeydown(event: KeyboardEvent) {
if (props.trigger !== 'click') return
if (event.key === 'Escape') {
closeTooltip()
}
}
function onViewportChange() {
if (!show.value) return
updatePosition()
}
function updatePosition() {
const el = triggerRef.value
if (!el) return
@@ -27,6 +73,20 @@ function updatePosition() {
left: `${rect.left + rect.width / 2 + window.scrollX}px`,
}
}
onMounted(() => {
document.addEventListener('click', onDocumentClick, true)
document.addEventListener('keydown', onDocumentKeydown)
window.addEventListener('resize', onViewportChange)
window.addEventListener('scroll', onViewportChange, true)
})
onBeforeUnmount(() => {
document.removeEventListener('click', onDocumentClick, true)
document.removeEventListener('keydown', onDocumentKeydown)
window.removeEventListener('resize', onViewportChange)
window.removeEventListener('scroll', onViewportChange, true)
})
</script>
<template>
@@ -35,6 +95,7 @@ function updatePosition() {
class="group relative ml-1 inline-flex items-center align-middle"
@mouseenter="onEnter"
@mouseleave="onLeave"
@click="onClick"
>
<!-- Trigger Icon -->
<slot name="trigger">
@@ -56,10 +117,26 @@ function updatePosition() {
<!-- Teleport to body to escape modal overflow clipping -->
<Teleport to="body">
<div
ref="tooltip"
v-show="show"
class="fixed z-[99999] w-64 -translate-x-1/2 -translate-y-full rounded-lg bg-gray-900 p-3 text-xs leading-relaxed text-white shadow-xl ring-1 ring-white/10 dark:bg-gray-800"
role="tooltip"
:class="[
'fixed z-[99999] -translate-x-1/2 -translate-y-full rounded-lg bg-gray-900 p-3 text-xs leading-relaxed text-white shadow-xl ring-1 ring-white/10 dark:bg-gray-800',
props.widthClass,
]"
:style="{ top: `calc(${tooltipStyle.top} - 8px)`, left: tooltipStyle.left }"
>
<button
v-if="props.trigger === 'click'"
type="button"
class="absolute right-1.5 top-1.5 rounded p-1 text-gray-300 transition-colors hover:bg-white/10 hover:text-white"
aria-label="Close"
@click.stop="closeTooltip"
>
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
<slot>{{ content }}</slot>
<div class="absolute -bottom-1 left-1/2 h-2 w-2 -translate-x-1/2 rotate-45 bg-gray-900 dark:bg-gray-800"></div>
</div>

View File

@@ -0,0 +1,80 @@
import { afterEach, describe, expect, it } from 'vitest'
import { mount } from '@vue/test-utils'
import { nextTick } from 'vue'
import HelpTooltip from '@/components/common/HelpTooltip.vue'
function getTooltipElement(): HTMLDivElement {
const tooltip = document.body.querySelector('[role="tooltip"]')
if (!(tooltip instanceof HTMLDivElement)) {
throw new Error('tooltip element not found')
}
return tooltip
}
describe('HelpTooltip', () => {
afterEach(() => {
document.body.innerHTML = ''
})
it('keeps the existing hover interaction by default', async () => {
const wrapper = mount(HelpTooltip, {
attachTo: document.body,
props: {
content: 'hover details',
},
})
const trigger = wrapper.get('.group')
const tooltip = getTooltipElement()
expect(tooltip.style.display).toBe('none')
await trigger.trigger('mouseenter')
await nextTick()
expect(tooltip.style.display).not.toBe('none')
await trigger.trigger('mouseleave')
await nextTick()
expect(tooltip.style.display).toBe('none')
wrapper.unmount()
})
it('supports click-to-toggle details and closes on outside click', async () => {
const wrapper = mount(HelpTooltip, {
attachTo: document.body,
props: {
content: 'click details',
trigger: 'click',
},
})
const trigger = wrapper.get('.group')
const tooltip = getTooltipElement()
expect(tooltip.style.display).toBe('none')
await trigger.trigger('click')
await nextTick()
expect(tooltip.style.display).not.toBe('none')
expect(tooltip.textContent).toContain('click details')
const closeButton = tooltip.querySelector('button[aria-label="Close"]')
if (!(closeButton instanceof HTMLButtonElement)) {
throw new Error('close button not found')
}
closeButton.click()
await nextTick()
expect(tooltip.style.display).toBe('none')
await trigger.trigger('click')
await nextTick()
expect(tooltip.style.display).not.toBe('none')
document.body.dispatchEvent(new MouseEvent('click', { bubbles: true }))
await nextTick()
expect(tooltip.style.display).toBe('none')
wrapper.unmount()
})
})