From 22d0aeb01375805bd43da8d839ebc069c0cc82e2 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Tue, 20 Jan 2026 10:57:30 +0000 Subject: [PATCH 1/2] wip --- resources/js/bootstrap/statamic.js | 4 +- resources/js/components/Tooltips.vue | 67 +++++++++++++++++++++++++ resources/js/composables/tooltip.js | 74 ++++++++++++++++++++++++++++ resources/js/directives/tooltip.js | 49 ++++++++++++++++++ resources/js/pages/layout/Layout.vue | 2 + 5 files changed, 194 insertions(+), 2 deletions(-) create mode 100644 resources/js/components/Tooltips.vue create mode 100644 resources/js/composables/tooltip.js create mode 100644 resources/js/directives/tooltip.js diff --git a/resources/js/bootstrap/statamic.js b/resources/js/bootstrap/statamic.js index fc0fd03c44..4bfd496b7f 100644 --- a/resources/js/bootstrap/statamic.js +++ b/resources/js/bootstrap/statamic.js @@ -7,8 +7,8 @@ import registerGlobalCommandPalette from './commands.js'; import registerUiComponents from './ui.js'; import registerFieldtypes from './fieldtypes.js'; import VueClickAway from 'vue3-click-away'; -import FloatingVue from 'floating-vue'; import 'floating-vue/dist/style.css'; +import tooltipDirective from '@/directives/tooltip.js'; import { createInertiaApp } from '@inertiajs/vue3'; import { router } from '@inertiajs/vue3'; import PortalVue from 'portal-vue'; @@ -244,7 +244,7 @@ export default { this.$app.use(createPinia()); this.$app.use(PortalVue, { portalName: 'v-portal' }); this.$app.use(VueClickAway); - this.$app.use(FloatingVue, { disposeTimeout: 30000, distance: 10 }); + this.$app.directive('tooltip', tooltipDirective); this.$app.use(VueComponentDebug, { enabled: import.meta.env.VITE_VUE_COMPONENT_DEBUG === 'true' }); toast.initialize(this.$app); diff --git a/resources/js/components/Tooltips.vue b/resources/js/components/Tooltips.vue new file mode 100644 index 0000000000..d421e31def --- /dev/null +++ b/resources/js/components/Tooltips.vue @@ -0,0 +1,67 @@ + + + diff --git a/resources/js/composables/tooltip.js b/resources/js/composables/tooltip.js new file mode 100644 index 0000000000..2b5c1778da --- /dev/null +++ b/resources/js/composables/tooltip.js @@ -0,0 +1,74 @@ +import { ref, shallowRef, readonly } from 'vue'; + +const isVisible = ref(false); +const content = ref(''); +const html = ref(false); +const targetEl = shallowRef(null); + +let hideTimeout = null; +let showTimeout = null; + +function setContent(el, options) { + targetEl.value = el; + + if (typeof options === 'string') { + content.value = options; + html.value = false; + } else if (options && typeof options === 'object') { + content.value = options.content || ''; + html.value = options.html || false; + } else { + content.value = ''; + html.value = false; + } +} + +function show(el, options) { + if (hideTimeout) { + clearTimeout(hideTimeout); + hideTimeout = null; + } + + if (showTimeout) { + clearTimeout(showTimeout); + } + + // If already visible, update immediately (for moving between adjacent elements) + if (isVisible.value) { + setContent(el, options); + return; + } + + showTimeout = setTimeout(() => { + setContent(el, options); + + if (content.value) { + isVisible.value = true; + } + }, 200); +} + +function hide() { + if (showTimeout) { + clearTimeout(showTimeout); + showTimeout = null; + } + + hideTimeout = setTimeout(() => { + isVisible.value = false; + targetEl.value = null; + content.value = ''; + html.value = false; + }, 50); +} + +export function useTooltip() { + return { + isVisible: readonly(isVisible), + content: readonly(content), + html: readonly(html), + targetEl: readonly(targetEl), + show, + hide, + }; +} diff --git a/resources/js/directives/tooltip.js b/resources/js/directives/tooltip.js new file mode 100644 index 0000000000..8ae42ce4e2 --- /dev/null +++ b/resources/js/directives/tooltip.js @@ -0,0 +1,49 @@ +import { useTooltip } from '@/composables/tooltip.js'; + +const { show, hide } = useTooltip(); + +function getOptions(binding) { + const value = binding.value; + + if (value === null || value === undefined || value === false || value === '') { + return null; + } + + return value; +} + +function handleMouseEnter(el, binding) { + const options = getOptions(binding); + if (options) { + show(el, options); + } +} + +function handleMouseLeave() { + hide(); +} + +export default { + mounted(el, binding) { + el._tooltipBinding = binding; + el._tooltipMouseEnter = () => handleMouseEnter(el, el._tooltipBinding); + el._tooltipMouseLeave = handleMouseLeave; + + el.addEventListener('mouseenter', el._tooltipMouseEnter); + el.addEventListener('mouseleave', el._tooltipMouseLeave); + el.addEventListener('focus', el._tooltipMouseEnter); + el.addEventListener('blur', el._tooltipMouseLeave); + }, + + updated(el, binding) { + el._tooltipBinding = binding; + }, + + beforeUnmount(el) { + el.removeEventListener('mouseenter', el._tooltipMouseEnter); + el.removeEventListener('mouseleave', el._tooltipMouseLeave); + el.removeEventListener('focus', el._tooltipMouseEnter); + el.removeEventListener('blur', el._tooltipMouseLeave); + hide(); + }, +}; diff --git a/resources/js/pages/layout/Layout.vue b/resources/js/pages/layout/Layout.vue index e3a6e18b9d..865662d685 100644 --- a/resources/js/pages/layout/Layout.vue +++ b/resources/js/pages/layout/Layout.vue @@ -5,6 +5,7 @@ import { ConfigProvider } from 'reka-ui'; import SessionExpiry from '@/components/SessionExpiry.vue'; import LicensingAlert from '@/components/LicensingAlert.vue'; import PortalTargets from '@/components/portals/PortalTargets.vue'; +import Tooltips from '@/components/Tooltips.vue'; import { provide, watch, ref } from 'vue'; import useBodyClasses from './body-classes.js'; import useStatamicPageProps from '@/composables/page-props.js'; @@ -61,5 +62,6 @@ provide('layout', { + From 1fb453012c4b066fdb3f0b1a9d2982b3c1e24fd8 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Thu, 22 Jan 2026 00:33:57 +0000 Subject: [PATCH 2/2] fix logo tooltip --- resources/js/components/Tooltips.vue | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/resources/js/components/Tooltips.vue b/resources/js/components/Tooltips.vue index d421e31def..8a522fadf0 100644 --- a/resources/js/components/Tooltips.vue +++ b/resources/js/components/Tooltips.vue @@ -7,6 +7,7 @@ const { isVisible, content, html, targetEl } = useTooltip(); const showTooltip = ref(false); const wrapperStyle = ref({}); +const spanStyle = ref({}); const tooltipKey = ref(0); const displayContent = ref(''); const displayHtml = ref(false); @@ -14,6 +15,7 @@ const displayHtml = ref(false); function updatePosition() { if (!targetEl.value) { wrapperStyle.value = { display: 'none' }; + spanStyle.value = {}; return; } @@ -25,9 +27,14 @@ function updatePosition() { left: `${rect.left}px`, width: `${rect.width}px`, height: `${rect.height}px`, - zIndex: 9999, pointerEvents: 'none', }; + + spanStyle.value = { + display: 'block', + width: `${rect.width}px`, + height: `${rect.height}px`, + }; } watch([isVisible, targetEl, content], async ([visible, target]) => { @@ -56,7 +63,7 @@ watch([isVisible, targetEl, content], async ([visible, target]) => { placement="top" :distance="10" > - +