fix: restore tooltips on icon-only buttons inside popover triggers (#5935)
Co-authored-by: nivedin <nivedinp@gmail.com> Co-authored-by: James George <25279263+jamesgeorge007@users.noreply.github.com>
This commit is contained in:
parent
f012c31ba2
commit
08921786e7
6 changed files with 192 additions and 9 deletions
|
|
@ -173,7 +173,7 @@
|
|||
|
||||
.tippy-box[data-theme~="tooltip"] {
|
||||
@apply bg-primaryDark;
|
||||
@apply border-solid;
|
||||
@apply border-solid border-divider;
|
||||
@apply shadow-lg;
|
||||
|
||||
.tippy-content {
|
||||
|
|
@ -187,7 +187,7 @@
|
|||
|
||||
.tippy-svg-arrow {
|
||||
svg:first-child {
|
||||
@apply fill-primaryDark;
|
||||
@apply fill-divider;
|
||||
}
|
||||
|
||||
svg:last-child {
|
||||
|
|
@ -220,7 +220,7 @@
|
|||
|
||||
.tippy-box[data-theme~="tooltip"] {
|
||||
@apply bg-primaryLight;
|
||||
@apply border-solid border-[#33363d];
|
||||
@apply border-solid border-divider;
|
||||
@apply shadow-lg;
|
||||
|
||||
.tippy-content {
|
||||
|
|
@ -234,7 +234,7 @@
|
|||
|
||||
.tippy-svg-arrow {
|
||||
svg:first-child {
|
||||
@apply fill-primaryLight;
|
||||
@apply fill-divider;
|
||||
}
|
||||
|
||||
svg:last-child {
|
||||
|
|
|
|||
|
|
@ -132,6 +132,7 @@
|
|||
"web_app": "Web App",
|
||||
"cli": "CLI"
|
||||
},
|
||||
"downloads": "Downloads",
|
||||
"chat_with_us": "Chat with us",
|
||||
"contact_us": "Contact us",
|
||||
"cookies": "Cookies",
|
||||
|
|
|
|||
|
|
@ -87,6 +87,8 @@
|
|||
:on-shown="() => downloadableLinksRef.focus()"
|
||||
>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('app.downloads')"
|
||||
:icon="IconDownload"
|
||||
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -47,9 +47,9 @@
|
|||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('app.wiki')"
|
||||
to="https://docs.hoppscotch.io/documentation/features/graphql-api-testing"
|
||||
blank
|
||||
:title="t('app.wiki')"
|
||||
:icon="IconHelpCircle"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
|
|
|
|||
|
|
@ -1,11 +1,26 @@
|
|||
import { HoppModule } from "."
|
||||
import VueTippy, { roundArrow, setDefaultProps } from "vue-tippy"
|
||||
import type { DirectiveBinding, VNode } from "vue"
|
||||
import VueTippy, { useTippy, roundArrow, setDefaultProps } from "vue-tippy"
|
||||
|
||||
import "tippy.js/dist/tippy.css"
|
||||
import "tippy.js/animations/scale-subtle.css"
|
||||
import "tippy.js/dist/border.css"
|
||||
import "tippy.js/dist/svg-arrow.css"
|
||||
|
||||
// Extended HTMLElement with tippy instance properties added by vue-tippy
|
||||
interface TippyElement extends HTMLElement {
|
||||
$tippy?: {
|
||||
props: Record<string, unknown>
|
||||
setProps: (opts: Record<string, unknown>) => void
|
||||
destroy: () => void
|
||||
}
|
||||
_tippy?: {
|
||||
props: Record<string, unknown>
|
||||
setProps: (opts: Record<string, unknown>) => void
|
||||
destroy: () => void
|
||||
}
|
||||
}
|
||||
|
||||
export type TippyState = {
|
||||
isEnabled: boolean
|
||||
isVisible: boolean
|
||||
|
|
@ -14,9 +29,86 @@ export type TippyState = {
|
|||
isShown: boolean
|
||||
}
|
||||
|
||||
// Resolves tooltip options from directive binding, vnode props, and DOM attrs.
|
||||
// Reads from vnode.props first (immune to DOM stripping by parent <tippy>
|
||||
// components), then falls back to DOM attributes.
|
||||
// Pure — does not mutate the DOM. Callers are responsible for removing the
|
||||
// title attribute to suppress the browser's native tooltip.
|
||||
function resolveOpts(
|
||||
el: TippyElement,
|
||||
binding: DirectiveBinding,
|
||||
vnode: VNode
|
||||
): Record<string, unknown> {
|
||||
const opts =
|
||||
typeof binding.value === "string"
|
||||
? { content: binding.value }
|
||||
: { ...(binding.value || {}) }
|
||||
|
||||
if (!opts.content) {
|
||||
const title = vnode.props?.title ?? el.getAttribute("title")
|
||||
if (title) {
|
||||
opts.content = title
|
||||
}
|
||||
}
|
||||
|
||||
if (!opts.content && el.getAttribute("content")) {
|
||||
opts.content = el.getAttribute("content")
|
||||
}
|
||||
|
||||
return opts
|
||||
}
|
||||
|
||||
export default <HoppModule>{
|
||||
onVueAppInit(app) {
|
||||
app.use(VueTippy)
|
||||
// Register VueTippy with a noop directive name so the plugin's built-in
|
||||
// v-tippy directive doesn't conflict with our custom one below.
|
||||
app.use(VueTippy, { directive: "tippy-original" })
|
||||
|
||||
// Custom v-tippy directive: reads content from vnode.props instead of
|
||||
// el.getAttribute('title') to fix tooltips inside <tippy> wrappers where
|
||||
// tippy.js strips the title attribute before the child directive reads it.
|
||||
// Fixes #5915
|
||||
app.directive("tippy", {
|
||||
mounted(el: TippyElement, binding: DirectiveBinding, vnode: VNode) {
|
||||
const opts = resolveOpts(el, binding, vnode)
|
||||
el.removeAttribute("title")
|
||||
// useTippy (not tippy() directly) so setDefaultProps are applied.
|
||||
// Works outside setup() because vue-tippy v6.x guards getCurrentInstance().
|
||||
useTippy(el, opts)
|
||||
},
|
||||
|
||||
// Explicit cleanup for v-if removals — useTippy's internal
|
||||
// onBeforeUnmount only fires on component unmount, not element removal.
|
||||
unmounted(el: TippyElement) {
|
||||
if (el._tippy) {
|
||||
el._tippy.destroy()
|
||||
} else if (el.$tippy) {
|
||||
el.$tippy.destroy()
|
||||
}
|
||||
},
|
||||
|
||||
// Sync tooltip content when reactive :title bindings change
|
||||
// (e.g. "Add star" ↔ "Remove star" in History).
|
||||
updated(el: TippyElement, binding: DirectiveBinding, vnode: VNode) {
|
||||
const tippy = el.$tippy || el._tippy
|
||||
if (!tippy) return
|
||||
|
||||
const opts = resolveOpts(el, binding, vnode)
|
||||
|
||||
if (!opts.content) {
|
||||
opts.content = ""
|
||||
}
|
||||
|
||||
// Vue re-applies :title on each patch cycle
|
||||
el.removeAttribute("title")
|
||||
|
||||
// Skip if content unchanged — setProps is expensive (recreates Popper).
|
||||
const currentContent = tippy.props?.content
|
||||
if (opts.content === currentContent) return
|
||||
|
||||
tippy.setProps(opts)
|
||||
},
|
||||
})
|
||||
|
||||
setDefaultProps({
|
||||
animation: "scale-subtle",
|
||||
|
|
|
|||
|
|
@ -1,14 +1,102 @@
|
|||
import { HoppModule } from '.';
|
||||
import VueTippy, { roundArrow, setDefaultProps } from 'vue-tippy';
|
||||
import type { DirectiveBinding, VNode } from 'vue';
|
||||
import VueTippy, { useTippy, roundArrow, setDefaultProps } from 'vue-tippy';
|
||||
|
||||
import 'tippy.js/dist/tippy.css';
|
||||
import 'tippy.js/animations/scale-subtle.css';
|
||||
import 'tippy.js/dist/border.css';
|
||||
import 'tippy.js/dist/svg-arrow.css';
|
||||
|
||||
interface TippyElement extends HTMLElement {
|
||||
$tippy?: {
|
||||
props: Record<string, unknown>;
|
||||
setProps: (opts: Record<string, unknown>) => void;
|
||||
destroy: () => void;
|
||||
};
|
||||
_tippy?: {
|
||||
props: Record<string, unknown>;
|
||||
setProps: (opts: Record<string, unknown>) => void;
|
||||
destroy: () => void;
|
||||
};
|
||||
}
|
||||
|
||||
// Resolves tooltip options from directive binding, vnode props, and DOM attrs.
|
||||
// Reads from vnode.props first (immune to DOM stripping by parent <tippy>
|
||||
// components), then falls back to DOM attributes.
|
||||
function resolveOpts(
|
||||
el: TippyElement,
|
||||
binding: DirectiveBinding,
|
||||
vnode: VNode,
|
||||
): Record<string, unknown> {
|
||||
const opts =
|
||||
typeof binding.value === 'string'
|
||||
? { content: binding.value }
|
||||
: { ...(binding.value || {}) };
|
||||
|
||||
if (!opts.content) {
|
||||
const title = vnode.props?.title ?? el.getAttribute('title');
|
||||
if (title) {
|
||||
opts.content = title;
|
||||
}
|
||||
}
|
||||
|
||||
if (!opts.content && el.getAttribute('content')) {
|
||||
opts.content = el.getAttribute('content');
|
||||
}
|
||||
|
||||
return opts;
|
||||
}
|
||||
|
||||
export default <HoppModule>{
|
||||
onVueAppInit(app) {
|
||||
app.use(VueTippy);
|
||||
// Register VueTippy under a noop directive name so it doesn't conflict
|
||||
// with the custom v-tippy below.
|
||||
app.use(VueTippy, { directive: 'tippy-original' });
|
||||
|
||||
// Custom v-tippy directive: reads content from vnode.props instead of
|
||||
// el.getAttribute('title') to fix tooltips inside <tippy> wrappers where
|
||||
// tippy.js strips the title attribute before the child directive reads it.
|
||||
app.directive('tippy', {
|
||||
mounted(el: TippyElement, binding: DirectiveBinding, vnode: VNode) {
|
||||
const opts = resolveOpts(el, binding, vnode);
|
||||
el.removeAttribute('title');
|
||||
// useTippy (not tippy() directly) so setDefaultProps are applied.
|
||||
// Works outside setup() because vue-tippy v6.x guards getCurrentInstance().
|
||||
useTippy(el, opts);
|
||||
},
|
||||
|
||||
// Explicit cleanup for v-if removals — useTippy's internal
|
||||
// onBeforeUnmount only fires on component unmount, not element removal.
|
||||
unmounted(el: TippyElement) {
|
||||
if (el._tippy) {
|
||||
el._tippy.destroy();
|
||||
} else if (el.$tippy) {
|
||||
el.$tippy.destroy();
|
||||
}
|
||||
},
|
||||
|
||||
// Sync tooltip content when reactive :title bindings change
|
||||
// (e.g. "Add star" ↔ "Remove star" in History).
|
||||
updated(el: TippyElement, binding: DirectiveBinding, vnode: VNode) {
|
||||
const tippy = el.$tippy || el._tippy;
|
||||
if (!tippy) return;
|
||||
|
||||
const opts = resolveOpts(el, binding, vnode);
|
||||
|
||||
if (!opts.content) {
|
||||
opts.content = '';
|
||||
}
|
||||
|
||||
// Vue re-applies :title on each patch cycle
|
||||
el.removeAttribute('title');
|
||||
|
||||
// Skip if content unchanged — setProps is expensive (recreates Popper).
|
||||
const currentContent = tippy.props?.content;
|
||||
if (opts.content === currentContent) return;
|
||||
|
||||
tippy.setProps(opts);
|
||||
},
|
||||
});
|
||||
|
||||
setDefaultProps({
|
||||
animation: 'scale-subtle',
|
||||
|
|
|
|||
Loading…
Reference in a new issue