-
Notifications
You must be signed in to change notification settings - Fork 327
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Tooltips fixes #12067
Tooltips fixes #12067
Changes from all commits
6d65e6b
ba19496
2d0eb3d
8477b74
d0d85b8
e693d57
e13329e
3b13e9f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,34 +8,47 @@ import { | |
type ShallowReactive, | ||
type Slot, | ||
} from 'vue' | ||
import { assert } from 'ydoc-shared/util/assert' | ||
|
||
interface TooltipEntry { | ||
contents: Ref<Slot | undefined> | ||
isHidden: boolean | ||
key: symbol | ||
} | ||
|
||
/** A hovered element with its corresponding tooltip. */ | ||
export interface HoveredElement { | ||
element: HTMLElement | ||
entry: TooltipEntry | ||
} | ||
|
||
export type TooltipRegistry = ReturnType<typeof useTooltipRegistry> | ||
export const [provideTooltipRegistry, useTooltipRegistry] = createContextStore( | ||
'tooltip registry', | ||
() => { | ||
type EntriesSet = ShallowReactive<Set<TooltipEntry>> | ||
// A map of hovered elements to their corresponding tooltips. | ||
// There can be multiple tooltips for the same element, because we can have | ||
// multiple nested tooltip triggers (components calling `registerTooltip`). | ||
// The last hovered element is always on top of the map. | ||
const hoveredElements = shallowReactive<Map<HTMLElement, EntriesSet>>(new Map()) | ||
|
||
const lastHoveredElement = computed(() => { | ||
return iter.last(hoveredElements.keys()) | ||
/** The last hovered element with its corresponding tooltip. Undefined if no element with tooltip is hovered. */ | ||
const lastHoveredElement = computed<HoveredElement | undefined>(() => { | ||
const lastKey = iter.last(hoveredElements.keys()) | ||
if (lastKey == null) return undefined | ||
const entries = hoveredElements.get(lastKey) | ||
assert(entries != null, 'entries is never null if lastKey is not null') | ||
const lastEntry = iter.last(entries) | ||
if (lastEntry == null) return undefined | ||
return { element: lastKey, entry: lastEntry } | ||
}) | ||
|
||
return { | ||
lastHoveredElement, | ||
getElementEntry(el: HTMLElement | undefined): TooltipEntry | undefined { | ||
const set = el && hoveredElements.get(el) | ||
return set ? iter.last(set) : undefined | ||
}, | ||
/** Registers a tooltip and returns methods to control it. See `TooltipTrigger` component for usage. */ | ||
registerTooltip(slot: Ref<Slot | undefined>) { | ||
const entry: TooltipEntry = { | ||
contents: slot, | ||
key: Symbol(), | ||
} | ||
const key = Symbol() | ||
const registeredElements = new Set<HTMLElement>() | ||
onUnmounted(() => { | ||
for (const el of registeredElements) { | ||
|
@@ -44,22 +57,43 @@ export const [provideTooltipRegistry, useTooltipRegistry] = createContextStore( | |
}) | ||
|
||
const methods = { | ||
/** The registered tooltip must be shown when hovering this element. */ | ||
onTargetEnter(target: HTMLElement) { | ||
const entriesSet: EntriesSet = hoveredElements.get(target) ?? shallowReactive(new Set()) | ||
entriesSet.add(entry) | ||
entriesSet.add({ contents: slot, isHidden: false, key }) | ||
// make sure that the newly entered target is on top of the map | ||
hoveredElements.delete(target) | ||
hoveredElements.set(target, entriesSet) | ||
registeredElements.add(target) | ||
}, | ||
/** The registered tooltip must be hidden when finishing hovering this element. */ | ||
onTargetLeave(target: HTMLElement) { | ||
const entriesSet = hoveredElements.get(target) | ||
entriesSet?.delete(entry) | ||
if (entriesSet) { | ||
for (const e of entriesSet) { | ||
if (e.key === key) entriesSet.delete(e) | ||
} | ||
} | ||
Comment on lines
+72
to
+76
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We may have quite a lot of entries in this set - could it be a map? Also, why didn't the previous approach work? In general I feel a bit more documentation would be helpful; for example I don't get why There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The set is usually just one element long. But in case of nested tooltip triggers (which are extremely rare) it can be longer. |
||
registeredElements.delete(target) | ||
if (entriesSet?.size === 0) { | ||
hoveredElements.delete(target) | ||
} | ||
}, | ||
/** | ||
* Forcefully hides the registered tooltip. | ||
* Useful when we need to hide the tooltip without moving the mouse out of the element, | ||
* like when clicking on a button. | ||
* | ||
* If several tooltips are registered for the same element, all of them will hide. | ||
*/ | ||
forceHide() { | ||
for (const el of registeredElements) { | ||
const entriesSet = hoveredElements.get(el) | ||
const newSet = new Set(entriesSet) | ||
newSet.forEach((entry) => (entry.isHidden = true)) | ||
hoveredElements.set(el, newSet) | ||
} | ||
}, | ||
} | ||
return methods | ||
}, | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add a comment explaining why we sometimes display the tooltip at the previous element - I think it's not obvious without the context of the bug you're fixing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done